[UUID 5/8] UUID aggregation, group-by and distinct - #18873
Conversation
6120bc9 to
478757f
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #18873 +/- ##
==========================================
Coverage 66.63% 66.63%
Complexity 1423 1423
==========================================
Files 3443 3443
Lines 218663 218798 +135
Branches 34801 34847 +46
==========================================
+ Hits 145705 145802 +97
- Misses 61230 61265 +35
- Partials 11728 11731 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
b5e8bc6 to
64cb941
Compare
ab8f6a0 to
adf9c85
Compare
5dbed17 to
474fae5
Compare
474fae5 to
ac891db
Compare
0cd8e0c to
56f96cc
Compare
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for Pinot’s logical UUID type across aggregation/group-by and DISTINCT-style result paths, aligning behavior between stored BYTES (16-byte) representation and canonical UUID semantics in query results and sketch-based distinct-count functions.
Changes:
- Extend no-dictionary group key generation to treat
UUIDas a first-class logical type usingUuidKey/UuidToIdMap(instead of genericByteArray) for faster hashing/equality. - Fix broker reduce group-key conversion for
UUID(GroupByDataTableReducer#getConvertedKey) to return the convertedjava.util.UUIDform instead of raw bytes. - Update DISTINCT and distinct-count aggregations (HLL/HLLPlus/ULL/CPC/Theta/Bitmap) plus
ANY_VALUE/utility paths to render/hash UUIDs consistently (canonical UUID strings), with new unit + integration coverage.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/UuidAggregationTest.java | Integration coverage for UUID GROUP BY, HAVING reduce branches, DISTINCT formatting, and distinct-count behaviors. |
| pinot-core/src/test/java/org/apache/pinot/core/query/distinct/table/BytesDistinctTableTest.java | Verifies DISTINCT result formatting differs correctly for UUID vs BYTES. |
| pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/groupby/NoDictionaryGroupKeyGeneratorTest.java | Extends group key generator tests to include UUID (and additional logical types). |
| pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/DistinctCountHLLAggregationFunctionTest.java | Adds regression + contract tests for UUID hashing behavior in HLL distinct count. |
| pinot-core/src/main/java/org/apache/pinot/core/query/reduce/GroupByDataTableReducer.java | Ensures UUID group keys are converted via ColumnDataType#convert to avoid reducer-path divergence. |
| pinot-core/src/main/java/org/apache/pinot/core/query/distinct/table/BytesDistinctTable.java | Formats DISTINCT output via ColumnDataType#convertAndFormat rather than hard-coded hex. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/ValueToIdMapFactory.java | Registers UUID-specific on-the-fly dictionary map. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/UuidToIdMap.java | Adds UUID-specialized ValueToIdMap using UuidKey keys and ByteArray storage. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/NoDictionarySingleColumnGroupKeyGenerator.java | Preserves UUID logical type for group-key dispatch and uses UuidKey in hot paths. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/NoDictionaryMultiColumnGroupKeyGenerator.java | Preserves UUID logical type per column and routes raw bytes through UuidKey for on-the-fly dictionaries. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/IntegerTupleSketchAggregationFunction.java | Rejects raw UUID inputs with a clearer error instead of sketch-deserialization failures. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountULLAggregationFunction.java | Hashes canonical UUID strings for UUID-typed columns to match STRING semantics. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountThetaSketchAggregationFunction.java | Converts UUID bytes to canonical strings before updating theta sketches to avoid treating them as serialized sketches. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountHLLPlusAggregationFunction.java | Offers canonical UUID strings for UUID columns rather than deserializing raw bytes. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountHLLAggregationFunction.java | Offers canonical UUID strings for UUID columns rather than deserializing raw bytes. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountCPCSketchAggregationFunction.java | Updates CPC sketch with canonical UUID strings for UUID-typed columns. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountBitmapAggregationFunction.java | Adds UUID handling by hashing canonical UUID strings into the bitmap path. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AnyValueAggregationFunction.java | Ensures UUID columns return ColumnDataType.UUID for canonical rendering. |
| pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java | Handles UUID dictionary entries for distinct-count sketch results by offering canonical UUID strings instead of treating dictionary BYTES as serialized sketches. |
Suppressed comments (1)
pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/UuidAggregationTest.java:148
- This comment block contains lines over the 120-character Checkstyle LineLength limit (config/checkstyle.xml). Please reflow the comment to avoid
checkstyle:checkfailures.
/// The `serverReturnFinalResult` variant. This is the option that gates the single-data-table branch in
/// `GroupByDataTableReducer#reduceAndSetResults` (`isServerReturnFinalResult() && dataTables.size() == 1`), so it
/// covers a different reduce branch from the test above. Verified by reverting the `getConvertedKey` fix: both
/// this and the plain `GROUP BY ... HAVING` test fail with
/// `ClassCastException: class [B cannot be cast to class java.util.UUID`, and both pass with it.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Part 5/8 of splitting apache#18140 (logical UUID type). Rebased onto latest master; stacked on uuid-split/04-sse-predicates-cast. Downstream references use the UuidKey class merged in apache#18869.
… path getConvertedKey had `case UUID` falling through to BYTES, returning the raw byte[]. The other reduce path converts group keys via ColumnDataType#convert, and UUID is the one type whose converted form is not its stored bytes -- it yields a java.util.UUID. PredicateRowMatcher casts that directly (see apache#18872), so the byte[] made GROUP BY ... HAVING over a UUID column fail with "ClassCastException: class [B cannot be cast to class java.util.UUID". Delegating to columnDataType.convert(...) keeps the two paths identical by construction rather than by duplicated knowledge. Covered by a new UuidAggregationTest integration test rather than a unit test. The unit-level BaseQueriesTest harness cannot reach this code, which is why it was uncovered; a query-level test goes through the real broker reduce. Verified by reverting the fix: testGroupByUuidColumnWithHaving and testGroupByUuidColumnWithHavingReturningFinalResult both fail with the ClassCastException and both pass with it. The test also covers GROUP BY key rendering, DISTINCT (BytesDistinctTable no longer hard-codes hex) and DISTINCTCOUNT / DISTINCTCOUNTHLL / DISTINCTCOUNTBITMAP over a UUID column.
Both callers key on UuidKey already (NoDictionary{Single,Multi}ColumnGroupKey
Generator, via UuidKey.fromBytes), so UuidKey.fromObject accepted five input
types where exactly one is ever passed, and ran an instanceof chain per row in
the group-by loop.
Casting directly matches the sibling maps -- DoubleToIdMap casts to double --
and keeps the input type deterministic, which is what was asked for on the
equivalent PredicateRowMatcher branch in apache#18872.
…cal string The previous version rendered each UUID as its 36-char canonical string so that DISTINCTCOUNTHLL(uuidCol) would equal DISTINCTCOUNTHLL(CAST(uuidCol AS STRING)). No other logical type provides that guarantee: the scan path switches on the stored type, so TIMESTAMP offers its raw millis and BOOLEAN its int, and neither matches a CAST to STRING. The UUID rendering was inventing a cross-type equivalence at the cost of a String allocation per row in the aggregation loop. UUID now hashes its stored 16 bytes. Verified byte[] is content-hashed rather than identity-hashed by both HyperLogLog (clearspring MurmurHash) and UltraLogLogUtils.OBJECT_FUNNEL (putBytes). A minimal guard is still needed at each site, because unlike LONG or INT the stored BYTES type is not a scalar case in this family: the scan path has no `case BYTES`, and the dictionary path reads BYTES as serialized sketch state. - AggregationFunctionUtils: the three UUID blocks are gone; the BYTES guard now excludes UUID so it falls through to the scalar path, which offers dictionary.get(i) -- the stored byte[] -- exactly as the scan path does. - DistinctCountBitmap hashes Arrays.hashCode(bytes). - DistinctCountThetaSketch cannot take scalar bytes (BYTES there means "serialized sketch"), so it surfaces the stored hex rendering instead. Replaces testUuidDistinctCountHllMatchesStringDistinctCountHll, which asserted the invariant being dropped, with one pinning the new behaviour.
c18108c to
a81af4e
Compare
Applying the rule that if TIMESTAMP and BIG_DECIMAL need no special handling at
a site, UUID does not either -- their stored types carry them, and UUID's
should too.
Removed:
- NoDictionary{Single,Multi}ColumnGroupKeyGenerator, UuidToIdMap and its
ValueToIdMapFactory entry. `case BYTES` is already supported there and is
structurally identical to the UUID branch: same getBytesValuesSV(), same
loop, same map, differing only in wrapping UuidKey vs ByteArray. Both yield
ByteArray downstream, which is what ColumnDataType.UUID#convert consumes, so
this was a pure key-representation optimization -- two primitive longs
instead of a byte[] wrapper -- with no benchmark to justify 5 files and
+237/-36. UuidAggregationTest#testGroupByUuidColumn still passes, confirming
group keys render identically without it.
- AnyValueAggregationFunction. Its switch is on getStoredType(), so TIMESTAMP
already collapses to LONG (raw millis) and BOOLEAN to INT. UUID collapsing to
BYTES is the consistent behaviour; the branch made it the odd one out.
- IntegerTupleSketchAggregationFunction. The UUID branch only produced a
friendlier error message; TIMESTAMP gets no such treatment when it is equally
unusable there.
Kept, because the same rule shows they are needed:
- BytesDistinctTable: LongDistinctTable and BigDecimalDistinctTable already
render by ColumnDataType, and MultiColumnDistinctTable already calls
convertAndFormat. BytesDistinctTable hard-coding toHexString() was the
outlier; this brings it in line rather than special-casing UUID.
- The distinct-count guards and GroupByDataTableReducer, both verified
necessary by probe -- BYTES means "serialized sketch" in one and the group
key must convert in the other.
19 files / +958 -80 -> 12 files / +694 -39.
The previous version added every row holding a ByteArray, then walked the whole list again in formatRows() to rewrite row[0]. Two traversals and two writes per row where master did one. The formatting now happens inline in addRows, with the ColumnDataType passed in -- the same shape LongDistinctTable already uses for its TIMESTAMP handling. Not an extra String allocation either way: ColumnDataType#convertAndFormat for BYTES is ((ByteArray) value).toHexString(), the exact call master made, and ByteArray#getBytes() returns the internal array without copying.
Rebased onto master now that #18872 has merged — this PR was stacked on it and carried its 13 files. It is now just the aggregation / group-by / distinct work: 19 files, +967 −77 (was 31 files, +1212).
What
UUID support across the aggregation and grouping paths:
NoDictionarySingleColumnGroupKeyGeneratorandNoDictionaryMultiColumnGroupKeyGeneratorhandle UUID, backed by a newUuidToIdMapregistered inValueToIdMapFactory. Group keys useUuidKey(two primitive longs) rather thanByteArray.DistinctCountBitmap,DistinctCountHLL,DistinctCountHLLPlus,DistinctCountCPCSketch,DistinctCountThetaSketch,DistinctCountULLandIntegerTupleSketch.DISTINCT—BytesDistinctTableformats result rows throughColumnDataType#convertAndFormatinstead of hard-codingtoHexString(), so a UUID column returns canonical UUIDs while BYTES still returns hex.ANY_VALUEand the sharedAggregationFunctionUtilsplumbing.A bug this PR would have introduced, and its fix
The original version added
case UUIDtoGroupByDataTableReducer#getConvertedKeyas a fall-through toBYTES, returning the rawbyte[].That breaks against #18872, now on master: the other reduce path converts group keys with
ColumnDataType#convert, and UUID is the one type whose converted form is not its stored bytes — it yields ajava.util.UUID.PredicateRowMatchercasts directly on that, so abyte[]producesClassCastException: class [B cannot be cast to class java.util.UUIDonGROUP BY <uuidCol> ... HAVING <uuidCol> = ....Fixed by delegating rather than duplicating the knowledge, so the two paths cannot diverge again:
Covered by an integration test, not a unit test. The unit-level
BaseQueriesTestharness reduces two data tables and so cannot reach this code — which is exactly why the path was uncovered. The newUuidAggregationTestgoes through a real broker reduce. I checked the test is actually worth something by reverting the fix:testGroupByUuidColumnWithHavingandtestGroupByUuidColumnWithHavingReturningFinalResultboth fail with theClassCastException, and both pass with it.Review follow-up:
UuidToIdMapnow casts directlyUuidToIdMapcalledUuidKey.fromObject(value), which accepts five input types. Both callers already key onUuidKey(viaUuidKey.fromBytes), so exactly one type is ever passed and the rest was aninstanceofchain per row in the group-by loop. It now casts directly, matching the sibling maps (DoubleToIdMapcasts todouble) and keeping the input type deterministic — the same property asked for on the equivalentPredicateRowMatcherbranch in #18872.Distinct-count hashes the stored bytes
An earlier revision rendered each UUID as its 36-char canonical string so that
DISTINCTCOUNTHLL(uuidCol)would equalDISTINCTCOUNTHLL(CAST(uuidCol AS STRING)). No other logical type provides that guarantee — the scan path switches on the stored type, so TIMESTAMP hashes its raw millis and BOOLEAN its int, and neither matches a CAST to STRING. It also cost aStringallocation per row in the aggregation loop.UUID now hashes its stored 16 bytes, which is what "the stored type is the contract" means for a logical type. I verified
byte[]is content-hashed rather than identity-hashed by bothHyperLogLog(clearspringMurmurHash) andUltraLogLogUtils.OBJECT_FUNNEL(putBytes) before relying on it.A minimal guard is still needed at each site: unlike LONG or INT, the stored BYTES type is not a scalar case in this function family — the scan path has no
case BYTESat all, and the dictionary path reads BYTES as serialized sketch state. The guard exists only to avoid that misreading:AggregationFunctionUtils— the three duplicated UUID blocks are gone. The BYTES guard now excludes UUID so it falls through to the scalar path, which offersdictionary.get(i)(the storedbyte[]), exactly as the scan path does. That file went from +55/-9 to +27/-12.DistinctCountBitmaphashesArrays.hashCode(bytes).DistinctCountThetaSketchis the one exception:DataType.BYTESthere means "serialized sketch" with no scalar-bytes mode, so it must surface aString. It now uses the stored hex rendering, matching every other String-typed UUID boundary (PredicateUtils#getStoredValue, the Bloom filter key).testUuidDistinctCountHllMatchesStringDistinctCountHllasserted the invariant being dropped; it is replaced by a test pinning the new behaviour so the canonical-string rendering is not quietly reintroduced.Verification
Full
pinot-corereactor on this branch:The per-module counts are the evidence, not the exit code: the run used
-Dmaven.test.failure.ignore=true(needed becausepinot-coreis skipped entirely if an upstream module fails), soBUILD SUCCESSalone would not prove anything. There are zero<<< FAILURElines in the log.Integration:
UuidAggregationTest+UuidBloomFilterTest— 7 tests, 0 failures.Part of the #18140 UUID split. Depends on parts 1, 2 and 4, all merged.