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 3790129aa15 Generate UUID sample data in the schema recommender 
(#18973)
3790129aa15 is described below

commit 3790129aa15ec70d9870a5f61fa09b67d2a1fbc3
Author: Xiang Fu <[email protected]>
AuthorDate: Thu Jul 16 20:37:20 2026 +0200

    Generate UUID sample data in the schema recommender (#18973)
---
 .../data/generator/GeneratorFactory.java           |  3 +
 .../recommender/data/generator/UuidGenerator.java  | 76 +++++++++++++++++++
 .../data/generator/UuidGeneratorTest.java          | 87 ++++++++++++++++++++++
 .../recommender/data/writer/AvroWriterTest.java    | 50 +++++++++++++
 .../plugin/inputformat/avro/AvroSchemaUtil.java    | 18 ++++-
 .../inputformat/avro/AvroSchemaUtilTest.java       | 14 +++-
 6 files changed, 242 insertions(+), 6 deletions(-)

diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/recommender/data/generator/GeneratorFactory.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/recommender/data/generator/GeneratorFactory.java
index 4186543eec9..61c7614ad6a 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/recommender/data/generator/GeneratorFactory.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/recommender/data/generator/GeneratorFactory.java
@@ -42,6 +42,9 @@ public class GeneratorFactory {
     if (type == DataType.BYTES) {
       return new BytesGenerator(cardinality, entryLength);
     }
+    if (type == DataType.UUID) {
+      return new UuidGenerator(cardinality, numberOfValuesPerEntry);
+    }
     if (timeUnit != null) {
       return new TimeGenerator(cardinality, type, timeUnit);
     }
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/recommender/data/generator/UuidGenerator.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/recommender/data/generator/UuidGenerator.java
new file mode 100644
index 00000000000..5ba4ad952f0
--- /dev/null
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/recommender/data/generator/UuidGenerator.java
@@ -0,0 +1,76 @@
+/**
+ * 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.controller.recommender.data.generator;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Random;
+import java.util.UUID;
+
+
+/// Generates canonical, parseable UUID-format strings for a column of type 
UUID. UUID is a logical type whose
+/// recommender Avro schema is a `string` annotated with `logicalType: uuid` 
(see
+/// `AvroSchemaUtil#toAvroSchemaJsonObject`), so the generated value is the 
canonical string form and the writer
+/// serializes it directly.
+///
+/// The number of distinct values is bounded by the requested cardinality: a 
fixed random high-order half is combined
+/// with a low-order counter that cycles `[1, cardinality]`, mirroring 
`StringGenerator`. These are deliberately not
+/// RFC-4122 version-4 UUIDs (the version/variant bits are not set); sample 
data only needs distinct,
+/// canonically-formatted, parseable values.
+public class UuidGenerator implements Generator {
+  private static final double DEFAULT_NUMBER_OF_VALUES_PER_ENTRY = 1;
+
+  private final int _cardinality;
+  private final double _numberOfValuesPerEntry;
+  private final Random _random;
+  private final long _mostSignificantBits;
+  private long _counter;
+
+  public UuidGenerator(Integer cardinality, Double numberOfValuesPerEntry) {
+    this(cardinality, numberOfValuesPerEntry, new 
Random(System.currentTimeMillis()));
+  }
+
+  @VisibleForTesting
+  UuidGenerator(Integer cardinality, Double numberOfValuesPerEntry, Random 
random) {
+    _cardinality = cardinality;
+    _numberOfValuesPerEntry =
+        numberOfValuesPerEntry != null ? numberOfValuesPerEntry : 
DEFAULT_NUMBER_OF_VALUES_PER_ENTRY;
+    _random = random;
+    _mostSignificantBits = random.nextLong();
+  }
+
+  @Override
+  public void init() {
+  }
+
+  @Override
+  public Object next() {
+    if (_numberOfValuesPerEntry == 1) {
+      return nextUuid();
+    }
+    return 
MultiValueGeneratorHelper.generateMultiValueEntries(_numberOfValuesPerEntry, 
_random, this::nextUuid);
+  }
+
+  private String nextUuid() {
+    if (_counter >= _cardinality) {
+      _counter = 0;
+    }
+    _counter++;
+    return new UUID(_mostSignificantBits, _counter).toString();
+  }
+}
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/recommender/data/generator/UuidGeneratorTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/recommender/data/generator/UuidGeneratorTest.java
new file mode 100644
index 00000000000..9f6eccbbeb9
--- /dev/null
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/recommender/data/generator/UuidGeneratorTest.java
@@ -0,0 +1,87 @@
+/**
+ * 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.controller.recommender.data.generator;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.UUID;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+public class UuidGeneratorTest {
+
+  /// Single-value generation must yield canonical, parseable UUID strings.
+  @Test
+  public void testSingleValueProducesCanonicalUuids() {
+    UuidGenerator generator = new UuidGenerator(1000, 1.0, new Random(42));
+    generator.init();
+    for (int i = 0; i < 100; i++) {
+      String value = (String) generator.next();
+      // Round-trips through UUID.fromString and is already in canonical 
lowercase form.
+      assertEquals(UUID.fromString(value).toString(), value);
+    }
+  }
+
+  /// Distinct values must be bounded by the requested cardinality, and the 
low-order counter must cycle.
+  @Test
+  public void testCardinalityBoundsAndCycles() {
+    int cardinality = 3;
+    UuidGenerator generator = new UuidGenerator(cardinality, 1.0, new 
Random(42));
+    generator.init();
+
+    String first = (String) generator.next();
+    String second = (String) generator.next();
+    String third = (String) generator.next();
+    String fourth = (String) generator.next();
+
+    Set<String> distinct = new HashSet<>(List.of(first, second, third, 
fourth));
+    assertEquals(distinct.size(), cardinality, "distinct values must not 
exceed cardinality");
+    assertEquals(fourth, first, "the counter must cycle back to the first 
value after cardinality entries");
+  }
+
+  /// A null numberOfValuesPerEntry must default to single-value generation (a 
bare string, not a list).
+  @Test
+  public void testNullNumberOfValuesPerEntryDefaultsToSingleValue() {
+    UuidGenerator generator = new UuidGenerator(1000, null, new Random(42));
+    generator.init();
+    assertTrue(generator.next() instanceof String, "null 
numberOfValuesPerEntry must yield a single string value");
+  }
+
+  /// Multi-value generation must return a list of parseable UUID strings.
+  @Test
+  public void testMultiValueReturnsListOfUuids() {
+    UuidGenerator generator = new UuidGenerator(1000, 3.0, new Random(42));
+    generator.init();
+
+    Object next = generator.next();
+    assertTrue(next instanceof List, "multi-value entry must be a List");
+    List<?> values = (List<?>) next;
+    // For an integer numberOfValuesPerEntry the count is deterministic (see 
MultiValueGeneratorHelper).
+    assertEquals(values.size(), 3);
+    for (Object value : values) {
+      String uuid = (String) value;
+      assertEquals(UUID.fromString(uuid).toString(), uuid);
+    }
+  }
+}
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/recommender/data/writer/AvroWriterTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/recommender/data/writer/AvroWriterTest.java
index 8362c362707..f8bfae6cd49 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/recommender/data/writer/AvroWriterTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/recommender/data/writer/AvroWriterTest.java
@@ -23,6 +23,7 @@ import java.nio.file.Files;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import org.apache.avro.Schema;
 import org.apache.avro.file.DataFileReader;
@@ -117,6 +118,55 @@ public class AvroWriterTest {
     assertEquals(count, totalDocs);
   }
 
+  /// The recommender must support UUID columns end to end: the generated 
schema declares a logicalType "uuid" string
+  /// and the values round-trip as canonical UUID strings. Before UUID 
generation was wired up, writing a UUID column
+  /// threw because the number generator rejected the type.
+  @Test
+  public void testUuidRoundTrip()
+      throws Exception {
+    List<String> columns = List.of("uuidCol");
+    Map<String, DataType> dataTypes = new HashMap<>();
+    dataTypes.put("uuidCol", DataType.UUID);
+    Map<String, FieldType> fieldTypes = new HashMap<>();
+    Map<String, Integer> cardinality = new HashMap<>();
+    for (String column : columns) {
+      fieldTypes.put(column, FieldType.DIMENSION);
+      cardinality.put(column, 5);
+    }
+
+    DataGeneratorSpec spec =
+        new DataGeneratorSpec(columns, cardinality, new HashMap<String, 
IntegerRange>(), new HashMap<>(),
+            new HashMap<>(), new HashMap<>(), dataTypes, fieldTypes, new 
HashMap<String, TimeUnit>(), new HashMap<>(),
+            new HashMap<>());
+    DataGenerator generator = new DataGenerator();
+    generator.init(spec);
+
+    int totalDocs = 10;
+    AvroWriterSpec writerSpec = new AvroWriterSpec(generator, _baseDir, 
totalDocs, 1, 0);
+    AvroWriter writer = new AvroWriter();
+    writer.init(writerSpec);
+    writer.write();
+
+    Schema avroSchema = AvroWriter.getAvroSchema(writerSpec.getSchema());
+    Schema uuidBranch = nonNullBranch(avroSchema, "uuidCol");
+    assertEquals(uuidBranch.getType(), Schema.Type.STRING);
+    assertEquals(uuidBranch.getProp("logicalType"), "uuid");
+
+    File avroFile = new File(_baseDir, "part-0.avro");
+    assertTrue(avroFile.exists());
+    int count = 0;
+    try (DataFileReader<GenericRecord> reader = new DataFileReader<>(avroFile, 
new GenericDatumReader<>())) {
+      for (GenericRecord record : reader) {
+        Object value = record.get("uuidCol");
+        assertTrue(value instanceof CharSequence, "uuidCol should read back as 
a string");
+        String uuid = value.toString();
+        assertEquals(UUID.fromString(uuid).toString(), uuid, "value must be a 
canonical UUID string");
+        count++;
+      }
+    }
+    assertEquals(count, totalDocs);
+  }
+
   /// The generator emits Pinot's stored form for BOOLEAN (int 0/1); coercion 
must map it to the right Boolean and
   /// leave non-boolean columns untouched.
   @Test
diff --git 
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
 
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
index 52ea2240bd4..0b1f8ad3ab0 100644
--- 
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
+++ 
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
@@ -80,8 +80,9 @@ public class AvroSchemaUtil {
   /// (see `AvroWriter`). Each field is emitted as a nullable union `["null", 
<type>]`.
   ///
   /// The switch is driven by the original (logical) [DataType] rather than 
the stored type, so logical types are
-  /// represented faithfully instead of collapsing to their physical storage 
type: BOOLEAN maps to Avro `boolean`
-  /// and TIMESTAMP to a `timestamp-millis` long, not a plain `int`/`long`.
+  /// represented faithfully instead of collapsing to their physical storage 
type: BOOLEAN maps to Avro `boolean`,
+  /// TIMESTAMP to a `timestamp-millis` long (not a plain `int`/`long`), and 
UUID to a `uuid`-logical-type string
+  /// (not raw `bytes`).
   ///
   /// This intentionally differs from the segment-processing converters 
`AvroUtils.getAvroSchemaFromPinotSchema` and
   /// `SegmentProcessorAvroUtils.convertPinotSchemaToAvroSchema`, which switch 
on the stored type because they
@@ -121,9 +122,18 @@ public class AvroSchemaUtil {
       case JSON:
         jsonSchema.set("type", convertStringsToJsonArray("null", "string"));
         return jsonSchema;
-      case BYTES:
       case UUID:
-        // UUID is a logical type stored as fixed-width 16-byte BYTES.
+        // UUID is a logical type; represent it faithfully as an Avro string 
annotated with logicalType "uuid" rather
+        // than collapsing to raw bytes, so generated sample data round-trips 
as canonical UUID strings.
+        ObjectNode uuidType = JsonUtils.newObjectNode();
+        uuidType.put("type", "string");
+        uuidType.put("logicalType", "uuid");
+        ArrayNode uuidUnion = JsonUtils.newArrayNode();
+        uuidUnion.add("null");
+        uuidUnion.add(uuidType);
+        jsonSchema.set("type", uuidUnion);
+        return jsonSchema;
+      case BYTES:
         jsonSchema.set("type", convertStringsToJsonArray("null", "bytes"));
         return jsonSchema;
       default:
diff --git 
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
 
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
index 924add4bb78..09142d201f0 100644
--- 
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
+++ 
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
@@ -41,8 +41,6 @@ public class AvroSchemaUtilTest {
     assertPrimitiveType(DataType.STRING, "string");
     assertPrimitiveType(DataType.JSON, "string");
     assertPrimitiveType(DataType.BYTES, "bytes");
-    // UUID is a logical type stored as BYTES; it must keep mapping to Avro 
bytes (not fall through to default).
-    assertPrimitiveType(DataType.UUID, "bytes");
     // Logical types must not collapse to their stored INT/LONG type.
     assertPrimitiveType(DataType.BOOLEAN, "boolean");
 
@@ -60,6 +58,18 @@ public class AvroSchemaUtilTest {
         () -> AvroSchemaUtil.toAvroSchemaJsonObject(new 
DimensionFieldSpec("col", DataType.BIG_DECIMAL, true)));
   }
 
+  /// A UUID column is represented faithfully as an Avro string annotated with 
logicalType "uuid".
+  /// `DataGenerator#buildSpec` always marks the recommender schema FieldSpec 
single-value, so this emits a scalar
+  /// union like every sibling type.
+  @Test
+  public void testToAvroSchemaJsonObjectForUuid() {
+    JsonNode type = typeOf(DataType.UUID);
+    assertEquals(type.get(0).asText(), "null");
+    JsonNode uuidBranch = type.get(1);
+    assertEquals(uuidBranch.get("type").asText(), "string");
+    assertEquals(uuidBranch.get("logicalType").asText(), "uuid");
+  }
+
   private static void assertPrimitiveType(DataType dataType, String 
expectedAvroType) {
     JsonNode type = typeOf(dataType);
     assertEquals(type.get(0).asText(), "null");


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to