This is an automated email from the ASF dual-hosted git repository.
xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new dded09d82a8 [UUID 2/8] UUID ingest and segment storage (#18870)
dded09d82a8 is described below
commit dded09d82a84ae5e637c3e7c42e0240aacb4e23b
Author: Xiang Fu <[email protected]>
AuthorDate: Thu Jul 23 19:58:48 2026 -0700
[UUID 2/8] UUID ingest and segment storage (#18870)
Part 2/8 of splitting apache/pinot#18140 (logical UUID type). Rebased onto
master, which includes the merged #18869 type foundation.
Downstream references use the UuidKey class merged in #18869.
---
.../connector/flink/sink/FlinkSegmentWriter.java | 3 +-
.../pinot/core/util/SegmentProcessorAvroUtils.java | 111 +++++++++++++---
.../core/util/SegmentProcessorAvroUtilsTest.java | 147 +++++++++++++++++++++
.../avro/AvroIngestionSchemaValidator.java | 2 +-
.../plugin/inputformat/avro/AvroSchemaUtil.java | 32 ++++-
.../pinot/plugin/inputformat/avro/AvroUtils.java | 29 ++--
.../inputformat/avro/AvroSchemaUtilTest.java | 45 ++++++-
.../plugin/inputformat/avro/AvroUtilsTest.java | 55 ++++++++
.../filebased/FileBasedSegmentWriter.java | 3 +-
pinot-segment-local/pom.xml | 5 +
.../indexsegment/mutable/MutableSegmentImpl.java | 67 ++++++----
.../stats/MutableNoDictColumnStatistics.java | 4 +-
.../dictionary/BytesOffHeapMutableDictionary.java | 9 +-
.../dictionary/BytesOnHeapMutableDictionary.java | 9 +-
.../forward/FixedByteMVMutableForwardIndex.java | 60 +++++++++
.../recordtransformer/DataTypeTransformer.java | 78 ++++++++++-
.../stats/BytesColumnPreIndexStatsCollector.java | 7 +-
.../stats/NoDictColumnStatisticsCollector.java | 6 +-
.../DefaultNullValueVirtualColumnProvider.java | 1 +
.../segment/index/forward/ForwardIndexType.java | 9 +-
.../ColumnMinMaxValueGenerator.java | 2 +
.../segment/readers/PinotSegmentColumnReader.java | 2 +
.../pinot/segment/local/utils/HashUtils.java | 23 +++-
.../impl/dictionary/MultiValueDictionaryTest.java | 53 ++++++++
.../recordtransformer/DataTypeTransformerTest.java | 112 ++++++++++++++++
.../DefaultNullValueVirtualColumnProviderTest.java | 10 ++
.../index/creator/BloomFilterCreatorTest.java | 28 ++++
.../ColumnMinMaxValueGeneratorTest.java | 43 ++++--
.../segment/local/segment/readers/LazyRowTest.java | 12 ++
...ertMetadataManagerForConsistentDeletesTest.java | 14 ++
...rrentMapPartitionUpsertMetadataManagerTest.java | 17 +++
.../pinot/segment/local/utils/HashUtilsTest.java | 42 ++++++
.../spi/index/creator/BloomFilterCreator.java | 16 +++
.../converter/PinotSegmentToAvroConverter.java | 3 +-
.../converter/PinotSegmentToParquetConverter.java | 1 +
.../converter/PinotSegmentConverterTest.java | 57 ++++++++
36 files changed, 1026 insertions(+), 91 deletions(-)
diff --git
a/pinot-connectors/pinot-flink-connector/src/main/java/org/apache/pinot/connector/flink/sink/FlinkSegmentWriter.java
b/pinot-connectors/pinot-flink-connector/src/main/java/org/apache/pinot/connector/flink/sink/FlinkSegmentWriter.java
index ef2b684dc03..e7ddce376bd 100644
---
a/pinot-connectors/pinot-flink-connector/src/main/java/org/apache/pinot/connector/flink/sink/FlinkSegmentWriter.java
+++
b/pinot-connectors/pinot-flink-connector/src/main/java/org/apache/pinot/connector/flink/sink/FlinkSegmentWriter.java
@@ -191,7 +191,8 @@ public class FlinkSegmentWriter implements SegmentWriter {
throws IOException {
FileUtils.deleteQuietly(_bufferFile);
_rowCount = 0;
- _recordWriter = new DataFileWriter<>(new
GenericDatumWriter<>(_avroSchema));
+ _recordWriter =
+ new DataFileWriter<>(new GenericDatumWriter<>(_avroSchema,
SegmentProcessorAvroUtils.getAvroDataModel()));
_recordWriter.create(_avroSchema, _bufferFile);
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/util/SegmentProcessorAvroUtils.java
b/pinot-core/src/main/java/org/apache/pinot/core/util/SegmentProcessorAvroUtils.java
index 0a47f5f2029..0c7f5b5e2a7 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/util/SegmentProcessorAvroUtils.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/util/SegmentProcessorAvroUtils.java
@@ -24,56 +24,116 @@ import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
+import org.apache.avro.Conversion;
+import org.apache.avro.LogicalType;
+import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema;
import org.apache.avro.SchemaBuilder;
import org.apache.avro.generic.GenericData;
-import
org.apache.pinot.core.segment.processing.framework.SegmentProcessorFramework;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.data.readers.GenericRow;
-import org.apache.pinot.spi.ingestion.segment.writer.SegmentWriter;
+import org.apache.pinot.spi.utils.UuidUtils;
-/**
- * Helper methods for avro related conversions needed, when using AVRO as
intermediate format in segment processing.
- * AVRO is used as intermediate processing format in {@link
SegmentProcessorFramework} and file-based impl of
- * {@link SegmentWriter}
- */
+/// Helper methods for Avro conversions used when serializing Pinot
[GenericRow]s to Avro — by the file-based
+/// `SegmentWriter` implementations and the segment-to-Avro/Parquet
converters. (The multi-stage
+/// `SegmentProcessorFramework` itself serializes intermediate records with a
custom binary `GenericRowFile`
+/// format, not Avro.)
public final class SegmentProcessorAvroUtils {
private SegmentProcessorAvroUtils() {
}
- /**
- * Convert a GenericRow to an avro GenericRecord
- */
+ /// Convert a GenericRow to an avro GenericRecord
public static GenericData.Record convertGenericRowToAvroRecord(GenericRow
genericRow,
GenericData.Record reusableRecord) {
return convertGenericRowToAvroRecord(genericRow, reusableRecord,
genericRow.getFieldToValueMap().keySet());
}
- /**
- * Convert a GenericRow to an avro GenericRecord
- */
+ /// Convert a GenericRow to an avro GenericRecord
public static GenericData.Record convertGenericRowToAvroRecord(GenericRow
genericRow,
GenericData.Record reusableRecord, Set<String> fields) {
+ Schema avroSchema = reusableRecord.getSchema();
for (String field : fields) {
Object value = genericRow.getValue(field);
if (value instanceof Object[]) {
+ // Array elements are written as-is. For MV UUID
(array<string{logicalType:uuid}>) the elements are the raw
+ // 16-byte values; the uuid Conversion registered on the writer's data
model (getAvroDataModel) renders each
+ // element to its canonical string at write time.
reusableRecord.put(field, Arrays.asList((Object[]) value));
- } else {
- if (value instanceof byte[]) {
- value = ByteBuffer.wrap((byte[]) value);
+ } else if (value instanceof byte[]) {
+ // A byte[] bound for a plain BYTES field must be wrapped as
ByteBuffer (GenericDatumWriter requires it for the
+ // bytes type). A byte[] bound for a UUID field
(string{logicalType:uuid}) is left raw so the uuid Conversion
+ // registered on the writer's data model (getAvroDataModel) renders it
to a canonical string at write time.
+ Schema.Field avroField = avroSchema.getField(field);
+ if (avroField != null && avroField.schema().getType() ==
Schema.Type.BYTES) {
+ reusableRecord.put(field, ByteBuffer.wrap((byte[]) value));
+ } else {
+ reusableRecord.put(field, value);
}
+ } else {
reusableRecord.put(field, value);
}
}
return reusableRecord;
}
- /**
- * Converts a Pinot schema to an Avro schema
- */
+ /// Shared Avro data model with [UuidConversion] registered. Populated once
at class initialization and never
+ /// mutated afterward (effectively immutable), so it is safe to share across
writers.
+ private static final GenericData AVRO_DATA_MODEL = createAvroDataModel();
+
+ /// Returns the shared Avro data model that a `GenericDatumWriter` (or
`AvroParquetWriter`) must be constructed with
+ /// to serialize UUID columns produced by [#convertGenericRowToAvroRecord]:
it registers [UuidConversion] so the
+ /// internal 16-byte UUID form is rendered as the canonical string required
by `string{logicalType:uuid}` fields.
+ /// The UUID column's field schema must be `string{logicalType:uuid}` — as
emitted by
+ /// [#convertPinotSchemaToAvroSchema] and
`AvroUtils.getAvroSchemaFromPinotSchema` — for the conversion to apply.
+ ///
+ /// The returned instance is shared and must be treated as read-only: do not
call its mutators
+ /// (`addLogicalTypeConversion`, `setStringType`, ...), which are not
thread-safe against concurrent writer reads.
+ public static GenericData getAvroDataModel() {
+ return AVRO_DATA_MODEL;
+ }
+
+ private static GenericData createAvroDataModel() {
+ GenericData model = new GenericData();
+ model.addLogicalTypeConversion(new UuidConversion());
+ return model;
+ }
+
+ /// Avro logical-type [Conversion] for the `uuid` logical type, operating
directly on Pinot's internal storage form
+ /// (a 16-byte big-endian `byte[]`). It renders that value to its canonical
RFC-4122 string when writing a
+ /// `string{logicalType:uuid}` field and parses it back on read.
+ ///
+ /// The converted type is `byte[]` rather than [java.util.UUID] so no
intermediate object is allocated, and rather
+ /// than [java.nio.ByteBuffer] because Avro resolves conversions by exact
datum class: a `byte[]` instance always
+ /// reports `byte[].class`, so the conversion resolves for both single
values and array elements, whereas a
+ /// concrete `HeapByteBuffer` would not match a `ByteBuffer`-typed
conversion.
+ private static final class UuidConversion extends Conversion<byte[]> {
+ private static final String UUID_LOGICAL_TYPE_NAME = "uuid";
+
+ @Override
+ public Class<byte[]> getConvertedType() {
+ return byte[].class;
+ }
+
+ @Override
+ public String getLogicalTypeName() {
+ return UUID_LOGICAL_TYPE_NAME;
+ }
+
+ @Override
+ public CharSequence toCharSequence(byte[] value, Schema schema,
LogicalType type) {
+ return UuidUtils.toString(value);
+ }
+
+ @Override
+ public byte[] fromCharSequence(CharSequence value, Schema schema,
LogicalType type) {
+ return UuidUtils.toBytes(value.toString());
+ }
+ }
+
+ /// Converts a Pinot schema to an Avro schema
public static Schema
convertPinotSchemaToAvroSchema(org.apache.pinot.spi.data.Schema pinotSchema) {
SchemaBuilder.FieldAssembler<org.apache.avro.Schema> fieldAssembler =
SchemaBuilder.record("record").fields();
@@ -82,6 +142,19 @@ public final class SegmentProcessorAvroUtils {
.collect(Collectors.toList());
for (FieldSpec fieldSpec : orderedFieldSpecs) {
String name = fieldSpec.getName();
+ // Emit UUID columns as Avro string{logicalType:uuid} (matching
AvroUtils.getAvroSchemaFromPinotSchema)
+ // so the runtime byte[] → canonical-string conversion in
convertGenericRowToAvroRecord lines up with
+ // the field schema. Without this branch SV UUID would fall through to
BYTES (losing UUID semantics) and
+ // MV UUID would throw at this point (MV switch below has no BYTES case).
+ if (fieldSpec.getDataType() == DataType.UUID) {
+ Schema uuidSchema =
LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING));
+ if (fieldSpec.isSingleValueField()) {
+ fieldAssembler =
fieldAssembler.name(name).type(uuidSchema).noDefault();
+ } else {
+ fieldAssembler =
fieldAssembler.name(name).type().array().items(uuidSchema).noDefault();
+ }
+ continue;
+ }
DataType storedType = fieldSpec.getDataType().getStoredType();
if (fieldSpec.isSingleValueField()) {
switch (storedType) {
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/util/SegmentProcessorAvroUtilsTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/util/SegmentProcessorAvroUtilsTest.java
new file mode 100644
index 00000000000..2f3012a2c66
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/util/SegmentProcessorAvroUtilsTest.java
@@ -0,0 +1,147 @@
+/**
+ * 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.core.util;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.avro.LogicalTypes;
+import org.apache.avro.Schema;
+import org.apache.avro.SchemaBuilder;
+import org.apache.avro.file.DataFileReader;
+import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericDatumReader;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.commons.io.FileUtils;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.utils.UuidUtils;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+public class SegmentProcessorAvroUtilsTest {
+
+ /// convertGenericRowToAvroRecord keeps a UUID value as its raw 16-byte form
(it does NOT render a string here); the
+ /// rendering is deferred to the uuid Conversion on the writer's data model.
Plain BYTES columns are still wrapped as
+ /// ByteBuffer.
+ @Test
+ public void testConvertGenericRowToAvroRecordKeepsUuidBytesRaw() {
+ Schema uuidSchema =
LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING));
+ Schema recordSchema = SchemaBuilder.record("record").fields()
+ .name("uuidCol").type(uuidSchema).noDefault()
+ .name("bytesCol").type().bytesType().noDefault()
+ .endRecord();
+
+ byte[] uuidBytes =
UuidUtils.toBytes("12345678-1234-1234-1234-1234567890ab");
+ byte[] rawBytes = {1, 2, 3, 4};
+ GenericRow row = new GenericRow();
+ row.putValue("uuidCol", uuidBytes);
+ row.putValue("bytesCol", rawBytes);
+
+ GenericData.Record record = new GenericData.Record(recordSchema);
+ SegmentProcessorAvroUtils.convertGenericRowToAvroRecord(row, record);
+
+ assertTrue(record.get("uuidCol") instanceof byte[], "UUID must be left as
raw byte[] for the uuid Conversion");
+ assertEquals((byte[]) record.get("uuidCol"), uuidBytes);
+ assertEquals(record.get("bytesCol"), ByteBuffer.wrap(rawBytes), "BYTES
byte[] must be wrapped as ByteBuffer");
+ }
+
+ /// End-to-end: a GenericDatumWriter built with getAvroDataModel()
serializes the raw 16-byte UUID values as their
+ /// canonical string (via the registered uuid Conversion) for both SV and
MV, and the on-disk value reads back as
+ /// that string with a vanilla reader. Without the registered Conversion the
write would fail (a ByteBuffer/byte[]
+ /// cannot be written to a string{logicalType:uuid} field).
+ @Test
+ public void testUuidRoundTripThroughAvroDataModel()
+ throws Exception {
+ org.apache.pinot.spi.data.Schema pinotSchema = new
org.apache.pinot.spi.data.Schema.SchemaBuilder()
+ .setSchemaName("uuidSchema")
+ .addSingleValueDimension("uuidSv", DataType.UUID)
+ .addMultiValueDimension("uuidMv", DataType.UUID)
+ .build();
+ Schema avroSchema =
SegmentProcessorAvroUtils.convertPinotSchemaToAvroSchema(pinotSchema);
+
+ String svCanonical = "12345678-1234-1234-1234-1234567890ab";
+ String mvCanonical = "550e8400-e29b-41d4-a716-446655440000";
+ GenericRow row = new GenericRow();
+ row.putValue("uuidSv", UuidUtils.toBytes(svCanonical));
+ row.putValue("uuidMv", new Object[]{UuidUtils.toBytes(mvCanonical)});
+ GenericData.Record record =
+ SegmentProcessorAvroUtils.convertGenericRowToAvroRecord(row, new
GenericData.Record(avroSchema));
+
+ File tmp = File.createTempFile("uuidRoundTrip", ".avro");
+ try {
+ GenericData model = SegmentProcessorAvroUtils.getAvroDataModel();
+ try (DataFileWriter<GenericData.Record> writer =
+ new DataFileWriter<>(new GenericDatumWriter<>(avroSchema, model))) {
+ writer.create(avroSchema, tmp);
+ writer.append(record);
+ }
+ // Read back with a vanilla reader: the on-disk representation must be
the canonical UUID string.
+ try (DataFileReader<GenericRecord> reader = new DataFileReader<>(tmp,
new GenericDatumReader<>(avroSchema))) {
+ assertTrue(reader.hasNext());
+ GenericRecord read = reader.next();
+ assertEquals(read.get("uuidSv").toString(), svCanonical, "SV UUID must
serialize as canonical string");
+ List<?> mv = (List<?>) read.get("uuidMv");
+ assertEquals(mv.size(), 1);
+ assertEquals(mv.get(0).toString(), mvCanonical, "MV UUID element must
serialize as canonical string");
+ }
+ // Read back WITH the data model: the uuid Conversion's fromCharSequence
must reconstruct the raw 16-byte value.
+ try (DataFileReader<GenericRecord> reader = new DataFileReader<>(tmp,
+ new GenericDatumReader<>(avroSchema, avroSchema,
SegmentProcessorAvroUtils.getAvroDataModel()))) {
+ GenericRecord read = reader.next();
+ assertEquals((byte[]) read.get("uuidSv"),
UuidUtils.toBytes(svCanonical),
+ "reading with the model must convert the uuid string back to raw
bytes");
+ assertEquals((byte[]) ((List<?>) read.get("uuidMv")).get(0),
UuidUtils.toBytes(mvCanonical),
+ "MV uuid element must convert back to raw bytes when reading with
the model");
+ }
+ } finally {
+ FileUtils.deleteQuietly(tmp);
+ }
+ }
+
+ /// convertPinotSchemaToAvroSchema must emit SV UUID as
string{logicalType:uuid} and MV UUID as
+ /// array<string{logicalType:uuid}>, which is what the uuid Conversion above
pairs with.
+ @Test
+ public void testConvertPinotSchemaToAvroSchemaEmitsUuidLogicalType() {
+ org.apache.pinot.spi.data.Schema pinotSchema = new
org.apache.pinot.spi.data.Schema.SchemaBuilder()
+ .setSchemaName("uuidSchema")
+ .addSingleValueDimension("uuidSv", DataType.UUID)
+ .addMultiValueDimension("uuidMv", DataType.UUID)
+ .build();
+
+ Schema avroSchema =
SegmentProcessorAvroUtils.convertPinotSchemaToAvroSchema(pinotSchema);
+
+ Schema svFieldSchema = avroSchema.getField("uuidSv").schema();
+ assertEquals(svFieldSchema.getType(), Schema.Type.STRING, "SV UUID must be
string{logicalType:uuid}");
+ assertEquals(LogicalTypes.fromSchemaIgnoreInvalid(svFieldSchema),
LogicalTypes.uuid(),
+ "SV UUID must carry the uuid logical type");
+
+ Schema mvFieldSchema = avroSchema.getField("uuidMv").schema();
+ assertEquals(mvFieldSchema.getType(), Schema.Type.ARRAY, "MV UUID must be
emitted as an array");
+ Schema mvElementSchema = mvFieldSchema.getElementType();
+ assertEquals(mvElementSchema.getType(), Schema.Type.STRING, "MV UUID
elements must be string{logicalType:uuid}");
+ assertEquals(LogicalTypes.fromSchemaIgnoreInvalid(mvElementSchema),
LogicalTypes.uuid(),
+ "MV UUID elements must carry the uuid logical type");
+ }
+}
diff --git
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroIngestionSchemaValidator.java
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroIngestionSchemaValidator.java
index 1ec33656460..a90cee65c6a 100644
---
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroIngestionSchemaValidator.java
+++
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroIngestionSchemaValidator.java
@@ -139,7 +139,7 @@ public class AvroIngestionSchemaValidator implements
IngestionSchemaValidator {
if (fieldSpec.getDataType() != dataTypeForSVColumn) {
_dataTypeMismatch.addMismatchReason(String
.format("The Pinot column: (%s: %s) doesn't match with the
column (%s: %s) in input %s schema.",
- columnName, fieldSpec.getDataType().name(), avroColumnName,
avroColumnType.name(),
+ columnName, fieldSpec.getDataType().name(), avroColumnName,
dataTypeForSVColumn.name(),
getInputSchemaType()));
}
} else {
diff --git
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
index 0b1f8ad3ab0..099ccae9170 100644
---
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
+++
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
@@ -20,18 +20,27 @@ package org.apache.pinot.plugin.inputformat.avro;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.avro.LogicalType;
+import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.utils.JsonUtils;
-
+import org.apache.pinot.spi.utils.UuidUtils;
/// Stateless helpers for mapping between Avro schema shapes and Pinot's
[DataType] / Avro JSON schema representations.
public class AvroSchemaUtil {
+
private AvroSchemaUtil() {
}
- /// Returns the data type stored in Pinot that is associated with the given
Avro type.
+ // Avro logical-type name for UUID (see org.apache.avro.LogicalTypes).
Value-level logical-type conversion lives
+ // in AvroRecordExtractor; this class only deals with schema-shape mapping.
+ private static final String UUID = "uuid";
+
+ /// Returns the Pinot data type for a bare Avro type. This does not honor
logical types (e.g. a `string` or `fixed`
+ /// carrying `logicalType:uuid` maps to STRING/BYTES, not UUID); prefer
[#valueOf(Schema)] when a full [Schema] is
+ /// available.
public static DataType valueOf(Schema.Type avroType) {
switch (avroType) {
case INT:
@@ -60,6 +69,25 @@ public class AvroSchemaUtil {
}
}
+ /// Returns the Pinot data type associated with the given Avro schema,
including logical types.
+ ///
+ /// Recognizes the UUID logical type on both STRING-backed schemas (Avro
spec §logical-types.uuid) and FIXED(16)
+ /// schemas (used by some producers including Confluent's fixed-uuid mode).
Both forms arrive at
+ /// [AvroRecordExtractor] as either a [java.util.UUID] (for STRING-backed
logical UUIDs) or a 16-byte `byte[]`
+ /// (for FIXED-backed ones), and both are accepted by [UuidUtils#toBytes].
+ public static DataType valueOf(Schema schema) {
+ LogicalType logicalType = LogicalTypes.fromSchemaIgnoreInvalid(schema);
+ if (logicalType != null && UUID.equals(logicalType.getName())) {
+ if (schema.getType() == Schema.Type.STRING) {
+ return DataType.UUID;
+ }
+ if (schema.getType() == Schema.Type.FIXED && schema.getFixedSize() ==
UuidUtils.UUID_NUM_BYTES) {
+ return DataType.UUID;
+ }
+ }
+ return valueOf(schema.getType());
+ }
+
/// Returns whether the given Avro type is a primitive type.
public static boolean isPrimitiveType(Schema.Type avroType) {
switch (avroType) {
diff --git
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroUtils.java
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroUtils.java
index 8dc688adef6..b01916df360 100644
---
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroUtils.java
+++
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroUtils.java
@@ -27,6 +27,7 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;
import javax.annotation.Nullable;
+import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema.Field;
import org.apache.avro.SchemaBuilder;
import org.apache.avro.file.DataFileStream;
@@ -160,6 +161,16 @@ public class AvroUtils {
SchemaBuilder.FieldAssembler<org.apache.avro.Schema> fieldAssembler =
SchemaBuilder.record("record").fields();
for (FieldSpec fieldSpec : pinotSchema.getAllFieldSpecs()) {
+ if (fieldSpec.getDataType() == DataType.UUID) {
+ org.apache.avro.Schema uuidSchema =
LogicalTypes.uuid().addToSchema(org.apache.avro.Schema.create(
+ org.apache.avro.Schema.Type.STRING));
+ if (fieldSpec.isSingleValueField()) {
+ fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type(uuidSchema).noDefault();
+ } else {
+ fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().array().items(uuidSchema).noDefault();
+ }
+ continue;
+ }
DataType storedType = fieldSpec.getDataType().getStoredType();
if (fieldSpec.isSingleValueField()) {
switch (storedType) {
@@ -244,12 +255,13 @@ public class AvroUtils {
public static DataType extractFieldDataType(Field field) {
try {
org.apache.avro.Schema fieldSchema =
extractSupportedSchema(field.schema());
- org.apache.avro.Schema.Type fieldType = fieldSchema.getType();
- if (fieldType == org.apache.avro.Schema.Type.ARRAY) {
- return
AvroSchemaUtil.valueOf(extractSupportedSchema(fieldSchema.getElementType()).getType());
+ if (fieldSchema.getType() == org.apache.avro.Schema.Type.ARRAY) {
+ return
AvroSchemaUtil.valueOf(extractSupportedSchema(fieldSchema.getElementType()));
} else {
- return AvroSchemaUtil.valueOf(fieldType);
+ return AvroSchemaUtil.valueOf(fieldSchema);
}
+ } catch (RuntimeException e) {
+ throw e;
} catch (Exception e) {
throw new RuntimeException("Caught exception while extracting data type
from field: " + field.name(), e);
}
@@ -323,16 +335,17 @@ public class AvroUtils {
extractSchemaWithComplexTypeHandling(elementType, fieldsToUnnest,
delimiter, path, pinotSchema, fieldTypeMap,
timeUnit, collectionNotUnnestedToJson);
} else if (collectionNotUnnestedToJson ==
ComplexTypeConfig.CollectionNotUnnestedToJson.NON_PRIMITIVE
- && AvroSchemaUtil.isPrimitiveType(elementType.getType())) {
- addFieldToPinotSchema(pinotSchema,
AvroSchemaUtil.valueOf(elementType.getType()), path, false, fieldTypeMap,
- timeUnit);
+ && (AvroSchemaUtil.isPrimitiveType(elementType.getType())
+ || AvroSchemaUtil.valueOf(elementType) == DataType.UUID)) {
+ DataType elementDataType = AvroSchemaUtil.valueOf(elementType);
+ addFieldToPinotSchema(pinotSchema, elementDataType, path, false,
fieldTypeMap, timeUnit);
} else if (shallConvertToJson(collectionNotUnnestedToJson,
elementType)) {
addFieldToPinotSchema(pinotSchema, DataType.STRING, path, true,
fieldTypeMap, timeUnit);
}
// do not include the node for other cases
break;
default:
- DataType dataType = AvroSchemaUtil.valueOf(fieldType);
+ DataType dataType = AvroSchemaUtil.valueOf(fieldSchema);
addFieldToPinotSchema(pinotSchema, dataType, path, true, fieldTypeMap,
timeUnit);
break;
}
diff --git
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
index 09142d201f0..436e3d8c68f 100644
---
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
+++
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
@@ -20,6 +20,7 @@ package org.apache.pinot.plugin.inputformat.avro;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.avro.Schema;
import org.apache.pinot.spi.data.DimensionFieldSpec;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.testng.annotations.Test;
@@ -58,16 +59,48 @@ public class AvroSchemaUtilTest {
() -> AvroSchemaUtil.toAvroSchemaJsonObject(new
DimensionFieldSpec("col", DataType.BIG_DECIMAL, true)));
}
- /// A UUID column is represented faithfully as an Avro string annotated with
logicalType "uuid".
- /// `DataGenerator#buildSpec` always marks the recommender schema FieldSpec
single-value, so this emits a scalar
- /// union like every sibling type.
+ /// UUID is a logical type; a single-value column maps to an Avro string
carrying the "uuid" logical type.
@Test
public void testToAvroSchemaJsonObjectForUuid() {
JsonNode type = typeOf(DataType.UUID);
assertEquals(type.get(0).asText(), "null");
- JsonNode uuidBranch = type.get(1);
- assertEquals(uuidBranch.get("type").asText(), "string");
- assertEquals(uuidBranch.get("logicalType").asText(), "uuid");
+ assertEquals(type.get(1).get("type").asText(), "string");
+ assertEquals(type.get(1).get("logicalType").asText(), "uuid");
+ }
+
+ @Test
+ public void testValueOfUuidStringLogicalType() {
+ Schema schema = new
Schema.Parser().parse("{\"type\":\"record\",\"name\":\"r\",\"fields\":[{\"name\":\"id\","
+ + "\"type\":{\"type\":\"string\",\"logicalType\":\"uuid\"}}]}");
+ assertEquals(AvroSchemaUtil.valueOf(schema.getField("id").schema()),
DataType.UUID,
+ "STRING logicalType:uuid should map to UUID");
+ }
+
+ @Test
+ public void testValueOfUuidFixed16LogicalType() {
+ // FIXED(16) + logicalType:uuid — produced by Confluent fixed-uuid mode
and Parquet uuid
+ Schema schema = new
Schema.Parser().parse("{\"type\":\"record\",\"name\":\"r\",\"fields\":[{\"name\":\"id\","
+ +
"\"type\":{\"type\":\"fixed\",\"name\":\"uuid_fixed\",\"size\":16,\"logicalType\":\"uuid\"}}]}");
+ assertEquals(AvroSchemaUtil.valueOf(schema.getField("id").schema()),
DataType.UUID,
+ "FIXED(16) logicalType:uuid should map to UUID");
+ }
+
+ @Test
+ public void testValueOfFixed16WithoutLogicalTypeIsBytes() {
+ // FIXED(16) without logicalType should stay as BYTES
+ Schema schema = new
Schema.Parser().parse("{\"type\":\"record\",\"name\":\"r\",\"fields\":[{\"name\":\"raw\","
+ + "\"type\":{\"type\":\"fixed\",\"name\":\"raw16\",\"size\":16}}]}");
+ assertEquals(AvroSchemaUtil.valueOf(schema.getField("raw").schema()),
DataType.BYTES,
+ "FIXED(16) without logicalType:uuid should stay BYTES");
+ }
+
+ @Test
+ public void testValueOfFixedWrongSizeWithUuidLogicalTypeIsBytes() {
+ // FIXED of non-16 size with logicalType:uuid should not map to UUID
+ Schema schema = new
Schema.Parser().parse("{\"type\":\"record\",\"name\":\"r\",\"fields\":[{\"name\":\"id\","
+ +
"\"type\":{\"type\":\"fixed\",\"name\":\"uuid32\",\"size\":32,\"logicalType\":\"uuid\"}}]}");
+ assertEquals(AvroSchemaUtil.valueOf(schema.getField("id").schema()),
DataType.BYTES,
+ "FIXED(32) with logicalType:uuid should stay BYTES");
}
private static void assertPrimitiveType(DataType dataType, String
expectedAvroType) {
diff --git
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroUtilsTest.java
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroUtilsTest.java
index 5af642aca6b..704de6ca58a 100644
---
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroUtilsTest.java
+++
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroUtilsTest.java
@@ -22,6 +22,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.TimeUnit;
+import org.apache.avro.LogicalTypes;
import org.apache.pinot.spi.config.table.ingestion.ComplexTypeConfig;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.FieldSpec.DataType;
@@ -131,4 +132,58 @@ public class AvroUtilsTest {
.addDateTime("hoursSinceEpoch", DataType.LONG, "EPOCH|HOURS",
"1:HOURS").build();
assertEquals(inferredPinotSchema, expectedSchema);
}
+
+ @Test
+ public void testGetPinotSchemaFromAvroSchemaWithUuidLogicalType() {
+ org.apache.avro.Schema avroSchema =
org.apache.avro.Schema.createRecord("uuidRecord", null, null, false);
+ org.apache.avro.Schema uuidSchema =
+
LogicalTypes.uuid().addToSchema(org.apache.avro.Schema.create(org.apache.avro.Schema.Type.STRING));
+ avroSchema.setFields(Lists.newArrayList(
+ new org.apache.avro.Schema.Field("uuidCol", uuidSchema, null, null)
+ ));
+
+ Schema inferredPinotSchema =
AvroUtils.getPinotSchemaFromAvroSchema(avroSchema, null, null);
+
+ Schema expectedSchema =
+ new Schema.SchemaBuilder().addSingleValueDimension("uuidCol",
DataType.UUID).build();
+ assertEquals(inferredPinotSchema, expectedSchema);
+ }
+
+ @Test
+ public void testGetAvroSchemaFromPinotSchemaWithUuidLogicalType() {
+ Schema pinotSchema = new
Schema.SchemaBuilder().addSingleValueDimension("uuidCol",
DataType.UUID).build();
+
+ org.apache.avro.Schema avroSchema =
AvroUtils.getAvroSchemaFromPinotSchema(pinotSchema);
+ org.apache.avro.Schema fieldSchema =
avroSchema.getField("uuidCol").schema();
+
+ assertEquals(fieldSchema.getType(), org.apache.avro.Schema.Type.STRING);
+ assertEquals(fieldSchema.getLogicalType().getName(), "uuid");
+ }
+
+ @Test
+ public void testGetPinotSchemaFromAvroSchemaWithUuidArray() {
+ org.apache.avro.Schema uuidSchema =
+
LogicalTypes.uuid().addToSchema(org.apache.avro.Schema.create(org.apache.avro.Schema.Type.STRING));
+ org.apache.avro.Schema avroSchema =
org.apache.avro.SchemaBuilder.record("uuidArrayRecord").fields()
+
.name("uuidArray").type().array().items(uuidSchema).noDefault().endRecord();
+
+ Schema inferredPinotSchema =
AvroUtils.getPinotSchemaFromAvroSchemaWithComplexTypeHandling(avroSchema, null,
null,
+ new ArrayList<>(), ".",
ComplexTypeConfig.CollectionNotUnnestedToJson.NON_PRIMITIVE);
+
+ Schema expectedSchema =
+ new Schema.SchemaBuilder().addMultiValueDimension("uuidArray",
DataType.UUID).build();
+ assertEquals(inferredPinotSchema, expectedSchema);
+ }
+
+ @Test
+ public void testGetAvroSchemaFromPinotSchemaWithMvUuid() {
+ Schema pinotSchema = new
Schema.SchemaBuilder().addMultiValueDimension("uuidMv", DataType.UUID).build();
+
+ org.apache.avro.Schema avroSchema =
AvroUtils.getAvroSchemaFromPinotSchema(pinotSchema);
+ org.apache.avro.Schema fieldSchema =
avroSchema.getField("uuidMv").schema();
+
+ assertEquals(fieldSchema.getType(), org.apache.avro.Schema.Type.ARRAY);
+ assertEquals(fieldSchema.getElementType().getType(),
org.apache.avro.Schema.Type.STRING);
+ assertEquals(fieldSchema.getElementType().getLogicalType().getName(),
"uuid");
+ }
}
diff --git
a/pinot-plugins/pinot-segment-writer/pinot-segment-writer-file-based/src/main/java/org/apache/pinot/plugin/segmentwriter/filebased/FileBasedSegmentWriter.java
b/pinot-plugins/pinot-segment-writer/pinot-segment-writer-file-based/src/main/java/org/apache/pinot/plugin/segmentwriter/filebased/FileBasedSegmentWriter.java
index 28a1f218064..07ee87f5cbf 100644
---
a/pinot-plugins/pinot-segment-writer/pinot-segment-writer-file-based/src/main/java/org/apache/pinot/plugin/segmentwriter/filebased/FileBasedSegmentWriter.java
+++
b/pinot-plugins/pinot-segment-writer/pinot-segment-writer-file-based/src/main/java/org/apache/pinot/plugin/segmentwriter/filebased/FileBasedSegmentWriter.java
@@ -132,7 +132,8 @@ public class FileBasedSegmentWriter implements
SegmentWriter {
private void resetBuffer()
throws IOException {
FileUtils.deleteQuietly(_bufferFile);
- _recordWriter = new DataFileWriter<>(new
GenericDatumWriter<>(_avroSchema));
+ _recordWriter =
+ new DataFileWriter<>(new GenericDatumWriter<>(_avroSchema,
SegmentProcessorAvroUtils.getAvroDataModel()));
_recordWriter.create(_avroSchema, _bufferFile);
}
diff --git a/pinot-segment-local/pom.xml b/pinot-segment-local/pom.xml
index 5235fd1503c..ac18a6da9bf 100644
--- a/pinot-segment-local/pom.xml
+++ b/pinot-segment-local/pom.xml
@@ -109,6 +109,11 @@
<artifactId>pinot-avro</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.pinot</groupId>
+ <artifactId>pinot-avro-base</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.pinot</groupId>
<artifactId>pinot-csv</artifactId>
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java
index 0c3e2840ba3..5b32acb369f 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java
@@ -126,6 +126,7 @@ import org.apache.pinot.spi.utils.BooleanUtils;
import org.apache.pinot.spi.utils.ByteArray;
import org.apache.pinot.spi.utils.FixedIntArray;
import org.apache.pinot.spi.utils.MapUtils;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.apache.pinot.spi.utils.builder.TableNameBuilder;
import org.roaringbitmap.BatchIterator;
import org.roaringbitmap.buffer.MutableRoaringBitmap;
@@ -374,16 +375,9 @@ public class MutableSegmentImpl implements MutableSegment {
} else {
dictionary = null;
if (!fieldSpec.isSingleValueField()) {
- // Raw MV columns
- switch (storedType) {
- case INT:
- case LONG:
- case FLOAT:
- case DOUBLE:
- break;
- default:
- throw new UnsupportedOperationException(
- "Unsupported data type: " + dataType + " for MV
no-dictionary column: " + column);
+ if (!dataType.isFixedWidth()) {
+ throw new UnsupportedOperationException(
+ "Unsupported data type: " + dataType + " for MV no-dictionary
column: " + column);
}
}
}
@@ -773,7 +767,8 @@ public class MutableSegmentImpl implements MutableSegment {
private Comparable getComparisonValue(GenericRow row) {
int numComparisonColumns = _upsertComparisonColumns.size();
if (numComparisonColumns == 1) {
- return (Comparable) row.getValue(_upsertComparisonColumns.get(0));
+ String comparisonColumn = _upsertComparisonColumns.get(0);
+ return toComparable(row.getValue(comparisonColumn));
}
Comparable[] comparisonValues = new Comparable[numComparisonColumns];
@@ -790,11 +785,7 @@ public class MutableSegmentImpl implements MutableSegment {
"Documents must have exactly 1 non-null comparison column value");
comparableIndex = i;
-
- Object comparisonValue = row.getValue(columnName);
- Preconditions.checkState(comparisonValue instanceof Comparable,
- "Upsert comparison column: %s must be comparable", columnName);
- comparisonValues[i] = (Comparable) comparisonValue;
+ comparisonValues[i] = toComparable(row.getValue(columnName));
}
}
Preconditions.checkState(comparableIndex != -1, "Documents must have
exactly 1 non-null comparison column value");
@@ -952,14 +943,7 @@ public class MutableSegmentImpl implements MutableSegment {
// Update min/max value from raw value
// NOTE: Skip updating min/max value for aggregated metrics because
the value will change over time.
if (!isAggregateMetricsEnabled() || fieldSpec.getFieldType() !=
FieldSpec.FieldType.METRIC) {
- Comparable comparable;
- if (dataType == BYTES) {
- comparable = new ByteArray((byte[]) value);
- } else if (dataType == MAP) {
- comparable = new ByteArray(MapUtils.serializeMap((Map) value));
- } else {
- comparable = (Comparable) value;
- }
+ Comparable comparable = toComparableValue(value, dataType, column);
if (indexContainer._minValue == null) {
indexContainer._minValue = comparable;
indexContainer._maxValue = comparable;
@@ -1012,6 +996,29 @@ public class MutableSegmentImpl implements MutableSegment
{
}
}
+ /// Wraps a raw comparison-column value as a Comparable without a per-row
schema lookup: a byte[] (a BYTES or UUID
+ /// comparison column) becomes a ByteArray; every other type is already
Comparable. Mirrors
+ /// UpsertUtils.SingleComparisonColumnReader so the write and read paths
agree.
+ private static Comparable toComparable(Object value) {
+ if (value instanceof byte[]) {
+ return new ByteArray((byte[]) value);
+ }
+ Preconditions.checkState(value instanceof Comparable, "Upsert comparison
column value must be comparable: %s",
+ value);
+ return (Comparable) value;
+ }
+
+ private Comparable toComparableValue(Object value, DataType dataType,
@Nullable String columnName) {
+ if (dataType == MAP) {
+ return new ByteArray(MapUtils.serializeMap((Map) value));
+ }
+ if (dataType.getStoredType() == BYTES) {
+ return new ByteArray((byte[]) value);
+ }
+ Preconditions.checkState(value instanceof Comparable, "Column: %s must be
comparable", columnName);
+ return (Comparable) value;
+ }
+
private void updateIndexCapacityThresholdBreached(MutableIndex mutableIndex,
IndexType indexType, String column) {
// Few of the Immutable version of the mutable index are bounded by size
like
// {@link VarByteChunkForwardIndexWriterV4#putBytes(byte[])} and {@link
FixedBitMVForwardIndex}
@@ -1466,7 +1473,8 @@ public class MutableSegmentImpl implements MutableSegment
{
docIds[i] = i;
}
- DataType storedType =
indexContainer._fieldSpec.getDataType().getStoredType();
+ DataType dataType = indexContainer._fieldSpec.getDataType();
+ DataType storedType = dataType.getStoredType();
switch (storedType) {
case INT:
IntArrays.quickSort(docIds, (d1, d2) ->
Integer.compare(forwardIndex.getInt(d1), forwardIndex.getInt(d2)));
@@ -1488,8 +1496,13 @@ public class MutableSegmentImpl implements
MutableSegment {
IntArrays.quickSort(docIds, (d1, d2) ->
forwardIndex.getString(d1).compareTo(forwardIndex.getString(d2)));
break;
case BYTES:
- IntArrays.quickSort(docIds,
- (d1, d2) -> ByteArray.compare(forwardIndex.getBytes(d1),
forwardIndex.getBytes(d2)));
+ if (dataType == DataType.UUID) {
+ IntArrays.quickSort(docIds,
+ (d1, d2) -> UuidUtils.compare(forwardIndex.getBytes(d1),
forwardIndex.getBytes(d2)));
+ } else {
+ IntArrays.quickSort(docIds,
+ (d1, d2) -> ByteArray.compare(forwardIndex.getBytes(d1),
forwardIndex.getBytes(d2)));
+ }
break;
default:
throw new UnsupportedOperationException(
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/converter/stats/MutableNoDictColumnStatistics.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/converter/stats/MutableNoDictColumnStatistics.java
index 92d3455425f..85c0e242329 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/converter/stats/MutableNoDictColumnStatistics.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/converter/stats/MutableNoDictColumnStatistics.java
@@ -117,7 +117,9 @@ public class MutableNoDictColumnStatistics implements
ColumnStatistics, CLPStats
int numDocs = _dataSourceMetadata.getNumDocs();
- // Verify that values are non-decreasing when iterated in the given order
+ // Verify that values are non-decreasing when iterated in the given order.
The BYTES path uses
+ // ByteArray.compare (unsigned byte-wise lexicographic), which is
identical to UuidUtils.compare's unsigned
+ // 64-bit-word ordering on canonical 16-byte big-endian UUIDs, so a single
comparator handles both.
DataType storedType = getStoredType();
if (_sortedDocIds != null) {
switch (storedType) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/dictionary/BytesOffHeapMutableDictionary.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/dictionary/BytesOffHeapMutableDictionary.java
index f8afb134544..e03d20bb832 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/dictionary/BytesOffHeapMutableDictionary.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/dictionary/BytesOffHeapMutableDictionary.java
@@ -66,7 +66,14 @@ public class BytesOffHeapMutableDictionary extends
BaseOffHeapMutableDictionary
@Override
public int[] index(Object[] values) {
- throw new UnsupportedOperationException();
+ int numValues = values.length;
+ int[] dictIds = new int[numValues];
+ for (int i = 0; i < numValues; i++) {
+ byte[] bytesValue = (byte[]) values[i];
+ updateStats(bytesValue);
+ dictIds[i] = indexValue(new ByteArray(bytesValue), bytesValue);
+ }
+ return dictIds;
}
@Override
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/dictionary/BytesOnHeapMutableDictionary.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/dictionary/BytesOnHeapMutableDictionary.java
index 49b20161289..fd22f15244f 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/dictionary/BytesOnHeapMutableDictionary.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/dictionary/BytesOnHeapMutableDictionary.java
@@ -46,7 +46,14 @@ public class BytesOnHeapMutableDictionary extends
BaseOnHeapMutableDictionary {
@Override
public int[] index(Object[] values) {
- throw new UnsupportedOperationException();
+ int numValues = values.length;
+ int[] dictIds = new int[numValues];
+ for (int i = 0; i < numValues; i++) {
+ byte[] bytesValue = (byte[]) values[i];
+ updateStats(bytesValue);
+ dictIds[i] = indexValue(new ByteArray(bytesValue));
+ }
+ return dictIds;
}
@Override
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/forward/FixedByteMVMutableForwardIndex.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/forward/FixedByteMVMutableForwardIndex.java
index d42da3add11..e51854c8a59 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/forward/FixedByteMVMutableForwardIndex.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/forward/FixedByteMVMutableForwardIndex.java
@@ -121,6 +121,7 @@ public class FixedByteMVMutableForwardIndex implements
MutableForwardIndex {
private final String _context;
private final boolean _isDictionaryEncoded;
private final FieldSpec.DataType _storedType;
+ private final FieldSpec.DataType _dataType;
private FixedByteSingleValueMultiColWriter _curHeaderWriter;
private FixedByteSingleValueMultiColWriter _currentDataWriter;
@@ -132,6 +133,13 @@ public class FixedByteMVMutableForwardIndex implements
MutableForwardIndex {
public FixedByteMVMutableForwardIndex(int maxNumberOfMultiValuesPerRow, int
avgMultiValueCount, int rowCountPerChunk,
int columnSizeInBytes, PinotDataBufferMemoryManager memoryManager,
String context, boolean isDictionaryEncoded,
FieldSpec.DataType storedType) {
+ this(maxNumberOfMultiValuesPerRow, avgMultiValueCount, rowCountPerChunk,
columnSizeInBytes, memoryManager, context,
+ isDictionaryEncoded, storedType, storedType);
+ }
+
+ public FixedByteMVMutableForwardIndex(int maxNumberOfMultiValuesPerRow, int
avgMultiValueCount, int rowCountPerChunk,
+ int columnSizeInBytes, PinotDataBufferMemoryManager memoryManager,
String context, boolean isDictionaryEncoded,
+ FieldSpec.DataType storedType, FieldSpec.DataType dataType) {
_memoryManager = memoryManager;
_context = context;
int initialCapacity = Math.max(maxNumberOfMultiValuesPerRow,
rowCountPerChunk * avgMultiValueCount);
@@ -148,6 +156,7 @@ public class FixedByteMVMutableForwardIndex implements
MutableForwardIndex {
//init(_rowCountPerChunk, _columnSizeInBytes,
_maxNumberOfMultiValuesPerRow, initialCapacity, _incrementalCapacity);
_isDictionaryEncoded = isDictionaryEncoded;
_storedType = storedType;
+ _dataType = dataType;
}
private void addHeaderBuffer() {
@@ -380,6 +389,37 @@ public class FixedByteMVMutableForwardIndex implements
MutableForwardIndex {
return valueBuffer;
}
+ @Override
+ public int getBytesMV(int docId, byte[][] valueBuffer) {
+ checkBytesMvSupported();
+ FixedByteSingleValueMultiColReader headerReader = getCurrentReader(docId);
+ int rowInCurrentHeader = getRowInCurrentHeader(docId);
+ int bufferIndex = headerReader.getInt(rowInCurrentHeader, 0);
+ int startIndex = headerReader.getInt(rowInCurrentHeader, 1);
+ int length = headerReader.getInt(rowInCurrentHeader, 2);
+ FixedByteSingleValueMultiColReader dataReader =
_dataReaders.get(bufferIndex);
+ for (int i = 0; i < length; i++) {
+ valueBuffer[i] = dataReader.getBytes(startIndex + i, 0);
+ }
+ return length;
+ }
+
+ @Override
+ public byte[][] getBytesMV(int docId) {
+ checkBytesMvSupported();
+ FixedByteSingleValueMultiColReader headerReader = getCurrentReader(docId);
+ int rowInCurrentHeader = getRowInCurrentHeader(docId);
+ int bufferIndex = headerReader.getInt(rowInCurrentHeader, 0);
+ int startIndex = headerReader.getInt(rowInCurrentHeader, 1);
+ int length = headerReader.getInt(rowInCurrentHeader, 2);
+ FixedByteSingleValueMultiColReader dataReader =
_dataReaders.get(bufferIndex);
+ byte[][] valueBuffer = new byte[length][];
+ for (int i = 0; i < length; i++) {
+ valueBuffer[i] = dataReader.getBytes(startIndex + i, 0);
+ }
+ return valueBuffer;
+ }
+
@Override
public int getNumValuesMV(int docId) {
FixedByteSingleValueMultiColReader headerReader = getCurrentReader(docId);
@@ -424,6 +464,26 @@ public class FixedByteMVMutableForwardIndex implements
MutableForwardIndex {
}
}
+ @Override
+ public void setBytesMV(int docId, byte[][] values) {
+ checkBytesMvSupported();
+ int newStartIndex = updateHeader(docId, values.length);
+ for (int i = 0; i < values.length; i++) {
+ byte[] value = values[i];
+ if (value.length != _columnSizeInBytes) {
+ throw new IllegalArgumentException(
+ "Expected fixed-width bytes value of length: " +
_columnSizeInBytes + ", got: " + value.length);
+ }
+ _currentDataWriter.setBytes(newStartIndex + i, 0, value);
+ }
+ }
+
+ private void checkBytesMvSupported() {
+ if (_dataType != DataType.UUID) {
+ throw new UnsupportedOperationException("Unsupported data type: " +
_dataType + " for raw bytes MV index");
+ }
+ }
+
@Override
public boolean canAddMore() {
return _numValues < DEFAULT_THRESHOLD_FOR_NUM_OF_VALUES_PER_COLUMN;
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/recordtransformer/DataTypeTransformer.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/recordtransformer/DataTypeTransformer.java
index 6deb8390855..98c0a2f9190 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/recordtransformer/DataTypeTransformer.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/recordtransformer/DataTypeTransformer.java
@@ -19,8 +19,12 @@
package org.apache.pinot.segment.local.recordtransformer;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
+import javax.annotation.Nullable;
import org.apache.pinot.common.utils.ThrottledLogger;
import org.apache.pinot.segment.local.utils.DataTypeTransformerUtils;
import org.apache.pinot.spi.config.table.TableConfig;
@@ -52,21 +56,52 @@ public class DataTypeTransformer implements
RecordTransformer {
private final Map<String, PinotDataType> _dataTypes;
private final boolean _continueOnError;
private final ThrottledLogger _throttledLogger;
+ // UUID primary key columns for upsert/dedup tables; non-null and non-empty
when applicable.
+ // Non-canonical (uppercase) UUID strings in these columns will be rejected
at ingest time because
+ // Kafka partition routing is decided by the producer before Pinot
normalises the value. Accepting
+ // uppercase UUIDs as primary keys silently causes dedup failures in
multi-partition realtime
+ // upsert tables when the same logical UUID is routed to different
partitions.
+ @Nullable
+ private final Set<String> _upsertUuidPrimaryKeyColumns;
/// Creates a [DataTypeTransformer] that converts the (non-virtual) schema
columns to the data types defined in the
/// [Schema].
public DataTypeTransformer(TableConfig tableConfig, Schema schema) {
- this(tableConfig, extractSchemaDataTypes(schema));
+ this(tableConfig, extractSchemaDataTypes(schema), schema);
}
/// Creates a [DataTypeTransformer] that converts the given columns to the
provided [PinotDataType]s. This is useful
/// for fixing the data types of source fields before other transformers
(such as [ExpressionTransformer]) consume
/// them.
public DataTypeTransformer(TableConfig tableConfig, Map<String,
PinotDataType> dataTypes) {
+ this(tableConfig, dataTypes, null);
+ }
+
+ private DataTypeTransformer(TableConfig tableConfig, Map<String,
PinotDataType> dataTypes, @Nullable Schema schema) {
_dataTypes = dataTypes;
IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
_continueOnError = ingestionConfig != null &&
ingestionConfig.isContinueOnError();
_throttledLogger = new ThrottledLogger(LOGGER, ingestionConfig);
+
+ // Enforce canonical-form UUID PKs only when upsert/dedup is actually
enabled — a present-but-disabled config
+ // (e.g., UpsertConfig with Mode.NONE) must not reject otherwise-valid
rows.
+ if (schema != null && (tableConfig.isUpsertEnabled() ||
tableConfig.isDedupEnabled())) {
+ List<String> primaryKeyColumns = schema.getPrimaryKeyColumns();
+ if (primaryKeyColumns != null && !primaryKeyColumns.isEmpty()) {
+ Set<String> uuidPkCols = new HashSet<>();
+ for (String col : primaryKeyColumns) {
+ FieldSpec spec = schema.getFieldSpecFor(col);
+ if (spec != null && spec.getDataType() == FieldSpec.DataType.UUID) {
+ uuidPkCols.add(col);
+ }
+ }
+ _upsertUuidPrimaryKeyColumns = uuidPkCols.isEmpty() ? null :
Set.copyOf(uuidPkCols);
+ } else {
+ _upsertUuidPrimaryKeyColumns = null;
+ }
+ } else {
+ _upsertUuidPrimaryKeyColumns = null;
+ }
}
private static Map<String, PinotDataType> extractSchemaDataTypes(Schema
schema) {
@@ -95,6 +130,10 @@ public class DataTypeTransformer implements
RecordTransformer {
String column = entry.getKey();
try {
Object value = record.getValue(column);
+ if (_upsertUuidPrimaryKeyColumns != null &&
_upsertUuidPrimaryKeyColumns.contains(column)
+ && value instanceof CharSequence) {
+ validateCanonicalUuidPrimaryKey(column, value.toString());
+ }
value = DataTypeTransformerUtils.transformValue(column, value,
entry.getValue());
record.putValue(column, value);
} catch (Exception e) {
@@ -107,4 +146,41 @@ public class DataTypeTransformer implements
RecordTransformer {
}
}
}
+
+ /**
+ * Validates that a UUID primary key string value is in canonical lowercase
RFC 4122 form.
+ *
+ * <p>UUID primary keys in upsert/dedup realtime tables must be canonical
because Kafka partition routing
+ * is determined by the raw string value that the producer sends. If the
producer sends the same logical
+ * UUID with different casing (e.g. uppercase vs lowercase), Kafka will hash
the strings differently
+ * and the messages may land on different partitions. Pinot then normalises
them to the same bytes
+ * inside each consuming segment, so within-segment dedup works, but
cross-partition dedup never
+ * fires - producing duplicate or stale rows silently.
+ *
+ * <p>By rejecting any non-canonical UUID here (before bytes conversion
loses the original string),
+ * we ensure that a producer-side inconsistency surfaces as an ingestion
error rather than a silent
+ * correctness failure. This covers not only casing but also the dash-less
32-hex form and
+ * whitespace-padded values: {@code UuidUtils.toBytes(String)} (used by the
downstream conversion)
+ * accepts those and would normalise them to the same bytes as the canonical
string, so they must be
+ * rejected here too. Producers must canonicalise UUID primary keys to
lowercase RFC 4122 form before
+ * publishing to Kafka.
+ */
+ private static void validateCanonicalUuidPrimaryKey(String column, String
uuidStr) {
+ String canonical = null;
+ try {
+ canonical = UUID.fromString(uuidStr).toString();
+ } catch (IllegalArgumentException e) {
+ // Not parseable as a dashed RFC 4122 UUID (e.g. dash-less 32-hex or
whitespace-padded). Reject below rather
+ // than defer to DataTypeTransformerUtils.transformValue, whose
UuidUtils.toBytes would accept it as a
+ // non-canonical value and defeat the Kafka partition-routing guarantee.
+ }
+ if (!uuidStr.equals(canonical)) {
+ throw new IllegalArgumentException(
+ "Non-canonical UUID primary key value '" + uuidStr + "' in
upsert/dedup column '" + column + "'. "
+ + "Expected canonical lowercase RFC 4122 form"
+ + (canonical != null ? ": '" + canonical + "'" : " (8-4-4-4-12
dashed lowercase)") + ". "
+ + "UUID primary keys must be in canonical lowercase form to
ensure consistent Kafka partition "
+ + "routing. Non-canonical values cause silent dedup failures in
multi-partition realtime tables.");
+ }
+ }
}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/stats/BytesColumnPreIndexStatsCollector.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/stats/BytesColumnPreIndexStatsCollector.java
index 169d4d61f44..fc076605c9c 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/stats/BytesColumnPreIndexStatsCollector.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/stats/BytesColumnPreIndexStatsCollector.java
@@ -74,7 +74,12 @@ public class BytesColumnPreIndexStatsCollector extends
AbstractColumnStatisticsC
addressSorted(value);
if (_values.add(value)) {
if (isPartitionEnabled()) {
- updatePartition(value.toString());
+ // Use DataType.toString so UUID columns get their canonical RFC
4122 form (with dashes) rather
+ // than the bare hex from ByteArray.toString(). The runtime ingest
path (MutableSegmentImpl) and
+ // every partition function (Murmur, Uuid) expect this canonical
form; passing bare hex here
+ // produces a different partition value than runtime — silently
breaking partition pruning — and
+ // throws for the Uuid partition function (UuidUtils.toBytes rejects
no-dash strings).
+ updatePartition(_fieldSpec.getDataType().toString(entry));
}
int length = value.length();
_minLength = Math.min(_minLength, length);
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/stats/NoDictColumnStatisticsCollector.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/stats/NoDictColumnStatisticsCollector.java
index 4aa23fa791f..d0894b29273 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/stats/NoDictColumnStatisticsCollector.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/stats/NoDictColumnStatisticsCollector.java
@@ -155,7 +155,11 @@ public class NoDictColumnStatisticsCollector extends
AbstractColumnStatisticsCol
}
}
if (isPartitionEnabled()) {
- updatePartition(comparable.toString());
+ // See BytesColumnPreIndexStatsCollector: UUID columns must use
DataType.toString to produce the
+ // canonical dashed form that matches the runtime MutableSegmentImpl
partition path and
+ // UuidPartitionFunction expectation. comparable.toString() for UUID
would yield the bare hex
+ // (ByteArray.toString) which breaks both Murmur (different partition
value) and Uuid (rejected).
+ updatePartition(_fieldSpec.getDataType().toString(entry));
}
_totalNumberOfEntries++;
}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProvider.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProvider.java
index a9d5bc0bcc8..41adfa4aa1e 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProvider.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProvider.java
@@ -70,6 +70,7 @@ public class DefaultNullValueVirtualColumnProvider implements
VirtualColumnProvi
case STRING:
return new ConstantValueStringDictionary((String)
fieldSpec.getDefaultNullValue());
case BYTES:
+ case UUID:
return new ConstantValueBytesDictionary((byte[])
fieldSpec.getDefaultNullValue());
default:
throw new IllegalStateException();
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/forward/ForwardIndexType.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/forward/ForwardIndexType.java
index ac542042a81..7f4ae0a210b 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/forward/ForwardIndexType.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/forward/ForwardIndexType.java
@@ -364,7 +364,8 @@ public class ForwardIndexType extends
AbstractIndexType<ForwardIndexConfig, Forw
}
String column = context.getFieldSpec().getName();
String segmentName = context.getSegmentName();
- FieldSpec.DataType storedType =
context.getFieldSpec().getDataType().getStoredType();
+ FieldSpec.DataType dataType = context.getFieldSpec().getDataType();
+ FieldSpec.DataType storedType = dataType.getStoredType();
int fixedLengthBytes = context.getFixedLengthBytes();
boolean isSingleValue = context.getFieldSpec().isSingleValueField();
if (!context.hasDictionary()) {
@@ -402,13 +403,15 @@ public class ForwardIndexType extends
AbstractIndexType<ForwardIndexConfig, Forw
}
} else {
// TODO: Add support for variable width (bytes, string, big decimal)
MV RAW column types
- assert storedType.isFixedWidth();
+ Preconditions.checkState(dataType.isFixedWidth(), "Unsupported stored
type: %s for no-dictionary MV column: %s",
+ storedType, column);
String allocationContext =
IndexUtil.buildAllocationContext(context.getSegmentName(),
context.getFieldSpec().getName(),
V1Constants.Indexes.RAW_MV_FORWARD_INDEX_FILE_EXTENSION);
// TODO: Start with a smaller capacity on
FixedByteMVForwardIndexReaderWriter and let it expand
return new FixedByteMVMutableForwardIndex(MAX_MULTI_VALUES_PER_ROW,
context.getAvgNumMultiValues(),
- context.getCapacity(), storedType.size(),
context.getMemoryManager(), allocationContext, false, storedType);
+ context.getCapacity(), dataType.size(),
context.getMemoryManager(), allocationContext, false, storedType,
+ dataType);
}
} else {
if (isSingleValue) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGenerator.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGenerator.java
index 318b7173dc8..4e52a9245bb 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGenerator.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGenerator.java
@@ -360,6 +360,8 @@ public class ColumnMinMaxValueGenerator {
break;
}
case BYTES: {
+ // ByteArray.compare is unsigned byte-wise lexicographic; for
canonical 16-byte big-endian UUIDs this is
+ // identical to UuidUtils.compare's unsigned 64-bit-word ordering,
so a single comparator is sufficient.
byte[] min = null;
byte[] max = null;
if (isSingleValue) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/readers/PinotSegmentColumnReader.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/readers/PinotSegmentColumnReader.java
index 39ee9a240cb..12c7ec5a747 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/readers/PinotSegmentColumnReader.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/readers/PinotSegmentColumnReader.java
@@ -641,6 +641,8 @@ public class PinotSegmentColumnReader implements Closeable {
return _forwardIndexReader.getString(docId1,
_forwardIndexReaderContext)
.compareTo(_forwardIndexReader.getString(docId2,
_forwardIndexReaderContext));
case BYTES:
+ // ByteArray.compare is unsigned byte-wise lexicographic; for
canonical 16-byte big-endian UUIDs this is
+ // identical to UuidUtils.compare's unsigned 64-bit-word ordering, so
a single comparator handles both.
return ByteArray.compare(_forwardIndexReader.getBytes(docId1,
_forwardIndexReaderContext),
_forwardIndexReader.getBytes(docId2, _forwardIndexReaderContext));
default:
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
index 578280fb0d2..d37d8463fcb 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
@@ -30,6 +30,7 @@ import org.apache.pinot.spi.data.readers.PrimaryKey;
import org.apache.pinot.spi.utils.ByteArray;
import org.apache.pinot.spi.utils.CommonConstants;
import org.apache.pinot.spi.utils.PinotMd5Mode;
+import org.apache.pinot.spi.utils.UuidUtils;
public class HashUtils {
@@ -47,6 +48,15 @@ public class HashUtils {
/**
* Returns a byte array that is a concatenation of the binary representation
of each of the passed UUID values.
* If any of the values is not a valid UUID, then we return the result of
{@link PrimaryKey#asBytes()}.
+ *
+ * <p>String inputs are parsed via {@link UUID#fromString(String)} (lenient
— accepts non-canonical short
+ * forms like {@code "1-2-3-4-5"}) for backward compatibility with tables
that have been hashed under the
+ * pre-UUID-type {@code HashFunction.UUID} contract. Binary inputs ({@code
byte[]} or {@link UUID}) go
+ * through {@link UuidUtils#toBytes(Object)} since they carry no parse
ambiguity. Un-parseable strings
+ * (and any other invalid value) still fall through to {@link
PrimaryKey#asBytes()} via the outer
+ * {@code catch (RuntimeException)} below — matching master's behavior.
Newly-declared
+ * {@code DataType.UUID} primary-key columns are independently constrained
to canonical form at ingest
+ * time by {@code DataTypeTransformer.validateCanonicalUuidPrimaryKey}.
*/
public static byte[] hashUUID(PrimaryKey primaryKey) {
Object[] values = primaryKey.getValues();
@@ -56,14 +66,17 @@ public class HashUtils {
if (value == null) {
throw new IllegalArgumentException("Found null value in primary key");
}
- UUID uuid;
try {
- uuid = UUID.fromString(value.toString());
- } catch (Throwable t) {
+ if (value instanceof CharSequence) {
+ UUID uuid = UUID.fromString(value.toString());
+ byteBuffer.putLong(uuid.getMostSignificantBits());
+ byteBuffer.putLong(uuid.getLeastSignificantBits());
+ } else {
+ byteBuffer.put(UuidUtils.toBytes(value));
+ }
+ } catch (RuntimeException e) {
return primaryKey.asBytes();
}
- byteBuffer.putLong(uuid.getMostSignificantBits());
- byteBuffer.putLong(uuid.getLeastSignificantBits());
}
return result;
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/realtime/impl/dictionary/MultiValueDictionaryTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/realtime/impl/dictionary/MultiValueDictionaryTest.java
index 7d3ba9433c8..b3aae871b1f 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/realtime/impl/dictionary/MultiValueDictionaryTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/realtime/impl/dictionary/MultiValueDictionaryTest.java
@@ -20,11 +20,14 @@ package
org.apache.pinot.segment.local.realtime.impl.dictionary;
import java.nio.charset.StandardCharsets;
import java.util.Random;
+import java.util.UUID;
import org.apache.pinot.segment.local.PinotBuffersAfterClassCheckRule;
import org.apache.pinot.segment.local.io.writer.impl.DirectMemoryManager;
import
org.apache.pinot.segment.local.realtime.impl.forward.FixedByteMVMutableForwardIndex;
+import org.apache.pinot.segment.spi.index.mutable.MutableDictionary;
import org.apache.pinot.segment.spi.memory.PinotDataBufferMemoryManager;
import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
@@ -88,6 +91,31 @@ public class MultiValueDictionaryTest implements
PinotBuffersAfterClassCheckRule
}
}
+ @Test
+ public void testMultiValueBytesIndexingWithDictionary()
+ throws Exception {
+ try (MutableDictionary dictionary = new BytesOnHeapMutableDictionary()) {
+ assertMultiValueBytesIndexingWithDictionary(dictionary);
+ }
+ try (MutableDictionary dictionary =
+ new BytesOffHeapMutableDictionary(4, 10, _memoryManager,
"bytesDictionary", 4)) {
+ assertMultiValueBytesIndexingWithDictionary(dictionary);
+ }
+ }
+
+ private static void
assertMultiValueBytesIndexingWithDictionary(MutableDictionary dictionary) {
+ byte[] first = new byte[]{1, 2};
+ byte[] second = new byte[]{3, 4, 5};
+ byte[] third = new byte[]{6};
+
+ assertEquals(dictionary.index(new Object[]{first, second, first}), new
int[]{0, 1, 0});
+ assertEquals(dictionary.index(new Object[]{third, second}), new int[]{2,
1});
+ assertEquals(dictionary.length(), 3);
+ assertEquals(dictionary.getBytesValue(0), first);
+ assertEquals(dictionary.getBytesValue(1), second);
+ assertEquals(dictionary.getBytesValue(2), third);
+ }
+
@Test
public void testMultiValueIndexingWithRawInt() {
long seed = System.nanoTime();
@@ -286,4 +314,29 @@ public class MultiValueDictionaryTest implements
PinotBuffersAfterClassCheckRule
fail("Failed with random seed: " + seed, t);
}
}
+
+ @Test
+ public void testMultiValueIndexingWithRawUuidBytes()
+ throws Exception {
+ try (DirectMemoryManager memManager = new DirectMemoryManager("test");
+ FixedByteMVMutableForwardIndex indexer = new
FixedByteMVMutableForwardIndex(MAX_N_VALUES, MAX_N_VALUES / 2,
+ NROWS / 3, UuidUtils.UUID_NUM_BYTES, memManager, "indexer", false,
FieldSpec.DataType.BYTES,
+ FieldSpec.DataType.UUID)) {
+ byte[][] values = new byte[][]{
+ UuidUtils.toBytes(new UUID(1L, 2L)),
+ UuidUtils.toBytes(new UUID(3L, 4L))
+ };
+ indexer.setBytesMV(0, values);
+
+ byte[][] buffer = new byte[values.length][];
+ assertEquals(indexer.getBytesMV(0, buffer), values.length);
+ for (int i = 0; i < values.length; i++) {
+ assertEquals(buffer[i], values[i]);
+ }
+ byte[][] actualValues = indexer.getBytesMV(0);
+ for (int i = 0; i < values.length; i++) {
+ assertEquals(actualValues[i], values[i]);
+ }
+ }
+ }
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/recordtransformer/DataTypeTransformerTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/recordtransformer/DataTypeTransformerTest.java
index fa98adf2218..17c1ba1874e 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/recordtransformer/DataTypeTransformerTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/recordtransformer/DataTypeTransformerTest.java
@@ -23,6 +23,14 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.pinot.segment.local.utils.DataTypeTransformerUtils;
+import org.apache.pinot.spi.config.table.DedupConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.config.table.UpsertConfig;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
@@ -201,4 +209,108 @@ public class DataTypeTransformerTest {
}
assertEqualsNoOrder((Object[])
DataTypeTransformerUtils.standardize(COLUMN, values, false), expectedValues);
}
+
+ /**
+ * Verifies that non-canonical (uppercase) UUID strings in upsert/dedup
primary key columns are rejected,
+ * while canonical lowercase UUIDs are accepted, and non-primary-key UUID
columns are unaffected.
+ */
+ @Test
+ public void testUuidUpsertPrimaryKeyCanonicalValidation() {
+ String uuidCol = "uuidPk";
+ String nonPkUuidCol = "uuidOther";
+ String canonicalUuid = "550e8400-e29b-41d4-a716-446655440000";
+ String uppercaseUuid = "550E8400-E29B-41D4-A716-446655440000";
+ // Dash-less 32-hex and whitespace-padded forms are accepted by
UuidUtils.toBytes(String) (and would normalise to
+ // the same bytes), but they are NOT canonical and route to a different
Kafka partition, so they must be rejected.
+ String noDashUuid = "550e8400e29b41d4a716446655440000";
+ String whitespaceUuid = " 550e8400-e29b-41d4-a716-446655440000 ";
+
+ Schema schema = new Schema.SchemaBuilder()
+ .addSingleValueDimension(uuidCol, FieldSpec.DataType.UUID)
+ .addSingleValueDimension(nonPkUuidCol, FieldSpec.DataType.UUID)
+ .setPrimaryKeyColumns(List.of(uuidCol))
+ .build();
+ TableConfig upsertTableConfig = new TableConfigBuilder(TableType.REALTIME)
+ .setTableName("testUpsertUuid")
+ .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.FULL))
+ .build();
+
+ DataTypeTransformer upsertTransformer = new
DataTypeTransformer(upsertTableConfig, schema);
+
+ // Canonical lowercase UUID primary key: accepted
+ GenericRow canonicalRow = new GenericRow();
+ canonicalRow.putValue(uuidCol, canonicalUuid);
+ canonicalRow.putValue(nonPkUuidCol, canonicalUuid);
+ upsertTransformer.transform(canonicalRow); // must not throw
+
+ // Non-canonical uppercase UUID primary key: rejected
+ GenericRow uppercaseRow = new GenericRow();
+ uppercaseRow.putValue(uuidCol, uppercaseUuid);
+ uppercaseRow.putValue(nonPkUuidCol, canonicalUuid);
+ try {
+ upsertTransformer.transform(uppercaseRow);
+ fail("Expected RuntimeException for non-canonical UUID primary key in
upsert table");
+ } catch (RuntimeException e) {
+ // Expected: DataTypeTransformer wraps the IllegalArgumentException in a
RuntimeException
+ }
+
+ // Dash-less 32-hex UUID primary key: rejected (parses via
UuidUtils.toBytes hex fallback, but not canonical)
+ GenericRow noDashRow = new GenericRow();
+ noDashRow.putValue(uuidCol, noDashUuid);
+ noDashRow.putValue(nonPkUuidCol, canonicalUuid);
+ try {
+ upsertTransformer.transform(noDashRow);
+ fail("Expected RuntimeException for dash-less UUID primary key in upsert
table");
+ } catch (RuntimeException e) {
+ // Expected
+ }
+
+ // Whitespace-padded UUID primary key: rejected
+ GenericRow whitespaceRow = new GenericRow();
+ whitespaceRow.putValue(uuidCol, whitespaceUuid);
+ whitespaceRow.putValue(nonPkUuidCol, canonicalUuid);
+ try {
+ upsertTransformer.transform(whitespaceRow);
+ fail("Expected RuntimeException for whitespace-padded UUID primary key
in upsert table");
+ } catch (RuntimeException e) {
+ // Expected
+ }
+
+ // Non-canonical uppercase UUID in a NON-primary-key column: accepted (no
restriction)
+ GenericRow nonPkUppercaseRow = new GenericRow();
+ nonPkUppercaseRow.putValue(uuidCol, canonicalUuid);
+ nonPkUppercaseRow.putValue(nonPkUuidCol, uppercaseUuid);
+ upsertTransformer.transform(nonPkUppercaseRow); // must not throw
+
+ // For a non-upsert table, non-canonical UUID in primary-key column is
also accepted
+ TableConfig offlineTableConfig =
+ new
TableConfigBuilder(TableType.OFFLINE).setTableName("testOfflineUuid").build();
+ DataTypeTransformer offlineTransformer = new
DataTypeTransformer(offlineTableConfig, schema);
+ GenericRow offlineUppercaseRow = new GenericRow();
+ offlineUppercaseRow.putValue(uuidCol, uppercaseUuid);
+ offlineUppercaseRow.putValue(nonPkUuidCol, uppercaseUuid);
+ offlineTransformer.transform(offlineUppercaseRow); // must not throw
+
+ // A present-but-disabled upsert config (Mode.NONE) must not enforce the
canonical-PK restriction
+ TableConfig disabledUpsertTableConfig = new
TableConfigBuilder(TableType.REALTIME)
+ .setTableName("testDisabledUpsertUuid")
+ .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.NONE))
+ .build();
+ DataTypeTransformer disabledUpsertTransformer = new
DataTypeTransformer(disabledUpsertTableConfig, schema);
+ GenericRow disabledUpsertUppercaseRow = new GenericRow();
+ disabledUpsertUppercaseRow.putValue(uuidCol, uppercaseUuid);
+ disabledUpsertUppercaseRow.putValue(nonPkUuidCol, canonicalUuid);
+ disabledUpsertTransformer.transform(disabledUpsertUppercaseRow); // must
not throw
+
+ // A present-but-disabled dedup config must not enforce the canonical-PK
restriction either
+ TableConfig disabledDedupTableConfig = new
TableConfigBuilder(TableType.REALTIME)
+ .setTableName("testDisabledDedupUuid")
+ .setDedupConfig(new DedupConfig(false, null))
+ .build();
+ DataTypeTransformer disabledDedupTransformer = new
DataTypeTransformer(disabledDedupTableConfig, schema);
+ GenericRow disabledDedupUppercaseRow = new GenericRow();
+ disabledDedupUppercaseRow.putValue(uuidCol, uppercaseUuid);
+ disabledDedupUppercaseRow.putValue(nonPkUuidCol, canonicalUuid);
+ disabledDedupTransformer.transform(disabledDedupUppercaseRow); // must not
throw
+ }
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProviderTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProviderTest.java
index f1f655ae9bd..3e7cc400dee 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProviderTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/column/DefaultNullValueVirtualColumnProviderTest.java
@@ -31,6 +31,7 @@ import org.apache.pinot.spi.data.DimensionFieldSpec;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.utils.ByteArray;
+import org.apache.pinot.spi.utils.BytesUtils;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
@@ -45,6 +46,7 @@ public class DefaultNullValueVirtualColumnProviderTest {
private static final FieldSpec SV_STRING_WITH_DEFAULT =
new DimensionFieldSpec("svStringColumn", DataType.STRING, true,
"default");
private static final FieldSpec SV_BYTES = new
DimensionFieldSpec("svBytesColumn", DataType.BYTES, true);
+ private static final FieldSpec SV_UUID = new
DimensionFieldSpec("svUuidColumn", DataType.UUID, true);
private static final FieldSpec MV_INT = new
DimensionFieldSpec("mvIntColumn", DataType.INT, false);
private static final FieldSpec MV_LONG = new
DimensionFieldSpec("mvLongColumn", DataType.LONG, false);
private static final FieldSpec MV_FLOAT = new
DimensionFieldSpec("mvFloatColumn", DataType.FLOAT, false);
@@ -146,6 +148,14 @@ public class DefaultNullValueVirtualColumnProviderTest {
assertEquals(dictionary.getClass(), ConstantValueBytesDictionary.class);
assertEquals(dictionary.getBytesValue(0), new byte[0]);
+ virtualColumnContext = new VirtualColumnContext(SV_UUID, 1);
+ dictionary = new
DefaultNullValueVirtualColumnProvider().buildDictionary(virtualColumnContext);
+ assertEquals(dictionary.getClass(), ConstantValueBytesDictionary.class);
+ byte[] uuidDefaultNullValue = (byte[]) SV_UUID.getDefaultNullValue();
+ assertEquals(dictionary.getBytesValue(0), uuidDefaultNullValue);
+ assertEquals(dictionary.getStringValue(0),
BytesUtils.toHexString(uuidDefaultNullValue));
+
assertEquals(dictionary.indexOf(BytesUtils.toHexString(uuidDefaultNullValue)),
0);
+
virtualColumnContext = new VirtualColumnContext(MV_INT, 1);
dictionary = new
DefaultNullValueVirtualColumnProvider().buildDictionary(virtualColumnContext);
assertEquals(dictionary.getClass(), ConstantValueIntDictionary.class);
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/BloomFilterCreatorTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/BloomFilterCreatorTest.java
index 589016d7bc8..d81ef7fe1e9 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/BloomFilterCreatorTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/BloomFilterCreatorTest.java
@@ -29,6 +29,7 @@ import
org.apache.pinot.segment.spi.index.reader.BloomFilterReader;
import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
import org.apache.pinot.spi.config.table.BloomFilterConfig;
import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.apache.pinot.util.TestUtils;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
@@ -75,6 +76,33 @@ public class BloomFilterCreatorTest implements
PinotBuffersAfterMethodCheckRule
}
}
+ @Test
+ public void testUuidBloomFilterCreatorWithBytesValues()
+ throws Exception {
+ int cardinality = 100;
+ String columnName = "uuidColumn";
+ String uuid0 = "550e8400-e29b-41d4-a716-446655440000";
+ String uuid1 = "550e8400-e29b-41d4-a716-446655440001";
+ try (BloomFilterCreator bloomFilterCreator = new
OnHeapGuavaBloomFilterCreator(TEMP_DIR, columnName, cardinality,
+ new BloomFilterConfig(BloomFilterConfig.DEFAULT_FPP, 0, false),
FieldSpec.DataType.UUID)) {
+ bloomFilterCreator.add(UuidUtils.toBytes(uuid0), -1);
+ bloomFilterCreator.add(UuidUtils.toBytes(uuid1), -1);
+ bloomFilterCreator.seal();
+ }
+
+ File bloomFilterFile = new File(TEMP_DIR, columnName +
V1Constants.Indexes.BLOOM_FILTER_FILE_EXTENSION);
+ try (PinotDataBuffer dataBuffer =
PinotDataBuffer.mapReadOnlyBigEndianFile(bloomFilterFile);
+ BloomFilterReader onHeapBloomFilter =
BloomFilterReaderFactory.getBloomFilterReader(dataBuffer, true);
+ BloomFilterReader offHeapBloomFilter =
BloomFilterReaderFactory.getBloomFilterReader(dataBuffer, false)) {
+ Assert.assertTrue(onHeapBloomFilter.mightContain(uuid0));
+ Assert.assertTrue(onHeapBloomFilter.mightContain(uuid1));
+
Assert.assertFalse(onHeapBloomFilter.mightContain("550e8400-e29b-41d4-a716-4466554400ff"));
+ Assert.assertTrue(offHeapBloomFilter.mightContain(uuid0));
+ Assert.assertTrue(offHeapBloomFilter.mightContain(uuid1));
+
Assert.assertFalse(offHeapBloomFilter.mightContain("550e8400-e29b-41d4-a716-4466554400ff"));
+ }
+ }
+
@AfterClass
public void tearDown()
throws Exception {
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGeneratorTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGeneratorTest.java
index c33aa0662a7..1e38c4dfc69 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGeneratorTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGeneratorTest.java
@@ -39,6 +39,7 @@ import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.data.readers.GenericRow;
import org.apache.pinot.spi.utils.ByteArray;
import org.apache.pinot.spi.utils.ReadMode;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
import org.testng.annotations.AfterClass;
import org.testng.annotations.Test;
@@ -48,30 +49,39 @@ import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
-/// Regression tests for [ColumnMinMaxValueGenerator] on raw BYTES columns.
+/// Regression tests for [ColumnMinMaxValueGenerator] on raw (no-dictionary)
BYTES and UUID columns.
///
/// The generator reads the forward index when column metadata does not
contain min/max values. These tests exercise
-/// both single-value and multi-value forward index paths and verify their
unsigned byte-wise ordering.
+/// the single-value, multi-value, and UUID (storedType=BYTES) forward index
paths and verify their unsigned byte-wise
+/// ordering. The raw-BYTES loop once had its comparison directions inverted,
silently persisting swapped min/max
+/// metadata that value-based segment pruning then consumed.
public class ColumnMinMaxValueGeneratorTest {
private static final File TEMP_DIR =
new File(FileUtils.getTempDirectory(),
ColumnMinMaxValueGeneratorTest.class.getSimpleName());
private static final String SEGMENT_NAME = "testSegment";
private static final String BYTES_COLUMN = "bytesCol";
private static final String BYTES_MV_COLUMN = "bytesMvCol";
+ private static final String UUID_COLUMN = "uuidCol";
+ // Ordered ascending by unsigned byte-wise comparison
private static final byte[] BYTES_SMALL = new byte[]{0x00, 0x01};
private static final byte[] BYTES_MID = new byte[]{0x10, (byte) 0xff};
private static final byte[] BYTES_LARGE = new byte[]{(byte) 0x80, 0x00};
+ private static final String UUID_SMALL =
"00000000-0000-0000-0000-000000000001";
+ private static final String UUID_MID =
"550e8400-e29b-41d4-a716-446655440000";
+ private static final String UUID_LARGE =
"ffffffff-ffff-ffff-ffff-fffffffffffe";
+
@Test
public void testRawBytesMinMaxDirection()
throws Exception {
Schema schema = new Schema.SchemaBuilder().setSchemaName("testSchema")
.addSingleValueDimension(BYTES_COLUMN, DataType.BYTES)
.addMultiValueDimension(BYTES_MV_COLUMN, DataType.BYTES)
+ .addSingleValueDimension(UUID_COLUMN, DataType.UUID)
.build();
TableConfig tableConfig = new
TableConfigBuilder(TableType.OFFLINE).setTableName("testTable")
- .setNoDictionaryColumns(List.of(BYTES_COLUMN, BYTES_MV_COLUMN))
+ .setNoDictionaryColumns(List.of(BYTES_COLUMN, BYTES_MV_COLUMN,
UUID_COLUMN))
.build();
File indexDir = buildSegment(tableConfig, schema, createRows());
@@ -80,8 +90,11 @@ public class ColumnMinMaxValueGeneratorTest {
generateMinMaxValues(indexDir);
SegmentMetadataImpl reloaded = new SegmentMetadataImpl(indexDir);
- assertMinMax(reloaded, BYTES_COLUMN);
- assertMinMax(reloaded, BYTES_MV_COLUMN);
+ assertMinMax(reloaded, BYTES_COLUMN, BYTES_SMALL, BYTES_LARGE);
+ // The raw-BYTES MV loop is a distinct code path sharing the comparator
with the SV loop
+ assertMinMax(reloaded, BYTES_MV_COLUMN, BYTES_SMALL, BYTES_LARGE);
+ // UUID stores as BYTES; its min/max must order by the same unsigned
byte-wise comparison
+ assertMinMax(reloaded, UUID_COLUMN, UuidUtils.toBytes(UUID_SMALL),
UuidUtils.toBytes(UUID_LARGE));
}
@Test
@@ -104,11 +117,12 @@ public class ColumnMinMaxValueGeneratorTest {
assertTrue(reloaded.getColumnMetadataFor(BYTES_COLUMN).isMinMaxValueInvalid());
}
- private static void assertMinMax(SegmentMetadataImpl segmentMetadata, String
column) {
+ private static void assertMinMax(SegmentMetadataImpl segmentMetadata, String
column, byte[] expectedMin,
+ byte[] expectedMax) {
ByteArray min = (ByteArray)
segmentMetadata.getColumnMetadataFor(column).getMinValue();
ByteArray max = (ByteArray)
segmentMetadata.getColumnMetadataFor(column).getMaxValue();
- assertEquals(min, new ByteArray(BYTES_SMALL));
- assertEquals(max, new ByteArray(BYTES_LARGE));
+ assertEquals(min, new ByteArray(expectedMin));
+ assertEquals(max, new ByteArray(expectedMax));
assertTrue(min.compareTo(max) < 0);
}
@@ -125,10 +139,17 @@ public class ColumnMinMaxValueGeneratorTest {
private static List<GenericRow> createRows() {
List<GenericRow> rows = new ArrayList<>();
- for (byte[] value : new byte[][]{BYTES_MID, BYTES_LARGE, BYTES_SMALL}) {
+ // Deliberately insert in non-sorted order so the running min/max
comparisons are both exercised
+ for (Object[] values : new Object[][]{
+ {BYTES_MID, UuidUtils.toBytes(UUID_MID)},
+ {BYTES_LARGE, UuidUtils.toBytes(UUID_LARGE)},
+ {BYTES_SMALL, UuidUtils.toBytes(UUID_SMALL)}
+ }) {
GenericRow row = new GenericRow();
- row.putValue(BYTES_COLUMN, value);
- row.putValue(BYTES_MV_COLUMN, new Object[]{value, BYTES_MID});
+ row.putValue(BYTES_COLUMN, values[0]);
+ // Each MV row carries two values so the inner per-value loop is
exercised as well
+ row.putValue(BYTES_MV_COLUMN, new Object[]{values[0], BYTES_MID});
+ row.putValue(UUID_COLUMN, values[1]);
rows.add(row);
}
return rows;
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/readers/LazyRowTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/readers/LazyRowTest.java
index d941988f30c..63f90672161 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/readers/LazyRowTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/readers/LazyRowTest.java
@@ -22,9 +22,12 @@ import java.util.Arrays;
import java.util.HashSet;
import org.apache.pinot.segment.spi.IndexSegment;
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.reader.ForwardIndexReader;
import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+import org.apache.pinot.spi.data.DimensionFieldSpec;
+import org.apache.pinot.spi.data.FieldSpec;
import org.testng.annotations.Test;
import static org.mockito.ArgumentMatchers.any;
@@ -93,6 +96,15 @@ public class LazyRowTest {
when(segment.getDataSource("col1")).thenReturn(_col1Datasource);
when(segment.getDataSource("col2")).thenReturn(col2Datasource);
+ DataSourceMetadata col1Metadata = mock(DataSourceMetadata.class);
+ DataSourceMetadata col2Metadata = mock(DataSourceMetadata.class);
+ FieldSpec col1FieldSpec = new DimensionFieldSpec("col1",
FieldSpec.DataType.STRING, true);
+ FieldSpec col2FieldSpec = new DimensionFieldSpec("col2",
FieldSpec.DataType.STRING, true);
+ when(col1Metadata.getFieldSpec()).thenReturn(col1FieldSpec);
+ when(col2Metadata.getFieldSpec()).thenReturn(col2FieldSpec);
+ when(_col1Datasource.getDataSourceMetadata()).thenReturn(col1Metadata);
+ when(col2Datasource.getDataSourceMetadata()).thenReturn(col2Metadata);
+
NullValueVectorReader col1NullVectorReader =
mock(NullValueVectorReader.class);
when(col1NullVectorReader.isNull(1)).thenReturn(true);
NullValueVectorReader col2NullVectorReader =
mock(NullValueVectorReader.class);
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletesTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletesTest.java
index 7acfc0ace54..760055735a2 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletesTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletesTest.java
@@ -44,12 +44,14 @@ import org.apache.pinot.segment.spi.IndexSegment;
import org.apache.pinot.segment.spi.MutableSegment;
import org.apache.pinot.segment.spi.V1Constants;
import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
import
org.apache.pinot.segment.spi.index.mutable.ThreadSafeMutableRoaringBitmap;
import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
import org.apache.pinot.spi.config.table.HashFunction;
import org.apache.pinot.spi.config.table.TableConfig;
import org.apache.pinot.spi.config.table.UpsertConfig;
+import org.apache.pinot.spi.data.DimensionFieldSpec;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.data.readers.PrimaryKey;
@@ -105,6 +107,10 @@ public class
ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletesTest
when(forwardIndex.getInt(anyInt(), any())).thenAnswer(
invocation ->
primaryKeys.get(invocation.getArgument(0)).getValues()[0]);
when(dataSource.getForwardIndex()).thenReturn(forwardIndex);
+ DataSourceMetadata dataSourceMetadata = mock(DataSourceMetadata.class);
+ when(dataSourceMetadata.getFieldSpec()).thenReturn(
+ new DimensionFieldSpec(PRIMARY_KEY_COLUMNS.get(0),
FieldSpec.DataType.INT, true));
+ when(dataSource.getDataSourceMetadata()).thenReturn(dataSourceMetadata);
SegmentMetadataImpl segmentMetadata = mock(SegmentMetadataImpl.class);
long creationTimeMs = System.currentTimeMillis();
when(segmentMetadata.getIndexCreationTime()).thenReturn(creationTimeMs);
@@ -134,6 +140,10 @@ public class
ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletesTest
when(forwardIndex.getInt(anyInt(), any())).thenAnswer(
invocation ->
primaryKeys.get(invocation.getArgument(0)).getValues()[0]);
when(dataSource.getForwardIndex()).thenReturn(forwardIndex);
+ DataSourceMetadata uploadedDataSourceMetadata =
mock(DataSourceMetadata.class);
+ when(uploadedDataSourceMetadata.getFieldSpec()).thenReturn(
+ new DimensionFieldSpec(PRIMARY_KEY_COLUMNS.get(0),
FieldSpec.DataType.INT, true));
+
when(dataSource.getDataSourceMetadata()).thenReturn(uploadedDataSourceMetadata);
SegmentMetadataImpl segmentMetadata = mock(SegmentMetadataImpl.class);
when(segmentMetadata.getIndexCreationTime()).thenReturn(creationTimeMs);
when(segmentMetadata.getZkCreationTime()).thenReturn(creationTimeMs);
@@ -186,6 +196,10 @@ public class
ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletesTest
return docId;
});
when(dataSource.getForwardIndex()).thenReturn(forwardIndex);
+ DataSourceMetadata mutableDataSourceMetadata =
mock(DataSourceMetadata.class);
+ when(mutableDataSourceMetadata.getFieldSpec()).thenReturn(
+ new DimensionFieldSpec(PRIMARY_KEY_COLUMNS.get(0),
FieldSpec.DataType.INT, true));
+
when(dataSource.getDataSourceMetadata()).thenReturn(mutableDataSourceMetadata);
when(segment.getDataSource(anyString())).thenReturn(dataSource);
when(segment.getDataSource(PRIMARY_KEY_COLUMNS.get(0))).thenReturn(dataSource);
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerTest.java
index dbba9b0e2eb..2e9dc43ddb8 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerTest.java
@@ -50,6 +50,7 @@ import org.apache.pinot.segment.spi.MutableSegment;
import org.apache.pinot.segment.spi.V1Constants;
import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
import
org.apache.pinot.segment.spi.index.mutable.ThreadSafeMutableRoaringBitmap;
import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
@@ -924,6 +925,10 @@ public class
ConcurrentMapPartitionUpsertMetadataManagerTest {
return MOCK_FALLBACK_BASE_OFFSET + docId;
});
when(primaryKeyDataSource.getForwardIndex()).thenReturn(primaryKeyForwardIndex);
+ DataSourceMetadata primaryKeyDataSourceMetadata =
mock(DataSourceMetadata.class);
+ when(primaryKeyDataSourceMetadata.getFieldSpec()).thenReturn(
+ new DimensionFieldSpec(PRIMARY_KEY_COLUMNS.get(0), DataType.INT,
true));
+
when(primaryKeyDataSource.getDataSourceMetadata()).thenReturn(primaryKeyDataSourceMetadata);
// Mock comparison column data source
DataSource comparisonDataSource = mock(DataSource.class);
@@ -940,6 +945,10 @@ public class
ConcurrentMapPartitionUpsertMetadataManagerTest {
return MOCK_FALLBACK_BASE_OFFSET + (docId * 100);
});
when(comparisonDataSource.getForwardIndex()).thenReturn(comparisonForwardIndex);
+ DataSourceMetadata comparisonDataSourceMetadata =
mock(DataSourceMetadata.class);
+ when(comparisonDataSourceMetadata.getFieldSpec()).thenReturn(
+ new DimensionFieldSpec(COMPARISON_COLUMNS.get(0), DataType.INT, true));
+
when(comparisonDataSource.getDataSourceMetadata()).thenReturn(comparisonDataSourceMetadata);
// Set up data source mapping - IMPORTANT: anyString() must be registered
FIRST,
// then specific matchers override it (Mockito uses last matching stub)
@@ -1037,6 +1046,10 @@ public class
ConcurrentMapPartitionUpsertMetadataManagerTest {
when(forwardIndex.getInt(anyInt(), any())).thenAnswer(
invocation ->
primaryKeys.get(invocation.getArgument(0)).getValues()[0]);
when(dataSource.getForwardIndex()).thenReturn(forwardIndex);
+ DataSourceMetadata uploadedDataSourceMetadata =
mock(DataSourceMetadata.class);
+ when(uploadedDataSourceMetadata.getFieldSpec()).thenReturn(
+ new DimensionFieldSpec(PRIMARY_KEY_COLUMNS.get(0), DataType.INT,
true));
+
when(dataSource.getDataSourceMetadata()).thenReturn(uploadedDataSourceMetadata);
SegmentMetadataImpl segmentMetadata = mock(SegmentMetadataImpl.class);
when(segmentMetadata.getIndexCreationTime()).thenReturn(creationTimeMs);
when(segmentMetadata.getZkCreationTime()).thenReturn(creationTimeMs);
@@ -1108,6 +1121,10 @@ public class
ConcurrentMapPartitionUpsertMetadataManagerTest {
return docId;
});
when(dataSource.getForwardIndex()).thenReturn(forwardIndex);
+ DataSourceMetadata mutableDataSourceMetadata =
mock(DataSourceMetadata.class);
+ when(mutableDataSourceMetadata.getFieldSpec()).thenReturn(
+ new DimensionFieldSpec(PRIMARY_KEY_COLUMNS.get(0), DataType.INT,
true));
+
when(dataSource.getDataSourceMetadata()).thenReturn(mutableDataSourceMetadata);
when(segment.getDataSource(anyString())).thenReturn(dataSource);
when(segment.getDataSource(PRIMARY_KEY_COLUMNS.get(0))).thenReturn(dataSource);
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/HashUtilsTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/HashUtilsTest.java
index 8c60ad6f374..c573265424b 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/HashUtilsTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/HashUtilsTest.java
@@ -21,9 +21,11 @@ package org.apache.pinot.segment.local.utils;
import java.util.UUID;
import org.apache.pinot.spi.config.table.HashFunction;
import org.apache.pinot.spi.data.readers.PrimaryKey;
+import org.apache.pinot.spi.utils.ByteArray;
import org.apache.pinot.spi.utils.BytesUtils;
import org.apache.pinot.spi.utils.CommonConstants;
import org.apache.pinot.spi.utils.PinotMd5Mode;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
@@ -64,6 +66,46 @@ public class HashUtilsTest {
}
}
+ @Test
+ public void testHashUUIDNormalizedPrimaryKeyValues() {
+ UUID firstUuid = UUID.randomUUID();
+ UUID secondUuid = UUID.randomUUID();
+
+ byte[] expectedHash = HashUtils.hashUUID(new PrimaryKey(new
Object[]{firstUuid, secondUuid}));
+ assertEquals(HashUtils.hashUUID(new PrimaryKey(
+ new Object[]{new ByteArray(UuidUtils.toBytes(firstUuid)), new
ByteArray(UuidUtils.toBytes(secondUuid))})),
+ expectedHash);
+ assertEquals(HashUtils.hashUUID(
+ new PrimaryKey(new Object[]{UuidUtils.toBytes(firstUuid),
UuidUtils.toBytes(secondUuid)})), expectedHash);
+ }
+
+ /**
+ * Regression: {@link HashUtils#hashUUID} must accept
non-canonical-but-Java-parseable UUID strings
+ * (e.g. {@code "1-2-3-4-5"}) so existing upsert tables using {@code
HashFunction.UUID} keep producing
+ * the same hash before and after this PR. {@link UuidUtils#toBytes(String)}
is strict and would reject
+ * such inputs — for the legacy hash path we route String inputs through
{@code UUID.fromString} directly
+ * to preserve the lenient pre-PR behavior.
+ */
+ @Test
+ public void testHashUUIDLenientForNonCanonicalStrings() {
+ String nonCanonical = "1-2-3-4-5";
+ UUID expected = UUID.fromString(nonCanonical);
+
+ byte[] hashResult = HashUtils.hashUUID(new PrimaryKey(new
Object[]{nonCanonical}));
+ assertEquals(hashResult.length, 16);
+
+ long msb = 0;
+ long lsb = 0;
+ for (int i = 0; i < 8; i++) {
+ msb = (msb << 8) | (hashResult[i] & 0xFF);
+ }
+ for (int i = 8; i < 16; i++) {
+ lsb = (lsb << 8) | (hashResult[i] & 0xFF);
+ }
+ assertEquals(new UUID(msb, lsb), expected,
+ "hashUUID(\"1-2-3-4-5\") must equal UUID.fromString output for
backward compatibility");
+ }
+
@Test
public void testHashPrimaryKeyWithMd5Disabled() {
PrimaryKey primaryKey = new PrimaryKey(new Object[]{"hello world"});
diff --git
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/creator/BloomFilterCreator.java
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/creator/BloomFilterCreator.java
index 760a2310ece..271188b0831 100644
---
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/creator/BloomFilterCreator.java
+++
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/creator/BloomFilterCreator.java
@@ -23,6 +23,7 @@ import javax.annotation.Nullable;
import org.apache.pinot.segment.spi.index.IndexCreator;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.utils.BytesUtils;
+import org.apache.pinot.spi.utils.UuidUtils;
public interface BloomFilterCreator extends IndexCreator {
@@ -33,6 +34,8 @@ public interface BloomFilterCreator extends IndexCreator {
default void add(Object value, int dictId) {
if (getDataType() == FieldSpec.DataType.BYTES) {
add(BytesUtils.toHexString((byte[]) value));
+ } else if (getDataType() == FieldSpec.DataType.UUID) {
+ add(uuidToCanonicalString(value));
} else {
add(value.toString());
}
@@ -44,12 +47,25 @@ public interface BloomFilterCreator extends IndexCreator {
for (Object value : values) {
add(BytesUtils.toHexString((byte[]) value));
}
+ } else if (getDataType() == FieldSpec.DataType.UUID) {
+ for (Object value : values) {
+ add(uuidToCanonicalString(value));
+ }
} else {
for (Object value : values) {
add(value.toString());
}
}
}
+
+ /// Renders a UUID value (typically a 16-byte big-endian `byte[]` from
segment ingest) as its canonical
+ /// lowercase RFC 4122 string. The `byte[]` fast path just skips the type
dispatch of `UuidUtils.toBytes(Object)`;
+ /// neither path copies the buffer (`UuidUtils.toBytes(byte[])` validates
the width and returns it as-is).
+ private static String uuidToCanonicalString(Object value) {
+ return value instanceof byte[]
+ ? UuidUtils.toString((byte[]) value)
+ : UuidUtils.toString(UuidUtils.toBytes(value));
+ }
/**
* Adds a value to the bloom filter.
*/
diff --git
a/pinot-tools/src/main/java/org/apache/pinot/tools/segment/converter/PinotSegmentToAvroConverter.java
b/pinot-tools/src/main/java/org/apache/pinot/tools/segment/converter/PinotSegmentToAvroConverter.java
index dd0d56d2cf4..a7a6d41dcc5 100644
---
a/pinot-tools/src/main/java/org/apache/pinot/tools/segment/converter/PinotSegmentToAvroConverter.java
+++
b/pinot-tools/src/main/java/org/apache/pinot/tools/segment/converter/PinotSegmentToAvroConverter.java
@@ -56,7 +56,8 @@ public class PinotSegmentToAvroConverter implements
PinotSegmentConverter {
PinotSegmentRecordReader pinotSegmentRecordReader = new
PinotSegmentRecordReader();
pinotSegmentRecordReader.init(indexDir, null, null, false,
_forwardIndexOnly);
try (pinotSegmentRecordReader) {
- try (DataFileWriter<Record> recordWriter = new DataFileWriter<>(new
GenericDatumWriter<>(avroSchema))) {
+ try (DataFileWriter<Record> recordWriter =
+ new DataFileWriter<>(new GenericDatumWriter<>(avroSchema,
SegmentProcessorAvroUtils.getAvroDataModel()))) {
recordWriter.create(avroSchema, new File(_outputFile));
GenericRow row = new GenericRow();
diff --git
a/pinot-tools/src/main/java/org/apache/pinot/tools/segment/converter/PinotSegmentToParquetConverter.java
b/pinot-tools/src/main/java/org/apache/pinot/tools/segment/converter/PinotSegmentToParquetConverter.java
index 8a901d56779..f50ff8e1dd4 100644
---
a/pinot-tools/src/main/java/org/apache/pinot/tools/segment/converter/PinotSegmentToParquetConverter.java
+++
b/pinot-tools/src/main/java/org/apache/pinot/tools/segment/converter/PinotSegmentToParquetConverter.java
@@ -69,6 +69,7 @@ public class PinotSegmentToParquetConverter implements
PinotSegmentConverter {
try (pinotSegmentRecordReader) {
try (ParquetWriter<Record> parquetWriter =
AvroParquetWriter.<Record>builder(outputFile).withSchema(avroSchema)
+ .withDataModel(SegmentProcessorAvroUtils.getAvroDataModel())
.withCompressionCodec(_compressionCodec)
.withConf(hadoopConf).build()) {
GenericRow row = new GenericRow();
diff --git
a/pinot-tools/src/test/java/org/apache/pinot/tools/segment/converter/PinotSegmentConverterTest.java
b/pinot-tools/src/test/java/org/apache/pinot/tools/segment/converter/PinotSegmentConverterTest.java
index bc5244b17f6..666c06a4ba6 100644
---
a/pinot-tools/src/test/java/org/apache/pinot/tools/segment/converter/PinotSegmentConverterTest.java
+++
b/pinot-tools/src/test/java/org/apache/pinot/tools/segment/converter/PinotSegmentConverterTest.java
@@ -36,6 +36,7 @@ import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.data.readers.GenericRow;
import org.apache.pinot.spi.utils.BytesUtils;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
@@ -216,6 +217,62 @@ public class PinotSegmentConverterTest {
}
}
+ private static final String UUID_SV_COLUMN = "uuidSVColumn";
+ private static final String UUID_MV_COLUMN = "uuidMVColumn";
+ private static final String UUID_SV_VALUE =
"12345678-1234-1234-1234-1234567890ab";
+ private static final String UUID_MV_VALUE_1 =
"550e8400-e29b-41d4-a716-446655440000";
+ private static final String UUID_MV_VALUE_2 =
"550e8400-e29b-41d4-a716-446655440001";
+
+ /// Builds a segment with SV + MV UUID columns and converts it through both
the Avro and Parquet converters. UUID is
+ /// stored as a 16-byte value but exported as a string{logicalType:uuid}
field, so this exercises the uuid
+ /// Conversion registered on each writer's data model (getAvroDataModel) —
including the distinct Parquet write path
+ /// (AvroParquetWriter.withDataModel), which a plain writer without the
Conversion could not serialize.
+ @Test
+ public void testUuidConverters()
+ throws Exception {
+ Schema uuidSchema = new
Schema.SchemaBuilder().addSingleValueDimension(UUID_SV_COLUMN, DataType.UUID)
+ .addMultiValueDimension(UUID_MV_COLUMN, DataType.UUID).build();
+ TableConfig uuidTableConfig = new
TableConfigBuilder(TableType.OFFLINE).setTableName("uuidTable").build();
+
+ GenericRow record = new GenericRow();
+ record.putValue(UUID_SV_COLUMN, UUID_SV_VALUE);
+ record.putValue(UUID_MV_COLUMN, new Object[]{UUID_MV_VALUE_1,
UUID_MV_VALUE_2});
+
+ SegmentGeneratorConfig config = new
SegmentGeneratorConfig(uuidTableConfig, uuidSchema);
+ config.setTableName("uuidTable");
+ config.setSegmentName("uuidSegment");
+ config.setOutDir(new File(TEMP_DIR, "uuidSegment").getPath());
+ SegmentIndexCreationDriverImpl driver = new
SegmentIndexCreationDriverImpl();
+ driver.init(config, new GenericRowRecordReader(List.of(record)));
+ driver.build();
+ String segmentDir = driver.getOutputDirectory().getPath();
+
+ File avroOut = new File(TEMP_DIR, "uuidSegment.avro");
+ new PinotSegmentToAvroConverter(segmentDir, avroOut.getPath()).convert();
+ try (AvroRecordReader reader = new AvroRecordReader()) {
+ reader.init(avroOut, uuidSchema.getFieldSpecMap().keySet(), null);
+ assertUuidRecord(reader.next());
+ assertFalse(reader.hasNext());
+ }
+
+ File parquetOut = new File(TEMP_DIR, "uuidSegment.parquet");
+ new PinotSegmentToParquetConverter(segmentDir,
parquetOut.getPath()).convert();
+ try (ParquetRecordReader reader = new ParquetRecordReader()) {
+ reader.init(parquetOut, uuidSchema.getFieldSpecMap().keySet(), null);
+ assertUuidRecord(reader.next());
+ assertFalse(reader.hasNext());
+ }
+ }
+
+ private static void assertUuidRecord(GenericRow record) {
+ // The reader may surface the uuid value as a String/UUID/byte[];
UuidUtils.toBytes normalizes all of them.
+ assertEquals(UuidUtils.toBytes(record.getValue(UUID_SV_COLUMN)),
UuidUtils.toBytes(UUID_SV_VALUE));
+ Object[] mv = (Object[]) record.getValue(UUID_MV_COLUMN);
+ assertEquals(mv.length, 2);
+ assertEquals(UuidUtils.toBytes(mv[0]), UuidUtils.toBytes(UUID_MV_VALUE_1));
+ assertEquals(UuidUtils.toBytes(mv[1]), UuidUtils.toBytes(UUID_MV_VALUE_2));
+ }
+
@AfterClass
public void tearDown()
throws IOException {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]