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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpQualifyMissingSlot.java:
##########
@@ -158,23 +216,50 @@ public List<Rule> buildRules() {
                qualify -> having -> project(distinct)
              */
             RuleType.FILL_UP_QUALIFY_HAVING_PROJECT.build(
-                logicalQualify(logicalHaving(logicalProject())).then(qualify 
-> {
+                logicalQualify(logicalHaving(logicalProject())).thenApply(ctx 
-> {
+                    LogicalQualify<LogicalHaving<LogicalProject<Plan>>> 
qualify = ctx.root;
                     checkWindow(qualify);
+                    Optional<Scope> outerScope = 
ctx.cascadesContext.getOuterScope();
                     LogicalHaving<LogicalProject<Plan>> having = 
qualify.child();
                     LogicalProject<Plan> project = qualify.child().child();
-                    return createPlan(project, qualify.getConjuncts(), 
(newConjuncts, projects) -> {
+                    return createPlan(project, qualify.getConjuncts(), 
outerScope, (newConjuncts, projects) -> {
                         ImmutableList<NamedExpression> copyOutput = 
ImmutableList.copyOf(project.getOutput());
                         if (project.isDistinct()) {
+                            // Keep correlated predicates that only depend on 
outer slots together with the
+                            // having's own correlated predicates, on the same 
decorrelatable side of the
+                            // distinct barrier, so subquery unnesting can 
collect and decorrelate them
+                            // together (otherwise one of them is left 
dangling in the apply's right side).
+                            // A predicate that is constant per outer row is 
equivalent before/after
+                            // distinct, so moving it above the distinct 
project preserves semantics.
+                            Set<Expression> newHavingConjuncts = new 
LinkedHashSet<>(having.getConjuncts());
+                            Set<Expression> distinctQualifyConjuncts = new 
LinkedHashSet<>();
+                            if (outerScope.isPresent()) {
+                                Set<Slot> correlatedSlots = 
outerScope.get().getCorrelatedSlots();
+                                for (Expression conjunct : newConjuncts) {
+                                    Set<Slot> inputSlots = 
conjunct.getInputSlots();
+                                    if (!inputSlots.isEmpty() && 
correlatedSlots.containsAll(inputSlots)) {

Review Comment:
   [P1] Keep volatile QUALIFY predicates on their original side of DISTINCT
   
   Visible outer slots do not make a predicate constant. For example, `(o.flag 
<> 1 OR random() < 0.5)` has input slots `{o.flag}`, so this branch moves it 
from QUALIFY into HAVING:
   
   ```text
   intended: ProjectDistinct[k] -> Qualify[window_ok, P(random)]
   patched:  Having[P(random)] -> ProjectDistinct[k] -> Qualify[window_ok]
   ```
   
   With two duplicate `k` rows and `o.flag=1`, QUALIFY evaluates `P` twice 
before DISTINCT (the key survives with probability 3/4), while the patched plan 
evaluates it once after DISTINCT (probability 1/2). This is separate from the 
existing deterministic lost-correlation thread: the new repair changes a 
volatile predicate's evaluation domain. Require determinism/movability before 
this motion, or expose correlation without relocating the predicate, and add 
duplicate-key volatile/non-movable coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpQualifyMissingSlot.java:
##########
@@ -189,10 +274,49 @@ interface PlanGenerator {
         Plan apply(Set<Expression> newConjuncts, List<NamedExpression> 
projects);
     }
 
-    private Plan createPlan(LogicalProject<Plan> project, Set<Expression> 
conjuncts, PlanGenerator planGenerator) {
+    private Plan createPlan(LogicalProject<Plan> project, Set<Expression> 
conjuncts,
+            Optional<Scope> outerScope, PlanGenerator planGenerator) {
         Set<Slot> projectOutputSet = project.getOutputSet();
         List<NamedExpression> newOutputSlots = Lists.newArrayList();
         Set<Expression> newConjuncts = new LinkedHashSet<>();
+
+        // A correlated column referenced in qualify may be hidden behind a 
project alias, e.g.
+        // `QUALIFY f = 1` where f is aliased as an outer column o.flag. If 
the project also
+        // contains a window expression, filter pushdown cannot rewrite f back 
to its producer
+        // before apply decorrelation, so the alias-producer dependency would 
be lost and the
+        // correlation slot would never be collected into the apply. Resolve 
such aliases whose
+        // producers reference only outer correlated slots, so the correlation 
stays visible to
+        // subquery unnesting.
+        Map<Slot, Expression> correlatedAliasToProducer = Maps.newHashMap();
+        // If any qualify conjunct contains a subquery (e.g. an IN/NOT IN 
predicate), the alias
+        // replacement below would descend into the subquery and break the 
apply decorrelation
+        // (the substituted slot would be owned by neither side of the apply). 
Skip the rewrite
+        // in that case.
+        boolean conjunctsHaveSubquery = conjuncts.stream().anyMatch(c -> 
c.containsType(SubqueryExpr.class));
+        if (outerScope.isPresent() && !conjunctsHaveSubquery) {
+            Set<Slot> correlatedSlots = outerScope.get().getCorrelatedSlots();
+            for (Map.Entry<Slot, Expression> entry : 
project.getAliasToProducer().entrySet()) {
+                Expression producer = entry.getValue();
+                if (!producer.getInputSlots().isEmpty()
+                        && 
correlatedSlots.containsAll(producer.getInputSlots())
+                        // a window producer would be re-extracted into a 
fresh alias, which would be
+                        // rewritten again, causing an infinite rewrite loop 
before a fixed point.
+                        && !producer.containsType(WindowExpression.class)
+                        // a producer containing a subquery would be copied 
into both the project and
+                        // the rewritten qualify, and only one copy would be 
unnested, leaving a
+                        // dangling slot in the other.
+                        && !producer.containsType(SubqueryExpr.class)) {

Review Comment:
   [P1] Keep volatile alias producers single-evaluated
   
   These exclusions still admit an alias such as `random() + o.flag AS f`. The 
replacement then creates this reduced tree:
   
   ```text
   Project[f]
     Qualify[random2()+o.flag > 1.5, rn=1]
       Project[random1()+o.flag AS f, window AS rn]
   ```
   
   In a correlated scalar subquery, the copied predicate can admit the row 
using `random2()` while the returned `f` from `random1()` does not satisfy the 
QUALIFY condition. `PushDownFilterThroughProject` already blocks alias 
expansion when a producer `containsVolatileExpression()` for exactly this 
identity reason; none-movable/error expressions need the same audit. 
Materialize the producer once (or reject the unsupported class) and add a 
full-pipeline scalar regression.



##########
regression-test/suites/query_p0/sql_functions/window_functions/test_qualify_query.groovy:
##########
@@ -112,6 +112,288 @@ suite("test_qualify_query") {
     qt_select_35 "select year + 1, country from sales having profit >= 100 
qualify row_number() over (order by profit) = 6;"
 
     qt_select_36 "select year + 1, country, row_number() over (order by 
profit) rk from sales having profit >= 100 qualify rk = 6;"
+
+    // correlated subquery: an outer column referenced in qualify after an 
explicit group by
+    // should not be treated as a missing inner group-by column under 
ONLY_FULL_GROUP_BY.
+    qt_select_37 """
+        SELECT o.k
+        FROM (
+          SELECT CAST(10 AS INT) AS k, CAST(1 AS INT) AS flag
+          UNION ALL
+          SELECT CAST(20 AS INT) AS k, CAST(0 AS INT) AS flag
+        ) AS o
+        WHERE EXISTS (
+          SELECT i.k
+          FROM (
+            SELECT CAST(1 AS INT) AS k
+            UNION ALL
+            SELECT CAST(2 AS INT) AS k
+          ) AS i
+          GROUP BY i.k
+          QUALIFY row_number() OVER (ORDER BY i.k) = 1
+                  AND o.flag = 1
+        )
+        ORDER BY o.k;
+    """
+
+    // an inner non-grouped column referenced in qualify should still be 
rejected
+    // under ONLY_FULL_GROUP_BY.
+    test {
+        sql """
+            SELECT o.k
+            FROM (
+              SELECT CAST(10 AS INT) AS k, CAST(1 AS INT) AS flag
+              UNION ALL
+              SELECT CAST(20 AS INT) AS k, CAST(0 AS INT) AS flag
+            ) AS o
+            WHERE EXISTS (
+              SELECT i.k
+              FROM (
+                SELECT CAST(1 AS INT) AS k, CAST(1 AS INT) AS not_grouped
+                UNION ALL
+                SELECT CAST(2 AS INT) AS k, CAST(2 AS INT) AS not_grouped
+              ) AS i
+              GROUP BY i.k
+              QUALIFY row_number() OVER (ORDER BY i.k) = 1
+                      AND i.not_grouped = 1
+            )
+        """
+        exception "must appear in the GROUP BY clause"
+    }
+
+    // correlated subquery over a plain project (no group by): the outer 
column in
+    // qualify must not be pushed into the inner project's output.
+    qt_select_38 """
+        SELECT o.k
+        FROM (
+          SELECT CAST(10 AS INT) AS k, CAST(1 AS INT) AS flag
+          UNION ALL
+          SELECT CAST(20 AS INT) AS k, CAST(0 AS INT) AS flag
+        ) AS o
+        WHERE EXISTS (
+          SELECT i.k
+          FROM (
+            SELECT CAST(1 AS INT) AS k
+            UNION ALL
+            SELECT CAST(2 AS INT) AS k
+          ) AS i
+          QUALIFY row_number() OVER (ORDER BY i.k) = 1
+                  AND o.flag = 1
+        )
+        ORDER BY o.k;
+    """
+
+    // correlated subquery over qualify -> having -> project: the outer column 
in
+    // qualify must not be pushed into the inner project's output.
+    qt_select_39 """
+        SELECT o.k
+        FROM (
+          SELECT CAST(10 AS INT) AS k, CAST(1 AS INT) AS flag
+          UNION ALL
+          SELECT CAST(20 AS INT) AS k, CAST(0 AS INT) AS flag
+        ) AS o
+        WHERE EXISTS (
+          SELECT i.k
+          FROM (
+            SELECT CAST(1 AS INT) AS k
+            UNION ALL
+            SELECT CAST(2 AS INT) AS k
+          ) AS i
+          HAVING i.k >= 1
+          QUALIFY row_number() OVER (ORDER BY i.k) = 1
+                  AND o.flag = 1
+        )
+        ORDER BY o.k;
+    """
+
+    // qualify -> having -> agg where both the having and the qualify 
reference correlated outer
+    // columns. The window in qualify is extracted into a project above the 
having, so the having's
+    // correlated predicate must be conjoined into the qualify to be 
decorrelated together.
+    // o.h = 0 for k = 10 while o.flag = 1: the having predicate must still 
filter it out.
+    qt_select_40 """
+        SELECT o.k
+        FROM (
+          SELECT CAST(10 AS INT) AS k, CAST(1 AS INT) AS flag, CAST(0 AS INT) 
AS h
+          UNION ALL
+          SELECT CAST(20 AS INT) AS k, CAST(0 AS INT) AS flag, CAST(1 AS INT) 
AS h
+        ) AS o
+        WHERE EXISTS (
+          SELECT i.k
+          FROM (
+            SELECT CAST(1 AS INT) AS k
+            UNION ALL
+            SELECT CAST(2 AS INT) AS k
+          ) AS i
+          GROUP BY i.k
+          HAVING o.h = 1
+          QUALIFY row_number() OVER (ORDER BY i.k) = 1
+                  AND o.flag = 1
+        )
+        ORDER BY o.k;
+    """
+
+    // positive counterpart of select_40: only k = 10 satisfies both o.h = 1 
and o.flag = 1.
+    qt_select_41 """
+        SELECT o.k
+        FROM (
+          SELECT CAST(10 AS INT) AS k, CAST(1 AS INT) AS flag, CAST(1 AS INT) 
AS h
+          UNION ALL
+          SELECT CAST(20 AS INT) AS k, CAST(0 AS INT) AS flag, CAST(0 AS INT) 
AS h
+        ) AS o
+        WHERE EXISTS (
+          SELECT i.k
+          FROM (
+            SELECT CAST(1 AS INT) AS k
+            UNION ALL
+            SELECT CAST(2 AS INT) AS k
+          ) AS i
+          GROUP BY i.k
+          HAVING o.h = 1
+          QUALIFY row_number() OVER (ORDER BY i.k) = 1
+                  AND o.flag = 1
+        )
+        ORDER BY o.k;
+    """
+
+    // qualify -> project where the qualify references a project alias (f) 
whose producer is a
+    // correlated outer column (o.flag). The alias-producer dependency must be 
preserved so the
+    // correlation is still extracted even though the project contains a 
window expression.
+    qt_select_42 """
+        SELECT o.k
+        FROM (
+          SELECT CAST(10 AS INT) AS k, CAST(1 AS INT) AS flag
+          UNION ALL
+          SELECT CAST(20 AS INT) AS k, CAST(0 AS INT) AS flag
+        ) AS o
+        WHERE EXISTS (
+          SELECT i.k, o.flag AS f, row_number() OVER (ORDER BY i.k) AS rn
+          FROM (
+            SELECT CAST(1 AS INT) AS k
+            UNION ALL
+            SELECT CAST(2 AS INT) AS k
+          ) AS i
+          QUALIFY rn = 1
+                  AND f = 1
+        )
+        ORDER BY o.k;
+    """
+
+    // negative counterpart of select_42: with o.flag = 0 everywhere the 
alias-resolved
+    // correlation must still filter out every outer row.
+    qt_select_43 """
+        SELECT o.k
+        FROM (
+          SELECT CAST(10 AS INT) AS k, CAST(0 AS INT) AS flag
+          UNION ALL
+          SELECT CAST(20 AS INT) AS k, CAST(0 AS INT) AS flag
+        ) AS o
+        WHERE EXISTS (
+          SELECT i.k, o.flag AS f, row_number() OVER (ORDER BY i.k) AS rn
+          FROM (
+            SELECT CAST(1 AS INT) AS k
+            UNION ALL
+            SELECT CAST(2 AS INT) AS k
+          ) AS i
+          QUALIFY rn = 1
+                  AND f = 1
+        )
+        ORDER BY o.k;
+    """
+
+    // DISTINCT subquery where the having and the qualify carry separate 
correlated outer
+    // predicates: both must stay on the same decorrelatable side of the 
distinct barrier,
+    // otherwise one of them is left dangling in the apply's right subtree.
+    // o.h = 0 < count(*) (2 distinct groups) and o.flag = 1 both hold only 
for k = 10.
+    qt_select_44 """
+        SELECT o.k
+        FROM (
+          SELECT CAST(10 AS INT) AS k, CAST(1 AS INT) AS flag, CAST(0 AS INT) 
AS h
+          UNION ALL
+          SELECT CAST(20 AS INT) AS k, CAST(1 AS INT) AS flag, CAST(5 AS INT) 
AS h
+        ) AS o
+        WHERE EXISTS (
+          SELECT DISTINCT i.k
+          FROM (
+            SELECT CAST(1 AS INT) AS k
+            UNION ALL
+            SELECT CAST(2 AS INT) AS k
+          ) AS i
+          HAVING o.h < count(*)
+          QUALIFY row_number() OVER (ORDER BY i.k) = 1
+                  AND o.flag = 1
+        )
+        ORDER BY o.k;
+    """
+
+    // a project over a constant UNION ALL that references a correlated outer 
column: the
+    // project must not be pushed through the union (the outer slot has no 
producer inside
+    // the union children), and the correlation must still be decorrelated 
correctly.
+    qt_select_45 """
+        SELECT o.k
+        FROM (
+          SELECT CAST(10 AS INT) AS k, CAST(1 AS INT) AS flag
+          UNION ALL
+          SELECT CAST(20 AS INT) AS k, CAST(0 AS INT) AS flag
+        ) AS o
+        WHERE EXISTS (
+          SELECT i.k, o.flag AS f
+          FROM (
+            SELECT CAST(1 AS INT) AS k
+            UNION ALL
+            SELECT CAST(2 AS INT) AS k
+          ) AS i
+          QUALIFY row_number() OVER (ORDER BY i.k) = 1
+                  AND f = 1
+        )
+        ORDER BY o.k;
+    """
+
+    // a correlated having predicate that depends on the aggregate result 
cannot be evaluated
+    // below the window project and must be rejected instead of being silently 
dropped or
+    // moved above the window (which would change the evaluation order).
+    test {
+        sql """
+            SELECT o.k
+            FROM (
+              SELECT CAST(10 AS INT) AS k, CAST(0 AS INT) AS h
+            ) AS o
+            WHERE EXISTS (
+              SELECT i.k
+              FROM (
+                SELECT CAST(1 AS INT) AS k
+                UNION ALL
+                SELECT CAST(2 AS INT) AS k
+              ) AS i
+              GROUP BY i.k
+              HAVING count(*) = o.h
+              QUALIFY row_number() OVER (ORDER BY i.k) = 1
+            )
+        """
+        exception "in HAVING depending on the aggregate result is not 
supported together with QUALIFY"
+    }
+
+    // an aggregate output alias that only depends on outer correlated columns 
cannot be
+    // produced by the aggregate and must be rejected explicitly.
+    test {
+        sql """
+            SET sql_mode = '';

Review Comment:
   [P1] Execute the SQL-mode change as a separate statement
   
   `test.sql` sends this entire string once through `JdbcUtils.executeToList`, 
which uses one prepared statement. The normal and p0 regression URLs do not 
enable `allowMultiQueries`, so `SET ...; SELECT ...` fails as a 
multi-statement/parser error before reaching 
`resolveCorrelatedAggregateOutputAlias`; it cannot match the intended exception 
and leaves this negative path untested. Run `sql "SET sql_mode = ''"` 
separately before the `test` block and keep only the target SELECT here.



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