[UUID 4/8] Server-side predicate evaluation for the logical UUID type - #18872
Conversation
9a2a841 to
54e9158
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #18872 +/- ##
============================================
- Coverage 66.65% 66.64% -0.02%
Complexity 1423 1423
============================================
Files 3443 3443
Lines 218632 218663 +31
Branches 34793 34801 +8
============================================
- Hits 145726 145722 -4
- Misses 61192 61228 +36
+ Partials 11714 11713 -1
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:
|
25e3d0c to
8f7a1d6
Compare
4969a9a to
b501df6
Compare
ba8e60d to
050d032
Compare
80120dc to
ce1d3d0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated no new comments.
Suppressed comments (5)
pinot-spi/src/main/java/org/apache/pinot/spi/utils/ArrayCopyUtils.java:1
- These are public utility methods but the new comments use
///and Markdown-style references (e.g.[#copyFromUuid(...)]) which won’t be picked up by Javadoc tooling and won’t resolve as links. Prefer standard Javadoc (/** ... */) and{@link ArrayCopyUtils#copyFromUuid(...)}so IDEs and generated docs correctly hyperlink and surface the documentation.
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CaseTransformFunction.java:854 - For UUID-typed CASE with a bare STRING literal branch, this allocates a fresh
byte[][]for every block evaluation. That can add noticeable GC pressure in tight loops. Consider reusing an existing per-instance buffer (e.g., initialize_bytesValuesSVand fill it) or caching the parsed UUID bytes inside theLiteralTransformFunction/CASE branch metadata so repeated calls don’t allocate a new array each time.
private byte[][] getBytesValues(TransformFunction transformFunction, ValueBlock valueBlock) {
if (_resultMetadata.getDataType() != DataType.UUID || !(transformFunction instanceof LiteralTransformFunction)) {
return transformFunction.transformToBytesValuesSV(valueBlock);
}
LiteralTransformFunction literalTransformFunction = (LiteralTransformFunction) transformFunction;
if (literalTransformFunction.isNull()
|| literalTransformFunction.getResultMetadata().getDataType() != DataType.STRING) {
return transformFunction.transformToBytesValuesSV(valueBlock);
}
int numDocs = valueBlock.getNumDocs();
byte[][] bytesValues = new byte[numDocs][];
byte[] uuidBytes = UuidUtils.toBytes(literalTransformFunction.getStringLiteral());
Arrays.fill(bytesValues, uuidBytes);
return bytesValues;
}
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CastTransformFunction.java:107
- These are user-facing query validation failures, but
Preconditions.checkState(...)throwsIllegalStateException, which can surface as an internal error depending on where it’s caught/wrapped. For query-type errors, prefer throwingBadQueryRequestException(or the project’s standard query validation exception) to keep error classification consistent and to avoid treating invalid queries as server faults.
case "UUID":
Preconditions.checkState(sourceSV, "Cannot cast from MV to UUID");
_resultMetadata = UUID_SV_NO_DICTIONARY_METADATA;
break;
case "UUID_ARRAY":
Preconditions.checkState(!sourceSV, "Cannot cast from SV to UUID_ARRAY");
_resultMetadata = UUID_MV_NO_DICTIONARY_METADATA;
break;
pinot-common/src/test/java/org/apache/pinot/common/request/context/RequestContextUtilsTest.java:103
- This test mutates the JVM-wide default
Locale, which can cause flaky failures if tests run in parallel (other tests can observe the modified default during this window). To make it robust, consider marking the test/class as single-threaded/non-parallel in TestNG, or restructuring the code under test to accept an explicitLocaleso the test doesn’t need to change global process state.
Locale originalDefault = Locale.getDefault();
Locale.setDefault(Locale.forLanguageTag("tr-TR"));
try {
FilterContext filter = compileFilter("uuidCol = CAST('" + UUID_1 + "' AS uuid)");
assertEquals(filter.getType(), FilterContext.Type.PREDICATE);
EqPredicate predicate = (EqPredicate) filter.getPredicate();
assertEquals(predicate.getValue(), UUID_1_STORED);
} finally {
Locale.setDefault(originalDefault);
}
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CaseTransformFunction.java:223
- The newly added UUID literal validation drops the underlying exception cause, which can make debugging malformed literals harder (e.g., distinguishing parse failures vs length issues). Consider chaining the caught exception as the cause (e.g.,
new IllegalArgumentException(..., e)) so logs/error handlers retain the original failure details.
case UUID:
try {
UuidUtils.toBytes(literal);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid literal: " + literal + " for UUID");
}
break;
61adb41 to
e3e70bd
Compare
Adds UUID handling to the predicate evaluators, so =, !=, IN, NOT IN and range predicates work against a UUID column on both the raw and the dictionary path. UUID follows the pattern TIMESTAMP already uses: a logical type whose stored type does the work. The literal is parsed to its 16-byte stored form once, when the evaluator is built, and from there the existing BYTES evaluators apply -- no per-value conversion in the scan loop. The dictionary path needs no UUID branch: Dictionary#getStoredValue returns hex for a UUID column and indexOf(String) hex-decodes, so the existing String-keyed lookup is already correct. PredicateUtils renders the literal to that hex form for those String-typed lookup APIs. Split into apache#19181 (CAST), apache#19182 (bloom filter pruning) and apache#19183 (transform functions); this PR is now just the predicate evaluators.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (3)
pinot-core/src/test/java/org/apache/pinot/core/operator/filter/predicate/NoDictionaryInPredicateEvaluatorTest.java:410
- The comment claims the raw BYTES/UUID IN evaluators are keyed directly on
byte[]to avoid per-row wrapping, butBytesRawValueBased(In|NotIn)PredicateEvaluator.applySV(byte[])currently doescontains(new ByteArray(value)). Please update this comment to match the current implementation (or switch the evaluator to aSet<byte[]>+ByteArrays.HASH_STRATEGYif allocation-free lookup is desired).
/// The BYTES/UUID raw evaluators key their matching set on the raw `byte[]` so that `applySV` does not
/// wrap every scanned value. That only works if the set compares by *content*: with identity semantics a
/// scanned array would never match a predicate array, and IN would silently return nothing while NOT IN returned
/// everything. Probe with arrays that are equal but deliberately not the same instance.
pinot-core/src/main/java/org/apache/pinot/core/operator/filter/predicate/InPredicateEvaluatorFactory.java:149
- The UUID IN path reuses
BytesRawValueBasedInPredicateEvaluator, which currently allocates anew ByteArray(value)on everyapplySV(byte[])call (see the evaluator implementation later in this file). For raw UUID columns this can create significant allocation/GC pressure during scans. Consider changing the bytes IN evaluator to key the set on rawbyte[]with value semantics (e.g.,ObjectOpenCustomHashSet<byte[]>+ByteArrays.HASH_STRATEGY) soapplySVcan probe with the already-scannedbyte[]without wrapping.
// UUID is a logical type stored as 16 raw bytes, so -- like TIMESTAMP over LONG above -- convert the
// literals to their stored form and reuse the stored-type evaluator.
case UUID: {
pinot-core/src/main/java/org/apache/pinot/core/operator/filter/predicate/NotInPredicateEvaluatorFactory.java:149
- The UUID NOT IN path reuses
BytesRawValueBasedNotInPredicateEvaluator, which currently allocates anew ByteArray(value)on everyapplySV(byte[])call (see the evaluator implementation later in this file). For raw UUID columns this can create significant allocation/GC pressure during scans. Consider changing the bytes NOT IN evaluator to key the set on rawbyte[]with value semantics (e.g.,ObjectOpenCustomHashSet<byte[]>+ByteArrays.HASH_STRATEGY) soapplySVcan probe with the already-scannedbyte[]without wrapping.
// UUID is a logical type stored as 16 raw bytes, so -- like TIMESTAMP over LONG above -- convert the
// literals to their stored form and reuse the stored-type evaluator.
case UUID: {
The UUID branch accepted several input forms via UuidUtils.toBytes(Object), which was inconsistent with every other branch -- they each cast to one type. The input is in fact deterministic. GroupByDataTableReducer runs every column through ColumnDataType#convert immediately before calling isMatch, and convert returns UuidUtils.toUUID(...) for UUID, so the value is always a java.util.UUID. The other reduce path cannot deliver a UUID here at all: getConvertedKey has no UUID case and throws for it. Adds testHavingFilterOnUuidColumn, which pins the contract -- this line was previously untested in either direction. Verified it reaches the UUID branch rather than passing vacuously.
|
Documentation follow-up: pinot-contrib/pinot-docs#970 (merged). |
Rebased onto master now that the three split-out PRs have merged: #19181 (CAST), #19182 (bloom filter) and #19183 (transform functions). This PR is the remaining piece — the predicate evaluators — at 12 files, +443, with no overlap against what landed (master has no
case UUIDin any of these factories).What
UUID handling in the predicate evaluators, so
=,!=,IN,NOT INand range predicates work against a UUID column on both the raw and the dictionary path.Approach
UUID follows the pattern
TIMESTAMPalready uses: a logical type whose stored type does the work. The literal is parsed to its 16-byte stored form once, when the evaluator is built, and from there the existing BYTES evaluators apply unchanged. There is no per-value conversion in the scan loop.Why the dictionary path needs no UUID branch
Dictionary#getStoredValuereturns hex for a UUID column, andindexOf(String)hex-decodes — so the existing String-keyed lookup is already correct, and an added UUID branch would be dead code.PredicateUtilsrenders the literal into that hex form, which is what those String-typed lookup APIs consume.This is the same principle #19182 settled for bloom filters: at a String-typed API boundary, a UUID is carried as its stored BYTES rendering (lowercase hex), not as the canonical dashed form. Storage itself remains the raw 16 bytes in both cases.
Testing
UuidDictionaryPredicateEvaluatorTestcovers the dictionary path across all five predicate kinds. The threeNoDictionary*PredicateEvaluatorTestclasses cover the raw path. Between them: canonical, dashless and mixed-case input forms, and rejection of malformed literals.About this PR
Part of the #18140 UUID split. Depends on
UuidUtils(#18869) and the UUID stored type (#18870), both on master.