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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpQualifyMissingSlot.java:
##########
@@ -115,10 +121,11 @@ public List<Rule> buildRules() {
                qualify -> agg
              */
             RuleType.FILL_UP_QUALIFY_AGGREGATE.build(
-                logicalQualify(aggregate()).then(qualify -> {
+                logicalQualify(aggregate()).thenApply(ctx -> {
+                    LogicalQualify<Aggregate<Plan>> qualify = ctx.root;
                     checkWindow(qualify);
                     Aggregate<Plan> agg = qualify.child();
-                    Resolver resolver = new Resolver(agg);
+                    Resolver resolver = new Resolver(agg, 
ctx.cascadesContext.getOuterScope());
                     qualify.getConjuncts().forEach(expr -> 
resolver.resolve(expr, ResolvePlanType.QUALIFY));

Review Comment:
   [P1] Resolve grouped output aliases to their outer producer
   
   Passing `outerScope` to `Resolver` does not expose an outer dependency 
hidden by an aggregate-output alias. A reduced legal EXISTS subquery is:
   
   ```text
   Qualify[f = 1, rn = 1]
     Aggregate[groupBy=i.k; output=i.k, o.flag AS f, window AS rn]
       Scan(i)
   ```
   
   `Resolver.lookUp(f)` finds the local output slot and records only `f -> f`; 
it adds no aggregate output and this rule returns unchanged. 
`NormalizeAggregate` later encounters `o.flag` in the output project without a 
child/group producer, so analysis/final slot validation fails. The added tests 
cover a direct outer slot with GROUP BY and an alias without GROUP BY, but not 
this cross-product. Please resolve a safe outer-only aggregate-output alias to 
its producer while preserving valid aggregate outputs, or reject this shape 
explicitly, and add a complete-pipeline grouped-alias regression.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpQualifyMissingSlot.java:
##########
@@ -189,10 +238,37 @@ 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 (outerScope.isPresent()) {
+            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())) {
+                    correlatedAliasToProducer.put(entry.getKey(), producer);

Review Comment:
   [P1] Exclude window producers from this alias replacement cycle
   
   A project alias can itself be a window over only outer slots:
   
   ```text
   Qualify[rn = 1]
     Project[row_number() over(order by o.flag) AS rn]
       Scan(i)
   ```
   
   The map replaces `rn` with the window expression, then `visitWindow` 
immediately creates a fresh alias `w1`. The generated child is another 
`Qualify(Project)` whose `w1` maps to the same window; the top-down analysis 
job revisits it, creates `w2`, and repeats without a fixed point before 
normalization or Apply unnesting. Restrict this repair to a producer class that 
cannot re-enter window extraction (or make the transformation explicitly 
idempotent), and add a full analyzer regression with a window partition/order 
key that is entirely correlated.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpQualifyMissingSlot.java:
##########
@@ -189,10 +238,37 @@ 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 (outerScope.isPresent()) {
+            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())) {
+                    correlatedAliasToProducer.put(entry.getKey(), producer);
+                }

Review Comment:
   [P1] Do not copy a producer subquery into both plan nodes
   
   A producer such as `o.flag + (SELECT max(j.k) FROM j)` has visible input 
slots `{o.flag}`, so it enters this map even though it also contains a 
`SubqueryExpr`. Replacement leaves the same subquery in the lower project and 
copied upper QUALIFY expression:
   
   ```text
   Qualify[o.flag + scalarSubquery > 0]
     Project[o.flag + scalarSubquery AS f, window AS rn]
   ```
   
   Bottom-up `SubqueryToApply` unnests the lower occurrence and marks that 
subquery analyzed. The upper occurrence is then replaced with the same output 
slot, but creation of its Apply is skipped by the global analyzed-expression 
fence; the intervening project does not output that slot, so final validation 
rejects the filter. Exclude producers containing `SubqueryExpr` from this 
substitution or materialize/unnest the producer exactly once at a shared level, 
with scalar and EXISTS producer regressions through `CheckAfterRewrite`.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushProjectIntoUnion.java:
##########
@@ -100,6 +100,15 @@ private boolean 
canPushProjectIntoUnion(LogicalProject<LogicalUnion> project) {
         if (union.getQualifier() != Qualifier.ALL || union.arity() != 0) {
             return false;
         }
+        // The project must only consume slots produced by the union. A 
correlated subquery may
+        // still have a project referencing an outer correlated slot (e.g. an 
aliased outer column
+        // that is resolved back to its producer); such slots have no constant 
producer in the
+        // union, so pushing the project into the union would leave a dangling 
slot reference.
+        for (NamedExpression ne : project.getProjects()) {
+            if (!union.getOutputSet().containsAll(ne.getInputSlots())) {
+                return false;

Review Comment:
   [P1] Enforce this ownership invariant in the earlier UNION rewrite
   
   This late guard is bypassed by `PushProjectThroughUnion`, which runs before 
`MergeOneRowRelationIntoUnion` and this rule in both rewrite pipelines. For a 
width-matched project over a constant `UNION ALL`:
   
   ```text
   Project[i.k, o.flag AS f]
     UnionAll[OneRow(...), OneRow(...)]
   ```
   
   `PushProjectThroughUnion.canPushProject` admits the project from width and 
bare/cast-slot shape alone; its replacement leaves `o.flag` unmapped in each 
one-row child. `ProjectProcessor` merges those projects and 
`MergeOneRowRelationIntoUnion` converts them to arity-zero constant rows, so 
this new check never executes and final validation sees a dangling outer slot. 
Apply the same child-output containment invariant before 
`PushProjectThroughUnion` rewrites the children (including the CTE 
registration), and add an end-to-end constant-UNION regression whose normalized 
project width matches the UNION width.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpQualifyMissingSlot.java:
##########
@@ -158,17 +203,21 @@ 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()) {
                             Set<Slot> missingSlots = 
having.getExpressions().stream()
                                     .map(Expression::getInputSlots)
                                     .flatMap(Set::stream)
                                     .filter(s -> !projects.contains(s))
+                                    .filter(s -> !(outerScope.isPresent()
+                                            && 
outerScope.get().getCorrelatedSlots().contains(s)))

Review Comment:
   [P1] Keep both DISTINCT correlations reachable by the enclosing Apply
   
   For a DISTINCT subquery with separately correlated HAVING and QUALIFY 
predicates, this branch builds the reduced tree:
   
   ```text
   Having[o.h]
     ProjectDistinct[i.k]
       Qualify[o.flag, rn]
         Project[i.k, rn := window]
           Scan(i)
   ```
   
   Distinct conversion plus `NormalizeAggregate` inserts a `Project -> 
Aggregate` barrier between the two filters. `UnCorrelatedApplyFilter` first 
records `o.h`; the Apply's `alreadyExecutedEliminateFilter` fence then prevents 
pulling through the normalization project, and no aggregate-filter rule reaches 
the lower `o.flag`. Apply-to-Join therefore uses only `o.h`, leaving `o.flag` 
dangling in the right subtree. This is the DISTINCT/project parallel shape, not 
the existing `Qualify(Having(Aggregate))` thread. Keep the correlated 
predicates on the same decorrelatable side of the distinct/window barriers, or 
reject the shape, and add the missing full-pipeline regression.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpQualifyMissingSlot.java:
##########
@@ -135,21 +142,59 @@ public List<Rule> buildRules() {
                qualify -> having -> agg
              */
             RuleType.FILL_UP_QUALIFY_HAVING_AGGREGATE.build(
-                logicalQualify(logicalHaving(aggregate())).then(qualify -> {
+                logicalQualify(logicalHaving(aggregate())).thenApply(ctx -> {
+                    LogicalQualify<LogicalHaving<Aggregate<Plan>>> qualify = 
ctx.root;
                     checkWindow(qualify);
                     LogicalHaving<Aggregate<Plan>> having = qualify.child();
                     Aggregate<Plan> agg = qualify.child().child();
-                    Resolver resolver = new Resolver(agg);
-                    qualify.getConjuncts().forEach(expr -> 
resolver.resolve(expr, ResolvePlanType.QUALIFY));
+                    Optional<Scope> outerScope = 
ctx.cascadesContext.getOuterScope();
+                    // The window expression in qualify will be extracted into 
a project above the having
+                    // during NormalizeAggregate. Filter pushdown refuses to 
go through a project that
+                    // contains a window expression, so a correlated predicate 
that stays in the having
+                    // would sit below the window project and could never be 
collected into the apply
+                    // during subquery unnesting (it would then be silently 
dropped later). Conjoin the
+                    // having's outer-only correlated predicates into the 
qualify, so both correlated
+                    // predicates stay above the window project and are 
decorrelated together.
+                    Set<Expression> newHavingConjuncts = new LinkedHashSet<>();
+                    Set<Expression> qualifyConjuncts = new 
LinkedHashSet<>(qualify.getConjuncts());
+                    if (outerScope.isPresent()) {
+                        Set<Slot> correlatedSlots = 
outerScope.get().getCorrelatedSlots();
+                        for (Expression conjunct : having.getConjuncts()) {
+                            Set<Slot> inputSlots = conjunct.getInputSlots();
+                            if (inputSlots.isEmpty()) {
+                                newHavingConjuncts.add(conjunct);
+                            } else if 
(correlatedSlots.containsAll(inputSlots)) {
+                                // the predicate only depends on the outer 
row, so it can safely be

Review Comment:
   [P1] Do not infer HAVING/window commutativity from visible input slots
   
   This containment check is not proof that a conjunct is constant over 
aggregate rows or safe to move across window evaluation. For example:
   
   ```text
   Qualify[rn = 1]
     Project[rn := row_number(order by i.k desc)]
       Having[count(*) = o.h]
         Aggregate[groupBy=i.k, count(*)]
   ```
   
   `count(*) = o.h` reports only `{o.h}` as input slots, so the patch ranks 
every group before applying the predicate. With `o.h=1`, a one-row `k=1` group 
and a two-row `k=2` group, correct HAVING-first execution keeps `k=1` and 
numbers it 1; the rewrite numbers `k=2` first and returns no row. 
`SubqueryExpr` similarly hides a current-group correlation from 
`getInputSlots()`, and even a pure false outer predicate may be required to 
suppress an error-producing window key. Preserve HAVING below the window while 
making it decorrelatable, or conservatively reject shapes whose complete 
dependencies, determinism, and evaluation domain are not proven; add aggregate, 
nested-subquery, and error-gating regressions.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/FillUpQualifyMissingSlot.java:
##########
@@ -189,10 +238,37 @@ 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 (outerScope.isPresent()) {
+            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())) {
+                    correlatedAliasToProducer.put(entry.getKey(), producer);
+                }
+            }
+        }
+        boolean conjunctsRewritten = false;
+        if (!correlatedAliasToProducer.isEmpty()) {
+            Set<Expression> rewrittenConjuncts = 
ExpressionUtils.replace(conjuncts, correlatedAliasToProducer);
+            conjunctsRewritten = !rewrittenConjuncts.equals(conjuncts);

Review Comment:
   [P1] Preserve slot ownership when replacement crosses IN boundaries
   
   Replacing only the QUALIFY reference does not make the producer legal for 
every Apply caller. Two reduced failures remain:
   
   ```text
   Apply[IN, corr=o.flag]
     Scan(o)
     Project[o.flag AS f] -> Filter[o.flag=1, window]
   
   Apply[EXISTS, corr=o.flag]
     Scan(o)
     Apply[IN, compare=o.flag](Project[f, window](Scan(i)), Scan(j))
   ```
   
   In the first, IN/NOT IN must retain `f` as its build value; the IN-specific 
`Project(Filter)` fence preserves the right project, and `InApplyToJoin` leaves 
it consuming the left-only `o.flag`. In the second, `ExpressionUtils.replace` 
descends into `InSubquery.child()`, then `SubqueryToApply` removes that filter 
and stores `o.flag` only in the nested IN Apply's `compareExpr`; 
`InApplyToJoin` emits a nested join condition whose slot is produced by neither 
child. Both fail final slot validation (mark/OR has the same ownership issue). 
Reject unsupported outer-dependent IN outputs/nested compares, or 
materialize/pass the value at the correct Apply level, and add IN, NOT IN, 
nested-IN, and mark regressions.



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