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


##########
regression-test/suites/nereids_rules_p0/set_preagg/set_preagg.groovy:
##########
@@ -349,4 +585,471 @@ suite("set_preagg") {
             select count(*) from (select * from numbers("number"="10")) t;
         """)
     }
+    order_qt_q13 """
+        select count(*) from (select * from numbers("number"="10")) t;
+    """
+
+    explain {
+        sql("""select count(distinct k6, v7) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q14 """
+        select count(distinct k6, v7) from preagg_t1;
+    """
+
+    explain {
+        sql("""select count(distinct k6, k5) from preagg_t1;""")
+        contains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q15 """
+        select count(distinct k6, k5) from preagg_t1;
+    """
+
+    // Negative: count(DISTINCT IF(...), value_col) — 
checkAggWithKeyAndValueSlots
+    // only inspects child(0) (the IF). Without a multi-arg guard, it would
+    // miss v7 in child(1) and incorrectly return ON.
+    explain {
+        sql("""
+            select count(distinct if(k6 > 0, k5, 0), v7) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q16 """
+        select count(distinct if(k6 > 0, k5, 0), v7) from preagg_t1;
+    """
+
+    explain {
+        sql("""
+            select count(distinct case when k6 > 0 then k5 else 0 end, v7) 
from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q17 """
+        select count(distinct case when k6 > 0 then k5 else 0 end, v7) from 
preagg_t1;
+    """
+
+    // Negative: count(DISTINCT key + random()) — volatile in the expression
+    // argument. With pre-agg ON, random() would be evaluated per partial row
+    // instead of per merged logical row, changing the distinct count.
+    explain {
+        sql("""select count(distinct k6 + random()) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    // max/min(key + random()) have the same volatile concern.
+    explain {
+        sql("""select max(k6 + random()) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    explain {
+        sql("""select min(k6 + random()) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    // Volatile in an IF condition inside a mixed-key-value aggregate.
+    // The condition k6 + random() > 0 uses only key input slots but is
+    // volatile, so pre-agg must be OFF.
+    explain {
+        sql("""
+            select sum(if(k6 + random() > 0, v7, 0)) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    // Positive: two-project join where both aliases resolve to key-only
+    // expressions. With merge+resolve, x = k5 + 1 resolves fully to base
+    // key columns, so pre-agg can be ON for both scans.
+    explain {
+        sql("""
+            select count(distinct a)
+            from (
+                select l.k1 + l.x as a
+                from (
+                    select t1.k1, t1.k5 + 1 as x from preagg_t1 t1
+                ) l
+                inner join (
+                    select abs(t2.k1) as rk from preagg_t2 t2
+                ) r on l.k1 = r.rk
+            ) t;
+        """)
+        contains "(preagg_t1), PREAGGREGATION: ON"
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_q18 """
+        select count(distinct a)
+        from (
+            select l.k1 + l.x as a
+            from (
+                select t1.k1, t1.k5 + 1 as x from preagg_t1 t1
+            ) l
+            inner join (
+                select abs(t2.k1) as rk from preagg_t2 t2
+            ) r on l.k1 = r.rk
+        ) t;
+    """
+
+    // Negative: two-project join; x carries v7 + 1 (a value column). Even
+    // with merge+resolve, the fully resolved expression contains v7 so
+    // pre-agg is correctly OFF.
+    explain {
+        sql("""
+            select count(distinct a)
+            from (
+                select l.k1 + l.x as a
+                from (
+                    select t1.k1, t1.v7 + 1 as x from preagg_t1 t1
+                ) l
+                inner join (
+                    select abs(t2.k1) as rk from preagg_t2 t2
+                ) r on l.k1 = r.rk
+            ) t;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q19 """
+        select count(distinct a)
+        from (
+            select l.k1 + l.x as a
+            from (
+                select t1.k1, t1.v7 + 1 as x from preagg_t1 t1
+            ) l
+            inner join (
+                select abs(t2.k1) as rk from preagg_t2 t2
+            ) r on l.k1 = r.rk
+        ) t;
+    """
+
+    // Bypass 1: volatile in an other-table aggregate function. max(r.k1 + 
random())
+    // is whitelisted as a duplicate-insensitive MAX for scan l; the candidate 
set
+    // for l is empty, so without a central volatile check it returns ON before
+    // reaching the per-function guard. Both scans must be OFF.
+    explain {
+        sql("""
+            select max(r.k1 + random())
+            from preagg_t1 l
+            inner join preagg_t2 r on l.k1 = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        notContains "(preagg_t2), PREAGGREGATION: ON"
+    }
+
+    // Bypass 2: volatile filter with no input slots. random() < 0.5 has an
+    // empty input-slot set, so the slot-based value-column check bypasses it.
+    // The central volatile guard must reject pre-agg on this scan.
+    explain {
+        sql("""
+            select sum(v7) from preagg_t1 where random() < 0.5;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    // Foreign value column in mixed aggregate: sum(if(abs(l.k1) > 0, r.v7, 0))
+    // references l.k1 (local key) and r.v7 (foreign value). The mixed helper
+    // must not use r.v7's SUM type to justify pre-agg on l — r.v7 is not
+    // a column of l. l must be OFF; r can be ON.
+    explain {
+        sql("""
+            select sum(if(t.a > 0, r.v7, 0))
+            from (select abs(k1) as a from preagg_t1) t
+            inner join preagg_t2 r on t.a = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        // r's local slots are only {r.v7} with IF; the value-only IF/CaseWhen
+        // handler validates conditions (foreign key t.a → safe) and returns 
ON.
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+
+    // CASE WHEN symmetry of the foreign-key-condition / local-value-return
+    // pattern on r: conditions reference only foreign keys, return references
+    // r.v7, so r should be ON.
+    explain {
+        sql("""
+            select sum(case when t.a > 0 then r.v7 else 0 end)
+            from (select abs(k1) as a from preagg_t1) t
+            inner join preagg_t2 r on t.a = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_q20 """
+        select sum(case when t.a > 0 then r.v7 else 0 end)
+        from (select abs(k1) as a from preagg_t1) t
+        inner join preagg_t2 r on t.a = r.k1;
+    """
+
+    // Negative: max(cast(v9 as double)) — no cast is peeled for MAX/MIN.
+    // DOUBLE/DECIMAL→FLOAT can underflow to -0.0 and change the observable
+    // tie representative (signed zero) under MAX/MIN, so even nondecreasing
+    // casts are not MAX/MIN homomorphisms. The checker sees a Cast and
+    // returns OFF.
+    explain {
+        sql("""select max(cast(v9 as double)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q21 """select max(cast(v9 as double)) from preagg_t1;"""
+
+    // Negative: sum(cast(v7 as double)) — sum(cast(x)) and cast(sum(x)) are
+    // not interchangeable due to precision/overflow, so cast must NOT be
+    // unwrapped. OneValueSlotAggChecker sees a Cast, not a SlotReference,
+    // and correctly returns OFF.
+    explain {
+        sql("""select sum(cast(v7 as double)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q22 """select sum(cast(v7 as double)) from preagg_t1;"""
+
+    // Negative: max(cast(v9 as string)) — non-numeric cast is never safe
+    // for MAX/MIN because string comparison differs from numeric comparison.
+    explain {
+        sql("""select max(cast(v9 as string)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q23 """select max(cast(v9 as string)) from preagg_t1;"""
+
+    // Negative mixed-path IF with cast in return: max(if(k6 > 0, cast(v9 as 
double), 0))
+    // — no cast is peeled for MAX/MIN, so the IF return stays wrapped in Cast
+    // and the checker returns OFF.
+    explain {
+        sql("""
+            select max(if(k6 > 0, cast(v9 as double), 0)) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q24 """
+        select max(if(k6 > 0, cast(v9 as double), 0)) from preagg_t1;
+    """
+
+    // Negative mixed-path IF with cast in return:
+    // sum(if(k6 > 0, cast(v7 as double), 0)) — sum(cast(x)) is not
+    // interchangeable with cast(sum(x)), so the guard keeps the Cast
+    // wrapper and the checker returns OFF.
+    explain {
+        sql("""
+            select sum(if(k6 > 0, cast(v7 as double), 0)) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q25 """
+        select sum(if(k6 > 0, cast(v7 as double), 0)) from preagg_t1;
+    """
+
+    // --- Cast regression tests ---
+    // No cast is peeled for MAX/MIN: DOUBLE/DECIMAL→FLOAT can underflow to
+    // -0.0 and change the observable tie representative (signed zero) under
+    // MAX/MIN, so even nondecreasing casts are not MAX/MIN homomorphisms.
+    // Any cast-wrapped aggregate is therefore conservatively OFF.
+
+    // Negative: BIGINT→DECIMAL(20,0) widening cast → OFF (no peeling).
+    explain {
+        sql("""select max(cast(v9 as decimal(20,0))) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q26 """select max(cast(v9 as decimal(20,0))) from preagg_t1;"""
+
+    // Negative: BIGINT→LARGEINT widening cast → OFF (no peeling).
+    explain {
+        sql("""select max(cast(v9 as largeint)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q27 """select max(cast(v9 as largeint)) from preagg_t1;"""
+
+    // Negative: BIGINT→INT is narrowing (not injective, not float) → OFF.
+    explain {
+        sql("""select max(cast(v9 as int)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q28 """select max(cast(v9 as int)) from preagg_t1;"""
+
+    // Negative: BIGINT→TINYINT is narrowing (not injective, not float) → OFF.
+    explain {
+        sql("""select max(cast(v9 as tinyint)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q29 """select max(cast(v9 as tinyint)) from preagg_t1;"""
+
+    // Negative mixed-path IF with widening cast: no peeling → OFF.
+    explain {
+        sql("""
+            select max(if(k6 > 0, cast(v9 as decimal(20,0)), 0)) from 
preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q30 """
+        select max(if(k6 > 0, cast(v9 as decimal(20,0)), 0)) from preagg_t1;
+    """
+
+    // Mixed-path IF with unsafe cast: max(if(..., cast(v9 as tinyint), 0))
+    // return cast is narrowing non-injective → not peeled → checker OFF.
+    explain {
+        sql("""
+            select max(if(k6 > 0, cast(v9 as tinyint), 0)) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q31 """
+        select max(if(k6 > 0, cast(v9 as tinyint), 0)) from preagg_t1;
+    """
+
+    // Negative: CASE WHEN with widening cast → OFF (no peeling).
+    explain {
+        sql("""
+            select max(case when k6 > 0 then cast(v9 as decimal(20,0)) else 0 
end) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q32 """
+        select max(case when k6 > 0 then cast(v9 as decimal(20,0)) else 0 end) 
from preagg_t1;
+    """
+
+    // CASE WHEN with unsafe narrowing cast → OFF.
+    explain {
+        sql("""
+            select max(case when k6 > 0 then cast(v9 as tinyint) else 0 end) 
from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q33 """
+        select max(case when k6 > 0 then cast(v9 as tinyint) else 0 end) from 
preagg_t1;
+    """
+
+    // 
-------------------------------------------------------------------------
+    // Two-table derived-key ownership tests (with repeated aggregate-key data)
+    //
+    // The mixed helper must validate returns relative to the current scan: a
+    // foreign value column must never justify exposing this scan's partial
+    // (unmerged) rows. Under join fan-out, a return that references a foreign
+    // value column would be evaluated once per partial row and double-counted.
+    // 
-------------------------------------------------------------------------
+
+    // Test A: derived-key fan-out. t has two different keys k1=1 and k1=-1 
that
+    // both map to the same derived key a=abs(k1)=1; r.v7 is a foreign value
+    // used in the IF return. t must stay OFF (aggSlots intersection keeps only
+    // local key k1 → key-only path → sum is not distinct), so each logical row
+    // of t joins every r row with r.k1 = a and counts r.v7 once. With the
+    // one-time dataset: a=1 (3 t-rows) × r.k1=1 (2 r-rows, v7=50,60) plus
+    // a=2 (1 t-row) × r.k1=2 (1 r-row, v7=70) → 3*(50+60)+70 = 400. If t were
+    // wrongly ON, its rows would fan out under join.
+    // r has local v7 and foreign key condition → ON is safe.
+    explain {
+        sql("""
+            select sum(if(t.a > 0, r.v7, 0))
+            from (select abs(k1) as a from preagg_t1) t
+            inner join preagg_t2 r on t.a = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_test_a """
+        select sum(if(t.a > 0, r.v7, 0)) as res
+        from (select abs(k1) as a from preagg_t1) t
+        inner join preagg_t2 r on t.a = r.k1;
+    """
+
+    // Test B: foreign value in IF return on BOTH sides — ownership check must
+    // turn both scans OFF:
+    //   sum(if(t.a > 0, t.v7, r.v7))
+    //   - for scan t: return r.v7 is foreign → t OFF
+    //   - for scan r: return t.v7 is foreign → r OFF
+    // With both OFF, storage merges by key and the logical result is exact.
+    // With the one-time dataset: a=1 (3 t-rows, v7=10,20,30) × r.k1=1 (2 rows)
+    // → 2*(10+20+30)=120, plus a=2 (t.v7=40) × r.k1=2 → 40, total 160.
+    explain {
+        sql("""
+            select sum(if(t.a > 0, t.v7, r.v7))
+            from (select abs(k1) as a, v7 from preagg_t1) t
+            inner join preagg_t2 r on t.a = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        notContains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_test_b """
+        select sum(if(t.a > 0, t.v7, r.v7)) as res
+        from (select abs(k1) as a, v7 from preagg_t1) t
+        inner join preagg_t2 r on t.a = r.k1;
+    """
+
+    // Positive MAX join case: MAX is idempotent (max(x, x) = x), so a foreign
+    // value branch in the IF return cannot change the result even under join
+    // fan-out. The ownership fence is skipped for MAX/MIN (it stays for
+    // SUM/COUNT), so both scans may be ON. With the one-time dataset:
+    // l.k1=1 (v9 1000,900) and l.k1=2 (v9 700) all satisfy l.k1 > 0, so
+    // max(if(l.k1 > 0, l.v9, r.v9)) = 1000.
+    explain {
+        sql("""
+            select max(if(l.k1 > 0, l.v9, r.v9))
+            from preagg_t1 l
+            inner join preagg_t2 r on l.k1 = r.k1;
+        """)
+        contains "(preagg_t1), PREAGGREGATION: ON"
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_q34 """
+        select max(if(l.k1 > 0, l.v9, r.v9))
+        from preagg_t1 l
+        inner join preagg_t2 r on l.k1 = r.k1;
+    """
+
+    // Negative: sum(DISTINCT ...) with a nested-aggregate condition slot.
+    //   sum(distinct if(t.c > 0, r.v7, 0))
+    // t.c = sum(t.v7) has no OriginalColumn, so splitKeyValueSlots drops it 
from
+    // the condition check (it is unclassified). Previously this let the route
+    // reach visitSum, which did NOT reject DISTINCT, so preagg_t4 was wrongly
+    // turned ON. Storage SUM would then merge the duplicate full key 
(v7=1+1=2)
+    // and break DISTINCT semantics: ON sees {1,1} → 1 while OFF sees merged 2.
+    // KeyAndValueSlotsAggChecker.visitSum must reject sum.isDistinct() exactly
+    // like OneValueSlotAggChecker.visitSum does.
+    explain {
+        sql("""
+            select sum(distinct if(t.c > 0, r.v7, 0))
+            from (select k1, sum(v7) as c from preagg_t1 group by k1) t
+            inner join preagg_t4 r on t.k1 = r.k1;
+        """)
+        contains "(preagg_t1), PREAGGREGATION: ON"
+        notContains "(preagg_t4), PREAGGREGATION: ON"
+    }
+    order_qt_q35 """
+        select sum(distinct if(t.c > 0, r.v7, 0))
+        from (select k1, sum(v7) as c from preagg_t1 group by k1) t
+        inner join preagg_t4 r on t.k1 = r.k1;
+    """
+
+    // Negative signed-zero: signbit(max(if(k1 > 0, cast(v as float), cast(0 
as float))))
+    // with v a DOUBLE MAX column. No cast is peeled for MAX/MIN: DOUBLE→FLOAT 
can
+    // underflow (-1e-300 → FLOAT -0.0) and change the observable tie 
representative
+    // under signbit, so preagg_t5 must stay OFF. preagg_t5 holds the same 
full key
+    // k1=1 in two loads (v = -1e-300 and +0.0); storage MAX merges to +0.0, 
so the
+    // merged result is signbit(+0.0) = 0.
+    explain {
+        sql("""
+            select signbit(max(if(k1 > 0, cast(v as float), cast(0 as 
float)))) from preagg_t5;
+        """)
+        notContains "(preagg_t5), PREAGGREGATION: ON"
+    }
+    order_qt_q36 """
+        select signbit(max(if(k1 > 0, cast(v as float), cast(0 as float)))) 
from preagg_t5;
+    """
+
+    // Negative ASOF join selected-side: r.v9 is a direct correctly typed MAX
+    // column, but ASOF's one-row selection does not commute with pre-agg ON.
+    // preagg_asof_r holds the same full key (grp=1, ts) in two loads with
+    // v9=100 and v9=200; storage MAX merges to 200. If r were ON, ASOF would
+    // see two equal-time rows and could pick the 100 partial before the upper
+    // MAX runs, returning 100 instead of 200. The selected side must stay OFF.
+    // l(1)↔r(1, v9=200), l(2)↔r(2, v9=300) → max(200, 300) = 300.
+    explain {
+        sql("""
+            select max(if(l.grp > 0, r.v9, 0))
+            from preagg_asof_l l asof left join preagg_asof_r r
+                MATCH_CONDITION(l.ts >= r.ts) on l.grp = r.grp;
+        """)
+        contains "(preagg_asof_l), PREAGGREGATION: ON"
+        notContains "(preagg_asof_r), PREAGGREGATION: ON"
+    }
+    order_qt_q37 """

Review Comment:
   [P1] Make the ASOF result observe the stale selected row
   
   `preagg_asof_r` has `grp=1` partials 100 and 200, but this query also 
includes `grp=2` = 300 and takes one outer MAX. The recorded result is 
therefore 300 whether the selected side is correctly storage-merged (so grp1 is 
200) or PREAGG ON exposes partials and ASOF picks 100. This is distinct from 
the existing production-code thread: it concerns the new oracle on this line, 
which cannot catch that regression. Please restrict the result to `l.grp = 1` 
(expect 200) or group by `l.grp`, then regenerate the output.



##########
regression-test/suites/nereids_rules_p0/set_preagg/set_preagg.groovy:
##########
@@ -349,4 +585,471 @@ suite("set_preagg") {
             select count(*) from (select * from numbers("number"="10")) t;
         """)
     }
+    order_qt_q13 """
+        select count(*) from (select * from numbers("number"="10")) t;
+    """
+
+    explain {
+        sql("""select count(distinct k6, v7) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q14 """
+        select count(distinct k6, v7) from preagg_t1;
+    """
+
+    explain {
+        sql("""select count(distinct k6, k5) from preagg_t1;""")
+        contains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q15 """
+        select count(distinct k6, k5) from preagg_t1;
+    """
+
+    // Negative: count(DISTINCT IF(...), value_col) — 
checkAggWithKeyAndValueSlots
+    // only inspects child(0) (the IF). Without a multi-arg guard, it would
+    // miss v7 in child(1) and incorrectly return ON.
+    explain {
+        sql("""
+            select count(distinct if(k6 > 0, k5, 0), v7) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q16 """
+        select count(distinct if(k6 > 0, k5, 0), v7) from preagg_t1;
+    """
+
+    explain {
+        sql("""
+            select count(distinct case when k6 > 0 then k5 else 0 end, v7) 
from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q17 """
+        select count(distinct case when k6 > 0 then k5 else 0 end, v7) from 
preagg_t1;
+    """
+
+    // Negative: count(DISTINCT key + random()) — volatile in the expression
+    // argument. With pre-agg ON, random() would be evaluated per partial row
+    // instead of per merged logical row, changing the distinct count.
+    explain {
+        sql("""select count(distinct k6 + random()) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    // max/min(key + random()) have the same volatile concern.
+    explain {
+        sql("""select max(k6 + random()) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    explain {
+        sql("""select min(k6 + random()) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    // Volatile in an IF condition inside a mixed-key-value aggregate.
+    // The condition k6 + random() > 0 uses only key input slots but is
+    // volatile, so pre-agg must be OFF.
+    explain {
+        sql("""
+            select sum(if(k6 + random() > 0, v7, 0)) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    // Positive: two-project join where both aliases resolve to key-only
+    // expressions. With merge+resolve, x = k5 + 1 resolves fully to base
+    // key columns, so pre-agg can be ON for both scans.
+    explain {
+        sql("""
+            select count(distinct a)
+            from (
+                select l.k1 + l.x as a
+                from (
+                    select t1.k1, t1.k5 + 1 as x from preagg_t1 t1
+                ) l
+                inner join (
+                    select abs(t2.k1) as rk from preagg_t2 t2
+                ) r on l.k1 = r.rk
+            ) t;
+        """)
+        contains "(preagg_t1), PREAGGREGATION: ON"
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_q18 """
+        select count(distinct a)
+        from (
+            select l.k1 + l.x as a
+            from (
+                select t1.k1, t1.k5 + 1 as x from preagg_t1 t1
+            ) l
+            inner join (
+                select abs(t2.k1) as rk from preagg_t2 t2
+            ) r on l.k1 = r.rk
+        ) t;
+    """
+
+    // Negative: two-project join; x carries v7 + 1 (a value column). Even
+    // with merge+resolve, the fully resolved expression contains v7 so
+    // pre-agg is correctly OFF.
+    explain {
+        sql("""
+            select count(distinct a)
+            from (
+                select l.k1 + l.x as a
+                from (
+                    select t1.k1, t1.v7 + 1 as x from preagg_t1 t1
+                ) l
+                inner join (
+                    select abs(t2.k1) as rk from preagg_t2 t2
+                ) r on l.k1 = r.rk
+            ) t;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q19 """
+        select count(distinct a)
+        from (
+            select l.k1 + l.x as a
+            from (
+                select t1.k1, t1.v7 + 1 as x from preagg_t1 t1
+            ) l
+            inner join (
+                select abs(t2.k1) as rk from preagg_t2 t2
+            ) r on l.k1 = r.rk
+        ) t;
+    """
+
+    // Bypass 1: volatile in an other-table aggregate function. max(r.k1 + 
random())
+    // is whitelisted as a duplicate-insensitive MAX for scan l; the candidate 
set
+    // for l is empty, so without a central volatile check it returns ON before
+    // reaching the per-function guard. Both scans must be OFF.
+    explain {
+        sql("""
+            select max(r.k1 + random())
+            from preagg_t1 l
+            inner join preagg_t2 r on l.k1 = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        notContains "(preagg_t2), PREAGGREGATION: ON"
+    }
+
+    // Bypass 2: volatile filter with no input slots. random() < 0.5 has an
+    // empty input-slot set, so the slot-based value-column check bypasses it.
+    // The central volatile guard must reject pre-agg on this scan.
+    explain {
+        sql("""
+            select sum(v7) from preagg_t1 where random() < 0.5;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+
+    // Foreign value column in mixed aggregate: sum(if(abs(l.k1) > 0, r.v7, 0))
+    // references l.k1 (local key) and r.v7 (foreign value). The mixed helper
+    // must not use r.v7's SUM type to justify pre-agg on l — r.v7 is not
+    // a column of l. l must be OFF; r can be ON.
+    explain {
+        sql("""
+            select sum(if(t.a > 0, r.v7, 0))
+            from (select abs(k1) as a from preagg_t1) t
+            inner join preagg_t2 r on t.a = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        // r's local slots are only {r.v7} with IF; the value-only IF/CaseWhen
+        // handler validates conditions (foreign key t.a → safe) and returns 
ON.
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+
+    // CASE WHEN symmetry of the foreign-key-condition / local-value-return
+    // pattern on r: conditions reference only foreign keys, return references
+    // r.v7, so r should be ON.
+    explain {
+        sql("""
+            select sum(case when t.a > 0 then r.v7 else 0 end)
+            from (select abs(k1) as a from preagg_t1) t
+            inner join preagg_t2 r on t.a = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_q20 """
+        select sum(case when t.a > 0 then r.v7 else 0 end)
+        from (select abs(k1) as a from preagg_t1) t
+        inner join preagg_t2 r on t.a = r.k1;
+    """
+
+    // Negative: max(cast(v9 as double)) — no cast is peeled for MAX/MIN.
+    // DOUBLE/DECIMAL→FLOAT can underflow to -0.0 and change the observable
+    // tie representative (signed zero) under MAX/MIN, so even nondecreasing
+    // casts are not MAX/MIN homomorphisms. The checker sees a Cast and
+    // returns OFF.
+    explain {
+        sql("""select max(cast(v9 as double)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q21 """select max(cast(v9 as double)) from preagg_t1;"""
+
+    // Negative: sum(cast(v7 as double)) — sum(cast(x)) and cast(sum(x)) are
+    // not interchangeable due to precision/overflow, so cast must NOT be
+    // unwrapped. OneValueSlotAggChecker sees a Cast, not a SlotReference,
+    // and correctly returns OFF.
+    explain {
+        sql("""select sum(cast(v7 as double)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q22 """select sum(cast(v7 as double)) from preagg_t1;"""
+
+    // Negative: max(cast(v9 as string)) — non-numeric cast is never safe
+    // for MAX/MIN because string comparison differs from numeric comparison.
+    explain {
+        sql("""select max(cast(v9 as string)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q23 """select max(cast(v9 as string)) from preagg_t1;"""
+
+    // Negative mixed-path IF with cast in return: max(if(k6 > 0, cast(v9 as 
double), 0))
+    // — no cast is peeled for MAX/MIN, so the IF return stays wrapped in Cast
+    // and the checker returns OFF.
+    explain {
+        sql("""
+            select max(if(k6 > 0, cast(v9 as double), 0)) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q24 """
+        select max(if(k6 > 0, cast(v9 as double), 0)) from preagg_t1;
+    """
+
+    // Negative mixed-path IF with cast in return:
+    // sum(if(k6 > 0, cast(v7 as double), 0)) — sum(cast(x)) is not
+    // interchangeable with cast(sum(x)), so the guard keeps the Cast
+    // wrapper and the checker returns OFF.
+    explain {
+        sql("""
+            select sum(if(k6 > 0, cast(v7 as double), 0)) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q25 """
+        select sum(if(k6 > 0, cast(v7 as double), 0)) from preagg_t1;
+    """
+
+    // --- Cast regression tests ---
+    // No cast is peeled for MAX/MIN: DOUBLE/DECIMAL→FLOAT can underflow to
+    // -0.0 and change the observable tie representative (signed zero) under
+    // MAX/MIN, so even nondecreasing casts are not MAX/MIN homomorphisms.
+    // Any cast-wrapped aggregate is therefore conservatively OFF.
+
+    // Negative: BIGINT→DECIMAL(20,0) widening cast → OFF (no peeling).
+    explain {
+        sql("""select max(cast(v9 as decimal(20,0))) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q26 """select max(cast(v9 as decimal(20,0))) from preagg_t1;"""
+
+    // Negative: BIGINT→LARGEINT widening cast → OFF (no peeling).
+    explain {
+        sql("""select max(cast(v9 as largeint)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q27 """select max(cast(v9 as largeint)) from preagg_t1;"""
+
+    // Negative: BIGINT→INT is narrowing (not injective, not float) → OFF.
+    explain {
+        sql("""select max(cast(v9 as int)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q28 """select max(cast(v9 as int)) from preagg_t1;"""
+
+    // Negative: BIGINT→TINYINT is narrowing (not injective, not float) → OFF.
+    explain {
+        sql("""select max(cast(v9 as tinyint)) from preagg_t1;""")
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q29 """select max(cast(v9 as tinyint)) from preagg_t1;"""
+
+    // Negative mixed-path IF with widening cast: no peeling → OFF.
+    explain {
+        sql("""
+            select max(if(k6 > 0, cast(v9 as decimal(20,0)), 0)) from 
preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q30 """
+        select max(if(k6 > 0, cast(v9 as decimal(20,0)), 0)) from preagg_t1;
+    """
+
+    // Mixed-path IF with unsafe cast: max(if(..., cast(v9 as tinyint), 0))
+    // return cast is narrowing non-injective → not peeled → checker OFF.
+    explain {
+        sql("""
+            select max(if(k6 > 0, cast(v9 as tinyint), 0)) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q31 """
+        select max(if(k6 > 0, cast(v9 as tinyint), 0)) from preagg_t1;
+    """
+
+    // Negative: CASE WHEN with widening cast → OFF (no peeling).
+    explain {
+        sql("""
+            select max(case when k6 > 0 then cast(v9 as decimal(20,0)) else 0 
end) from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q32 """
+        select max(case when k6 > 0 then cast(v9 as decimal(20,0)) else 0 end) 
from preagg_t1;
+    """
+
+    // CASE WHEN with unsafe narrowing cast → OFF.
+    explain {
+        sql("""
+            select max(case when k6 > 0 then cast(v9 as tinyint) else 0 end) 
from preagg_t1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+    }
+    order_qt_q33 """
+        select max(case when k6 > 0 then cast(v9 as tinyint) else 0 end) from 
preagg_t1;
+    """
+
+    // 
-------------------------------------------------------------------------
+    // Two-table derived-key ownership tests (with repeated aggregate-key data)
+    //
+    // The mixed helper must validate returns relative to the current scan: a
+    // foreign value column must never justify exposing this scan's partial
+    // (unmerged) rows. Under join fan-out, a return that references a foreign
+    // value column would be evaluated once per partial row and double-counted.
+    // 
-------------------------------------------------------------------------
+
+    // Test A: derived-key fan-out. t has two different keys k1=1 and k1=-1 
that
+    // both map to the same derived key a=abs(k1)=1; r.v7 is a foreign value
+    // used in the IF return. t must stay OFF (aggSlots intersection keeps only
+    // local key k1 → key-only path → sum is not distinct), so each logical row
+    // of t joins every r row with r.k1 = a and counts r.v7 once. With the
+    // one-time dataset: a=1 (3 t-rows) × r.k1=1 (2 r-rows, v7=50,60) plus
+    // a=2 (1 t-row) × r.k1=2 (1 r-row, v7=70) → 3*(50+60)+70 = 400. If t were
+    // wrongly ON, its rows would fan out under join.
+    // r has local v7 and foreign key condition → ON is safe.
+    explain {
+        sql("""
+            select sum(if(t.a > 0, r.v7, 0))
+            from (select abs(k1) as a from preagg_t1) t
+            inner join preagg_t2 r on t.a = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_test_a """
+        select sum(if(t.a > 0, r.v7, 0)) as res
+        from (select abs(k1) as a from preagg_t1) t
+        inner join preagg_t2 r on t.a = r.k1;
+    """
+
+    // Test B: foreign value in IF return on BOTH sides — ownership check must
+    // turn both scans OFF:
+    //   sum(if(t.a > 0, t.v7, r.v7))
+    //   - for scan t: return r.v7 is foreign → t OFF
+    //   - for scan r: return t.v7 is foreign → r OFF
+    // With both OFF, storage merges by key and the logical result is exact.
+    // With the one-time dataset: a=1 (3 t-rows, v7=10,20,30) × r.k1=1 (2 rows)
+    // → 2*(10+20+30)=120, plus a=2 (t.v7=40) × r.k1=2 → 40, total 160.
+    explain {
+        sql("""
+            select sum(if(t.a > 0, t.v7, r.v7))
+            from (select abs(k1) as a, v7 from preagg_t1) t
+            inner join preagg_t2 r on t.a = r.k1;
+        """)
+        notContains "(preagg_t1), PREAGGREGATION: ON"
+        notContains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_test_b """
+        select sum(if(t.a > 0, t.v7, r.v7)) as res
+        from (select abs(k1) as a, v7 from preagg_t1) t
+        inner join preagg_t2 r on t.a = r.k1;
+    """
+
+    // Positive MAX join case: MAX is idempotent (max(x, x) = x), so a foreign
+    // value branch in the IF return cannot change the result even under join
+    // fan-out. The ownership fence is skipped for MAX/MIN (it stays for
+    // SUM/COUNT), so both scans may be ON. With the one-time dataset:
+    // l.k1=1 (v9 1000,900) and l.k1=2 (v9 700) all satisfy l.k1 > 0, so
+    // max(if(l.k1 > 0, l.v9, r.v9)) = 1000.
+    explain {
+        sql("""
+            select max(if(l.k1 > 0, l.v9, r.v9))
+            from preagg_t1 l
+            inner join preagg_t2 r on l.k1 = r.k1;
+        """)
+        contains "(preagg_t1), PREAGGREGATION: ON"
+        contains "(preagg_t2), PREAGGREGATION: ON"
+    }
+    order_qt_q34 """

Review Comment:
   [P2] Exercise the foreign MAX branch with partial rows
   
   Every joined left key here is 1 or 2, so `l.k1 > 0` is always true and 
`r.v9` is never evaluated. The apparent repeated `k1=1` rows also differ in the 
full aggregate key and were loaded together, so PREAGG ON exposes no duplicate 
full-key partial states. The result 1000 is thus unchanged even if the new 
MAX/MIN foreign-return exception is wrong; only EXPLAIN eligibility is covered. 
Please add a matching non-positive key and duplicate one full key across 
separate loads so the result exercises foreign-branch fanout (with MIN symmetry 
for the parallel path).



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