This is an automated email from the ASF dual-hosted git repository.

Jackie-Jiang 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 69e57f657d5 Allow timestamp-index derived columns in star-tree 
dimensionsSplitOrder validation (#18774)
69e57f657d5 is described below

commit 69e57f657d5fe41f1386a7815c081eedcacbefd1
Author: Soumya Himanish Mohapatra 
<[email protected]>
AuthorDate: Fri Aug 14 02:20:17 2026 +0530

    Allow timestamp-index derived columns in star-tree dimensionsSplitOrder 
validation (#18774)
---
 .../pinot/core/startree/v2/BaseStarTreeV2Test.java |  34 ++++-
 .../startree/v2/TimestampIndexStarTreeV2Test.java  | 149 +++++++++++++++++++++
 .../segment/local/utils/TableConfigUtils.java      |  21 ++-
 .../segment/local/utils/TableConfigUtilsTest.java  |  38 ++++++
 4 files changed, 233 insertions(+), 9 deletions(-)

diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/BaseStarTreeV2Test.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/BaseStarTreeV2Test.java
index 6bba131d59d..4896b6865dd 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/BaseStarTreeV2Test.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/BaseStarTreeV2Test.java
@@ -97,7 +97,7 @@ abstract class BaseStarTreeV2Test<R, A> {
   // as AggregationFunctionColumnPair
   protected static final String DIMENSION1 = "d1__COLUMN_NAME";
   protected static final String DIMENSION2 = "DISTINCTCOUNTRAWHLL__d2";
-  private static final int DIMENSION_CARDINALITY = 100;
+  protected static final int DIMENSION_CARDINALITY = 100;
   private static final String AGG_COL = "m";
 
   // Supported filters
@@ -146,8 +146,8 @@ abstract class BaseStarTreeV2Test<R, A> {
     _aggregatedValueType = _valueAggregator.getAggregatedValueType();
     _aggregation = getAggregation(_valueAggregator.getAggregationType());
 
-    Schema.SchemaBuilder schemaBuilder = new 
Schema.SchemaBuilder().addSingleValueDimension(DIMENSION1, DataType.INT)
-        .addSingleValueDimension(DIMENSION2, DataType.INT);
+    Schema.SchemaBuilder schemaBuilder = new Schema.SchemaBuilder();
+    addDimensionFields(schemaBuilder);
     DataType rawValueType = getRawValueType();
     // Raw value type will be null for COUNT aggregation function
     if (rawValueType != null) {
@@ -160,8 +160,7 @@ abstract class BaseStarTreeV2Test<R, A> {
     List<GenericRow> segmentRecords = new ArrayList<>(NUM_SEGMENT_RECORDS);
     for (int i = 0; i < NUM_SEGMENT_RECORDS; i++) {
       GenericRow segmentRecord = new GenericRow();
-      segmentRecord.putValue(DIMENSION1, 
RANDOM.nextInt(DIMENSION_CARDINALITY));
-      segmentRecord.putValue(DIMENSION2, 
RANDOM.nextInt(DIMENSION_CARDINALITY));
+      addDimensionValues(segmentRecord, RANDOM);
       if (rawValueType != null) {
         segmentRecord.putValue(AGG_COL, getRandomRawValue(RANDOM));
       }
@@ -175,7 +174,7 @@ abstract class BaseStarTreeV2Test<R, A> {
     driver.init(segmentGeneratorConfig, new 
GenericRowRecordReader(segmentRecords));
     driver.build();
 
-    StarTreeIndexConfig starTreeIndexConfig = new 
StarTreeIndexConfig(Arrays.asList(DIMENSION1, DIMENSION2), null, null,
+    StarTreeIndexConfig starTreeIndexConfig = new 
StarTreeIndexConfig(getDimensionsSplitOrder(), null, null,
         List.of(new StarTreeAggregationConfig(AGG_COL,
                 _valueAggregator.getAggregationType().getName(), null, 
getCompressionCodec(),
                 true, getIndexVersion(), null, null)), MAX_LEAF_RECORDS);
@@ -254,7 +253,7 @@ abstract class BaseStarTreeV2Test<R, A> {
     assertNull(predicateEvaluatorsMap);
   }
 
-  private void testQuery(String query)
+  protected void testQuery(String query)
       throws IOException {
     QueryContext queryContext = 
QueryContextConverterUtils.getQueryContext(query);
 
@@ -513,6 +512,27 @@ abstract class BaseStarTreeV2Test<R, A> {
     return new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME).build();
   }
 
+  protected void addDimensionFields(Schema.SchemaBuilder schemaBuilder) {
+    schemaBuilder.addSingleValueDimension(DIMENSION1, 
DataType.INT).addSingleValueDimension(DIMENSION2, DataType.INT);
+  }
+
+  protected void addDimensionValues(GenericRow segmentRecord, Random random) {
+    segmentRecord.putValue(DIMENSION1, random.nextInt(DIMENSION_CARDINALITY));
+    segmentRecord.putValue(DIMENSION2, random.nextInt(DIMENSION_CARDINALITY));
+  }
+
+  protected List<String> getDimensionsSplitOrder() {
+    return List.of(DIMENSION1, DIMENSION2);
+  }
+
+  protected String getAggregation() {
+    return _aggregation;
+  }
+
+  protected StarTreeV2 getStarTreeV2() {
+    return _starTreeV2;
+  }
+
   abstract ValueAggregator<R, A> getValueAggregator();
 
   abstract DataType getRawValueType();
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/TimestampIndexStarTreeV2Test.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/TimestampIndexStarTreeV2Test.java
new file mode 100644
index 00000000000..b524e26a279
--- /dev/null
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/TimestampIndexStarTreeV2Test.java
@@ -0,0 +1,149 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.startree.v2;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
+import org.apache.pinot.core.query.request.context.QueryContext;
+import 
org.apache.pinot.core.query.request.context.utils.QueryContextConverterUtils;
+import org.apache.pinot.core.startree.StarTreeUtils;
+import org.apache.pinot.segment.local.aggregator.SumValueAggregator;
+import org.apache.pinot.segment.local.aggregator.ValueAggregator;
+import 
org.apache.pinot.segment.spi.index.startree.AggregationFunctionColumnPair;
+import org.apache.pinot.spi.config.table.FieldConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.config.table.TimestampConfig;
+import org.apache.pinot.spi.config.table.TimestampIndexGranularity;
+import org.apache.pinot.spi.data.DateTimeFieldSpec;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.utils.TimestampIndexUtils;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+
+public class TimestampIndexStarTreeV2Test extends BaseStarTreeV2Test<Object, 
Double> {
+  private static final String TIMESTAMP_COLUMN = "tsCol";
+  private static final String TIMESTAMP_COLUMN_WITH_GRANULARITY =
+      TimestampIndexUtils.getColumnWithGranularity(TIMESTAMP_COLUMN, 
TimestampIndexGranularity.MILLISECOND);
+
+  @Override
+  ValueAggregator<Object, Double> getValueAggregator() {
+    return new SumValueAggregator();
+  }
+
+  @Override
+  DataType getRawValueType() {
+    return DataType.INT;
+  }
+
+  @Override
+  Object getRandomRawValue(Random random) {
+    return random.nextInt();
+  }
+
+  @Override
+  void assertAggregatedValue(Double starTreeResult, Double nonStarTreeResult) {
+    assertEquals(starTreeResult, nonStarTreeResult, 1e-5);
+  }
+
+  @Override
+  protected void addDimensionFields(Schema.SchemaBuilder schemaBuilder) {
+    super.addDimensionFields(schemaBuilder);
+    schemaBuilder.addField(
+        new DateTimeFieldSpec(TIMESTAMP_COLUMN, DataType.TIMESTAMP, 
DateTimeFieldSpec.TimeFormat.TIMESTAMP.name(),
+            "1:MILLISECONDS"));
+  }
+
+  @Override
+  protected void addDimensionValues(GenericRow segmentRecord, Random random) {
+    super.addDimensionValues(segmentRecord, random);
+    segmentRecord.putValue(TIMESTAMP_COLUMN, (long) 
random.nextInt(DIMENSION_CARDINALITY));
+  }
+
+  @Override
+  protected TableConfig createTableConfig() {
+    return new TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
+        .setFieldConfigList(List.of(new 
FieldConfig.Builder(TIMESTAMP_COLUMN).withTimestampConfig(
+            new 
TimestampConfig(List.of(TimestampIndexGranularity.MILLISECOND))).build()))
+        .build();
+  }
+
+  @Override
+  protected List<String> getDimensionsSplitOrder() {
+    return List.of(DIMENSION1, DIMENSION2, TIMESTAMP_COLUMN_WITH_GRANULARITY);
+  }
+
+  @Test
+  public void testStarTreeBuiltOnTimestampIndexColumn() {
+    
assertTrue(getStarTreeV2().getMetadata().getDimensionsSplitOrder().contains(TIMESTAMP_COLUMN_WITH_GRANULARITY));
+    
assertEquals(getStarTreeV2().getDataSource(TIMESTAMP_COLUMN_WITH_GRANULARITY).getDictionary().length(),
+        DIMENSION_CARDINALITY);
+  }
+
+  @Test
+  public void testStarTreeFitForQueryOnTimestampIndexColumn() {
+    QueryContext queryContext = QueryContextConverterUtils.getQueryContext(
+        String.format("SELECT %s FROM %s WHERE %s < 10 GROUP BY %s", 
getAggregation(), TABLE_NAME,
+            TIMESTAMP_COLUMN_WITH_GRANULARITY, 
TIMESTAMP_COLUMN_WITH_GRANULARITY));
+    AggregationFunction[] aggregationFunctions = 
queryContext.getAggregationFunctions();
+    assertNotNull(aggregationFunctions);
+    AggregationFunctionColumnPair[] aggregationFunctionColumnPairs =
+        StarTreeUtils.extractAggregationFunctionPairs(aggregationFunctions);
+    assertNotNull(aggregationFunctionColumnPairs);
+    List<Pair<AggregationFunction, AggregationFunctionColumnPair>> 
aggregations =
+        new ArrayList<>(aggregationFunctions.length);
+    for (int i = 0; i < aggregationFunctions.length; i++) {
+      aggregations.add(Pair.of(aggregationFunctions[i], 
aggregationFunctionColumnPairs[i]));
+    }
+    List<ExpressionContext> groupByExpressions = 
queryContext.getGroupByExpressions();
+    assertNotNull(groupByExpressions);
+    assertTrue(StarTreeUtils.isFitForStarTree(getStarTreeV2().getMetadata(), 
aggregations,
+        groupByExpressions.toArray(new ExpressionContext[0]), 
Set.of(TIMESTAMP_COLUMN_WITH_GRANULARITY)));
+    assertFalse(StarTreeUtils.isFitForStarTree(getStarTreeV2().getMetadata(), 
aggregations,
+        groupByExpressions.toArray(new ExpressionContext[0]), 
Set.of(TIMESTAMP_COLUMN)));
+  }
+
+  @Test
+  public void testQueriesOnTimestampIndexColumn()
+      throws IOException {
+    String query = String.format("SELECT %s FROM %s", getAggregation(), 
TABLE_NAME);
+    testQuery(query + String.format(" WHERE %s = 0", 
TIMESTAMP_COLUMN_WITH_GRANULARITY));
+    testQuery(query + String.format(" WHERE %s < 10", 
TIMESTAMP_COLUMN_WITH_GRANULARITY));
+    testQuery(query + String.format(" WHERE %1$s > 10 OR %1$s < 50", 
TIMESTAMP_COLUMN_WITH_GRANULARITY));
+    testQuery(query + String.format(" WHERE NOT %s > 10", 
TIMESTAMP_COLUMN_WITH_GRANULARITY));
+    testQuery(
+        query + String.format(" WHERE %s < 10 AND %s > 50", 
TIMESTAMP_COLUMN_WITH_GRANULARITY, DIMENSION1));
+    testQuery(query + String.format(" WHERE %s > 90 AND NOT %s < 25", 
TIMESTAMP_COLUMN_WITH_GRANULARITY, DIMENSION2));
+    testQuery(query + " GROUP BY " + TIMESTAMP_COLUMN_WITH_GRANULARITY);
+  }
+}
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
index fc9158af589..fc2dcd1e84b 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
@@ -114,6 +114,7 @@ import org.apache.pinot.spi.utils.IngestionConfigUtils;
 import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.PinotMd5Mode;
 import org.apache.pinot.spi.utils.TimeUtils;
+import org.apache.pinot.spi.utils.TimestampIndexUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -1824,7 +1825,8 @@ public final class TableConfigUtils {
     // Star-tree index config is not managed by FieldIndexConfigs, and we need 
to validate it separately.
     List<StarTreeIndexConfig> starTreeIndexConfigs = 
indexingConfig.getStarTreeIndexConfigs();
     if (CollectionUtils.isNotEmpty(starTreeIndexConfigs)) {
-      validateStarTreeIndexConfigs(starTreeIndexConfigs, indexConfigsMap, 
schema);
+      validateStarTreeIndexConfigs(starTreeIndexConfigs, indexConfigsMap, 
schema,
+          TimestampIndexUtils.extractColumnsWithGranularity(tableConfig));
     }
 
     // TIMESTAMP index is not managed by FieldIndexConfigs, and we need to 
validate it separately.
@@ -1952,14 +1954,23 @@ public final class TableConfigUtils {
   /// - 'dimensionsSplitOrder' contains all dimensions in 
'skipStarNodeCreationForDimensions'
   /// - Either functionColumnPairs or aggregationConfigs must be specified, 
but not both
   /// - All referenced columns exist in the schema and are single-valued
+  ///
+  /// `timestampIndexColumns` holds the TIMESTAMP-index derived columns (e.g. 
`$ts$DAY`) declared via
+  /// [TimestampConfig#getGranularities()]. These are materialized as 
dictionary-encoded single-value TIMESTAMP
+  /// columns at segment generation time (see 
[TimestampIndexUtils#applyTimestampIndex(TableConfig, Schema)]), so
+  /// they are absent from the schema at config-validation time and are 
accepted here without a schema lookup.
   private static void validateStarTreeIndexConfigs(List<StarTreeIndexConfig> 
starTreeIndexConfigs,
-      Map<String, FieldIndexConfigs> indexConfigsMap, Schema schema) {
+      Map<String, FieldIndexConfigs> indexConfigsMap, Schema schema, 
Set<String> timestampIndexColumns) {
     Set<String> dimensionColumns = new HashSet<>();
     for (StarTreeIndexConfig starTreeIndexConfig : starTreeIndexConfigs) {
       // Validate dimension columns are dictionary encoded
       List<String> dimensionsSplitOrder = 
starTreeIndexConfig.getDimensionsSplitOrder();
       assert CollectionUtils.isNotEmpty(dimensionsSplitOrder);
       for (String dimension : dimensionsSplitOrder) {
+        if (timestampIndexColumns.contains(dimension)) {
+          dimensionColumns.add(dimension);
+          continue;
+        }
         FieldIndexConfigs indexConfigs = indexConfigsMap.get(dimension);
         Preconditions.checkState(indexConfigs != null,
             "Failed to find dimension column: %s specified in star-tree index 
config in schema", dimension);
@@ -2046,6 +2057,9 @@ public final class TableConfigUtils {
       }
 
       for (String column : Iterables.concat(dimensionColumns, 
aggregatedColumns)) {
+        if (timestampIndexColumns.contains(column)) {
+          continue;
+        }
         FieldSpec fieldSpec = schema.getFieldSpecFor(column);
         Preconditions.checkState(fieldSpec != null,
             "Failed to find column: %s specified in star-tree index config in 
schema", column);
@@ -2054,6 +2068,9 @@ public final class TableConfigUtils {
       }
 
       for (String column : dimensionColumns) {
+        if (timestampIndexColumns.contains(column)) {
+          continue;
+        }
         FieldSpec fieldSpec = schema.getFieldSpecFor(column);
         Preconditions.checkState(fieldSpec.isSingleValueField(),
             "Star-tree dimension columns must be single-value, but found 
multi-value column: %s", column);
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
index 310bbfadbc2..af4d7a85875 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
@@ -50,6 +50,8 @@ import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.config.table.TagOverrideConfig;
 import org.apache.pinot.spi.config.table.TenantConfig;
 import org.apache.pinot.spi.config.table.TierConfig;
+import org.apache.pinot.spi.config.table.TimestampConfig;
+import org.apache.pinot.spi.config.table.TimestampIndexGranularity;
 import org.apache.pinot.spi.config.table.UpsertConfig;
 import org.apache.pinot.spi.config.table.assignment.InstanceAssignmentConfig;
 import org.apache.pinot.spi.config.table.assignment.InstancePartitionsType;
@@ -2335,6 +2337,42 @@ public class TableConfigUtilsTest {
     }
   }
 
+  @Test
+  public void testValidateStarTreeIndexWithTimestampIndexDerivedColumns() {
+    Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+        .addDateTime("OrderDate", DataType.TIMESTAMP, "TIMESTAMP", 
"1:MILLISECONDS")
+        .addMetric("value", DataType.LONG)
+        .build();
+
+    // Derived TIMESTAMP-index columns ($OrderDate$DAY, ...) are declared via 
TimestampConfig granularities and are
+    // materialized only at segment generation time, so they are absent from 
the schema here. The star-tree config
+    // referencing them in dimensionsSplitOrder must still validate.
+    FieldConfig timestampFieldConfig = new 
FieldConfig.Builder("OrderDate").withTimestampConfig(
+        new TimestampConfig(List.of(TimestampIndexGranularity.DAY, 
TimestampIndexGranularity.WEEK,
+            TimestampIndexGranularity.MONTH))).build();
+    StarTreeIndexConfig starTreeIndexConfig = new StarTreeIndexConfig(
+        List.of("$OrderDate$DAY", "$OrderDate$WEEK", "$OrderDate$MONTH"), 
null, List.of("COUNT__*"), null, 10000);
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
+        .setFieldConfigList(List.of(timestampFieldConfig))
+        .setStarTreeIndexConfigs(List.of(starTreeIndexConfig))
+        .build();
+    TableConfigUtils.validate(tableConfig, schema);
+
+    // A derived-looking column whose granularity was NOT declared in 
TimestampConfig must still be rejected.
+    StarTreeIndexConfig undeclaredGranularity = new StarTreeIndexConfig(
+        List.of("$OrderDate$HOUR"), null, List.of("COUNT__*"), null, 10000);
+    TableConfig invalidTableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
+        .setFieldConfigList(List.of(timestampFieldConfig))
+        .setStarTreeIndexConfigs(List.of(undeclaredGranularity))
+        .build();
+    try {
+      TableConfigUtils.validate(invalidTableConfig, schema);
+      fail("Should fail for star-tree dimension referencing an undeclared 
timestamp-index granularity");
+    } catch (Exception e) {
+      // expected
+    }
+  }
+
   @Test
   public void testValidateStarTreeIndexDuplicateFunctionColumnPair() {
     Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)


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

Reply via email to