github-actions[bot] commented on code in PR #65846:
URL: https://github.com/apache/doris/pull/65846#discussion_r3643114331


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SetPreAggStatus.java:
##########
@@ -94,8 +94,22 @@ public static class PreAggInfoContext {
 
         private Map<Slot, Expression> replaceMap = new HashMap<>();
 
-        private void setReplaceMap(Map<Slot, Expression> replaceMap) {
-            this.replaceMap = replaceMap;
+        private void setReplaceMap(Map<Slot, Expression> newReplaceMap) {
+            // merge instead of replace: sibling projects under a join share 
one
+            // PreAggInfoContext, and a full replacement would lose mappings 
from
+            // the sibling. merge keeps all entries; new entries shadow old 
ones
+            // by putAll semantics so a chain of projects still resolves 
correctly.
+            //
+            // Before merging, resolve the new aliases' producers through the
+            // existing replaceMap so that upper-layer aliases reference base 
table
+            // columns directly instead of intermediate computed aliases.
+            Map<Slot, Expression> merged = new HashMap<>(this.replaceMap);

Review Comment:
   [P2] Avoid copying the accumulated alias map at every project
   
   Every `LogicalProject`, including an alias-free one, reaches this method, 
and this copy grows with all aliases seen in earlier sibling/nested projects. A 
plan with N projected branches therefore copies 0+1+...+(N-1) entries and 
allocates N increasingly large maps during this rule. The context owns 
`replaceMap` and no snapshot escapes, so please resolve only the current 
entries into a small temporary map against the unchanged old map, then `putAll` 
them into `this.replaceMap` (and return immediately when the new map is empty). 
That preserves the intended old-map-only resolution semantics while keeping 
accumulation linear.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SetPreAggStatus.java:
##########
@@ -326,28 +340,49 @@ private PreAggStatus 
checkAggregateFunctions(Set<AggregateFunction> aggregateFun
             }
             PreAggStatus preAggStatus = PreAggStatus.on();
             for (AggregateFunction aggFunc : aggregateFuncs) {
-                if (aggFunc.children().isEmpty()) {
+                Set<Slot> aggSlots = aggFunc.getInputSlots();
+                if (aggSlots.isEmpty()) {
                     preAggStatus = PreAggStatus.off(
                             String.format("can't turn preAgg on for aggregate 
function %s", aggFunc));
-                } else if (aggFunc.children().size() == 1 && aggFunc.child(0) 
instanceof Slot) {
-                    Slot aggSlot = (Slot) aggFunc.child(0);
-                    if (aggSlot instanceof SlotReference
-                            && ((SlotReference) 
aggSlot).getOriginalColumn().isPresent()) {
-                        if (((SlotReference) 
aggSlot).getOriginalColumn().get().isKey()) {
-                            preAggStatus = 
OneKeySlotAggChecker.INSTANCE.check(aggFunc);
+                } else {
+                    Pair<Set<SlotReference>, Set<SlotReference>> splitSlots = 
splitKeyValueSlots(aggSlots);

Review Comment:
   [P1] Validate mixed aggregates relative to the scan being enabled
   
   The merged map now survives the alias-free project that aggregate 
normalization inserts, exposing a scan-ownership bug in this global key/value 
split. A reduced plan is:
   
   ```text
   Aggregate(sum(if(a > 0, r.v7, 0)))
     Project(a, r.v7)
       Join(a = r.k1)
         Project(abs(l.k1) AS a) -> Scan(l AGG_KEYS)
         Scan(r AGG_KEYS)
   ```
   
   The new map composition restores `a -> abs(l.k1)`. While deciding `l`, 
candidate selection therefore sees local `l.k1`, this split sees that key plus 
foreign SUM column `r.v7`, and the mixed helper accepts both without checking 
ownership. If `l` has two partial rows for one logical key and `r.v7=10`, 
leaving `l` ON joins the right row twice and returns 20 instead of 10. Please 
make mixed validation relative to the current scan—do not use a foreign 
aggregate value to justify exposing this scan's partial rows—and add a 
two-table derived-key regression with repeated aggregate-key data.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SetPreAggStatus.java:
##########
@@ -326,28 +340,49 @@ private PreAggStatus 
checkAggregateFunctions(Set<AggregateFunction> aggregateFun
             }
             PreAggStatus preAggStatus = PreAggStatus.on();
             for (AggregateFunction aggFunc : aggregateFuncs) {
-                if (aggFunc.children().isEmpty()) {
+                Set<Slot> aggSlots = aggFunc.getInputSlots();
+                if (aggSlots.isEmpty()) {
                     preAggStatus = PreAggStatus.off(
                             String.format("can't turn preAgg on for aggregate 
function %s", aggFunc));
-                } else if (aggFunc.children().size() == 1 && aggFunc.child(0) 
instanceof Slot) {
-                    Slot aggSlot = (Slot) aggFunc.child(0);
-                    if (aggSlot instanceof SlotReference
-                            && ((SlotReference) 
aggSlot).getOriginalColumn().isPresent()) {
-                        if (((SlotReference) 
aggSlot).getOriginalColumn().get().isKey()) {
-                            preAggStatus = 
OneKeySlotAggChecker.INSTANCE.check(aggFunc);
+                } else {
+                    Pair<Set<SlotReference>, Set<SlotReference>> splitSlots = 
splitKeyValueSlots(aggSlots);
+                    if (splitSlots.first.isEmpty()) {
+                        // only value slots
+                        if (aggFunc.children().size() == 1 && aggFunc.child(0) 
instanceof SlotReference) {
+                            SlotReference slotRef = (SlotReference) 
aggFunc.child(0);
+                            if (slotRef.getOriginalColumn().isPresent()) {
+                                preAggStatus = 
OneValueSlotAggChecker.INSTANCE.check(aggFunc,
+                                        
slotRef.getOriginalColumn().get().getAggregationType());
+                            } else {
+                                preAggStatus = PreAggStatus.off(
+                                        String.format("can't turn preAgg on 
for aggregate function %s", aggFunc));
+                            }
+                        } else {
+                            preAggStatus = PreAggStatus.off(
+                                    String.format("can't turn preAgg on for 
aggregate function %s", aggFunc));
+                        }
+                    } else if (splitSlots.second.isEmpty()) {
+                        // only key slots
+                        // Volatile expressions (e.g., random()) must be 
rejected here:
+                        // with pre-agg ON they would be evaluated per partial 
row instead of
+                        // per merged logical row, changing DISTINCT/MAX/MIN 
semantics.
+                        if (aggFunc.containsVolatileExpression()) {

Review Comment:
   [P1] Check volatility before per-scan candidate filtering
   
   This guard is reached only for aggregate functions selected for the current 
scan, and it never covers the separately collected filter/join/grouping lists. 
That leaves two result-changing bypasses. For example:
   
   ```text
   Aggregate(max(r.k1 + random()))
     Join(l.k1 = r.k1)
       Scan(l AGG_KEYS)
       Scan(r AGG_KEYS)
   ```
   
   The MAX turns `r` OFF, but for `l` it is whitelisted as an otherwise 
duplicate-insensitive other-table aggregate; the candidate set is empty and 
`createPreAggStatus` returns ON before this check. Partial `l` rows then 
duplicate the joined `r` row and create extra random samples. Likewise, 
`Filter(random() < 0.5) -> Scan` passes because its input-slot set is empty, so 
two partial SUM rows can independently yield 1 or 2 instead of only 0 or their 
merged sum. Volatile grouping aliases and join conjuncts follow the same 
slot-only path. Please enforce row stability centrally—before per-scan 
candidate filtering—for the alias-expanded aggregate, filter, join, and 
grouping expressions, and add cases that assert both scans are OFF.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SetPreAggStatus.java:
##########
@@ -94,8 +94,22 @@ public static class PreAggInfoContext {
 
         private Map<Slot, Expression> replaceMap = new HashMap<>();
 
-        private void setReplaceMap(Map<Slot, Expression> replaceMap) {
-            this.replaceMap = replaceMap;
+        private void setReplaceMap(Map<Slot, Expression> newReplaceMap) {
+            // merge instead of replace: sibling projects under a join share 
one
+            // PreAggInfoContext, and a full replacement would lose mappings 
from
+            // the sibling. merge keeps all entries; new entries shadow old 
ones
+            // by putAll semantics so a chain of projects still resolves 
correctly.
+            //
+            // Before merging, resolve the new aliases' producers through the
+            // existing replaceMap so that upper-layer aliases reference base 
table
+            // columns directly instead of intermediate computed aliases.
+            Map<Slot, Expression> merged = new HashMap<>(this.replaceMap);
+            for (Map.Entry<Slot, Expression> entry : newReplaceMap.entrySet()) 
{
+                Expression resolvedProducer = ExpressionUtils.replace(

Review Comment:
   [P1] Preserve legal project chains when alias expansion hits expression 
limits
   
   This eager composition can reconstruct an expression that the normal project 
merger deliberately leaves split. For example:
   
   ```text
   Aggregate(count(DISTINCT x14))
     Project(x13 + x13 AS x14)
       ...
         Project(k1 + k1 AS x1)
           Scan(preagg_t1)
   ```
   
   With fourteen `xN = x(N-1) + x(N-1)` layers, every project expression has 
width two. `PlanUtils.tryMergeProjections` catches `EXPRESSION_EXCEEDS_LIMIT` 
and preserves the legal chain, but this loop expands widths 2, 4, ..., 16384; 
rebuilding through `withChildren` exceeds the default 10000-child limit and 
throws here. The old non-composing map did not materialize that tree. Please 
keep lineage compact, or honor the same limit and conservatively turn 
preaggregation OFF instead of failing the query, and add a nested-project 
planning regression.



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