Jackie-Jiang commented on code in PR #19367:
URL: https://github.com/apache/pinot/pull/19367#discussion_r4051401996


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/ValueAggregator.java:
##########
@@ -41,6 +41,28 @@ public interface ValueAggregator<R, A> {
   /// specified in the schema.
   A getInitialAggregatedValue(@Nullable R rawValue);
 
+  /// Returns the aggregated value of a group whose input values are all null, 
or `null` to have the star-tree record
+  /// the group in its null vector instead.
+  ///
+  /// Only consulted by null-aware star-trees, which exclude null input values 
from the pre-aggregation and can
+  /// therefore produce a group with no values at all.
+  ///
+  /// Returning `null` is safe whenever the aggregation function skips null 
rows while reading the pre-aggregated
+  /// column, which every aggregation function does apart from `COUNT`. A 
group recorded in the null vector is never

Review Comment:
   Fixed, though not the way you suggested — an empty `AvgPair` from 
`AvgValueAggregator` stops the crash but answers the wrong thing. 
`extractFinalResult` renders a zero-count pair as `DEFAULT_FINAL_RESULT`, i.e. 
`Double.NEGATIVE_INFINITY`, so the all-null group would come back as 
`-Infinity` instead of `NULL`. `CountValueAggregator` can override precisely 
because `0` is genuinely `COUNT`'s answer for an empty group; `AVG` has no such 
value.
   
   The gap was on the reading side, so that is where it is fixed: all three 
serialized paths now wrap their loop in `forEachNotNull`, like every other 
`BYTES` aggregator, and `AVG_MV` inherits it.
   
   Your repro is now a test — `anAllNullMetricGroupAveragesToNull`, with 
`AVG__m2` in the null-aware config exactly as you had it.
   
   I also expanded the contract here to spell out when returning `null` is 
safe, since "every aggregation function skips null rows while reading the 
pre-aggregated column" was the unstated assumption that `AVG` broke.
   



##########
pinot-core/src/main/java/org/apache/pinot/core/startree/StarTreeUtils.java:
##########
@@ -405,82 +449,117 @@ public static BaseProjectOperator<?> 
createStarTreeBasedProjectOperator(IndexSeg
             .toArray(new ExpressionContext[0]) : null;
 
     if (queryContext.isNullHandlingEnabled()) {
-      // We can still use the star-tree index if there aren't actually any 
null values in this segment for all the
-      // metrics being aggregated, all the dimensions being filtered on / 
grouped by.
-      for (int i = 0; i < aggregationFunctionColumnPairs.length; i++) {
-        AggregationFunctionColumnPair aggregationFunctionColumnPair = 
aggregationFunctionColumnPairs[i];
-        if (aggregationFunctionColumnPair == 
AggregationFunctionColumnPair.COUNT_STAR) {
-          // COUNT aggregation function returns a non-empty input expressions 
list only when null handling is enabled
-          // and the input operand is a non-star identifier or function.
-          List<ExpressionContext> inputExpressions = 
aggregationFunctions[i].getInputExpressions();
-          if (!inputExpressions.isEmpty()) {
-            if (inputExpressions.get(0).getType() == 
ExpressionContext.Type.IDENTIFIER) {
-              DataSource dataSource = 
indexSegment.getDataSource(inputExpressions.get(0).getIdentifier());
-              if (FilterOperatorUtils.hasNulls(dataSource)) {
-                return null;
-              }
-            }
-          }
-          // Null handling is irrelevant for COUNT(*), COUNT(literal), 
COUNT(nonNullColumn)
-          continue;
-        }
-
-        String column = aggregationFunctionColumnPair.getColumn();
-        DataSource dataSource = indexSegment.getDataSourceNullable(column);
-        if (dataSource == null) {
-          LOGGER.debug("Cannot use star-tree index because aggregation column: 
'{}' does not exist", column);
-          return null;
-        }
-        if (FilterOperatorUtils.hasNulls(dataSource)) {
-          LOGGER.debug("Cannot use star-tree index because aggregation column: 
'{}' has null values", column);
-          return null;
-        }
-      }
-
-      for (String column : predicateEvaluatorsMap.keySet()) {
-        DataSource dataSource = indexSegment.getDataSourceNullable(column);
-        if (dataSource == null) {
-          LOGGER.debug("Cannot use star-tree index because filter column: '{}' 
does not exist", column);
-          return null;
-        }
-        if (FilterOperatorUtils.hasNulls(dataSource)) {
-          LOGGER.debug("Cannot use star-tree index because filter column: '{}' 
has null values", column);
-          return null;
-        }
+      // A null-aware star-tree pre-aggregates with exactly the semantics the 
query asks for
+      StarTreeProjectPlan plan = createProjectPlan(indexSegment, queryContext, 
starTrees, true, aggregationFunctions,
+          groupByExpressions, predicateEvaluatorsMap);
+      if (plan != null) {
+        return plan;
       }
+    }
+    return createProjectPlan(indexSegment, queryContext, starTrees, false, 
aggregationFunctions, groupByExpressions,
+        predicateEvaluatorsMap);
+  }
 
-      Set<String> groupByColumns = new HashSet<>();
-      if (groupByExpressions != null) {
-        for (ExpressionContext groupByExpression : groupByExpressions) {
-          groupByExpression.getColumns(groupByColumns);
-        }
-      }
-      for (String column : groupByColumns) {
-        DataSource dataSource = indexSegment.getDataSourceNullable(column);
-        if (dataSource == null) {
-          LOGGER.debug("Cannot use star-tree index because group-by column: 
'{}' does not exist", column);
-          return null;
-        }
-        if (FilterOperatorUtils.hasNulls(dataSource)) {
-          LOGGER.debug("Cannot use star-tree index because group-by column: 
'{}' has null values", column);
-          return null;
-        }
-      }
+  /// Returns a [StarTreeProjectPlan] built on the first star-tree that both 
matches `nullAware` and fits the query,
+  /// or `null` if there is none.
+  ///
+  /// Resolves the function-column pairs against the same mode, because a 
null-aware star-tree stores `COUNT` per
+  /// column while a regular one stores a single count of every row, and the 
executors have to read back whichever
+  /// was projected.
+  @Nullable
+  private static StarTreeProjectPlan createProjectPlan(IndexSegment 
indexSegment, QueryContext queryContext,
+      List<StarTreeV2> starTrees, boolean nullAware, AggregationFunction[] 
aggregationFunctions,
+      @Nullable ExpressionContext[] groupByExpressions,
+      Map<String, List<CompositePredicateEvaluator>> predicateEvaluatorsMap) {
+    // Only `COUNT` resolves differently between the two, and never to `null`, 
so a query that cannot be represented
+    // as pairs at all fails here for either kind of star-tree
+    AggregationFunctionColumnPair[] functionColumnPairs =
+        extractAggregationFunctionPairs(aggregationFunctions, nullAware);
+    if (functionColumnPairs == null) {
+      return null;
+    }
+    // A regular star-tree folded nulls into the column's default value and 
counted them, so it can only answer a
+    // null-handling-on query when nothing the query touches is actually null
+    if (!nullAware && queryContext.isNullHandlingEnabled() && 
!hasNoNullValues(indexSegment, aggregationFunctions,
+        functionColumnPairs, predicateEvaluatorsMap.keySet(), 
groupByExpressions)) {
+      return null;
     }
 
     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]));
+      aggregations.add(Pair.of(aggregationFunctions[i], 
functionColumnPairs[i]));
     }
 
     for (StarTreeV2 starTreeV2 : starTrees) {
-      if (isFitForStarTree(starTreeV2.getMetadata(), aggregations, 
groupByExpressions,
-          predicateEvaluatorsMap.keySet())) {
-        return new StarTreeProjectPlanNode(queryContext, starTreeV2, 
aggregationFunctionColumnPairs, groupByExpressions,
-            predicateEvaluatorsMap).run();
+      StarTreeV2Metadata metadata = starTreeV2.getMetadata();
+      if (metadata.isNullHandlingEnabled() != nullAware) {
+        continue;
+      }
+      if (isFitForStarTree(metadata, aggregations, groupByExpressions, 
predicateEvaluatorsMap.keySet())) {

Review Comment:
   Good catch, and the data dependence makes it worse than it first looks. 
Rather than refuse the transform, I changed the encoding so the read works.
   
   The reserved id now exists only inside the tree. The forward index stores 
the column's default null value and marks the row in the dimension's null 
vector, which is exactly how a regular column stores a null, so a value-based 
read of a star-tree dimension resolves through the shared segment dictionary 
like any other column.
   
   The reserved id was never doing any work at query time in the first place — 
the group key generator takes the null group from the null vector, not from the 
stored id — so nothing is lost by not storing it, and the `cardinality + 1` 
widening plus the `StarTreeLoaderUtils` bit-width recomputation both go away 
with it.
   
   `groupingByATransformOverANullDimensionReturnsTheNullGroup` runs your query.
   



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/ValueAggregator.java:
##########
@@ -41,6 +41,28 @@ public interface ValueAggregator<R, A> {
   /// specified in the schema.
   A getInitialAggregatedValue(@Nullable R rawValue);
 
+  /// Returns the aggregated value of a group whose input values are all null, 
or `null` to have the star-tree record
+  /// the group in its null vector instead.
+  ///
+  /// Only consulted by null-aware star-trees, which exclude null input values 
from the pre-aggregation and can
+  /// therefore produce a group with no values at all.
+  ///
+  /// Returning `null` is safe whenever the aggregation function skips null 
rows while reading the pre-aggregated
+  /// column, which every aggregation function does apart from `COUNT`. A 
group recorded in the null vector is never
+  /// read back, so the placeholder left in the forward index is never 
deserialized.
+  ///
+  /// `COUNT` is the exception and overrides this: it is read back by summing 
the pre-aggregated column rather than
+  /// through the null vector, so it answers `0` itself. Every other 
aggregator takes the default, which keeps an
+  /// all-null group down to a placeholder plus one null-vector bit instead of 
a serialized empty sketch.
+  ///
+  /// An aggregator whose [#getAggregatedValueType] is `BYTES` must also make 
[#getMaxAggregatedValueByteSize] account

Review Comment:
   Fixed. The builder now tracks whether a metric aggregated anything at all, 
and sizes the forward index from the serialized length of 
`getAllNullAggregatedValue()` when it did not, falling back to zero when there 
is no such value.
   
   `getMaxAggregatedValueByteSize()` is therefore only consulted for a column 
that actually has values, which leaves `SumPrecisionValueAggregator`'s 
precondition intact — realtime ingestion aggregation still relies on it, so the 
fix belongs at the star-tree call site rather than in the aggregator.
   
   I also wrote the obligation into the interface contract: an aggregator whose 
`getAggregatedValueType()` is `BYTES` has to make 
`getMaxAggregatedValueByteSize` account for its all-null value, or the 
star-tree under-allocates. `anAllNullMetricColumnAggregatesToNull` covers a 
fully null `BIG_DECIMAL` column under `SUMPRECISION`.
   



##########
pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/startree/AggregationFunctionColumnPair.java:
##########
@@ -59,20 +74,37 @@ public static String toColumnName(AggregationFunctionType 
functionType, String c
   }
 
   public static AggregationFunctionColumnPair fromColumnName(String 
columnName) {
+    return fromColumnName(columnName, false);
+  }
+
+  /// Parses a function-column pair name such as `sum__col`.
+  ///
+  /// When `preserveCountColumn` is `false`, `count__col` resolves to 
[#COUNT_STAR], matching how a regular star-tree
+  /// stores counts. Pass `true` for a null-aware star-tree, where 
`count__col` denotes the non-null count of `col`.
+  public static AggregationFunctionColumnPair fromColumnName(String 
columnName, boolean preserveCountColumn) {
     String[] parts = columnName.split(DELIMITER, 2);
-    return fromFunctionAndColumnName(parts[0], parts[1]);
+    return fromFunctionAndColumnName(parts[0], parts[1], preserveCountColumn);
   }
 
-  public static AggregationFunctionColumnPair 
fromAggregationConfig(StarTreeAggregationConfig aggregationConfig) {
-    return 
fromFunctionAndColumnName(aggregationConfig.getAggregationFunction(), 
aggregationConfig.getColumnName());
+  /// Builds a pair from an aggregation config. See [#fromColumnName] for the 
meaning of `preserveCountColumn`.
+  public static AggregationFunctionColumnPair 
fromAggregationConfig(StarTreeAggregationConfig aggregationConfig,

Review Comment:
   The description gap was real and is fixed — the compatibility section now 
lists every API break and says which two are what leave the 
binary-compatibility check red.
   
   I am keeping the signature change rather than adding the delegating 
overload, though. `fromColumnName` kept its one-argument form because it has a 
genuine mode-free meaning (reading a pair name off an old segment) and callers 
outside this repo. `fromAggregationConfig` has two callers, both in-tree, and 
the pair it builds is only correct for one tree mode — so a one-argument form 
would be an overload whose only remaining purpose is to let a caller silently 
build the wrong pair.
   
   The check stays red regardless: `writeMetadata` breaks too, and, as the 
description now spells out, a null-aware tree cannot be read correctly by a 
server predating this change at all, so enabling the flag already requires a 
full cluster upgrade.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to