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]

Reply via email to