Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9817fef
[UUID 5/8] UUID aggregation, group-by and distinct
xiangfu0 Jul 7, 2026
479da55
Return the converted UUID form for group keys in the DataTable reduce…
xiangfu0 Aug 8, 2026
03e23aa
Cast directly to UuidKey in UuidToIdMap
xiangfu0 Aug 8, 2026
f897f21
Hash UUID's stored bytes in distinct-count aggregations, not a canoni…
xiangfu0 Aug 9, 2026
dbe467f
Drop UUID special-casing that the stored type already covers
xiangfu0 Aug 9, 2026
178cd8f
Format DISTINCT rows in one pass in BytesDistinctTable
xiangfu0 Aug 9, 2026
321783f
Route UUID through the normal dispatch in DistinctCountBitmap
xiangfu0 Aug 13, 2026
ed1b323
Route UUID through the normal dispatch in DistinctCountHLL
xiangfu0 Aug 13, 2026
b5245cd
Route UUID through the normal dispatch in DistinctCountHLLPlus and ULL
xiangfu0 Aug 13, 2026
4656d27
Read UUID as bytes in DistinctCountThetaSketch
xiangfu0 Aug 14, 2026
9a75654
Leave extractValues untouched in DistinctCountThetaSketch
xiangfu0 Aug 14, 2026
a127558
Route UUID through the normal dispatch in DistinctCountCPCSketch
xiangfu0 Aug 14, 2026
d6c97ba
Fix UUID aggregation dispatch for CPC and Theta sketches
xiangfu0 Aug 14, 2026
8c203fe
Address UUID aggregation review feedback
xiangfu0 Aug 14, 2026
49e33d5
Clarify serialized aggregation BYTES handling
xiangfu0 Aug 14, 2026
e73a5d1
Restore legacy serialized BYTES dispatch
xiangfu0 Aug 14, 2026
8733977
Simplify UUID aggregation coverage
xiangfu0 Aug 15, 2026
e50c47b
[UUID 6/8] UUID multi-stage engine (planner + runtime)
xiangfu0 Jul 7, 2026
ccdf3e1
[UUID 7/8] UUID partitioning
xiangfu0 Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public class ArrayLengthScalarFunction implements PinotScalarFunction {
TYPE_FUNCTION_INFO_MAP.put(DataSchema.ColumnDataType.BYTES_ARRAY,
new FunctionInfo(ArrayLengthScalarFunction.class.getMethod("arrayLength", byte[][].class),
ArrayLengthScalarFunction.class, false));
TYPE_FUNCTION_INFO_MAP.put(DataSchema.ColumnDataType.UUID_ARRAY,
new FunctionInfo(ArrayLengthScalarFunction.class.getMethod("arrayLength", byte[][].class),
ArrayLengthScalarFunction.class, false));
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ public class ArraysOverlapScalarFunction implements PinotScalarFunction {
TYPE_FUNCTION_INFO_MAP.put(DataSchema.ColumnDataType.BYTES_ARRAY,
new FunctionInfo(ArraysOverlapScalarFunction.class.getMethod("arraysOverlap", byte[][].class, byte[][].class),
ArraysOverlapScalarFunction.class, false));
TYPE_FUNCTION_INFO_MAP.put(DataSchema.ColumnDataType.UUID_ARRAY,
new FunctionInfo(ArraysOverlapScalarFunction.class.getMethod("arraysOverlap", byte[][].class, byte[][].class),
ArraysOverlapScalarFunction.class, false));
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.pinot.common.partition.function;

import com.google.common.base.Preconditions;
import java.util.Map;
import javax.annotation.Nullable;
import org.apache.pinot.segment.spi.partition.PartitionFunction;
import org.apache.pinot.segment.spi.partition.PartitionIdNormalizer;
import org.apache.pinot.spi.utils.UuidUtils;
import org.apache.pinot.spi.utils.hash.MurmurHashFunctions;


/// Partition function for Pinot's logical UUID type. Parses the canonical RFC 4122 UUID string into its
/// 16-byte binary form, hashes those bytes via Murmur2, and runs the configured [PartitionIdNormalizer]
/// (default [PartitionIdNormalizer#MASK]) to derive the partition id.
///
/// This matches what an external producer that hashes the 16-byte UUID via Murmur2 would compute (the
/// most common convention for UUID-keyed messages in Kafka/Pulsar/Kinesis).
///
/// Hashing the binary form avoids two pitfalls of hashing the canonical UUID string directly: the dashes
/// do not contribute entropy, and producers that emit raw UUID bytes on the wire would otherwise need a
/// Pinot-only canonical-format step in their partitioning code.
public class UuidPartitionFunction implements PartitionFunction {
private static final String NAME = "Uuid";
private static final PartitionIdNormalizer DEFAULT_NORMALIZER = PartitionIdNormalizer.MASK;
private final int _numPartitions;
private final PartitionIdNormalizer _normalizer;

public UuidPartitionFunction(int numPartitions, @Nullable Map<String, String> functionConfig) {
Preconditions.checkArgument(numPartitions > 0, "Number of partitions must be > 0, was: %s", numPartitions);
_numPartitions = numPartitions;
_normalizer = PartitionFunctionConfigs.normalizer(functionConfig, DEFAULT_NORMALIZER);
}

@Override
public int getPartition(String value) {
byte[] uuidBytes = UuidUtils.toBytes(value);
return _normalizer.getPartitionId(MurmurHashFunctions.murmurHash2(uuidBytes), _numPartitions);
}

@Override
public String getName() {
return NAME;
}

@Override
public int getNumPartitions() {
return _numPartitions;
}

@Override
public PartitionIdNormalizer getPartitionIdNormalizer() {
return _normalizer;
}

// Keep it for backward-compatibility, use getName() instead
@Override
public String toString() {
return NAME;
}
}
4 changes: 4 additions & 0 deletions pinot-common/src/main/proto/expressions.proto
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ enum ColumnDataType {
UNKNOWN = 19;
MAP = 20;
BIG_DECIMAL_ARRAY = 21;
// Rolling-upgrade limitation for UUID columns: in a mixed-version multi-stage query, an older broker/server that
// does not know UUID = 22 / UUID_ARRAY = 23 will fail planning with UnknownEnumValueException when receiving a plan
// that includes a UUID literal. Avoid issuing UUID queries until all brokers and servers are upgraded. See the
// matching note on DataSchema.toBytes and ProtoExpressionToRexExpression#convertColumnDataType.
UUID = 22;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces new UUID / UUID_ARRAY proto enum values with no compatibility path for older MSQ peers. Older brokers/servers decode them as UNRECOGNIZED and throw in convertColumnDataType(...), so a UUID literal can break mixed-version planning before execution even starts. Please encode UUID literals using an existing wire type until the cluster is homogeneous, or add version-gated dual-read/dual-write behavior with mixed-version tests.

UUID_ARRAY = 23;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.apache.pinot.segment.spi.partition.PartitionFunctionFactory;
import org.apache.pinot.spi.utils.BytesUtils;
import org.apache.pinot.spi.utils.JsonUtils;
import org.apache.pinot.spi.utils.UuidUtils;
import org.apache.pinot.spi.utils.hash.FnvHashFunctions;
import org.apache.pinot.spi.utils.hash.MurmurHashFunctions;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -755,6 +756,59 @@ public void testByteArrayPartitionFunctionEquivalence() {
testPartitionFunction(byteArrayPartitionFunction, expectedPartitions);
}

/// Unit test for [UuidPartitionFunction].
///
/// - Verifies factory registration.
/// - Verifies partition values are deterministic and in [0, numPartitions).
/// - Verifies the function hashes the 16-byte UUID form (canonical-string format does not affect the hash).
/// - Verifies an invalid UUID string throws.
@Test
public void testUuidPartitioner() {
int numPartitions = 64;

// Factory registration (case-insensitive name lookup) should produce a UuidPartitionFunction.
PartitionFunction viaFactory = PartitionFunctionFactory.getPartitionFunction("uUiD", numPartitions, null);
assertEquals(viaFactory.getName(), "Uuid");
assertEquals(viaFactory.getNumPartitions(), numPartitions);
assertTrue(viaFactory instanceof UuidPartitionFunction);

UuidPartitionFunction direct = new UuidPartitionFunction(numPartitions, null);
testBasicProperties(direct, "Uuid", numPartitions);

// Determinism + range.
String[] uuids = new String[]{
"00000000-0000-0000-0000-000000000000",
"ffffffff-ffff-ffff-ffff-ffffffffffff",
"12345678-1234-1234-1234-1234567890ab",
"9c5e1f24-0b8e-4c9d-87f1-0aa64a3b9d12",
"550e8400-e29b-41d4-a716-446655440000"
};
for (String uuid : uuids) {
int partition = direct.getPartition(uuid);
assertTrue(partition >= 0 && partition < numPartitions, "partition " + partition + " out of range");
assertEquals(direct.getPartition(uuid), partition, "non-deterministic partition for " + uuid);

// Hash matches the documented contract: murmurHash2 of the 16-byte canonical form, masked, modulo numPartitions.
byte[] uuidBytes = UuidUtils.toBytes(uuid);
int expected = (MurmurHashFunctions.murmurHash2(uuidBytes) & Integer.MAX_VALUE) % numPartitions;
assertEquals(partition, expected);
}

// Different UUIDs should not all collapse to the same partition (basic spread sanity check).
int firstPartition = direct.getPartition(uuids[0]);
boolean spread = false;
for (int i = 1; i < uuids.length; i++) {
if (direct.getPartition(uuids[i]) != firstPartition) {
spread = true;
break;
}
}
assertTrue(spread, "UuidPartitionFunction collapsed all sample UUIDs to the same partition");

// Invalid UUID must surface as an exception (not a silent zero partition).
expectThrows(IllegalArgumentException.class, () -> direct.getPartition("not-a-uuid"));
}

private void testPartitionInExpectedRange(PartitionFunction partitionFunction, Object value, int numPartitions) {
int partition = partitionFunction.getPartition(value.toString());
assertTrue(partition >= 0 && partition < numPartitions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import org.apache.pinot.segment.spi.AggregationFunctionType;
import org.apache.pinot.segment.spi.SegmentContext;
import org.apache.pinot.segment.spi.datasource.DataSource;
import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
import org.apache.pinot.segment.spi.index.reader.Dictionary;
import org.apache.pinot.segment.spi.index.startree.AggregationFunctionColumnPair;
import org.apache.pinot.spi.data.FieldSpec;
Expand Down Expand Up @@ -610,23 +611,23 @@ public static Object getAggregationResult(AggregationFunction aggregationFunctio
break;
case DISTINCTCOUNTHLL:
case DISTINCTCOUNTHLLMV:
result = getDistinctCountHLLResult(Objects.requireNonNull(dataSource.getDictionary()),
result = getDistinctCountHLLResult(dataSource,
(DistinctCountHLLAggregationFunction) aggregationFunction, explainPlanName);
break;
case DISTINCTCOUNTRAWHLL:
case DISTINCTCOUNTRAWHLLMV:
result = getDistinctCountHLLResult(Objects.requireNonNull(dataSource.getDictionary()),
result = getDistinctCountHLLResult(dataSource,
((DistinctCountRawHLLAggregationFunction) aggregationFunction).getDistinctCountHLLAggregationFunction(),
explainPlanName);
break;
case DISTINCTCOUNTHLLPLUS:
case DISTINCTCOUNTHLLPLUSMV:
result = getDistinctCountHLLPlusResult(Objects.requireNonNull(dataSource.getDictionary()),
result = getDistinctCountHLLPlusResult(dataSource,
(DistinctCountHLLPlusAggregationFunction) aggregationFunction, explainPlanName);
break;
case DISTINCTCOUNTRAWHLLPLUS:
case DISTINCTCOUNTRAWHLLPLUSMV:
result = getDistinctCountHLLPlusResult(Objects.requireNonNull(dataSource.getDictionary()),
result = getDistinctCountHLLPlusResult(dataSource,
((DistinctCountRawHLLPlusAggregationFunction) aggregationFunction)
.getDistinctCountHLLPlusAggregationFunction(), explainPlanName);
break;
Expand All @@ -642,15 +643,15 @@ public static Object getAggregationResult(AggregationFunction aggregationFunctio
(DistinctCountSmartHLLPlusAggregationFunction) aggregationFunction, explainPlanName);
break;
case DISTINCTCOUNTULL:
result = getDistinctCountULLResult(Objects.requireNonNull(dataSource.getDictionary()),
result = getDistinctCountULLResult(dataSource,
(DistinctCountULLAggregationFunction) aggregationFunction, explainPlanName);
break;
case DISTINCTCOUNTSMARTULL:
result = getDistinctCountSmartULLResult(Objects.requireNonNull(dataSource.getDictionary()),
(DistinctCountSmartULLAggregationFunction) aggregationFunction, explainPlanName);
break;
case DISTINCTCOUNTRAWULL:
result = getDistinctCountULLResult(Objects.requireNonNull(dataSource.getDictionary()),
result = getDistinctCountULLResult(dataSource,
(DistinctCountULLAggregationFunction) aggregationFunction, explainPlanName);
break;
default:
Expand Down Expand Up @@ -799,10 +800,12 @@ private static HyperLogLogPlus getDistinctValueHLLPlus(Dictionary dictionary, in
return hllPlus;
}

private static HyperLogLog getDistinctCountHLLResult(Dictionary dictionary,
private static HyperLogLog getDistinctCountHLLResult(DataSource dataSource,
DistinctCountHLLAggregationFunction function, String explainPlanName) {
if (dictionary.getValueType() == FieldSpec.DataType.BYTES) {
// Treat BYTES value as serialized HyperLogLog
Dictionary dictionary = Objects.requireNonNull(dataSource.getDictionary());
DataSourceMetadata metadata = dataSource.getDataSourceMetadata();
if (metadata.getDataType() == FieldSpec.DataType.BYTES) {
// Logical BYTES dictionary entries are serialized HyperLogLog objects.
try {
QueryThreadContext.checkTerminationAndSampleUsage(explainPlanName);
HyperLogLog hll = ObjectSerDeUtils.HYPER_LOG_LOG_SER_DE.deserialize(dictionary.getBytesValue(0));
Expand All @@ -820,10 +823,12 @@ private static HyperLogLog getDistinctCountHLLResult(Dictionary dictionary,
}
}

private static HyperLogLogPlus getDistinctCountHLLPlusResult(Dictionary dictionary,
private static HyperLogLogPlus getDistinctCountHLLPlusResult(DataSource dataSource,
DistinctCountHLLPlusAggregationFunction function, String explainPlanName) {
if (dictionary.getValueType() == FieldSpec.DataType.BYTES) {
// Treat BYTES value as serialized HyperLogLogPlus
Dictionary dictionary = Objects.requireNonNull(dataSource.getDictionary());
DataSourceMetadata metadata = dataSource.getDataSourceMetadata();
if (metadata.getDataType() == FieldSpec.DataType.BYTES) {
// Logical BYTES dictionary entries are serialized HyperLogLogPlus objects.
try {
QueryThreadContext.checkTerminationAndSampleUsage(explainPlanName);
HyperLogLogPlus hllplus = ObjectSerDeUtils.HYPER_LOG_LOG_PLUS_SER_DE.deserialize(dictionary.getBytesValue(0));
Expand Down Expand Up @@ -861,10 +866,12 @@ private static Object getDistinctCountSmartHLLPlusResult(Dictionary dictionary,
}
}

private static UltraLogLog getDistinctCountULLResult(Dictionary dictionary,
private static UltraLogLog getDistinctCountULLResult(DataSource dataSource,
DistinctCountULLAggregationFunction function, String explainPlanName) {
if (dictionary.getValueType() == FieldSpec.DataType.BYTES) {
// Treat BYTES value as serialized UltraLogLog and merge
Dictionary dictionary = Objects.requireNonNull(dataSource.getDictionary());
DataSourceMetadata metadata = dataSource.getDataSourceMetadata();
if (metadata.getDataType() == FieldSpec.DataType.BYTES) {
// Logical BYTES dictionary entries are serialized UltraLogLog objects.
try {
QueryThreadContext.checkTerminationAndSampleUsage(explainPlanName);
UltraLogLog ull = ObjectSerDeUtils.ULTRA_LOG_LOG_OBJECT_SER_DE.deserialize(dictionary.getBytesValue(0));
Expand Down
Loading
Loading