Repository navigation
AVRO-4354: [java] Fix generated equals() and hashCode() semantics - #4045
Open
sharanggupta wants to merge 1 commit into
Open
sharanggupta wants to merge 1 commit into
sharanggupta wants to merge 1 commit into
Conversation
Since AVRO-3527 the generated equals() compares array, map and union fields with java.util.Objects.equals(), and the generated hashCode() hashes CharSequence fields with their own hashCode(). Decoded records hold GenericData.Array and Utf8 values while built records hold java.util.List and String values, so: - equals() throws "Unknown datum type" for a decoded array whose elements contain a union with a logical type, because GenericData.Array.equals() uses the global GenericData, which has no logical type conversions, and List.equals(Array) is not symmetric with Array.equals(List) for string elements; - hashCode() differs for a String and a Utf8 with the same content, although equals() treats them as equal. Array, map and non-nullable union fields are now compared and hashed through the SpecificData model of the generated class, which knows the conversions and the Avro equality rules, and CharSequence fields are hashed with the new Utf8.hashCodeFor(), the hash code of the UTF-8 encoding of the sequence, so that the generated hashCode() agrees with equals() and with SpecificData.hashCode(). The four-argument GenericData.compare() is public, so that generated code can call it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the purpose of the change
Fixes AVRO-4354: since 1.12.1 (AVRO-3527) the generated
equals()compares array, map and union fields withjava.util.Objects.equals(), and the generatedhashCode()hashesCharSequencefields with their ownhashCode(). A decoded record holdsGenericData.ArrayandUtf8values while a built record holdsjava.util.ListandStringvalues, so:equals()throwsAvroRuntimeException: Unknown datum type java.util.UUID(orLocalDate,Instant, ...) for a decoded array whose elements contain a union with a logical type, becauseGenericData.Array.equals()compares through the globalGenericData, which has no logical type conversions. It is also asymmetric:List.equals(Array)compares element by element whileArray.equals(List)goes through the model, so for string elements one direction seesStringvsUtf8and returnsfalse(AVRO-4198).hashCode()of a record holding aStringdiffers from the same record after a round trip, which holds aUtf8, althoughequals()treats them as equal, soHashSet/HashMaplookups fail.The fix keeps the fast paths of AVRO-3527 for primitive,
CharSequence, record, enum, fixed, bytes and converted logical-type fields, and changes the generated code where the runtime type of the value is not under the control of the generated class:MODEL$.compare(this.x, other.x, fieldSchema, true)and hashed withMODEL$.hashCode(this.x, fieldSchema): theSpecificDataof the generated class knows the logical type conversions and applies the Avro equality rules (String/Utf8,List/GenericData.Array, map keys) symmetrically, asSpecificRecordBase.equals()/hashCode()did before 1.12.1 for the whole record;CharSequencefields are hashed with the newUtf8.hashCodeFor(CharSequence), which computes the hash code of the UTF-8 encoding of the sequence without encoding it, so that it matchesUtf8.hashCode()for the same content andUtf8.compareSequences(), used by the generatedequals(). The result is the hash thatSpecificData.hashCode()computes (used byGenericData.HashCodeCalculatornow too, which previously allocated aUtf8perString), so records generated by this compiler, records generated before 1.12.1 andGenericRecords with the same content keep the same hash code; only records generated by 1.12.1/1.12.2 that holdStrings change hash code, which they must, to honour theequals()contract.GenericData.compare(Object, Object, Schema, boolean)(and theSpecificData/ReflectDataoverrides) is now public so that generated code can call it; the three-argumentcompare()was already public.GenericData.hashCode(Object, Schema)gains aMAPcase: it used to fall back toMap.hashCode(), whose result depends on whether the keys and values areStrings orUtf8s, whilecompareMaps()treats them as equal. Map entries are now hashed order-independently with the UTF-8 hash of the key and the schema hash of the value; forUtf8-keyed maps withUtf8, numeric or record values the result is unchanged.As for
Utf8.compareSequences()in 1.12.1, code generated by this compiler needs this runtime version.Not addressed here, as they look like separate decisions:
GenericData.Array.equals()called directly with logical type elements (AVRO-4036), and a compiler/plugin option to disable the generation ofequals()/hashCode()(item 4 of the ticket). Happy to add the option in a follow-up if wanted.Verifying this change
This change added tests and can be verified as follows:
TestGeneratedEqualsAndHashCodein the compiler module, on a new test schemagenerated_equals.avsccompiled at build time like the otherSchemaTaskschemas: array of records with a nullableuuid, array of nullabledate, array of strings, nullable array of strings, map of strings,CharSequencefields with aHashSetlookup, and generatedhashCode()equal toSpecificData.hashCode(). Onmain,arrayOfRecordsWithNullableLogicalTypeandarrayOfNullableLogicalTypeerror withAvroRuntimeException: Unknown datum type java.util.UUID/java.time.LocalDate,arrayOfStrings,nullableArrayOfStringsandmapOfStringsfail onafter.equals(before), and the two hash tests fail with different hash codes (4066 vs 5058 forEqualsNamed("a", "b")).TestUtf8.hashCodeForMatchesUtf8HashCodeand...ForRandomStrings:Utf8.hashCodeFor()equalsnew Utf8(s).hashCode()for ASCII, 2-, 3- and 4-byte characters, unpaired surrogates and 1000 random strings.TestGenericData.testMapHashCodeIsConsistentWithEquals: aString-keyed and aUtf8-keyed map with the same content compare equal and hash the same, and theUtf8map keeps its previous hash code.spotless:applyapplied):avro(3463 tests),compiler(99),ipc(238),maven-plugin(15) and theintegration-testcodegen modules pass withverify.Documentation
Utf8.hashCodeFor()and the template helpers have JavaDoc)