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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SetPreAggStatus.java:
##########
@@ -371,51 +514,128 @@ private Pair<Set<SlotReference>, Set<SlotReference>> 
splitKeyValueSlots(Set<Slot
             return Pair.of(keySlots, valueSlots);
         }
 
-        private PreAggStatus checkAggWithKeyAndValueSlots(AggregateFunction 
aggFunc,
-                Set<SlotReference> keySlots, Set<SlotReference> valueSlots) {
+        private PreAggStatus checkAggWithKeyAndValueSlots(AggregateFunction 
aggFunc, Set<Slot> outputSlots) {
             Expression child = aggFunc.child(0);
             List<Expression> conditionExps = new ArrayList<>();
             List<Expression> returnExps = new ArrayList<>();
 
-            // ignore cast
-            while (child instanceof Cast) {
-                if (!((Cast) child).getDataType().isNumericType()) {
-                    return PreAggStatus.off(String.format("%s is not numeric 
CAST.", child.toSql()));
-                }
-                child = child.child(0);
-            }
-            // step 1: extract all condition exprs and return exprs
+            // Only peel casts that are proven order-preserving for MAX/MIN:
+            // 1. Injective numeric→numeric casts (widening integral/decimal)
+            // 2. Numeric→float casts (nondecreasing, e.g. BIGINT→DOUBLE)
+            // sum(cast(x)) and sum(x) are not interchangeable
+            // due to overflow/precision, so SUM must stay OFF.
+            if (aggFunc instanceof Max || aggFunc instanceof Min) {
+                child = peelCastForMaxMin(child);
+            }
+            // Reject remaining cast.
+            if (child instanceof Cast) {
+                return PreAggStatus.off(String.format("%s is not supported.", 
child.toSql()));
+            }
+            // step 1: extract all condition exprs and return exprs.
+            // child is guaranteed to be Cast-free here (rejected above), but
+            // individual IF/CaseWhen return expressions may still have their
+            // own Cast wrappers. Only strip those for MAX/MIN: sum(cast(x))
+            // and cast(sum(x)) are not interchangeable due to overflow.
             if (child instanceof If) {
                 conditionExps.add(child.child(0));
-                returnExps.add(removeCast(child.child(1)));
-                returnExps.add(removeCast(child.child(2)));
+                returnExps.add((aggFunc instanceof Max || aggFunc instanceof 
Min)
+                        ? peelCastForMaxMin(child.child(1)) : child.child(1));
+                returnExps.add((aggFunc instanceof Max || aggFunc instanceof 
Min)
+                        ? peelCastForMaxMin(child.child(2)) : child.child(2));
             } else if (child instanceof CaseWhen) {
                 CaseWhen caseWhen = (CaseWhen) child;
                 // WHEN THEN
                 for (WhenClause whenClause : caseWhen.getWhenClauses()) {
                     conditionExps.add(whenClause.getOperand());
-                    returnExps.add(removeCast(whenClause.getResult()));
+                    returnExps.add((aggFunc instanceof Max || aggFunc 
instanceof Min)
+                            ? peelCastForMaxMin(whenClause.getResult())
+                            : whenClause.getResult());
                 }
                 // ELSE
-                
returnExps.add(removeCast(caseWhen.getDefaultValue().orElse(new 
NullLiteral())));
+                returnExps.add((aggFunc instanceof Max || aggFunc instanceof 
Min)
+                        ? peelCastForMaxMin(
+                                caseWhen.getDefaultValue().orElse(new 
NullLiteral()))
+                        : caseWhen.getDefaultValue().orElse(new 
NullLiteral()));
             } else {
-                // currently, only IF and CASE WHEN are supported
-                returnExps.add(removeCast(child));
+                // Non-IF/CASE — conditionExps stays empty and returns OFF 
below.
+                returnExps.add(peelCastForMaxMin(child));
+            }
+
+            // step 1.5: ownership — every return expression must reference 
only
+            // this scan's own columns. PREAGG ON exposes this scan's partial
+            // (unmerged) rows; under join fan-out a return that references a
+            // foreign value column would then be evaluated once per partial 
row
+            // and double-counted. So a foreign slot (value or key) in any 
return
+            // forces this scan OFF — never use a foreign column to justify ON.
+            //
+            // Exception: MAX/MIN are idempotent — max(x, x) = x — so 
repeating a
+            // foreign value across partial rows cannot change the aggregate
+            // result. The fence is over-conservative for them: a foreign 
return
+            // branch is safe once the condition is row-stable (step 2) and the
+            // return slot still matches the aggregate type (enforced by
+            // KeyAndValueSlotsAggChecker). Keep the fence for non-idempotent
+            // aggregates (SUM, COUNT, ...) where a repeated foreign value 
would
+            // be double-counted.
+            if (!(aggFunc instanceof Max || aggFunc instanceof Min)) {
+                for (Expression returnExp : returnExps) {
+                    if (returnExp instanceof SlotReference && 
!outputSlots.contains(returnExp)) {
+                        return PreAggStatus.off(
+                                String.format("return expression %s references 
column not owned by this scan.",
+                                        returnExp.toSql()));
+                    }
+                }
+            }
+            if (conditionExps.isEmpty()) {
+                return PreAggStatus.off(
+                        String.format("can't turn preAgg on for aggregate 
function %s", aggFunc));
             }
 
-            // step 2: check condition expressions
+            // step 2: check condition expressions — all condition inputs must
+            // be key columns (from any table), not value columns.  A global
+            // splitKeyValueSlots check handles this correctly for both the
+            // mixed-path (called with local key/value sets) and the value-only
+            // path (foreign key conditions in IF/CaseWhen).
             Set<Slot> inputSlots = 
ExpressionUtils.getInputSlotSet(conditionExps);
-            if (!keySlots.containsAll(inputSlots)) {
+            Pair<Set<SlotReference>, Set<SlotReference>> condSplit =
+                    splitKeyValueSlots(inputSlots);
+            if (!condSplit.second.isEmpty()) {

Review Comment:
   [P1] Keep DISTINCT SUM off when a condition slot is unclassified
   
   ```text
   Aggregate(sum(DISTINCT if(t.c > 0, r.v7, 0)))
     Join(r.k1 = t.k1)
       Scan(r AGG_KEYS; v7 SUM)
       Aggregate(t.k1, sum(t.v7) AS c)
         Scan(t AGG_KEYS)
   ```
   
   The nested aggregate output `t.c` has no `OriginalColumn`. Previously the 
condition check required the known key set to contain every input and stayed 
OFF; now `splitKeyValueSlots` silently drops `t.c`, sees no classified value 
slot, and reaches `visitSum`, which does not reject `DISTINCT`. With two 
exact-full-key `r` rowsets containing `v7=1`, OFF merges storage SUM to `2`, 
while ON exposes `{1,1}` and the later DISTINCT returns `1`. Please reject 
`sum.isDistinct()` on this route (or preserve conservative handling of 
unclassified conditions) and add a nested-aggregate IF/CASE result regression 
with duplicate full keys.



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