PDGGK opened a new pull request, #9249:
URL: https://github.com/apache/paimon/pull/9249

   ### Purpose
   
   `collect(distinct)` and `merge_map` compare their elements and keys with 
`Object.equals`. When those are `BINARY` or `VARBINARY` the value is a 
`byte[]`, which inherits identity equality, so nothing with the same content is 
ever recognised as the same thing.
   
   Measured on `master` (`91ce4d6`), before any change:
   
   | aggregator | input | result | expected |
   |---|---|---|---|
   | `collect(distinct)` on `ARRAY<BYTES>` | `[0x0102]` merged with `[0x0102]` 
| **2 elements** | 1 |
   | `merge_map` on `MAP<BYTES, INT>` | `{0x0102: 1}` merged with `{0x0102: 2}` 
| **2 entries** | 1 |
   | `merge_map` retract | `{0x0102: 1}` retract `{0x0102: 1}` | **1 entry** | 
0 |
   
   The middle row is the worst of the three: the aggregator hands back a 
`GenericMap` containing the same logical key twice, which is not a well-formed 
map at all. The third means a retraction on such a table never removes anything.
   
   ### Why it happens
   
   **`FieldCollectAgg`** builds a generated `RecordEqualiser` and uses it 
instead of `Object.equals`, but only for constructed element types:
   
   ```java
   if (distinct
           && dataType.getElementType().getTypeRoot().getFamilies()
                   .contains(DataTypeFamily.CONSTRUCTED)) {
   ```
   
   `BINARY` and `VARBINARY` are `(PREDEFINED, BINARY_STRING)` — not 
`CONSTRUCTED` — so `equaliser` stays null, `agg` falls into the `new 
HashSet<>()` branch, and the private `equals(a, b)` that `retract` uses 
degrades to `a.equals(b)` on two `byte[]`.
   
   **`FieldMergeMapAgg`** has no equaliser at all: it keys a `HashMap` directly 
with `keyGetter.getElementOrNull(...)`, and its `retract` probes a `HashSet` of 
the same raw keys.
   
   ### What changes
   
   **`FieldCollectAgg`** — the element type is routed to the existing equaliser 
path when it is binary as well as when it is constructed. The generated 
equaliser already compares `VARBINARY` by value; I checked that directly rather 
than assuming:
   
   ```java
   RecordEqualiser eq = 
newRecordEqualiser(singletonList(DataTypes.VARBINARY(10)));
   eq.equals(GenericRow.of(new byte[] {1, 2}), GenericRow.of(new byte[] {1, 
2}))  // true
   eq.equals(GenericRow.of(new byte[] {1, 2}), GenericRow.of(new byte[] {1, 
3}))  // false
   ```
   
   The gate on `distinct` stays. It has to: `agg` treats a non-null equaliser 
as "deduplicate", so building one unconditionally would make plain `collect` 
start dropping duplicates.
   
   **`FieldMergeMapAgg`** — binary keys are held in `ByteArrayKey` 
(`paimon-common`, already used by `PartitionDictionary`, 
`ManifestEntryRunMergeEntry` and `DataEvolutionRowIdAssignmentPlanner`) while 
they sit in the hash collections, and unwrapped when the `GenericMap` is built. 
`ByteArrayKey.bytes()` becomes `public` for that; it was package-private and 
the class exists for nothing else.
   
   ### Blast radius
   
   Nothing changes for a non-binary key or element. `binaryKey` is false for 
every other type, so `hashKey`/`originalKey` are the identity and 
`toGenericMap` returns `new GenericMap(map)` — the same object construction as 
before. In `FieldCollectAgg` the added branch is reachable only for 
`BINARY`/`VARBINARY`, which is exactly the case that is broken today. All 100 
pre-existing cases in `FieldAggregatorTest` pass unchanged.
   
   ### Tests
   
   Four, in `FieldAggregatorTest`: distinct collect and its retraction on 
`ARRAY<VARBINARY>`, merge and retraction on `MAP<VARBINARY, INT>`. Each 
mutation is isolated — reverting one fix fails only its own two:
   
   | reverted | failures |
   |---|---|
   | `needsEqualiser` back to `CONSTRUCTED` only | 
`testFieldCollectAggWithDistinctBinary`, 
`testFieldCollectAggRetractWithDistinctBinary` |
   | `hashKey` back to the identity | `testFieldMergeMapAggWithBinaryKey`, 
`testFieldMergeMapAggRetractWithBinaryKey` |
   
   `FieldAggregatorTest` (100), `FieldAggregatorRetractNullTest` (19) and 
`AggregateMergeFunctionTest` (3): 122 tests, 0 failures. `spotless:apply` and 
`checkstyle:check` on `paimon-common` and `paimon-core` are clean.
   
   ### Not in this PR
   
   `FieldCollectAgg.retract` uses `equals(a, b)`, which is the raw 
`Object.equals` whenever `distinct` is false — so retraction of a non-distinct 
`collect` still compares constructed and binary elements by identity. That gap 
predates this change and applies to `ROW`/`ARRAY`/`MAP` elements too, so fixing 
it means giving `retract` an equality function of its own rather than borrowing 
the one `agg` uses for deduplication. Happy to do that separately if you want 
it.
   
   ### API and Format
   
   No change to any option, on-disk format or public signature, other than 
widening `ByteArrayKey.bytes()` from package-private to public.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to