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

morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 16a745d5125 [refactor](fe) Create PreAggInfoContext top-down from 
aggregate instead of bottom-up from scan in SetPreAggStatus (#65237)
16a745d5125 is described below

commit 16a745d51255933cf83ea0a57e709ddbc5268022
Author: starocean999 <[email protected]>
AuthorDate: Wed Jul 8 17:20:49 2026 +0800

    [refactor](fe) Create PreAggInfoContext top-down from aggregate instead of 
bottom-up from scan in SetPreAggStatus (#65237)
    
    related pr: #48502
    
    Problem Summary:
    The `SetPreAggStatus` rule previously created `PreAggInfoContext` at
    `LogicalOlapScan` nodes (bottom-up): the first encountered AGG_KEYS scan
    pushed a new context onto the stack, subsequent scans and intermediate
    nodes
    (filter/join/project) filled in information, and `LogicalAggregate`
    popped
    and consumed the context. This bottom-up approach had two issues:
    
    1. **Nested aggregate information loss**: When a query contains nested
    aggregates (e.g., an outer aggregate over an inner aggregate), the inner
       aggregate's `visitLogicalAggregate` would pop the only context on the
       stack, leaving the outer aggregate with no context for scans directly
       under it.
    
    2. **Unclear context ownership**: Context lifecycle was split across
    scan
       (creation) and aggregate (consumption), making the ownership boundary
    implicit and fragile — intermediate plan nodes that call
    `context.clear()`
    via the base `visit()` method could inadvertently destroy the context.
    
    This PR refactors the context propagation to a **top-down** model:
    
    - `visitLogicalAggregate` now creates a fresh `PreAggInfoContext`,
    pushes it
      onto the stack, visits children, then pops and stores it.
    - `visitLogicalOlapScan` only fills relation IDs into an existing
    context;
      it no longer creates contexts.
    - The `retainAll` filtering is no longer needed since each aggregate
    creates
      its own isolated context with only the scan IDs from its subtree.
    - Nested aggregates each get their own independent context, correctly
      isolating preagg information per aggregate level.
---
 .../nereids/rules/rewrite/SetPreAggStatus.java     | 46 +++++++++++++++-------
 .../nereids_rules_p0/set_preagg/set_preagg.groovy  | 25 +++++++++++-
 2 files changed, 54 insertions(+), 17 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SetPreAggStatus.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SetPreAggStatus.java
index 9b152efbb20..aef5f660056 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SetPreAggStatus.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SetPreAggStatus.java
@@ -54,10 +54,13 @@ import 
org.apache.doris.nereids.trees.plans.visitor.CustomRewriter;
 import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanRewriter;
 import org.apache.doris.nereids.util.ExpressionUtils;
 import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.SessionVariable;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -75,6 +78,7 @@ import java.util.stream.Collectors;
  */
 public class SetPreAggStatus extends 
DefaultPlanRewriter<Stack<SetPreAggStatus.PreAggInfoContext>>
         implements CustomRewriter {
+    private static final Logger LOG = 
LogManager.getLogger(SetPreAggStatus.class);
     private Map<RelationId, PreAggInfoContext> olapScanPreAggContexts = new 
HashMap<>();
 
     /**
@@ -141,8 +145,11 @@ public class SetPreAggStatus extends 
DefaultPlanRewriter<Stack<SetPreAggStatus.P
 
     @Override
     public Plan visit(Plan plan, Stack<PreAggInfoContext> context) {
+        // push null sentinel to stop preagg collection for children reached 
via generic visitor,
+        // while keeping the aggregate's own frame intact on the stack
+        context.push(null);
         Plan newPlan = super.visit(plan, context);
-        context.clear();
+        context.pop();
         return newPlan;
     }
 
@@ -161,10 +168,9 @@ public class SetPreAggStatus extends 
DefaultPlanRewriter<Stack<SetPreAggStatus.P
                             logicalOlapScan.getTable().getName()))) {
                 return logicalOlapScan.withPreAggStatus(PreAggStatus.on());
             } else {
-                if (context.empty()) {
-                    context.push(new PreAggInfoContext());
+                if (!context.empty() && context.peek() != null) {
+                    
context.peek().addRelationId(logicalOlapScan.getRelationId());
                 }
-                context.peek().addRelationId(logicalOlapScan.getRelationId());
                 return logicalOlapScan;
             }
         } else {
@@ -175,7 +181,7 @@ public class SetPreAggStatus extends 
DefaultPlanRewriter<Stack<SetPreAggStatus.P
     @Override
     public Plan visitLogicalFilter(LogicalFilter<? extends Plan> 
logicalFilter, Stack<PreAggInfoContext> context) {
         LogicalFilter plan = (LogicalFilter) super.visit(logicalFilter, 
context);
-        if (!context.empty()) {
+        if (!context.empty() && context.peek() != null) {
             context.peek().addFilterConjuncts(plan.getExpressions());
         }
         return plan;
@@ -185,7 +191,7 @@ public class SetPreAggStatus extends 
DefaultPlanRewriter<Stack<SetPreAggStatus.P
     public Plan visitLogicalJoin(LogicalJoin<? extends Plan, ? extends Plan> 
logicalJoin,
             Stack<PreAggInfoContext> context) {
         LogicalJoin plan = (LogicalJoin) super.visit(logicalJoin, context);
-        if (!context.empty()) {
+        if (!context.empty() && context.peek() != null) {
             context.peek().addJoinInfo(plan);
         }
         return plan;
@@ -195,7 +201,7 @@ public class SetPreAggStatus extends 
DefaultPlanRewriter<Stack<SetPreAggStatus.P
     public Plan visitLogicalProject(LogicalProject<? extends Plan> 
logicalProject,
             Stack<PreAggInfoContext> context) {
         LogicalProject plan = (LogicalProject) super.visit(logicalProject, 
context);
-        if (!context.empty()) {
+        if (!context.empty() && context.peek() != null) {
             context.peek().setReplaceMap(plan.getAliasToProducer());
         }
         return plan;
@@ -204,23 +210,33 @@ public class SetPreAggStatus extends 
DefaultPlanRewriter<Stack<SetPreAggStatus.P
     @Override
     public Plan visitLogicalAggregate(LogicalAggregate<? extends Plan> 
logicalAggregate,
             Stack<PreAggInfoContext> context) {
+        PreAggInfoContext preAggInfoContext = new PreAggInfoContext();
+        context.push(preAggInfoContext);
         Plan plan = super.visit(logicalAggregate, context);
-        if (!context.isEmpty()) {
-            PreAggInfoContext preAggInfoContext = context.pop();
-            
preAggInfoContext.olapScanIds.retainAll(logicalAggregate.child().getInputRelations());
-            
preAggInfoContext.addAggregateFunctions(logicalAggregate.getAggregateFunctions());
-            
preAggInfoContext.addGroupByExpresssions(logicalAggregate.getGroupByExpressions());
-            for (RelationId id : preAggInfoContext.olapScanIds) {
-                olapScanPreAggContexts.put(id, preAggInfoContext);
+        PreAggInfoContext popped = context.pop();
+        if (popped != preAggInfoContext) {
+            if (SessionVariable.isFeDebug()) {
+                Preconditions.checkState(popped == preAggInfoContext,
+                        "PreAggInfoContext stack mismatch in 
visitLogicalAggregate");
+            } else {
+                LOG.warn("PreAggInfoContext stack mismatch in 
visitLogicalAggregate: "
+                        + "expected {} but got {}. Skipping preagg for this 
aggregate.",
+                        preAggInfoContext, popped);
+                return plan;
             }
         }
+        popped.addAggregateFunctions(logicalAggregate.getAggregateFunctions());
+        
popped.addGroupByExpresssions(logicalAggregate.getGroupByExpressions());
+        for (RelationId id : popped.olapScanIds) {
+            olapScanPreAggContexts.put(id, popped);
+        }
         return plan;
     }
 
     @Override
     public Plan visitLogicalRepeat(LogicalRepeat<? extends Plan> repeat, 
Stack<PreAggInfoContext> context) {
         repeat = (LogicalRepeat<? extends Plan>) super.visit(repeat, context);
-        if (!context.isEmpty()) {
+        if (!context.isEmpty() && context.peek() != null) {
             
context.peek().addGroupingScalarFunctionExpresssion(repeat.getGroupingId().get());
             context.peek().addGroupingScalarFunctionExpresssions(
                     repeat.getOutputExpressions().stream()
diff --git 
a/regression-test/suites/nereids_rules_p0/set_preagg/set_preagg.groovy 
b/regression-test/suites/nereids_rules_p0/set_preagg/set_preagg.groovy
index 888892bc9c4..f47b2b40fc2 100644
--- a/regression-test/suites/nereids_rules_p0/set_preagg/set_preagg.groovy
+++ b/regression-test/suites/nereids_rules_p0/set_preagg/set_preagg.groovy
@@ -271,7 +271,7 @@ suite("set_preagg") {
         """)
         contains "(preagg_t1), PREAGGREGATION: ON"
         contains "(preagg_t2), PREAGGREGATION: ON"
-        contains "(preagg_t3), PREAGGREGATION: OFF"
+        contains "(preagg_t3), PREAGGREGATION: ON"
     }
 
     explain {
@@ -291,7 +291,7 @@ suite("set_preagg") {
         """)
         contains "(preagg_t1), PREAGGREGATION: ON"
         contains "(preagg_t2), PREAGGREGATION: ON"
-        contains "(preagg_t3), PREAGGREGATION: OFF"
+        contains "(preagg_t3), PREAGGREGATION: ON"
     }
 
     explain {
@@ -328,4 +328,25 @@ suite("set_preagg") {
         contains "(preagg_t1), PREAGGREGATION: OFF. Reason: No valid aggregate 
on scan."
         contains "(preagg_t1), PREAGGREGATION: ON"
     }
+
+    // Aggregate over limited subquery: Limit between aggregate and scan goes
+    // through generic visitor path, which should block preagg collection
+    // without clearing the aggregate's own frame.
+    explain {
+        sql("""
+            select k1, sum(v7)
+            from (select k1, v7 from preagg_t1 limit 10) s
+            group by k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    // Aggregate over non-Olap relation (numbers TVF).
+    // The TVF scan goes through the generic visitor path, which should not
+    // clear the aggregate's frame and cause EmptyStackException.
+    explain {
+        sql("""
+            select count(*) from (select * from numbers("number"="10")) t;
+        """)
+    }
 }


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

Reply via email to