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 92a0d98d8fd [UUID 1/8] Add logical UUID type foundation (pinot-spi)
(#18869)
92a0d98d8fd is described below
commit 92a0d98d8fd6f3c51aa2fa6acb97aa7dc3bf5e07
Author: Xiang Fu <[email protected]>
AuthorDate: Sun Jul 5 20:55:52 2026 +0200
[UUID 1/8] Add logical UUID type foundation (pinot-spi) (#18869)
Rebased onto master; resolves review feedback on #18869: markdown javadoc +
UuidKey extracted to its own class + dropped per-value null checks and the
defensive byte[] copy in UuidUtils; UUID placed right after its stored type
BYTES in the enum/switches and dropped the fragile ordinal-append hack; removed
the FieldSpec default-null defensive copy; restored fromUUIDBytes
null-on-exception (back-compat); PinotDataType STRING->UUID now trims; added
UUID version/timestamp/generator tests.
Part 1/8 of splitting apache/pinot#18140 (logical UUID type).
---
.../common/function/scalar/StringFunctions.java | 7 +-
.../Homepage/Operations/SchemaComponent.tsx | 4 +-
.../pinot/core/data/manager/TableIndexingTest.java | 5 +-
.../apache/pinot/core/util/SchemaUtilsTest.java | 29 +-
.../parquet/ParquetNativeRecordExtractor.java | 2 +-
.../org/apache/pinot/query/type/TypeFactory.java | 2 +
.../apache/pinot/query/type/TypeFactoryTest.java | 22 ++
.../pinot/segment/local/utils/SchemaUtils.java | 6 +-
.../java/org/apache/pinot/spi/data/FieldSpec.java | 85 ++++-
.../java/org/apache/pinot/spi/data/Schema.java | 11 +-
.../org/apache/pinot/spi/utils/PinotDataType.java | 102 ++++--
.../java/org/apache/pinot/spi/utils/UuidKey.java | 123 +++++++
.../java/org/apache/pinot/spi/utils/UuidUtils.java | 358 ++++++++++++++++++++-
.../org/apache/pinot/spi/data/FieldSpecTest.java | 64 ++++
.../java/org/apache/pinot/spi/data/SchemaTest.java | 31 ++
.../apache/pinot/spi/utils/PinotDataTypeTest.java | 52 ++-
.../org/apache/pinot/spi/utils/UuidUtilsTest.java | 196 +++++++++++
17 files changed, 1020 insertions(+), 79 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
index d3a4c626357..132dc56cea1 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
@@ -455,13 +455,12 @@ public class StringFunctions {
}
/**
- * @param input UUID serialized to bytes
- * @return String representation of UUID
- * returns bytes and null on exception
+ * @param input UUID serialized to its 16-byte form
+ * @return canonical RFC 4122 String representation of the UUID
*/
@ScalarFunction
public static String fromUUIDBytes(byte[] input) {
- return UuidUtils.fromBytes(input).toString();
+ return UuidUtils.toString(input);
}
/**
diff --git
a/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaComponent.tsx
b/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaComponent.tsx
index 68ba35c7613..639d5c71fc5 100644
---
a/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaComponent.tsx
+++
b/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaComponent.tsx
@@ -106,8 +106,8 @@ export default function SchemaComponent({
dateTimeFieldSpecs: []
};
const defaultDataTypeOptions = {
- dimension: ["INT", "LONG", "STRING", "FLOAT", "DOUBLE", "BYTES",
"BOOLEAN", "JSON"],
- metric: ["INT", "LONG", "DOUBLE", "FLOAT", "BYTES"],
+ dimension: ["INT", "LONG", "STRING", "FLOAT", "DOUBLE", "BIG_DECIMAL",
"BYTES", "BOOLEAN", "JSON", "UUID"],
+ metric: ["INT", "LONG", "DOUBLE", "FLOAT", "BIG_DECIMAL", "BYTES"],
datetime: ["STRING", "INT", "LONG", "TIMESTAMP"]
};
const preFilledData = {
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/TableIndexingTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/TableIndexingTest.java
index f2455426be5..21c473f4a75 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/TableIndexingTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/TableIndexingTest.java
@@ -159,7 +159,10 @@ public class TableIndexingTest {
protected void createSchemas() {
for (DataType type : DataType.values()) {
if (type == DataType.UNKNOWN || type == DataType.LIST || type ==
DataType.MAP || type == DataType.STRUCT
- || type == DataType.OPEN_STRUCT) {
+ || type == DataType.OPEN_STRUCT || type == DataType.UUID) {
+ // UUID is excluded because this static expectation matrix
(TableIndexingTest.csv) has no UUID rows.
+ // UUID-specific index behavior (inverted, bloom, range,
dictionary/no-dictionary, SV and MV) is covered by
+ // dedicated UUID unit and integration tests introduced in later PRs
of this stack.
continue;
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/util/SchemaUtilsTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/util/SchemaUtilsTest.java
index 8cbd1f602e1..a335a66fe36 100644
--- a/pinot-core/src/test/java/org/apache/pinot/core/util/SchemaUtilsTest.java
+++ b/pinot-core/src/test/java/org/apache/pinot/core/util/SchemaUtilsTest.java
@@ -291,12 +291,31 @@ public class SchemaUtilsTest {
@Test
public void testValidateMultiValueFieldSpec() {
- Schema pinotSchema = new Schema.SchemaBuilder()
- .setSchemaName(TABLE_NAME)
- .addSingleValueDimension("myCol", DataType.STRING)
- .addMultiValueDimension("myJsonCol", DataType.JSON)
- .build();
+ Schema pinotSchema;
+
+ // JSON MV is rejected by SchemaUtils.validate() — the multi-value
compatibility check lives there
+ // (controller-side ingest validation), not in Schema.validate() (pure
schema DTO validation).
+ pinotSchema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+ .addSingleValueDimension("myCol", FieldSpec.DataType.STRING)
+ .addMultiValueDimension("myJsonCol", FieldSpec.DataType.JSON).build();
checkValidationFails(pinotSchema);
+
+ pinotSchema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+ .addSingleValueDimension("myCol", FieldSpec.DataType.STRING)
+ .addSingleValueDimension("myJsonCol", FieldSpec.DataType.JSON).build();
+ SchemaUtils.validate(pinotSchema);
+
+ pinotSchema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+ .addSingleValueDimension("myCol", FieldSpec.DataType.STRING)
+ .addSingleValueDimension("myBigDecimalCol",
FieldSpec.DataType.BIG_DECIMAL)
+ .addMultiValueDimension("myBigDecimalMvCol",
FieldSpec.DataType.BIG_DECIMAL).build();
+ SchemaUtils.validate(pinotSchema);
+
+ pinotSchema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+ .addSingleValueDimension("myCol", FieldSpec.DataType.STRING)
+ .addSingleValueDimension("myUuidCol", FieldSpec.DataType.UUID)
+ .addMultiValueDimension("myUuidMvCol",
FieldSpec.DataType.UUID).build();
+ SchemaUtils.validate(pinotSchema);
}
@Test
diff --git
a/pinot-plugins/pinot-input-format/pinot-parquet/src/main/java/org/apache/pinot/plugin/inputformat/parquet/ParquetNativeRecordExtractor.java
b/pinot-plugins/pinot-input-format/pinot-parquet/src/main/java/org/apache/pinot/plugin/inputformat/parquet/ParquetNativeRecordExtractor.java
index 3618f01d87c..cbc92e6846d 100644
---
a/pinot-plugins/pinot-input-format/pinot-parquet/src/main/java/org/apache/pinot/plugin/inputformat/parquet/ParquetNativeRecordExtractor.java
+++
b/pinot-plugins/pinot-input-format/pinot-parquet/src/main/java/org/apache/pinot/plugin/inputformat/parquet/ParquetNativeRecordExtractor.java
@@ -196,7 +196,7 @@ public class ParquetNativeRecordExtractor extends
BaseRecordExtractor<Group> {
// `FIXED_LEN_BYTE_ARRAY(16) + UUID` → [UUID] (always converted;
the downstream type
// transformer adapts to the Pinot column's storage type). UUID
wire bytes are big-endian
// per RFC 4122.
- return UuidUtils.fromBytes(binaryBytes);
+ return UuidUtils.toUUID(binaryBytes);
}
return binaryBytes;
default:
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/query/type/TypeFactory.java
b/pinot-query-planner/src/main/java/org/apache/pinot/query/type/TypeFactory.java
index 7a6c120b82d..8718791c02d 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/query/type/TypeFactory.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/query/type/TypeFactory.java
@@ -102,6 +102,8 @@ public class TypeFactory extends JavaTypeFactoryImpl {
case STRING:
case JSON:
return SqlTypeName.VARCHAR;
+ case UUID:
+ return SqlTypeName.UUID;
case BYTES:
return SqlTypeName.VARBINARY;
case BIG_DECIMAL:
diff --git
a/pinot-query-planner/src/test/java/org/apache/pinot/query/type/TypeFactoryTest.java
b/pinot-query-planner/src/test/java/org/apache/pinot/query/type/TypeFactoryTest.java
index 334f4310fa2..52a44627bd7 100644
---
a/pinot-query-planner/src/test/java/org/apache/pinot/query/type/TypeFactoryTest.java
+++
b/pinot-query-planner/src/test/java/org/apache/pinot/query/type/TypeFactoryTest.java
@@ -104,6 +104,10 @@ public class TypeFactoryTest {
basicType = TYPE_FACTORY.createSqlType(SqlTypeName.VARCHAR);
break;
}
+ case UUID: {
+ basicType = TYPE_FACTORY.createSqlType(SqlTypeName.UUID);
+ break;
+ }
case BYTES: {
basicType = TYPE_FACTORY.createSqlType(SqlTypeName.VARBINARY);
break;
@@ -180,6 +184,9 @@ public class TypeFactoryTest {
@Test(dataProvider = "relDataTypeConversion")
public void testArrayTypes(FieldSpec.DataType dataType, RelDataType
arrayType, boolean columnNullMode) {
+ if (dataType == FieldSpec.DataType.BIG_DECIMAL || dataType ==
FieldSpec.DataType.JSON) {
+ return;
+ }
TypeFactory typeFactory = new TypeFactory();
Schema testSchema = new Schema.SchemaBuilder()
.addMultiValueDimension("col", dataType)
@@ -198,6 +205,9 @@ public class TypeFactoryTest {
@Test(dataProvider = "relDataTypeConversion")
public void testNullableArrayTypes(FieldSpec.DataType dataType, RelDataType
arrayType, boolean columnNullMode) {
+ if (dataType == FieldSpec.DataType.BIG_DECIMAL || dataType ==
FieldSpec.DataType.JSON) {
+ return;
+ }
TypeFactory typeFactory = new TypeFactory();
Schema testSchema = new Schema.SchemaBuilder()
.addDimensionField("col", dataType, field -> {
@@ -219,6 +229,9 @@ public class TypeFactoryTest {
@Test(dataProvider = "relDataTypeConversion")
public void testNotNullableArrayTypes(FieldSpec.DataType dataType,
RelDataType arrayType, boolean columnNullMode) {
+ if (dataType == FieldSpec.DataType.BIG_DECIMAL || dataType ==
FieldSpec.DataType.JSON) {
+ return;
+ }
TypeFactory typeFactory = new TypeFactory();
Schema testSchema = new Schema.SchemaBuilder()
.addDimensionField("col", dataType, field -> {
@@ -244,6 +257,7 @@ public class TypeFactoryTest {
.addSingleValueDimension("FLOAT_COL", FieldSpec.DataType.FLOAT)
.addSingleValueDimension("DOUBLE_COL", FieldSpec.DataType.DOUBLE)
.addSingleValueDimension("STRING_COL", FieldSpec.DataType.STRING)
+ .addSingleValueDimension("UUID_COL", FieldSpec.DataType.UUID)
.addSingleValueDimension("BYTES_COL", FieldSpec.DataType.BYTES)
.addSingleValueDimension("JSON_COL", FieldSpec.DataType.JSON)
.addMultiValueDimension("INT_ARRAY_COL", FieldSpec.DataType.INT)
@@ -251,6 +265,7 @@ public class TypeFactoryTest {
.addMultiValueDimension("FLOAT_ARRAY_COL", FieldSpec.DataType.FLOAT)
.addMultiValueDimension("DOUBLE_ARRAY_COL", FieldSpec.DataType.DOUBLE)
.addMultiValueDimension("STRING_ARRAY_COL", FieldSpec.DataType.STRING)
+ .addMultiValueDimension("UUID_ARRAY_COL", FieldSpec.DataType.UUID)
.addMultiValueDimension("BYTES_ARRAY_COL", FieldSpec.DataType.BYTES)
.build();
RelDataType relDataTypeFromSchema =
typeFactory.createRelDataTypeFromSchema(testSchema);
@@ -283,6 +298,9 @@ public class TypeFactoryTest {
TYPE_FACTORY.createTypeWithCharsetAndCollation(new
BasicSqlType(TypeSystem.INSTANCE, SqlTypeName.VARCHAR),
StandardCharsets.UTF_8, SqlCollation.IMPLICIT));
break;
+ case "UUID_COL":
+ Assert.assertEquals(field.getType(), new
BasicSqlType(TypeSystem.INSTANCE, SqlTypeName.UUID));
+ break;
case "BYTES_COL":
Assert.assertEquals(field.getType(), new
BasicSqlType(TypeSystem.INSTANCE, SqlTypeName.VARBINARY));
break;
@@ -307,6 +325,10 @@ public class TypeFactoryTest {
TYPE_FACTORY.createTypeWithCharsetAndCollation(new
BasicSqlType(TypeSystem.INSTANCE, SqlTypeName.VARCHAR),
StandardCharsets.UTF_8, SqlCollation.IMPLICIT), false));
break;
+ case "UUID_ARRAY_COL":
+ Assert.assertEquals(field.getType(),
+ new ArraySqlType(new BasicSqlType(TypeSystem.INSTANCE,
SqlTypeName.UUID), false));
+ break;
case "BYTES_ARRAY_COL":
Assert.assertEquals(field.getType(),
new ArraySqlType(new BasicSqlType(TypeSystem.INSTANCE,
SqlTypeName.VARBINARY), false));
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SchemaUtils.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SchemaUtils.java
index b4ae42641c0..7bcfbc053b6 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SchemaUtils.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/SchemaUtils.java
@@ -182,9 +182,9 @@ public class SchemaUtils {
fieldSpec.getName());
}
- /**
- * Validations for MV type columns
- */
+ /// Validations for MV type columns. Kept here (rather than in
[Schema#validate()]) so that schema construction
+ /// via `SchemaBuilder.build()` stays a pure DTO operation and only the
controller-side ingest validation rejects
+ /// MV JSON columns.
private static void validateMultiValueCompatibility(FieldSpec fieldSpec) {
Preconditions.checkState(!fieldSpec.getDataType().equals(FieldSpec.DataType.JSON),
"JSON columns cannot be of multi-value type");
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/data/FieldSpec.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/FieldSpec.java
index 433ed760823..511d2674435 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/data/FieldSpec.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/data/FieldSpec.java
@@ -37,6 +37,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.UUID;
import javax.annotation.Nullable;
import org.apache.pinot.spi.utils.BooleanUtils;
import org.apache.pinot.spi.utils.ByteArray;
@@ -44,6 +45,7 @@ import org.apache.pinot.spi.utils.BytesUtils;
import org.apache.pinot.spi.utils.EqualityUtils;
import org.apache.pinot.spi.utils.JsonUtils;
import org.apache.pinot.spi.utils.TimestampUtils;
+import org.apache.pinot.spi.utils.UuidUtils;
/**
@@ -418,7 +420,7 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
}
public String getDefaultNullValueString() {
- return getStringValue(_defaultNullValue);
+ return _dataType != null ? _dataType.toString(_defaultNullValue) :
getStringValue(_defaultNullValue);
}
/// Returns the [String] representation of the given object.
@@ -460,13 +462,22 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
@JsonIgnore
public void setDefaultNullValue(@Nullable Object defaultNullValue) {
if (defaultNullValue != null) {
- _stringDefaultNullValue = getStringValue(defaultNullValue);
+ _stringDefaultNullValue = getStringDefaultNullValue(defaultNullValue);
}
if (_dataType != null) {
_defaultNullValue = getDefaultNullValue(getFieldType(), _dataType,
_stringDefaultNullValue);
}
}
+ // UUID default values must stringify to their canonical 8-4-4-4-12 form
(not raw hex) so they round-trip back
+ // through getDefaultNullValue(...); all other types keep the generic
getStringValue() form.
+ private String getStringDefaultNullValue(Object defaultNullValue) {
+ if (_dataType == DataType.UUID && !(defaultNullValue instanceof String)) {
+ return _dataType.toString(defaultNullValue);
+ }
+ return getStringValue(defaultNullValue);
+ }
+
public static Object getDefaultNullValue(FieldType fieldType, DataType
dataType,
@Nullable String stringDefaultNullValue) {
if (stringDefaultNullValue != null) {
@@ -514,6 +525,10 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
return DEFAULT_DIMENSION_NULL_VALUE_OF_JSON;
case BYTES:
return DEFAULT_DIMENSION_NULL_VALUE_OF_BYTES;
+ case UUID:
+ // The nil UUID is the default null sentinel. Tables that ingest
nil UUID as a real value should enable
+ // column-based null handling to distinguish it from null rows.
+ return UuidUtils.nullUuidBytes();
case BIG_DECIMAL:
return DEFAULT_DIMENSION_NULL_VALUE_OF_BIG_DECIMAL;
default:
@@ -668,6 +683,9 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
case BYTES:
jsonNode.put(key, BytesUtils.toHexString((byte[])
_defaultNullValue));
break;
+ case UUID:
+ jsonNode.put(key, UuidUtils.toString((byte[]) _defaultNullValue));
+ break;
case MAP:
case OPEN_STRUCT:
case LIST:
@@ -747,6 +765,8 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
STRING(false),
JSON(STRING, false),
BYTES(false),
+ // UUID is a logical type stored as fixed-width 16-byte BYTES, kept right
after its stored type BYTES.
+ UUID(BYTES, UuidUtils.UUID_NUM_BYTES, false),
STRUCT(false),
MAP(false),
OPEN_STRUCT(false),
@@ -775,6 +795,13 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
_numeric = numeric;
}
+ // Logical type stored as another type with an explicit fixed size (e.g.
UUID stored as fixed 16-byte BYTES).
+ DataType(DataType storedType, int size, boolean numeric) {
+ _storedType = storedType;
+ _size = size;
+ _numeric = numeric;
+ }
+
/**
* Returns the data type stored in Pinot.
* <p>Pinot internally stores data (physical) in INT, LONG, FLOAT, DOUBLE,
STRING, BYTES type, other data types
@@ -819,7 +846,7 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
}
/**
- * Converts the given string value to the data type. Returns byte[] for
BYTES.
+ * Converts the given string value to the data type. Returns byte[] for
BYTES and UUID.
*/
public Object convert(String value) {
try {
@@ -843,6 +870,8 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
return value;
case BYTES:
return BytesUtils.toBytes(value);
+ case UUID:
+ return UuidUtils.toBytes(value);
case MAP:
case OPEN_STRUCT:
return JsonUtils.stringToObject(value, Map.class);
@@ -852,16 +881,23 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
throw new IllegalStateException();
}
} catch (Exception e) {
- throw new IllegalArgumentException("Cannot convert value: '" + value +
"' to type: " + this);
+ throw new IllegalArgumentException(
+ "Cannot convert value: '" + value + "' to type: " + this + " (" +
e.getMessage() + ")", e);
}
}
public boolean equals(Object value1, Object value2) {
- return this == BYTES ? Arrays.equals((byte[]) value1, (byte[]) value2) :
value1.equals(value2);
+ if (this == UUID) {
+ return UuidUtils.equals(toBytesValue(value1), toBytesValue(value2));
+ }
+ return this == BYTES ? Arrays.equals(toBytesValue(value1),
toBytesValue(value2)) : value1.equals(value2);
}
public int hashCode(Object value) {
- return this == BYTES ? Arrays.hashCode((byte[]) value) :
value.hashCode();
+ if (this == UUID) {
+ return UuidUtils.hashCode(toBytesValue(value));
+ }
+ return this == BYTES ? Arrays.hashCode(toBytesValue(value)) :
value.hashCode();
}
/**
@@ -891,7 +927,9 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
case JSON:
return ((String) value1).compareTo((String) value2);
case BYTES:
- return ByteArray.compare((byte[]) value1, (byte[]) value2);
+ return ByteArray.compare(toBytesValue(value1), toBytesValue(value2));
+ case UUID:
+ return UuidUtils.compare(toBytesValue(value1), toBytesValue(value2));
case MAP:
case OPEN_STRUCT:
case LIST:
@@ -902,14 +940,20 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
}
/**
- * Converts the given value of the data type to string.The input value for
BYTES type should be byte[].
+ * Converts the given value of the data type to string. The input value
for BYTES/UUID should be byte[].
*/
public String toString(Object value) {
if (this == BIG_DECIMAL) {
return ((BigDecimal) value).toPlainString();
}
+ if (this == UUID) {
+ if (value instanceof UUID) {
+ return UuidUtils.toString((UUID) value);
+ }
+ return UuidUtils.toString(toBytesValue(value));
+ }
if (this == BYTES) {
- return BytesUtils.toHexString((byte[]) value);
+ return BytesUtils.toHexString(toBytesValue(value));
}
if (this == MAP || this == OPEN_STRUCT || this == LIST) {
try {
@@ -922,7 +966,7 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
}
/**
- * Converts the given string value to the data type. Returns ByteArray for
BYTES.
+ * Converts the given string value to the data type. Returns ByteArray for
BYTES and UUID.
*/
public Comparable convertInternal(String value) {
try {
@@ -946,6 +990,8 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
return value;
case BYTES:
return BytesUtils.toByteArray(value);
+ case UUID:
+ return new ByteArray(UuidUtils.toBytes(value));
case MAP:
case OPEN_STRUCT:
case LIST:
@@ -954,8 +1000,25 @@ public abstract class FieldSpec implements
Comparable<FieldSpec>, Serializable {
throw new IllegalStateException();
}
} catch (Exception e) {
- throw new IllegalArgumentException("Cannot convert value: '" + value +
"' to type: " + this);
+ throw new IllegalArgumentException(
+ "Cannot convert value: '" + value + "' to type: " + this + " (" +
e.getMessage() + ")", e);
+ }
+ }
+
+ private byte[] toBytesValue(Object value) {
+ if (value instanceof byte[]) {
+ return (byte[]) value;
+ }
+ if (value instanceof ByteArray) {
+ return ((ByteArray) value).getBytes();
+ }
+ if (value instanceof UUID) {
+ return UuidUtils.toBytes((UUID) value);
+ }
+ if (value instanceof String && this == UUID) {
+ return UuidUtils.toBytes((String) value);
}
+ throw new IllegalArgumentException("Unsupported value for " + this + ":
" + value);
}
}
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/data/Schema.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/Schema.java
index 55319f73ed5..c20180446b6 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/data/Schema.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/data/Schema.java
@@ -134,6 +134,7 @@ public final class Schema implements Serializable {
case STRING:
case JSON:
case BYTES:
+ case UUID:
break;
default:
throw new IllegalStateException(
@@ -617,10 +618,14 @@ public final class Schema implements Serializable {
* Validates a pinot schema.
* <p>The following validations are performed:
* <ul>
- * <li>For dimension, time, date time fields, support {@link DataType}:
INT, LONG, FLOAT, DOUBLE, BOOLEAN,
- * TIMESTAMP, STRING, BYTES</li>
- * <li>For metric fields, support {@link DataType}: INT, LONG, FLOAT,
DOUBLE, BYTES</li>
+ * <li>For dimension, time, date time fields, support {@link DataType}:
INT, LONG, FLOAT, DOUBLE, BIG_DECIMAL,
+ * BOOLEAN, TIMESTAMP, STRING, JSON, UUID, BYTES</li>
+ * <li>For metric fields, support {@link DataType}: INT, LONG, FLOAT,
DOUBLE, BIG_DECIMAL, BYTES</li>
* </ul>
+ * <p>Note: multi-value compatibility checks (e.g. rejecting MV JSON
columns) live in
+ * {@code SchemaUtils.validateMultiValueCompatibility} so that schema
construction via
+ * {@link SchemaBuilder#build()} stays a pure DTO operation; controller-side
ingest validation continues
+ * to call {@code SchemaUtils.validate(Schema)} which enforces those
constraints.
*/
public void validate() {
for (FieldSpec fieldSpec : _fieldSpecMap.values()) {
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/PinotDataType.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/PinotDataType.java
index 9360194d7a0..15a950000e0 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/PinotDataType.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/PinotDataType.java
@@ -819,6 +819,12 @@ public enum PinotDataType {
return BytesUtils.toBytes(value.toString().trim());
}
+ @Override
+ public UUID toUUID(Object value) {
+ // Trim to match the other STRING->primitive conversions in this enum
(and UUID.convert(..., STRING)).
+ return UuidUtils.toUUID(value.toString().trim());
+ }
+
@Override
public String convert(Object value, PinotDataType sourceType) {
return sourceType.toString(value);
@@ -936,14 +942,18 @@ public enum PinotDataType {
return (byte[]) value;
}
+ @Override
+ public UUID toUUID(Object value) {
+ return UuidUtils.toUUID(value);
+ }
+
@Override
public Object convert(Object value, PinotDataType sourceType) {
return sourceType.toBytes(value);
}
},
- /// Wraps [UUID]. Internal representation is the canonical String form (e.g.
- /// `"550e8400-e29b-41d4-a716-446655440000"`).
+ /// Wraps [UUID]. Internal representation is the 16-byte big-endian binary
form.
///
/// When converting from UUID to other types:
/// - String / JSON: canonical UUID string
@@ -953,9 +963,6 @@ public enum PinotDataType {
/// - String / JSON: parsed via [UUID#fromString]
/// - BYTES (length 16): decoded big-endian
///
- /// Pinot has no UUID storage type — declaring the schema column as `STRING`
produces the canonical
- /// form, declaring it as `BYTES` produces the 16-byte big-endian form.
Decoders / extractors emit
- /// [UUID] uniformly and `PinotDataType` adapts at the type-transformer
boundary.
UUID {
@Override
public int toInt(Object value) {
@@ -994,21 +1001,26 @@ public enum PinotDataType {
@Override
public String toString(Object value) {
- return value.toString();
+ return UuidUtils.toUUID(value).toString();
}
@Override
public byte[] toBytes(Object value) {
- return UuidUtils.toBytes((UUID) value);
+ return UuidUtils.toBytes(value);
+ }
+
+ @Override
+ public UUID toUUID(Object value) {
+ return UuidUtils.toUUID(value);
}
@Override
public UUID convert(Object value, PinotDataType sourceType) {
switch (sourceType) {
case UUID:
- return (UUID) value;
+ return UuidUtils.toUUID(value);
case STRING:
- return java.util.UUID.fromString(value.toString().trim());
+ return UuidUtils.toUUID(value.toString().trim());
case JSON:
try {
// JSON-encoded UUID is a quoted JSON string; Jackson's
UUIDDeserializer handles both the
@@ -1018,15 +1030,28 @@ public enum PinotDataType {
throw new RuntimeException("Cannot parse JSON value as UUID: " +
value, e);
}
case BYTES:
- return UuidUtils.fromBytes((byte[]) value);
+ return UuidUtils.toUUID(value);
+ case OBJECT:
+ case COLLECTION:
+ case UUID_ARRAY:
+ case BYTES_ARRAY:
+ case STRING_ARRAY:
+ case OBJECT_ARRAY:
+ // OBJECT, collections and single-element arrays can carry a UUID
(e.g., scalar-function arguments
+ // resolved by FunctionInvoker, or an MV source feeding an SV UUID
column); these types have meaningful
+ // toUUID implementations.
+ return sourceType.toUUID(value);
default:
- throw new UnsupportedOperationException("Cannot convert value from "
+ sourceType + " to UUID");
+ // Numeric and other types have no meaningful UUID interpretation;
fail with an explicit message instead
+ // of the confusing "There is no single-value type ..." from the
generic single-value fallback.
+ throw new UnsupportedOperationException(
+ "Cannot convert value from " + sourceType + " to UUID. Input
value: " + value);
}
}
@Override
- public String toInternal(Object value) {
- return value.toString();
+ public byte[] toInternal(Object value) {
+ return UuidUtils.toBytes(value);
}
},
@@ -1076,6 +1101,11 @@ public enum PinotDataType {
throw new UnsupportedOperationException("Cannot convert value from
OBJECT to BYTES");
}
+ @Override
+ public UUID toUUID(Object value) {
+ return UuidUtils.toUUID(value);
+ }
+
@Override
public Object convert(Object value, PinotDataType sourceType) {
return value;
@@ -1310,23 +1340,15 @@ public enum PinotDataType {
}
},
- /// MV companion to [#UUID]; element type [UUID]. Pinot has no UUID storage
type — declaring the column as MV STRING
- /// produces canonical-form strings, MV BYTES produces 16-byte big-endian.
UUID_ARRAY {
@Override
- public UUID[] convert(Object value, PinotDataType sourceType) {
- return sourceType.toUuidArray(value);
+ public byte[][] convert(Object value, PinotDataType sourceType) {
+ return sourceType.toUuidBytesArray(value);
}
@Override
- public String[] toInternal(Object value) {
- UUID[] uuidArray = (UUID[]) value;
- int length = uuidArray.length;
- String[] result = new String[length];
- for (int i = 0; i < length; i++) {
- result[i] = uuidArray[i].toString();
- }
- return result;
+ public byte[][] toInternal(Object value) {
+ return toUuidBytesArray(value);
}
},
@@ -1397,6 +1419,10 @@ public enum PinotDataType {
return getSingleValueType().toBytes(toObjectArray(value)[0]);
}
+ public UUID toUUID(Object value) {
+ return getSingleValueType().toUUID(toObjectArray(value)[0]);
+ }
+
public int[] toPrimitiveIntArray(Object value) {
if (value instanceof int[]) {
return (int[]) value;
@@ -1681,6 +1707,28 @@ public enum PinotDataType {
}
}
+ public byte[][] toUuidBytesArray(Object value) {
+ if (value instanceof byte[][]) {
+ byte[][] values = (byte[][]) value;
+ byte[][] uuidBytes = new byte[values.length][];
+ for (int i = 0; i < values.length; i++) {
+ uuidBytes[i] = UuidUtils.toBytes(values[i]);
+ }
+ return uuidBytes;
+ }
+ if (isSingleValue()) {
+ return new byte[][]{UuidUtils.toBytes(value)};
+ } else {
+ Object[] valueArray = toObjectArray(value);
+ int length = valueArray.length;
+ byte[][] uuidBytes = new byte[length][];
+ for (int i = 0; i < length; i++) {
+ uuidBytes[i] = UuidUtils.toBytes(valueArray[i]);
+ }
+ return uuidBytes;
+ }
+ }
+
public LocalDate[] toLocalDateArray(Object value) {
if (value instanceof LocalDate[]) {
return (LocalDate[]) value;
@@ -1769,8 +1817,10 @@ public enum PinotDataType {
/// Converts to the internal representation of the value.
/// - `BOOLEAN` → `Integer` (0/1)
/// - `TIMESTAMP` → `Long` (epoch millis)
+ /// - `UUID` → `byte[]` (16-byte big-endian)
/// - `PRIMITIVE_BOOLEAN_ARRAY` / `BOOLEAN_ARRAY` → `Integer[]` (per-element
0/1)
/// - `TIMESTAMP_ARRAY` → `Long[]` (per-element epoch millis)
+ /// - `UUID_ARRAY` → `byte[][]` (per-element 16-byte big-endian)
public Object toInternal(Object value) {
return value;
}
@@ -1974,6 +2024,8 @@ public enum PinotDataType {
return JSON;
}
throw new IllegalStateException("There is no multi-value type for
JSON");
+ case UUID:
+ return fieldSpec.isSingleValueField() ? UUID : UUID_ARRAY;
case BYTES:
return fieldSpec.isSingleValueField() ? BYTES : BYTES_ARRAY;
case MAP:
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/UuidKey.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/UuidKey.java
new file mode 100644
index 00000000000..4184d680fca
--- /dev/null
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/UuidKey.java
@@ -0,0 +1,123 @@
+/**
+ * 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.spi.utils;
+
+import java.util.UUID;
+
+
+/// Immutable UUID key optimized for Pinot execution hot paths.
+///
+/// The key stores UUID values as two primitive longs to avoid repeated
[ByteArray] allocation and byte-by-byte
+/// equality/hashing while preserving Pinot's canonical 16-byte representation
at the edges.
+public final class UuidKey implements Comparable<UuidKey> {
+ private final long _mostSignificantBits;
+ private final long _leastSignificantBits;
+ private final int _hashCode;
+
+ private UuidKey(long mostSignificantBits, long leastSignificantBits) {
+ _mostSignificantBits = mostSignificantBits;
+ _leastSignificantBits = leastSignificantBits;
+ _hashCode = UuidUtils.hashCode(mostSignificantBits, leastSignificantBits);
+ }
+
+ public static UuidKey fromLongs(long mostSignificantBits, long
leastSignificantBits) {
+ return new UuidKey(mostSignificantBits, leastSignificantBits);
+ }
+
+ public static UuidKey fromBytes(byte[] uuidBytes) {
+ return fromLongs(UuidUtils.getMostSignificantBits(uuidBytes),
UuidUtils.getLeastSignificantBits(uuidBytes));
+ }
+
+ public static UuidKey fromByteArray(ByteArray uuidBytes) {
+ return fromBytes(uuidBytes.getBytes());
+ }
+
+ public static UuidKey fromUUID(UUID uuid) {
+ return fromLongs(uuid.getMostSignificantBits(),
uuid.getLeastSignificantBits());
+ }
+
+ public static UuidKey fromObject(Object value) {
+ if (value instanceof UuidKey) {
+ return (UuidKey) value;
+ }
+ if (value instanceof UUID) {
+ return fromUUID((UUID) value);
+ }
+ if (value instanceof byte[]) {
+ return fromBytes((byte[]) value);
+ }
+ if (value instanceof ByteArray) {
+ return fromByteArray((ByteArray) value);
+ }
+ if (value instanceof CharSequence) {
+ return fromUUID(UuidUtils.toUUID(value.toString()));
+ }
+ throw new IllegalArgumentException(
+ "Cannot convert value: '" + value + "' to UUID key, unsupported type:
" + value.getClass());
+ }
+
+ public long getMostSignificantBits() {
+ return _mostSignificantBits;
+ }
+
+ public long getLeastSignificantBits() {
+ return _leastSignificantBits;
+ }
+
+ public byte[] toBytes() {
+ return UuidUtils.toBytes(_mostSignificantBits, _leastSignificantBits);
+ }
+
+ public ByteArray toByteArray() {
+ return new ByteArray(toBytes());
+ }
+
+ public UUID toUUID() {
+ return UuidUtils.toUUID(_mostSignificantBits, _leastSignificantBits);
+ }
+
+ @Override
+ public int compareTo(UuidKey other) {
+ return UuidUtils.compare(_mostSignificantBits, _leastSignificantBits,
other._mostSignificantBits,
+ other._leastSignificantBits);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof UuidKey)) {
+ return false;
+ }
+ UuidKey otherUuidKey = (UuidKey) other;
+ return UuidUtils.equals(_mostSignificantBits, _leastSignificantBits,
otherUuidKey._mostSignificantBits,
+ otherUuidKey._leastSignificantBits);
+ }
+
+ @Override
+ public int hashCode() {
+ return _hashCode;
+ }
+
+ @Override
+ public String toString() {
+ return toUUID().toString();
+ }
+}
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/UuidUtils.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/UuidUtils.java
index d730a581b79..86365a49d08 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/UuidUtils.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/UuidUtils.java
@@ -18,32 +18,360 @@
*/
package org.apache.pinot.spi.utils;
-import java.nio.ByteBuffer;
import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
-/// Utilities for the 16-byte big-endian binary form of [UUID] (RFC 4122).
Bytes 0..7 are the
-/// most-significant 64 bits, bytes 8..15 are the least-significant 64 bits.
+/// Utilities for Pinot's logical UUID type.
+///
+/// UUID values are externally represented as canonical lowercase RFC 4122
strings and internally represented as
+/// fixed-width 16-byte values. Methods here are invoked on a per-value basis
on hot paths, so callers must not pass
+/// `null` values (there are no defensive null checks).
public class UuidUtils {
private UuidUtils() {
}
- /// Converts `uuid` to its 16-byte big-endian RFC 4122 form.
+ public static final int UUID_NUM_BYTES = 16;
+ private static final byte[] NULL_UUID_BYTES = new byte[UUID_NUM_BYTES];
+
+ // Gregorian-to-Unix offset in 100-nanosecond units. Matches RFC 4122 / RFC
9562.
+ // This is the count of 100-ns intervals between 1582-10-15T00:00:00Z and
1970-01-01T00:00:00Z.
+ private static final long GREGORIAN_TO_UNIX_OFFSET_100NS =
0x01b21dd213814000L;
+
+ public static byte[] nullUuidBytes() {
+ byte[] uuidBytes = new byte[UUID_NUM_BYTES];
+ System.arraycopy(NULL_UUID_BYTES, 0, uuidBytes, 0, UUID_NUM_BYTES);
+ return uuidBytes;
+ }
+
+ public static byte[] toBytes(long mostSignificantBits, long
leastSignificantBits) {
+ byte[] uuidBytes = new byte[UUID_NUM_BYTES];
+ writeLong(uuidBytes, 0, mostSignificantBits);
+ writeLong(uuidBytes, Long.BYTES, leastSignificantBits);
+ return uuidBytes;
+ }
+
public static byte[] toBytes(UUID uuid) {
- return ByteBuffer.allocate(16)
- .putLong(uuid.getMostSignificantBits())
- .putLong(uuid.getLeastSignificantBits())
- .array();
+ return toBytes(uuid.getMostSignificantBits(),
uuid.getLeastSignificantBits());
}
- /// Converts a 16-byte big-endian RFC 4122 form back to [UUID]. Throws
[IllegalArgumentException] if
- /// `bytes` is not exactly 16 bytes long.
- public static UUID fromBytes(byte[] bytes) {
- if (bytes.length != 16) {
+ public static byte[] toBytes(String uuidString) {
+ UUID uuid;
+ try {
+ uuid = UUID.fromString(uuidString);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid UUID value: '" + uuidString
+ "'", e);
+ }
+ String canonical = uuid.toString();
+ if (!canonical.equalsIgnoreCase(uuidString)) {
throw new IllegalArgumentException(
- "Cannot convert byte[" + bytes.length + "] to UUID; expected length
16");
+ "Invalid UUID value: '" + uuidString + "'. Expected RFC 4122 format:
" + canonical);
+ }
+ return toBytes(uuid);
+ }
+
+ public static byte[] toBytes(byte[] uuidBytes) {
+ // Caller owns the buffer; validate the width and return it as-is (no
defensive copy).
+ validateLength(uuidBytes);
+ return uuidBytes;
+ }
+
+ public static byte[] toBytes(ByteArray uuidBytes) {
+ return toBytes(uuidBytes.getBytes());
+ }
+
+ public static byte[] toBytes(Object value) {
+ if (value instanceof UUID) {
+ return toBytes((UUID) value);
+ }
+ if (value instanceof byte[]) {
+ return toBytes((byte[]) value);
}
- ByteBuffer buf = ByteBuffer.wrap(bytes);
- return new UUID(buf.getLong(), buf.getLong());
+ if (value instanceof ByteArray) {
+ return toBytes((ByteArray) value);
+ }
+ if (value instanceof CharSequence) {
+ return toBytes(value.toString());
+ }
+ throw new IllegalArgumentException(
+ "Cannot convert value: '" + value + "' to UUID bytes, unsupported
type: " + value.getClass());
+ }
+
+ public static UUID toUUID(byte[] uuidBytes) {
+ validateLength(uuidBytes);
+ return toUUID(getMostSignificantBitsInternal(uuidBytes),
getLeastSignificantBitsInternal(uuidBytes));
+ }
+
+ /// Alias of [#toUUID(byte[])], retained for binary compatibility with
existing callers.
+ public static UUID fromBytes(byte[] uuidBytes) {
+ return toUUID(uuidBytes);
+ }
+
+ public static UUID toUUID(ByteArray uuidBytes) {
+ return toUUID(uuidBytes.getBytes());
+ }
+
+ public static UUID toUUID(String uuidString) {
+ return toUUID(toBytes(uuidString));
+ }
+
+ public static UUID toUUID(long mostSignificantBits, long
leastSignificantBits) {
+ return new UUID(mostSignificantBits, leastSignificantBits);
+ }
+
+ public static UUID toUUID(Object value) {
+ if (value instanceof UUID) {
+ return (UUID) value;
+ }
+ if (value instanceof byte[]) {
+ return toUUID((byte[]) value);
+ }
+ if (value instanceof ByteArray) {
+ return toUUID((ByteArray) value);
+ }
+ if (value instanceof CharSequence) {
+ return toUUID(value.toString());
+ }
+ throw new IllegalArgumentException(
+ "Cannot convert value: '" + value + "' to UUID, unsupported type: " +
value.getClass());
+ }
+
+ public static String toString(byte[] uuidBytes) {
+ return toUUID(uuidBytes).toString();
+ }
+
+ public static String toString(ByteArray uuidBytes) {
+ return toString(uuidBytes.getBytes());
+ }
+
+ public static String toString(UUID uuid) {
+ return uuid.toString();
+ }
+
+ public static String toString(long mostSignificantBits, long
leastSignificantBits) {
+ return toUUID(mostSignificantBits, leastSignificantBits).toString();
+ }
+
+ public static long getMostSignificantBits(byte[] uuidBytes) {
+ validateLength(uuidBytes);
+ return getMostSignificantBitsInternal(uuidBytes);
+ }
+
+ public static long getLeastSignificantBits(byte[] uuidBytes) {
+ validateLength(uuidBytes);
+ return getLeastSignificantBitsInternal(uuidBytes);
+ }
+
+ public static boolean equals(byte[] left, byte[] right) {
+ if (left == right) {
+ return true;
+ }
+ validateLength(left);
+ validateLength(right);
+ return getMostSignificantBitsInternal(left) ==
getMostSignificantBitsInternal(right)
+ && getLeastSignificantBitsInternal(left) ==
getLeastSignificantBitsInternal(right);
+ }
+
+ public static boolean equals(ByteArray left, ByteArray right) {
+ if (left == right) {
+ return true;
+ }
+ return equals(left.getBytes(), right.getBytes());
+ }
+
+ public static boolean equals(long leftMostSignificantBits, long
leftLeastSignificantBits,
+ long rightMostSignificantBits, long rightLeastSignificantBits) {
+ return leftMostSignificantBits == rightMostSignificantBits &&
leftLeastSignificantBits == rightLeastSignificantBits;
+ }
+
+ public static int hashCode(byte[] uuidBytes) {
+ validateLength(uuidBytes);
+ return hashCode(getMostSignificantBitsInternal(uuidBytes),
getLeastSignificantBitsInternal(uuidBytes));
+ }
+
+ public static int hashCode(ByteArray uuidBytes) {
+ return hashCode(uuidBytes.getBytes());
+ }
+
+ public static int hashCode(long mostSignificantBits, long
leastSignificantBits) {
+ return updateHash(updateHash(1, mostSignificantBits),
leastSignificantBits);
+ }
+
+ public static int compare(byte[] left, byte[] right) {
+ if (left == right) {
+ return 0;
+ }
+ validateLength(left);
+ validateLength(right);
+ return compare(getMostSignificantBitsInternal(left),
getLeastSignificantBitsInternal(left),
+ getMostSignificantBitsInternal(right),
getLeastSignificantBitsInternal(right));
+ }
+
+ public static int compare(ByteArray left, ByteArray right) {
+ if (left == right) {
+ return 0;
+ }
+ return compare(left.getBytes(), right.getBytes());
+ }
+
+ public static int compare(long leftMostSignificantBits, long
leftLeastSignificantBits, long rightMostSignificantBits,
+ long rightLeastSignificantBits) {
+ int mostSignificantBitsComparison =
Long.compareUnsigned(leftMostSignificantBits, rightMostSignificantBits);
+ if (mostSignificantBitsComparison != 0) {
+ return mostSignificantBitsComparison;
+ }
+ return Long.compareUnsigned(leftLeastSignificantBits,
rightLeastSignificantBits);
+ }
+
+ public static boolean isUuid(String uuidString) {
+ if (uuidString == null) {
+ return false;
+ }
+ try {
+ toBytes(uuidString);
+ return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ public static boolean isUuid(byte[] uuidBytes) {
+ return uuidBytes != null && uuidBytes.length == UUID_NUM_BYTES;
+ }
+
+ public static boolean isUuid(ByteArray uuidBytes) {
+ return uuidBytes != null && isUuid(uuidBytes.getBytes());
+ }
+
+ public static boolean isUuid(Object value) {
+ if (value == null) {
+ return false;
+ }
+ if (value instanceof UUID) {
+ return true;
+ }
+ if (value instanceof byte[]) {
+ return isUuid((byte[]) value);
+ }
+ if (value instanceof ByteArray) {
+ return isUuid((ByteArray) value);
+ }
+ if (value instanceof CharSequence) {
+ return isUuid(value.toString());
+ }
+ return false;
+ }
+
+ /// Returns a random RFC 4122 version-4 UUID. Equivalent to
[UUID#randomUUID()] and uses the same
+ /// cryptographically-strong source of randomness.
+ public static UUID randomV4() {
+ return UUID.randomUUID();
+ }
+
+ /// Returns a random RFC 9562 version-7 UUID. The leading 48 bits encode the
current Unix time in milliseconds
+ /// (big-endian), making v7 UUIDs k-sortable and friendly to time-ordered
storage. Within a single millisecond,
+ /// 74 bits of randomness disambiguate concurrent generations; this
implementation does not guarantee strict
+ /// monotonic ordering within the same millisecond.
+ public static UUID randomV7() {
+ long unixMillis = System.currentTimeMillis() & 0xFFFFFFFFFFFFL;
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+ // MSB layout: [unix_ts_ms (48)] [ver=0b0111 (4)] [rand_a (12)]
+ long msb = (unixMillis << 16) | 0x7000L | (random.nextLong() & 0x0FFFL);
+ // LSB layout: [var=0b10 (2)] [rand_b (62)]
+ long lsb = 0x8000000000000000L | (random.nextLong() & 0x3FFFFFFFFFFFFFFFL);
+ return new UUID(msb, lsb);
+ }
+
+ /// Returns the 4-bit version field of the UUID (0-15). Common values: 1
(Gregorian time-based), 3 (MD5
+ /// name-based), 4 (random), 5 (SHA-1 name-based), 6 (reordered Gregorian
time-based), 7 (Unix time-based),
+ /// 8 (custom). The nil UUID returns 0.
+ public static int getVersion(UUID uuid) {
+ return (int) ((uuid.getMostSignificantBits() >>> 12) & 0xFL);
+ }
+
+ public static int getVersion(byte[] uuidBytes) {
+ validateLength(uuidBytes);
+ return (int) ((getMostSignificantBitsInternal(uuidBytes) >>> 12) & 0xFL);
+ }
+
+ /// Extracts the embedded timestamp from a time-based UUID (versions 1, 6,
or 7) and returns it as Unix epoch
+ /// milliseconds.
+ ///
+ /// @throws IllegalArgumentException if the UUID is not time-based (version
1, 6, or 7).
+ public static long getTimestampMillis(UUID uuid) {
+ return getTimestampMillisInternal(uuid.getMostSignificantBits(),
uuid.getLeastSignificantBits());
+ }
+
+ public static long getTimestampMillis(byte[] uuidBytes) {
+ validateLength(uuidBytes);
+ return
getTimestampMillisInternal(getMostSignificantBitsInternal(uuidBytes),
+ getLeastSignificantBitsInternal(uuidBytes));
+ }
+
+ private static long getTimestampMillisInternal(long msb, long lsb) {
+ int version = (int) ((msb >>> 12) & 0xFL);
+ switch (version) {
+ case 7:
+ // Upper 48 bits of MSB are the Unix milliseconds, big-endian.
+ return msb >>> 16;
+ case 6: {
+ // RFC 9562 v6 layout: [time_high (32) | time_mid (16) | ver (4) |
time_low (12)] in MSB.
+ long gregorian100Ns = ((msb >>> 32) << 28) | (((msb >>> 16) & 0xFFFFL)
<< 12) | (msb & 0x0FFFL);
+ return (gregorian100Ns - GREGORIAN_TO_UNIX_OFFSET_100NS) / 10_000L;
+ }
+ case 1: {
+ // RFC 4122 v1 layout: [time_low (32) | time_mid (16) | ver+time_high
(16)] in MSB.
+ long gregorian100Ns =
+ (msb >>> 32) | (((msb >>> 16) & 0xFFFFL) << 32) | ((msb & 0x0FFFL)
<< 48);
+ return (gregorian100Ns - GREGORIAN_TO_UNIX_OFFSET_100NS) / 10_000L;
+ }
+ default:
+ throw new IllegalArgumentException("UUID version " + version + " is
not time-based; only versions 1, 6, "
+ + "and 7 carry an embedded timestamp");
+ }
+ }
+
+ private static void validateLength(byte[] uuidBytes) {
+ if (uuidBytes.length != UUID_NUM_BYTES) {
+ throw new IllegalArgumentException(
+ "Invalid UUID byte length: " + uuidBytes.length + ", expected: " +
UUID_NUM_BYTES);
+ }
+ }
+
+ static long getMostSignificantBitsInternal(byte[] uuidBytes) {
+ return readLong(uuidBytes, 0);
+ }
+
+ static long getLeastSignificantBitsInternal(byte[] uuidBytes) {
+ return readLong(uuidBytes, Long.BYTES);
+ }
+
+ private static int updateHash(int hashCode, long value) {
+ for (int shift = Long.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) {
+ hashCode = 31 * hashCode + (byte) (value >>> shift);
+ }
+ return hashCode;
+ }
+
+ private static long readLong(byte[] bytes, int offset) {
+ return ((long) bytes[offset] & 0xFF) << 56
+ | ((long) bytes[offset + 1] & 0xFF) << 48
+ | ((long) bytes[offset + 2] & 0xFF) << 40
+ | ((long) bytes[offset + 3] & 0xFF) << 32
+ | ((long) bytes[offset + 4] & 0xFF) << 24
+ | ((long) bytes[offset + 5] & 0xFF) << 16
+ | ((long) bytes[offset + 6] & 0xFF) << 8
+ | (long) bytes[offset + 7] & 0xFF;
+ }
+
+ private static void writeLong(byte[] bytes, int offset, long value) {
+ bytes[offset] = (byte) (value >>> 56);
+ bytes[offset + 1] = (byte) (value >>> 48);
+ bytes[offset + 2] = (byte) (value >>> 40);
+ bytes[offset + 3] = (byte) (value >>> 32);
+ bytes[offset + 4] = (byte) (value >>> 24);
+ bytes[offset + 5] = (byte) (value >>> 16);
+ bytes[offset + 6] = (byte) (value >>> 8);
+ bytes[offset + 7] = (byte) value;
}
}
diff --git
a/pinot-spi/src/test/java/org/apache/pinot/spi/data/FieldSpecTest.java
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/FieldSpecTest.java
index 8c97ad61900..fd47f0312dc 100644
--- a/pinot-spi/src/test/java/org/apache/pinot/spi/data/FieldSpecTest.java
+++ b/pinot-spi/src/test/java/org/apache/pinot/spi/data/FieldSpecTest.java
@@ -28,7 +28,9 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
+import org.apache.pinot.spi.utils.ByteArray;
import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -44,6 +46,8 @@ public class FieldSpecTest {
private static final long RANDOM_SEED = System.currentTimeMillis();
private static final Random RANDOM = new Random(RANDOM_SEED);
private static final String ERROR_MESSAGE = "Random seed is: " + RANDOM_SEED;
+ private static final String UUID_VALUE =
"550e8400-e29b-41d4-a716-446655440000";
+ private static final String LARGER_UUID_VALUE =
"550e8400-e29b-41d4-a716-446655440001";
/**
* Test all {@link FieldSpec.DataType}.
@@ -60,6 +64,7 @@ public class FieldSpecTest {
Assert.assertEquals(STRING.getStoredType(), STRING);
Assert.assertEquals(JSON.getStoredType(), STRING);
Assert.assertEquals(BYTES.getStoredType(), BYTES);
+ Assert.assertEquals(UUID.getStoredType(), BYTES);
Assert.assertEquals(INT.size(), Integer.BYTES);
Assert.assertEquals(LONG.size(), Long.BYTES);
@@ -67,6 +72,7 @@ public class FieldSpecTest {
Assert.assertEquals(DOUBLE.size(), Double.BYTES);
Assert.assertEquals(BOOLEAN.size(), Integer.BYTES);
Assert.assertEquals(TIMESTAMP.size(), Long.BYTES);
+ Assert.assertEquals(UUID.size(), UuidUtils.UUID_NUM_BYTES);
}
/**
@@ -175,6 +181,11 @@ public class FieldSpecTest {
Assert.assertEquals(fieldSpec1.hashCode(), fieldSpec2.hashCode());
Assert.assertEquals(fieldSpec1.getDefaultNullValue(), BigDecimal.ZERO);
+ // Single-value BigDecimal type dimension field with numeric default null
value.
+ fieldSpec1 = new DimensionFieldSpec("bigDecimalDimension", BIG_DECIMAL,
true, -1);
+ Assert.assertEquals(fieldSpec1.getDefaultNullValue(), new
BigDecimal("-1"));
+ Assert.assertEquals(fieldSpec1.getDefaultNullValueString(), "-1");
+
// Metric field with default null value for byte column.
fieldSpec1 = new MetricFieldSpec();
fieldSpec1.setName("byteMetric");
@@ -185,6 +196,59 @@ public class FieldSpecTest {
Assert.assertEquals(fieldSpec1.toJsonObject(), fieldSpec2.toJsonObject());
Assert.assertEquals(fieldSpec1.hashCode(), fieldSpec2.hashCode());
Assert.assertEquals(fieldSpec1.getDefaultNullValue(),
fieldSpec2.getDefaultNullValue());
+
+ // Single-value uuid type dimension field with default null value.
+ fieldSpec1 = new DimensionFieldSpec();
+ fieldSpec1.setName("uuidDimension");
+ fieldSpec1.setDataType(UUID);
+ fieldSpec1.setDefaultNullValue(UUID_VALUE);
+ fieldSpec2 = new DimensionFieldSpec("uuidDimension", UUID, true,
UUID_VALUE);
+ Assert.assertEquals(fieldSpec1, fieldSpec2);
+ Assert.assertEquals(fieldSpec1.toJsonObject(), fieldSpec2.toJsonObject());
+ Assert.assertEquals(fieldSpec1.hashCode(), fieldSpec2.hashCode());
+ assertThat((byte[])
fieldSpec1.getDefaultNullValue()).isEqualTo(UuidUtils.toBytes(UUID_VALUE));
+ Assert.assertEquals(fieldSpec1.getDefaultNullValueString(), UUID_VALUE);
+
+ FieldSpec uuidBytesFieldSpec =
+ new DimensionFieldSpec("uuidBytesDimension", UUID, true,
UuidUtils.toBytes(UUID_VALUE));
+ assertThat((byte[])
uuidBytesFieldSpec.getDefaultNullValue()).isEqualTo(UuidUtils.toBytes(UUID_VALUE));
+ Assert.assertEquals(uuidBytesFieldSpec.getDefaultNullValueString(),
UUID_VALUE);
+
+ FieldSpec defaultUuidFieldSpec = new
DimensionFieldSpec("defaultUuidDimension", UUID, true);
+ byte[] firstDefaultNullValue = (byte[])
defaultUuidFieldSpec.getDefaultNullValue();
+ byte[] secondDefaultNullValue = (byte[])
defaultUuidFieldSpec.getDefaultNullValue();
+ assertThat(firstDefaultNullValue).isEqualTo(UuidUtils.nullUuidBytes());
+ assertThat(secondDefaultNullValue).isEqualTo(UuidUtils.nullUuidBytes());
+ // getDefaultNullValue() returns the stored default directly (no defensive
copy); the default is never mutated.
+ Assert.assertSame(firstDefaultNullValue, secondDefaultNullValue);
+ Assert.assertEquals(defaultUuidFieldSpec.getDefaultNullValueString(),
"00000000-0000-0000-0000-000000000000");
+ }
+
+ @Test
+ public void testUUIDConversions() {
+ byte[] uuidBytes = UuidUtils.toBytes(UUID_VALUE);
+ byte[] largerUuidBytes = UuidUtils.toBytes(LARGER_UUID_VALUE);
+ String mixedCaseUuidValue = UUID_VALUE.toUpperCase();
+ ByteArray uuidInternal = new ByteArray(uuidBytes);
+
+ assertThat((byte[]) UUID.convert(UUID_VALUE)).isEqualTo(uuidBytes);
+ assertThat((byte[]) UUID.convert(mixedCaseUuidValue)).isEqualTo(uuidBytes);
+ Assert.assertEquals(UUID.convertInternal(UUID_VALUE), uuidInternal);
+ Assert.assertEquals(UUID.convertInternal(mixedCaseUuidValue),
uuidInternal);
+ Assert.assertEquals(UUID.toString(uuidBytes), UUID_VALUE);
+ Assert.assertEquals(UUID.toString(uuidInternal), UUID_VALUE);
+ Assert.assertTrue(UUID.equals(uuidBytes, uuidInternal));
+ Assert.assertEquals(UUID.hashCode(uuidBytes), uuidInternal.hashCode());
+ Assert.assertEquals(UUID.compare(uuidBytes, uuidInternal), 0);
+ Assert.assertTrue(UUID.compare(uuidBytes, largerUuidBytes) < 0);
+ Assert.assertTrue(UUID.compare(largerUuidBytes, uuidBytes) > 0);
+ }
+
+ @Test
+ public void testInvalidUUIDConversion() {
+ IllegalArgumentException exception =
Assert.expectThrows(IllegalArgumentException.class,
+ () -> UUID.convert("not-a-uuid"));
+ assertThat(exception).hasMessageContaining("Invalid UUID");
}
/**
diff --git a/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaTest.java
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaTest.java
index c60195ce5be..ce4df4a78a0 100644
--- a/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaTest.java
+++ b/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaTest.java
@@ -30,6 +30,7 @@ import
org.apache.pinot.spi.data.TimeGranularitySpec.TimeFormat;
import org.apache.pinot.spi.utils.BytesUtils;
import org.apache.pinot.spi.utils.CommonConstants;
import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
@@ -42,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("deprecation")
public class SchemaTest {
public static final Logger LOGGER =
LoggerFactory.getLogger(SchemaTest.class);
+ private static final String UUID_VALUE =
"550e8400-e29b-41d4-a716-446655440000";
@Test
public void testValidation() {
@@ -56,6 +58,10 @@ public class SchemaTest {
schemaToValidate.addField(new DimensionFieldSpec("d",
FieldSpec.DataType.STRING, true));
schemaToValidate.validate();
+ schemaToValidate = new Schema();
+ schemaToValidate.addField(new DimensionFieldSpec("uuid",
FieldSpec.DataType.UUID, true));
+ schemaToValidate.validate();
+
schemaToValidate = new Schema();
schemaToValidate.addField(new MetricFieldSpec("m",
FieldSpec.DataType.STRING, "null"));
try {
@@ -100,6 +106,14 @@ public class SchemaTest {
schemaToValidate.validate();
}
+ @Test
+ public void testUUIDValidationAllowsMV() {
+ Schema schema = new Schema();
+ schema.addField(new DimensionFieldSpec("uuidMv", FieldSpec.DataType.UUID,
false));
+
+ schema.validate();
+ }
+
@Test
public void testSchemaBuilder() {
String defaultString = "default";
@@ -358,6 +372,23 @@ public class SchemaTest {
Assert.assertEquals(actualSchema.hashCode(), expectedSchema.hashCode());
}
+ @Test
+ public void testUUIDType()
+ throws Exception {
+ Schema expectedSchema = new Schema();
+ byte[] expectedDefault = UuidUtils.toBytes(UUID_VALUE);
+
+ expectedSchema.setSchemaName("test");
+ expectedSchema.addField(new DimensionFieldSpec("uuidDefault",
FieldSpec.DataType.UUID, true, UUID_VALUE));
+
+ String jsonSchema = expectedSchema.toSingleLineJsonString();
+ Schema actualSchema = Schema.fromString(jsonSchema);
+
+ assertThat((byte[])
actualSchema.getFieldSpecFor("uuidDefault").getDefaultNullValue()).isEqualTo(expectedDefault);
+ Assert.assertEquals(actualSchema, expectedSchema);
+ Assert.assertEquals(actualSchema.hashCode(), expectedSchema.hashCode());
+ }
+
@Test
public void testConversionFromTimeToDateTimeSpec() {
TimeFieldSpec timeFieldSpec;
diff --git
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/PinotDataTypeTest.java
b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/PinotDataTypeTest.java
index 21af9121ea2..fe6f86c1fb0 100644
--- a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/PinotDataTypeTest.java
+++ b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/PinotDataTypeTest.java
@@ -35,11 +35,13 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.pinot.spi.utils.PinotDataType.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
public class PinotDataTypeTest {
+ private static final String UUID_VALUE =
"550e8400-e29b-41d4-a716-446655440000";
private static final PinotDataType[] SOURCE_TYPES = {
BYTE, CHARACTER, SHORT, INT, LONG, FLOAT, DOUBLE, BIG_DECIMAL, STRING,
JSON,
BYTE_ARRAY, CHARACTER_ARRAY, SHORT_ARRAY, INT_ARRAY, LONG_ARRAY,
FLOAT_ARRAY, DOUBLE_ARRAY, STRING_ARRAY
@@ -157,9 +159,15 @@ public class PinotDataTypeTest {
new Timestamp[] { new Timestamp(1000000L), new Timestamp(2000000L) }},
{BYTES_ARRAY, BYTES_ARRAY, new byte[][] { "foo".getBytes(UTF_8),
"bar".getBytes(UTF_8) },
new byte[][] { "foo".getBytes(UTF_8), "bar".getBytes(UTF_8) }},
+ {STRING_ARRAY, UUID_ARRAY, new String[] {UUID_VALUE},
+ new byte[][] {UuidUtils.toBytes(UUID_VALUE)}},
+ {UUID_ARRAY, UUID_ARRAY, new java.util.UUID[]
{java.util.UUID.fromString(UUID_VALUE)},
+ new byte[][] {UuidUtils.toBytes(UUID_VALUE)}},
{COLLECTION, STRING_ARRAY, Arrays.asList("test1", "test2"), new
String[] {"test1", "test2"}},
+ {COLLECTION, UUID_ARRAY, Arrays.asList(UUID_VALUE), new byte[][]
{UuidUtils.toBytes(UUID_VALUE)}},
{COLLECTION, FLOAT_ARRAY, Arrays.asList(1.0f, 2.0f), new Float[]
{1.0f, 2.0f}},
{OBJECT_ARRAY, STRING_ARRAY, new Object[] {"test1", "test2"}, new
String[] {"test1", "test2"}},
+ {OBJECT_ARRAY, UUID_ARRAY, new Object[] {UUID_VALUE}, new byte[][]
{UuidUtils.toBytes(UUID_VALUE)}},
{OBJECT_ARRAY, FLOAT_ARRAY, new Object[] {1.0f, 2.0f}, new Float[]
{1.0f, 2.0f}},
};
}
@@ -211,6 +219,28 @@ public class PinotDataTypeTest {
assertEquals(BYTES.convert(new String[]{"0001"}, STRING_ARRAY), new
byte[]{0, 1});
}
+ @Test
+ public void testUUID() {
+ java.util.UUID uuid = java.util.UUID.fromString(UUID_VALUE);
+ byte[] uuidBytes = UuidUtils.toBytes(UUID_VALUE);
+ String mixedCaseUuid = UUID_VALUE.toUpperCase();
+
+ assertEquals(UUID.convert(UUID_VALUE, STRING), uuid);
+ assertEquals(UUID.convert(mixedCaseUuid, STRING), uuid);
+ assertEquals(UUID.convert(uuid, UUID), uuid);
+ assertEquals(UUID.convert(uuidBytes, BYTES), uuid);
+ assertEquals(STRING.convert(uuid, UUID), UUID_VALUE);
+ assertEquals(BYTES.convert(uuid, UUID), uuidBytes);
+ // Single-element collection/object/array sources carry a UUID
(FunctionInvoker argument resolution and MV
+ // sources feeding SV UUID columns)
+ assertEquals(UUID.convert(Arrays.asList(UUID_VALUE), COLLECTION), uuid);
+ assertEquals(UUID.convert(UUID_VALUE, OBJECT), uuid);
+ assertEquals(UUID.convert(new Object[]{UUID_VALUE}, OBJECT_ARRAY), uuid);
+ // Sources with no UUID interpretation fail with an explicit message
+ assertThrows(UnsupportedOperationException.class, () -> UUID.convert(1,
INT));
+ assertThrows(UnsupportedOperationException.class, () -> UUID.convert(1.0,
DOUBLE));
+ }
+
@Test
public void testTimestamp() {
Timestamp timestamp = new Timestamp(1620324238610L);
@@ -296,9 +326,9 @@ public class PinotDataTypeTest {
byte[] bytes = UUID.toBytes(uuid);
assertEquals(bytes.length, 16);
assertEquals(UUID.convert(bytes, BYTES), uuid);
- // toString / toInternal both return the canonical form (no FQN needed in
canonical 8-4-4-4-12 layout).
+ // toString returns the canonical form, while toInternal returns the
fixed-width byte form.
assertEquals(UUID.toString(uuid), canonical);
- assertEquals(UUID.toInternal(uuid), canonical);
+ assertEquals((byte[]) UUID.toInternal(uuid), bytes);
}
@Test
@@ -306,11 +336,14 @@ public class PinotDataTypeTest {
UUID u1 =
java.util.UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
UUID u2 =
java.util.UUID.fromString("00000000-0000-0000-0000-000000000001");
UUID[] uuids = {u1, u2};
- assertEquals(UUID_ARRAY.convert(uuids, UUID_ARRAY), uuids);
+ byte[][] convertedBytes = (byte[][]) UUID_ARRAY.convert(uuids, UUID_ARRAY);
+ assertEquals(UUID.convert(convertedBytes[0], BYTES), u1);
+ assertEquals(UUID.convert(convertedBytes[1], BYTES), u2);
// STRING_ARRAY → UUID_ARRAY: per-element parse.
- assertEquals(UUID_ARRAY.convert(
- new String[]{"550e8400-e29b-41d4-a716-446655440000",
"00000000-0000-0000-0000-000000000001"}, STRING_ARRAY),
- uuids);
+ byte[][] convertedStringBytes = (byte[][]) UUID_ARRAY.convert(
+ new String[]{"550e8400-e29b-41d4-a716-446655440000",
"00000000-0000-0000-0000-000000000001"}, STRING_ARRAY);
+ assertEquals(UUID.convert(convertedStringBytes[0], BYTES), u1);
+ assertEquals(UUID.convert(convertedStringBytes[1], BYTES), u2);
// STRING_ARRAY destination: canonical strings.
assertEquals(STRING_ARRAY.convert(uuids, UUID_ARRAY),
new String[]{"550e8400-e29b-41d4-a716-446655440000",
"00000000-0000-0000-0000-000000000001"});
@@ -323,9 +356,10 @@ public class PinotDataTypeTest {
assertEquals(UUID.convert(bytesArray[1], BYTES), u2);
// Lookup: Object[] of UUID routes to UUID_ARRAY.
assertEquals(getMultiValueType(u1), UUID_ARRAY);
- // toInternal: String[] of canonical form.
- assertEquals(UUID_ARRAY.toInternal(uuids),
- new String[]{"550e8400-e29b-41d4-a716-446655440000",
"00000000-0000-0000-0000-000000000001"});
+ // toInternal: byte[][] of 16-byte big-endian forms.
+ byte[][] internalBytes = (byte[][]) UUID_ARRAY.toInternal(uuids);
+ assertEquals(UUID.convert(internalBytes[0], BYTES), u1);
+ assertEquals(UUID.convert(internalBytes[1], BYTES), u2);
}
@Test
diff --git
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/UuidUtilsTest.java
b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/UuidUtilsTest.java
new file mode 100644
index 00000000000..2ccbfe52f51
--- /dev/null
+++ b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/UuidUtilsTest.java
@@ -0,0 +1,196 @@
+/**
+ * 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.spi.utils;
+
+import java.util.Arrays;
+import java.util.UUID;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+
+/**
+ * Tests for {@link UuidUtils}.
+ */
+public class UuidUtilsTest {
+ private static final String UUID_VALUE =
"550e8400-e29b-41d4-a716-446655440000";
+
+ @DataProvider(name = "invalidUuidStrings")
+ public Object[][] invalidUuidStrings() {
+ return new Object[][]{
+ {"550e8400-e29b-41d4-a716-44665544000"},
+ {"550e8400-e29b-41d4-a716-4466554400000"},
+ {"550e8400-e29b-41d4-a716-44665544000g"},
+ {"550e8400e29b41d4a716446655440000"},
+ {""}
+ };
+ }
+
+ @DataProvider(name = "invalidUuidBytes")
+ public Object[][] invalidUuidBytes() {
+ return new Object[][]{
+ {new byte[15]},
+ {new byte[17]}
+ };
+ }
+
+ @Test
+ public void testMixedCaseUuidStringRoundTrips() {
+ String mixedCaseUuid = "550E8400-E29B-41D4-A716-446655440000";
+ byte[] bytes = UuidUtils.toBytes(mixedCaseUuid);
+
+ assertEquals(UuidUtils.toString(bytes), UUID_VALUE);
+ assertEquals(UuidUtils.toUUID(mixedCaseUuid), UUID.fromString(UUID_VALUE));
+ }
+
+ @Test
+ public void testCharSequenceRoundTrips() {
+ CharSequence mixedCaseUuid = new
StringBuilder("550E8400-E29B-41D4-A716-446655440000");
+
+ assertEquals(UuidUtils.toBytes(mixedCaseUuid),
UuidUtils.toBytes(UUID_VALUE));
+ assertEquals(UuidUtils.toUUID(mixedCaseUuid), UUID.fromString(UUID_VALUE));
+ assertTrue(UuidUtils.isUuid(mixedCaseUuid));
+ }
+
+ @Test(dataProvider = "invalidUuidStrings")
+ public void testRejectsInvalidUuidStrings(String invalidUuid) {
+ Assert.expectThrows(IllegalArgumentException.class, () ->
UuidUtils.toBytes(invalidUuid));
+ Assert.expectThrows(IllegalArgumentException.class, () ->
UuidUtils.toUUID(invalidUuid));
+ assertFalse(UuidUtils.isUuid(invalidUuid));
+ }
+
+ @Test(dataProvider = "invalidUuidBytes")
+ public void testRejectsInvalidUuidBytes(byte[] invalidBytes) {
+ Assert.expectThrows(IllegalArgumentException.class, () ->
UuidUtils.toBytes(invalidBytes));
+ Assert.expectThrows(IllegalArgumentException.class, () ->
UuidUtils.toUUID(invalidBytes));
+ assertFalse(UuidUtils.isUuid(invalidBytes));
+ }
+
+ @Test
+ public void testIsUuidAcceptsValidStringAndBytes() {
+ byte[] uuidBytes = UuidUtils.toBytes(UUID_VALUE);
+
+ assertTrue(UuidUtils.isUuid(UUID_VALUE));
+ assertTrue(UuidUtils.isUuid(uuidBytes));
+ assertTrue(UuidUtils.isUuid(new ByteArray(uuidBytes)));
+ }
+
+ @Test
+ public void testIsUuidRejectsNull() {
+ assertFalse(UuidUtils.isUuid((String) null));
+ assertFalse(UuidUtils.isUuid((byte[]) null));
+ assertFalse(UuidUtils.isUuid((Object) null));
+ }
+
+ @Test
+ public void testRandomV4HasVersionFourAndNoTimestamp() {
+ UUID uuid = UuidUtils.randomV4();
+ assertEquals(UuidUtils.getVersion(uuid), 4);
+ assertEquals(UuidUtils.getVersion(UuidUtils.toBytes(uuid)), 4);
+ // v4 is not time-based, so timestamp extraction must fail.
+ Assert.expectThrows(IllegalArgumentException.class, () ->
UuidUtils.getTimestampMillis(uuid));
+ }
+
+ @Test
+ public void testRandomV7HasVersionSevenAndCurrentTimestamp() {
+ long before = System.currentTimeMillis();
+ UUID uuid = UuidUtils.randomV7();
+ long after = System.currentTimeMillis();
+
+ assertEquals(UuidUtils.getVersion(uuid), 7);
+ assertEquals(UuidUtils.getVersion(UuidUtils.toBytes(uuid)), 7);
+ long timestamp = UuidUtils.getTimestampMillis(uuid);
+ assertTrue(timestamp >= before && timestamp <= after,
+ "v7 timestamp " + timestamp + " not within [" + before + ", " + after
+ "]");
+ // Byte-array and UUID overloads must agree.
+ assertEquals(UuidUtils.getTimestampMillis(UuidUtils.toBytes(uuid)),
timestamp);
+ }
+
+ @Test
+ public void testGetTimestampMillisMatchesKnownVersionOneAndSevenLayouts() {
+ // Fixed v7 UUID whose leading 48 bits encode 0x0000018F00000000 ms.
+ UUID v7 = new UUID(0x018F00000000_7000L | 0x0123L, 0x8000000000000000L);
+ assertEquals(UuidUtils.getVersion(v7), 7);
+ assertEquals(UuidUtils.getTimestampMillis(v7), 0x018F00000000L);
+ }
+
+ @Test
+ public void testNullUuidBytesReturnsDefensiveCopy() {
+ byte[] first = UuidUtils.nullUuidBytes();
+ byte[] second = UuidUtils.nullUuidBytes();
+
+ first[0] = 1;
+ assertEquals(second, new byte[UuidUtils.UUID_NUM_BYTES]);
+ }
+
+ @Test
+ public void testComparePreservesUnsignedByteOrdering() {
+ byte[] larger = UuidUtils.toBytes("80000000-0000-0000-0000-000000000000");
+ byte[] smaller = UuidUtils.toBytes("7fffffff-ffff-ffff-ffff-ffffffffffff");
+
+ assertTrue(ByteArray.compare(larger, smaller) > 0);
+ assertEquals(UuidUtils.compare(larger, smaller), ByteArray.compare(larger,
smaller));
+ assertEquals(UuidUtils.compare(smaller, larger),
ByteArray.compare(smaller, larger));
+ }
+
+ @Test
+ public void testUuidEqualityHashAndBitExtraction() {
+ UUID uuid = UUID.fromString(UUID_VALUE);
+ byte[] uuidBytes = UuidUtils.toBytes(uuid);
+
+ assertEquals(UuidUtils.getMostSignificantBits(uuidBytes),
uuid.getMostSignificantBits());
+ assertEquals(UuidUtils.getLeastSignificantBits(uuidBytes),
uuid.getLeastSignificantBits());
+ assertTrue(UuidUtils.equals(uuidBytes, Arrays.copyOf(uuidBytes,
uuidBytes.length)));
+ assertTrue(UuidUtils.equals(uuid.getMostSignificantBits(),
uuid.getLeastSignificantBits(),
+ uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()));
+ assertEquals(UuidUtils.hashCode(uuidBytes), new
ByteArray(uuidBytes).hashCode());
+ assertEquals(UuidUtils.hashCode(uuid.getMostSignificantBits(),
uuid.getLeastSignificantBits()),
+ new ByteArray(uuidBytes).hashCode());
+ }
+
+ @Test
+ public void testUuidKeyNormalizesRepresentations() {
+ UUID uuid = UUID.fromString(UUID_VALUE);
+ byte[] uuidBytes = UuidUtils.toBytes(uuid);
+ UuidKey uuidKeyFromBytes = UuidKey.fromBytes(uuidBytes);
+ UuidKey uuidKeyFromObject = UuidKey.fromObject(new ByteArray(uuidBytes));
+
+ assertEquals(uuidKeyFromBytes, UuidKey.fromUUID(uuid));
+ assertEquals(uuidKeyFromBytes, uuidKeyFromObject);
+ assertEquals(uuidKeyFromBytes.hashCode(), UuidUtils.hashCode(uuidBytes));
+ assertEquals(uuidKeyFromBytes.toByteArray(), new ByteArray(uuidBytes));
+ assertEquals(uuidKeyFromBytes.toString(), UUID_VALUE);
+ }
+
+ @Test
+ public void testLongPairHelpersRoundTrip() {
+ UUID uuid = UUID.fromString(UUID_VALUE);
+ long mostSignificantBits = uuid.getMostSignificantBits();
+ long leastSignificantBits = uuid.getLeastSignificantBits();
+
+ assertEquals(UuidUtils.toBytes(mostSignificantBits, leastSignificantBits),
UuidUtils.toBytes(uuid));
+ assertEquals(UuidUtils.toUUID(mostSignificantBits, leastSignificantBits),
uuid);
+ assertEquals(UuidUtils.toString(mostSignificantBits,
leastSignificantBits), UUID_VALUE);
+ assertEquals(UuidKey.fromLongs(mostSignificantBits, leastSignificantBits),
UuidKey.fromUUID(uuid));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]