xiangfu0 commented on code in PR #20035:
URL: https://github.com/apache/pinot/pull/20035#discussion_r4406022973


##########
pinot-core/src/main/java/org/apache/pinot/core/plan/maker/InstancePlanMakerImplV2.java:
##########
@@ -657,86 +661,125 @@ private static boolean 
fitsByColumnUnionCardinalities(List<IndexSegment> segment
     return true;
   }
 
-  /// In-place rewrite QueryContext based on the information from local 
IndexSegment.
-  ///
-  /// @param queryContext
-  /// @param indexSegment
+  /// Rewrites the shared context before fetch planning, using only columns 
available on every selected segment.
+  /// A partially reloaded timestamp index must leave the original expression 
executable on all segments.
+  /// Rewriting per segment would mutate the expressions shared by already 
constructed segment plans.
   @VisibleForTesting
-  public static void rewriteQueryContextWithHints(QueryContext queryContext, 
IndexSegment indexSegment) {
-    Map<ExpressionContext, ExpressionContext> expressionOverrideHints = 
queryContext.getExpressionOverrideHints();
-    if (MapUtils.isEmpty(expressionOverrideHints)) {
+  static void rewriteQueryContextWithHints(QueryContext queryContext, 
List<SegmentContext> segmentContexts) {
+    Map<ExpressionContext, ExpressionContext> hints = 
queryContext.getExpressionOverrideHints();
+    if (MapUtils.isEmpty(hints) || segmentContexts.isEmpty()) {
       return;
     }
+    Map<ExpressionContext, ExpressionContext> applicableHints = 
getApplicableHints(hints, segmentContexts);
+    if (!applicableHints.isEmpty()) {
+      rewriteQueryContextWithHints(queryContext, applicableHints);
+    }
+  }
 
-    List<ExpressionContext> selectExpressions = 
queryContext.getSelectExpressions();
-    selectExpressions.replaceAll(
-        expression -> overrideWithExpressionHints(expression, indexSegment, 
expressionOverrideHints));
+  /// Makes a private copy so segment availability checks never mutate the 
broker-provided hint map.
+  private static Map<ExpressionContext, ExpressionContext> getApplicableHints(
+      Map<ExpressionContext, ExpressionContext> hints, List<SegmentContext> 
segmentContexts) {
+    Map<ExpressionContext, ExpressionContext> applicableHints = new 
HashMap<>(hints);
+    applicableHints.values().removeIf(expression -> expression.getType() != 
ExpressionContext.Type.IDENTIFIER);
+    for (SegmentContext segmentContext : segmentContexts) {

Review Comment:
   Addressed in df653c2d17e2cd833b110ff50d570892c4f5b26c. Empty hints return 
immediately. A single hint validates its identifier target once, checks that 
column across segments, and returns at the first missing column without a 
copied map or availability cache. Multiple hints deduplicate target columns and 
stop scanning when none remain. Successful eligibility still covers every 
selected segment, and the original hint map is unchanged. Unsupported 
non-identifier targets now fail the requested precondition before scanning. 
Focused regressions verify the early exit, duplicate-target checks, and 
positive cross-segment case.



##########
pinot-core/src/main/java/org/apache/pinot/core/query/request/context/QueryContext.java:
##########
@@ -876,70 +903,33 @@ public QueryContext build() {
         }
       }
 
+      queryContext.prepareForExecution(Set.of());
       return queryContext;
     }
 
-    /// Helper method to generate the aggregation functions for the query.
-    private void generateAggregationFunctions(QueryContext queryContext) {
-      List<Pair<AggregationFunction, FilterContext>> 
filteredAggregationFunctions = new ArrayList<>();
-      Map<Pair<FunctionContext, FilterContext>, Integer> 
filteredAggregationsIndexMap = new HashMap<>();
-
-      // Add aggregation functions in the SELECT clause
-      // NOTE: DO NOT deduplicate the aggregation functions in the SELECT 
clause because that involves protocol change.
-      List<Pair<FunctionContext, FilterContext>> filteredAggregations = new 
ArrayList<>();
+    /// Captures logical aggregation slots before rewriting; execution 
preparation must not deduplicate them again.
+    private void extractAggregationFunctionContexts(QueryContext queryContext) 
{
+      List<Pair<FunctionContext, FilterContext>> contexts = new ArrayList<>();
+      // SELECT duplicates are protocol-visible and must retain independent 
result slots.
       for (ExpressionContext selectExpression : 
queryContext._selectExpressions) {
-        getAggregations(selectExpression, filteredAggregations);
-      }
-      for (Pair<FunctionContext, FilterContext> pair : filteredAggregations) {
-        FunctionContext aggregation = pair.getLeft();
-        FilterContext filter = pair.getRight();
-        int functionIndex = filteredAggregationFunctions.size();
-        AggregationFunction aggregationFunction =
-            AggregationFunctionFactory.getAggregationFunction(aggregation, 
queryContext._nullHandlingEnabled);
-        filteredAggregationFunctions.add(Pair.of(aggregationFunction, filter));
-        filteredAggregationsIndexMap.put(Pair.of(aggregation, filter), 
functionIndex);
+        getAggregations(selectExpression, contexts);
       }
-
-      // Add aggregation functions in the HAVING and ORDER-BY clause but not 
in the SELECT clause
-      filteredAggregations.clear();
+      Set<Pair<FunctionContext, FilterContext>> existing = new 
HashSet<>(contexts);

Review Comment:
   Addressed in df653c2d17e2cd833b110ff50d570892c4f5b26c: HAVING/ORDER BY 
candidates are collected before creating the deduplication set, which is now 
allocated only when that list is nonempty. SELECT duplicates keep their 
separate original slots; the preparation regression checks those slots and 
unchanged aggregation reuse.



##########
pinot-core/src/main/java/org/apache/pinot/core/query/request/context/QueryContext.java:
##########
@@ -353,6 +355,37 @@ public Map<ExpressionContext, ExpressionContext> 
getExpressionOverrideHints() {
     return _expressionOverrideHints;
   }
 
+  /// Prepares derived execution state after logical expressions change, 
before fetch or segment planning.
+  /// Reuses compiled aggregations whose input trees did not change. The 
supplied set uses function identity because
+  /// expression rewrites can mutate function arguments, including their hash 
codes, in place.
+  public void prepareForExecution(Set<FunctionContext> changedFunctions) {
+    int numAggregations = _aggregationFunctionContexts.size();
+    if (numAggregations > 0) {
+      boolean initialPreparation = _aggregationFunctions == null;
+      if (initialPreparation) {
+        _aggregationFunctions = new AggregationFunction[numAggregations];
+      }
+      _filteredAggregationFunctions = new ArrayList<>(numAggregations);

Review Comment:
   Addressed in df653c2d17e2cd833b110ff50d570892c4f5b26c. Preparation retains 
the existing filtered-aggregation list and index map, preserves unchanged Pair 
instances, and replaces a Pair only when its compiled function instance 
changes. Filter-clause presence is computed during initial preparation only. 
Actual rewrites still clear and rehash the map, including FILTER-only changes; 
this keeps the change-tracking API small. Regressions verify list/pair reuse 
and a successful lookup after a FILTER-only key mutation, plus refreshed 
WHERE-only required columns.



##########
pinot-core/src/main/java/org/apache/pinot/core/plan/maker/InstancePlanMakerImplV2.java:
##########
@@ -657,86 +661,125 @@ private static boolean 
fitsByColumnUnionCardinalities(List<IndexSegment> segment
     return true;
   }
 
-  /// In-place rewrite QueryContext based on the information from local 
IndexSegment.
-  ///
-  /// @param queryContext
-  /// @param indexSegment
+  /// Rewrites the shared context before fetch planning, using only columns 
available on every selected segment.
+  /// A partially reloaded timestamp index must leave the original expression 
executable on all segments.
+  /// Rewriting per segment would mutate the expressions shared by already 
constructed segment plans.
   @VisibleForTesting
-  public static void rewriteQueryContextWithHints(QueryContext queryContext, 
IndexSegment indexSegment) {
-    Map<ExpressionContext, ExpressionContext> expressionOverrideHints = 
queryContext.getExpressionOverrideHints();
-    if (MapUtils.isEmpty(expressionOverrideHints)) {
+  static void rewriteQueryContextWithHints(QueryContext queryContext, 
List<SegmentContext> segmentContexts) {
+    Map<ExpressionContext, ExpressionContext> hints = 
queryContext.getExpressionOverrideHints();
+    if (MapUtils.isEmpty(hints) || segmentContexts.isEmpty()) {
       return;
     }
+    Map<ExpressionContext, ExpressionContext> applicableHints = 
getApplicableHints(hints, segmentContexts);
+    if (!applicableHints.isEmpty()) {
+      rewriteQueryContextWithHints(queryContext, applicableHints);
+    }
+  }
 
-    List<ExpressionContext> selectExpressions = 
queryContext.getSelectExpressions();
-    selectExpressions.replaceAll(
-        expression -> overrideWithExpressionHints(expression, indexSegment, 
expressionOverrideHints));
+  /// Makes a private copy so segment availability checks never mutate the 
broker-provided hint map.
+  private static Map<ExpressionContext, ExpressionContext> getApplicableHints(
+      Map<ExpressionContext, ExpressionContext> hints, List<SegmentContext> 
segmentContexts) {
+    Map<ExpressionContext, ExpressionContext> applicableHints = new 
HashMap<>(hints);
+    applicableHints.values().removeIf(expression -> expression.getType() != 
ExpressionContext.Type.IDENTIFIER);
+    for (SegmentContext segmentContext : segmentContexts) {
+      Set<String> columns = segmentContext.getIndexSegment().getColumnNames();
+      applicableHints.values().removeIf(expression -> 
!columns.contains(expression.getIdentifier()));
+    }
+    return applicableHints;
+  }
 
-    List<Pair<AggregationFunction, FilterContext>> filtAggrFuns = 
queryContext.getFilteredAggregationFunctions();
-    if (filtAggrFuns != null) {
-      for (Pair<AggregationFunction, FilterContext> 
filteredAggregationFunction : filtAggrFuns) {
-        FilterContext right = filteredAggregationFunction.getRight();
-        if (right != null) {
-          overrideWithExpressionHints(right, indexSegment, 
expressionOverrideHints);
+  private static void rewriteQueryContextWithHints(QueryContext queryContext,
+      Map<ExpressionContext, ExpressionContext> hints) {
+    Set<FunctionContext> changedFunctions = Collections.newSetFromMap(new 
IdentityHashMap<>());
+    // FILTER predicates have separate contexts; aggregation inputs are 
visited through SELECT/HAVING/ORDER-BY.
+    List<Pair<AggregationFunction, FilterContext>> filteredAggregations =
+        queryContext.getFilteredAggregationFunctions();
+    if (filteredAggregations != null) {
+      for (Pair<AggregationFunction, FilterContext> aggregation : 
filteredAggregations) {
+        if (aggregation.getRight() != null) {
+          overrideWithExpressionHints(aggregation.getRight(), hints, 
changedFunctions);
         }
       }
     }
-
+    queryContext.getSelectExpressions().replaceAll(
+        expression -> overrideWithExpressionHints(expression, hints, 
changedFunctions));
     List<ExpressionContext> groupByExpressions = 
queryContext.getGroupByExpressions();
     if (CollectionUtils.isNotEmpty(groupByExpressions)) {
       groupByExpressions.replaceAll(
-          expression -> overrideWithExpressionHints(expression, indexSegment, 
expressionOverrideHints));
+          expression -> overrideWithExpressionHints(expression, hints, 
changedFunctions));
     }
-
     List<OrderByExpressionContext> orderByExpressions = 
queryContext.getOrderByExpressions();
     if (CollectionUtils.isNotEmpty(orderByExpressions)) {
-      orderByExpressions.replaceAll(expression -> new OrderByExpressionContext(
-          overrideWithExpressionHints(expression.getExpression(), 
indexSegment, expressionOverrideHints),
-          expression.isAsc()));
+      orderByExpressions.replaceAll(orderBy -> {
+        ExpressionContext expression = orderBy.getExpression();
+        ExpressionContext rewritten = overrideWithExpressionHints(expression, 
hints, changedFunctions);
+        return rewritten == expression ? orderBy
+            : new OrderByExpressionContext(rewritten, orderBy.isAsc(), 
orderBy.isNullsLast());
+      });
     }
-
-    // In-place override
-    FilterContext filter = queryContext.getFilter();
-    if (filter != null) {
-      overrideWithExpressionHints(filter, indexSegment, 
expressionOverrideHints);
+    if (queryContext.getFilter() != null) {
+      overrideWithExpressionHints(queryContext.getFilter(), hints, 
changedFunctions);
     }
-
-    // In-place override
-    FilterContext havingFilter = queryContext.getHavingFilter();
-    if (havingFilter != null) {
-      overrideWithExpressionHints(havingFilter, indexSegment, 
expressionOverrideHints);
+    if (queryContext.getHavingFilter() != null) {
+      overrideWithExpressionHints(queryContext.getHavingFilter(), hints, 
changedFunctions);
+    }
+    if (!changedFunctions.isEmpty()) {
+      queryContext.prepareForExecution(changedFunctions);
     }
   }
 
   @VisibleForTesting
   public static void overrideWithExpressionHints(FilterContext filter, 
IndexSegment indexSegment,
       Map<ExpressionContext, ExpressionContext> expressionOverrideHints) {
+    overrideWithExpressionHints(filter,
+        getApplicableHints(expressionOverrideHints, List.of(new 
SegmentContext(indexSegment))),
+        Collections.newSetFromMap(new IdentityHashMap<>()));
+  }
+
+  private static void overrideWithExpressionHints(FilterContext filter,
+      Map<ExpressionContext, ExpressionContext> expressionOverrideHints, 
Set<FunctionContext> changedFunctions) {
+    if (filter.isConstant()) {
+      return;
+    }
     if (filter.getChildren() != null) {
-      // AND, OR, NOT
       for (FilterContext child : filter.getChildren()) {
-        overrideWithExpressionHints(child, indexSegment, 
expressionOverrideHints);
+        overrideWithExpressionHints(child, expressionOverrideHints, 
changedFunctions);
       }
     } else {
-      // PREDICATE
       Predicate predicate = filter.getPredicate();
-      predicate.setLhs(overrideWithExpressionHints(predicate.getLhs(), 
indexSegment, expressionOverrideHints));
+      predicate.setLhs(
+          overrideWithExpressionHints(predicate.getLhs(), 
expressionOverrideHints, changedFunctions));
     }
   }
 
   @VisibleForTesting
   public static ExpressionContext 
overrideWithExpressionHints(ExpressionContext expression, IndexSegment 
indexSegment,
       Map<ExpressionContext, ExpressionContext> expressionOverrideHints) {
+    return overrideWithExpressionHints(expression,
+        getApplicableHints(expressionOverrideHints, List.of(new 
SegmentContext(indexSegment))),
+        Collections.newSetFromMap(new IdentityHashMap<>()));
+  }
+
+  private static ExpressionContext 
overrideWithExpressionHints(ExpressionContext expression,
+      Map<ExpressionContext, ExpressionContext> expressionOverrideHints, 
Set<FunctionContext> changedFunctions) {
     if (expression.getType() != ExpressionContext.Type.FUNCTION) {
       return expression;
     }
     ExpressionContext overrideExpression = 
expressionOverrideHints.get(expression);

Review Comment:
   Addressed in df653c2d17e2cd833b110ff50d570892c4f5b26c. Multiple-hint 
traversal checks candidate function name/arity before structural lookup while 
continuing recursive traversal. Singleton matching uses the JDK 25 single-entry 
Map.of lookup, which compares equality directly. Expression hashes are not 
cached, and function-identity tracking remains in place for shared mutable 
descendants.



##########
pinot-core/src/main/java/org/apache/pinot/core/query/request/context/QueryContext.java:
##########
@@ -353,6 +355,37 @@ public Map<ExpressionContext, ExpressionContext> 
getExpressionOverrideHints() {
     return _expressionOverrideHints;
   }
 
+  /// Prepares derived execution state after logical expressions change, 
before fetch or segment planning.
+  /// Reuses compiled aggregations whose input trees did not change. The 
supplied set uses function identity because
+  /// expression rewrites can mutate function arguments, including their hash 
codes, in place.
+  public void prepareForExecution(Set<FunctionContext> changedFunctions) {
+    int numAggregations = _aggregationFunctionContexts.size();
+    if (numAggregations > 0) {
+      boolean initialPreparation = _aggregationFunctions == null;
+      if (initialPreparation) {
+        _aggregationFunctions = new AggregationFunction[numAggregations];
+      }
+      _filteredAggregationFunctions = new ArrayList<>(numAggregations);
+      _filteredAggregationsIndexMap = new HashMap<>();
+      _hasFilteredAggregations = false;
+      for (int i = 0; i < numAggregations; i++) {
+        Pair<FunctionContext, FilterContext> context = 
_aggregationFunctionContexts.get(i);
+        if (initialPreparation || 
changedFunctions.contains(context.getLeft())) {
+          _aggregationFunctions[i] =
+              
AggregationFunctionFactory.getAggregationFunction(context.getLeft(), 
_nullHandlingEnabled);
+        }
+        _filteredAggregationFunctions.add(Pair.of(_aggregationFunctions[i], 
context.getRight()));
+        _filteredAggregationsIndexMap.put(context, i);
+        _hasFilteredAggregations |= context.getRight() != null;
+      }
+    }
+    Builder.extractColumns(this);
+    if (_groupByExpressions != null) {
+      _isUnsafeTrim = isGroupingSets() || !isSameOrderAndGroupByColumns(this) 
|| _havingFilter != null;

Review Comment:
   Addressed in df653c2d17e2cd833b110ff50d570892c4f5b26c: the non-null HAVING 
check now precedes the order/group-column comparison, during both initial and 
post-rewrite preparation. All 76 focused tests and scoped checks passed.



-- 
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