This is an automated email from the ASF dual-hosted git repository.

CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new b6c3a630e9b [fix](dereference) Resolve relation-qualified columns 
before output alias fields in ORDER BY / HAVING / QUALIFY (#68260)
b6c3a630e9b is described below

commit b6c3a630e9b6ea4f8ac7d0b690ad15f8b6568d91
Author: Calvin Kirs <[email protected]>
AuthorDate: Tue Sep 22 14:42:57 2026 +0800

    [fix](dereference) Resolve relation-qualified columns before output alias 
fields in ORDER BY / HAVING / QUALIFY (#68260)
    
    Follow-up of #67438.
    
    ## The problem
    
    A multipart name is either a relation-qualified column (`t.col`) or a
    nested field reference (`col.field`). When a select output alias has the
    same name as a relation alias, a legal relation-qualified reference
    fails:
    
    ```sql
    SELECT q.v AS q FROM (SELECT 7 AS v) q ORDER BY q.v;
    -- No such field 'v' in 'q'
    
    SELECT q.v      FROM (SELECT 7 AS v) q ORDER BY q.v;   -- ok, 7
    SELECT q.v AS q FROM (SELECT 7 AS v) q ORDER BY v;     -- ok, 7
    ```
    
    ORDER BY, HAVING and QUALIFY do not bind against one scope. They bind
    against layered local scopes, for ORDER BY the select output first and
    then its child output. #67438 added a pass that tries the
    relation-qualified reading in every visible scope before any `col.field`
    reading, and threaded it through all these layered binders, but the pass
    only ran when a correlated outer scope existed. At the top level the
    nearer output alias `q` was taken as the first part of `col.field`, and
    because `q` is a scalar the lookup threw before the child output was
    ever tried. The same HAVING inside a subquery already worked, which is
    what pointed at the gate.
    
    It is worse when the alias is a struct, map or variant: nothing fails,
    `q.v` silently binds to `element_at(alias q, 'v')` and the query returns
    wrong results.
    
    Shapes that were broken at the top level (all fixed here):
    
    | Clause | Example | Before |
    |---|---|---|
    | ORDER BY | `select q.v as q from t q order by q.v` (also with an
    expression, `DISTINCT`, `LIMIT`, a join, inside an `IN` subquery,
    `db.tbl.col`) | No such field |
    | ORDER BY over aggregate | `select max(q.v) as q from t q group by q.id
    order by q.id` / `... order by max(q.v)` | No such field |
    | HAVING without aggregate | `select q.v as q from t q having q.v > 0` |
    No such field |
    | HAVING / QUALIFY after `GROUP BY <expr>` | `select q.id + 1 as q from
    t q group by q.id + 1 having q.id + 1 > 0` | No such field |
    | struct / map / variant alias | `select q.s as q from t q order by q.v`
    (`s` is `struct<v:int>`, `t` has a column `v`) | **wrong result**,
    sorted by `s.v` |
    
    While auditing the same family, one more bypass of the layered binders
    showed up, and it needs no alias at all: a **lambda body** inside these
    clauses resolved names against the default scope of the enclosing
    analyzer only, so it could not see the child output.
    
    ```sql
    SELECT id FROM t ORDER BY array_sum(array_map(x -> x + v, arr));
    -- Unknown lambda slot 'v in lambda arguments[x]      (the same expression 
works in WHERE)
    ```
    
    Letting a lambda body follow its clause made one more failure reachable,
    and it turned out to exist on master independently of this PR: a nested
    loop join whose condition has a lambda that references **both sides**
    fails in the translator.
    
    ```sql
    SELECT q.id, p.id FROM t q, t p WHERE array_sum(array_map(x -> x + p.v + 
q.v, [0])) > 40;
    -- java.lang.NullPointerException: Cannot read field "type" because "e" is 
null
    ```
    
    `ProjectOtherJoinConditionForNestedLoopJoin` moves a sub-expression of
    the join condition into a Project under the join when all of its input
    slots come from one side. A lambda argument is not an input slot (no
    child outputs it), so `x + p.v` was taken as a right-only expression and
    moved out of the lambda body into a Project where `x` does not exist.
    Before this PR a lambda in JOIN ON or in a correlated EXISTS failed
    during analysis, so only the WHERE-over-join shape could reach it; with
    the lambda fix those two shapes reach it too, so it is fixed here.
    
    ## The fix
    
    No new mechanism is added.
    
    1. `visitUnboundSlot`: run the relation-qualified pass over the local
    scopes for **every** multipart name. Only the lookup in the outer scope
    stays conditional on a correlated subquery. For an analyzer with a
    single scope the result is identical to before, the pass is just the
    first stage of the normal lookup.
    2. Lambda analyzer: bind the lambda arguments first as the nearest
    lexical scope, and delegate every other name to the enclosing analyzer,
    so a lambda body resolves names exactly like the clause around it. A
    nested lambda delegates to the lambda around it.
    The variant schema auto cast (`enable_variant_schema_auto_cast`) casts
    only the outermost `element_at` of a chain, and tracks the chain in a
    counter of the analyzer instance. The chain around a lambda body name is
    visited by the lambda analyzer, so the enclosing analyzer binds the
    delegated name under the counter of the lambda analyzer and restores its
    own afterwards (`visitUnboundSlotOfLambdaBody`). This keeps the casts of
    a lambda body exactly as they were before the delegation, in both
    directions: a chain inside the lambda body, and a chain around the
    higher order function.
    3. `ProjectOtherJoinConditionForNestedLoopJoin`: do not descend into a
    lambda. A higher order function whose inputs all come from one side is
    still projected as a whole.
    
    ## Binding priority after this PR
    
    For every name:
    
    1. Multipart name only: try the **relation-qualified** reading (`t.col`,
    `db.t.col`, `ctl.db.t.col`) through the local scope chain of the clause
    (table below). If nothing matches, the clause is in a correlated
    subquery, and no local relation has that name, try it in the outer
    scope.
    2. Full reading through the same local scope chain. Inside one scope:
    `ctl.db.t.col` → `db.t.col` → `t.col` → `col.field`.
    3. Full reading in the outer scope (one level up), if the clause may
    bind it.
    4. 0 matches → error, 1 → bound, more → the exact qualifier match if the
    clause enables it, otherwise ambiguous.
    
    Local scope chain per clause (near → far). Only step 1 is new for the
    top level, the chains themselves are unchanged:
    
    | Clause | Local scope chain | Outer scope |
    |---|---|---|
    | SELECT list, WHERE, JOIN ON, aggregate output, window, LATERAL VIEW |
    child output | yes |
    | GROUP BY | child output (exactly one match) → aggregate output alias,
    replaced by its expression | yes |
    | HAVING without aggregate | select output → its child output | yes |
    | HAVING over aggregate, outside an aggregate function | group by slots
    → aggregate output → aggregate child | yes |
    | HAVING over aggregate, inside an aggregate function | aggregate child
    | yes |
    | QUALIFY over project | project child output → project output | yes |
    | QUALIFY over aggregate | group by slots → aggregate output → aggregate
    child | yes |
    | ORDER BY | select output → its child output (skips QUALIFY, HAVING,
    DISTINCT project over aggregate); an integer literal is an ordinal | no
    |
    | ORDER BY, key contains an aggregate function | aggregate output
    without aggregate functions → child output | no |
    | ORDER BY over a set operation | set operation output | yes |
    | Lambda body | lambda arguments → **the enclosing clause, by the rules
    above** (new) | as the clause |
    
    What changes in priority: for a multipart name, a relation-qualified
    column in **any** local layer now wins over a `col.field` reading in a
    nearer layer. A single-part name is untouched, `ORDER BY q` still
    prefers the alias. A lambda argument keeps lexical priority over a
    relation of the same name (`array_map(x -> x.value, x.items) from t x`).
    
    ## Behavior changes
    
    - `q.v` prefers column `v` of relation `q` over field `v` of a
    same-named struct / map / variant output alias in HAVING, QUALIFY and
    ORDER BY. A query that relied on the alias reading while a same-named
    relation has a matching column now binds to the column. (GROUP BY looks
    at the child output first, its result does not change.)
    - The relation reading wins even when it then fails: with a relation `q`
    that has a scalar column `v` and a struct output alias `q` that has the
    path `v.b`, `ORDER BY q.v.b` used to read the alias and now reports `No
    such field 'b' in 'v'`. MySQL agrees on the priority: with an output
    alias `s` and a table `s`, it reads `s.a` as column `a` of the table.
    - Lambda bodies:
    - an unresolved name reports the error of the enclosing clause (`Unknown
    column ... in SORT clause`) instead of `Unknown lambda slot`;
    - a lambda body in a correlated subquery can reference an outer column,
    which is registered as a correlated slot;
    - a lambda body in a join condition no longer reports `Unsupported
    correlated subquery with correlated slot in join conjuncts` for a column
    of the join itself;
    - an ambiguous name follows the exact-match setting of the enclosing
    clause. The lambda analyzer used to pick the exact match on its own, so
    `select q.id as id, p.id from ... having array_sum(array_map(x -> x +
    id, q.arr)) > 0` used to bind `id` to the alias and now reports `id is
    ambiguous`, exactly as `having id > 0` does outside a lambda;
    - analyzers that override name resolution (generated columns, alias
    functions) now apply it inside lambda bodies too.
    
    ## Test matrix
    
    `U` = FE unit test (`TestDereference` unless another class is named;
    analysis only), `R` = regression case in `query_p0/test_dereference`
    (executed, result in `.out`).
    
    | Scenario | U | R |
    |---|---|---|
    | Reported repro: `select q.v as q from (select 7 as v) q order by q.v`
    | ✓ | ✓ |
    | ORDER BY on a table, scalar alias | ✓ | ✓ |
    | ORDER BY with DISTINCT / join / inside an `IN` subquery | ✓ | |
    | ORDER BY `db.tbl.col`, alias named like the database | ✓ | |
    | ORDER BY with an aggregate function in the key | ✓ | ✓ |
    | ORDER BY over aggregate, key is a group by column | ✓ | ✓ |
    | ORDER BY over aggregate, key is the group by expression | ✓ | |
    | HAVING without aggregate | ✓ | ✓ |
    | HAVING over aggregate, group by column / group by expression | ✓ | ✓
    (expression) |
    | QUALIFY over project / over aggregate with a group by expression | ✓ |
    ✓ (aggregate) |
    | struct alias, ORDER BY — result must follow column `v`, not `s.v` | ✓
    (no `element_at`) | ✓ (returns id 3, the wrong binding returns 1) |
    | struct alias, HAVING | ✓ | ✓ (returns id 1, the wrong binding returns
    nothing) |
    | struct alias, QUALIFY | ✓ | |
    | Fallback kept: alias only, `select q.s as a ... order by a.v` | ✓
    (`element_at`) | |
    | Fallback kept: alias named like a relation that has no such column | ✓
    | ✓ |
    | Negative: name is only a scalar output alias → still `No such field
    'v' in 'q'` | ✓ | ✓ |
    | Negative: `q.v.b`, relation column `v` is scalar, alias `q` has the
    path `v.b` → `No such field 'b' in 'v'` | ✓ | ✓ |
    | Lambda in ORDER BY, column not in the select list, unqualified /
    qualified | ✓ | ✓ |
    | Lambda in ORDER BY / HAVING with the alias shadow | ✓ | ✓ |
    | Lambda inside an aggregate function in HAVING, lambda in QUALIFY | ✓ |
    |
    | Nested lambda referencing a column of the clause | ✓ | |
    | Lambda in JOIN ON referencing both sides | ✓ | ✓ (NPE before the rule
    fix) |
    | Lambda in a correlated EXISTS referencing the outer and the inner
    relation | ✓ (correlated slot registered) | ✓ (NPE before the rule fix)
    |
    | Lambda in a correlated IN referencing an outer column | | ✓ |
    | Nested loop join rule: a lambda body is not projected, a one-sided
    higher order function still is | ✓
    (`ProjectOtherJoinConditionForNestedLoopJoinTest`) | |
    | Negative: ambiguous name in HAVING, outside and inside a lambda → `id
    is ambiguous` | ✓ | |
    | Variant auto cast: `array_map(x -> data.num_nested['l1']['l2'], arr)`,
    the dotted prefix in a chain inside the lambda body is not cast, same as
    outside a lambda | ✓ (`ExpressionAnalyzerVariantAutoCastTest`) | |
    | Variant auto cast: `array_map(x -> array(data.num_a), arr)[1][1]`, a
    chain around the higher order function does not suppress the cast in the
    lambda body | ✓ (`ExpressionAnalyzerVariantAutoCastTest`) | |
    | Lambda argument keeps priority over a same-named relation (existing
    test) | ✓ | ✓ |
    | Negative: unknown name in a lambda body → `Unknown column` | ✓ | ✓ |
    
    How it was run:
    
    - The unit tests of the broken shapes fail without the fix (`No such
    field 'v' in 'q'`, an `element_at` binding for the struct alias, a
    projected lambda body for the join rule) and pass with it. A few cases
    in the same tests pass either way and only pin the behavior down (HAVING
    on a group by column, QUALIFY over a project, the negatives).
    - FE unit tests under `nereids/rules/analysis`, the lambda tests and
    `nereids/rules/rewrite/*Join*` were run locally before the variant cast
    commit: 68 of 70 classes pass. `PushDownLimitDistinctThroughJoinTest`
    and `PushDownTopNThroughJoinTest` fail locally in `runBeforeAll` while
    creating their tables (`available backend num is 0`), before any test
    method runs; they passed in CI on an earlier head. After that commit
    `TestDereference` (18), `ExpressionAnalyzerVariantAutoCastTest` (18) and
    `ProjectOtherJoinConditionForNestedLoopJoinTest` (3) pass, and the two
    variant cast tests fail without the fix.
    - The new regression cases were executed, and their expected output
    generated with `run-regression-test.sh -genOut`, as a standalone copy of
    the new section against a local cluster of this FE with a **4.1.3 BE**
    (no master BE was available). The generated blocks were appended to
    `test_dereference.out`. The `test { sql; exception }` cases were
    executed on the same cluster and report the expected messages. The whole
    `test_dereference` suite cannot run end to end locally, because the
    existing `parse_to_variant` cases need a master BE; it passed in CI (P0)
    on an earlier head of this PR, before the negative and the join /
    correlated lambda cases were added.
    
    Not covered, left for a follow-up:
    
    - A lambda in a correlated **scalar** subquery with a non-equal
    correlated predicate reports `Unsupported correlated subquery with
    correlated predicate`, which is the existing limitation for such
    predicates and not specific to lambdas.
    - A regression case for the variant auto cast inside a lambda body, and
    a unit test for passing the cast counter through a nested lambda (traced
    in the code only).
    - A regression case for a variant-typed alias. It takes the same code
    path as the struct alias.
    - Lambdas in generated columns and alias functions.
    
    ## Known and deliberately not changed
    
    An output alias that shadows a same-named **column** (not a relation)
    for nested access still prefers the alias in ORDER BY and HAVING:
    
    ```sql
    select id, var.name as var from t order by cast(var.name as int), id;
    -- `var.name` reads field `name` of the alias `var`, i.e. var.name.name, 
which is NULL: the sort is a no-op
    ```
    
    This is the "column vs alias" priority, a separate rule from the
    "relation qualifier vs alias" one fixed here (QUALIFY and GROUP BY
    already prefer the underlying column, HAVING and ORDER BY prefer the
    alias). Changing it is a semantic decision with its own corner cases
    under aggregation, so it is left for a separate PR.
---
 .../nereids/rules/analysis/ExpressionAnalyzer.java |  59 +++++---
 ...ProjectOtherJoinConditionForNestedLoopJoin.java |  10 ++
 .../ExpressionAnalyzerVariantAutoCastTest.java     |  51 +++++++
 .../nereids/rules/analysis/TestDereference.java    | 150 +++++++++++++++++++++
 ...ectOtherJoinConditionForNestedLoopJoinTest.java |  56 ++++++++
 regression-test/data/query_p0/test_dereference.out |  59 ++++++++
 .../suites/query_p0/test_dereference.groovy        | 125 +++++++++++++++++
 7 files changed, 493 insertions(+), 17 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
index f8fd0910ae1..a37a0b40d72 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
@@ -326,18 +326,20 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
         boolean relationQualifierOccupied = false;
 
         // A multipart name can be either a relation-qualified column (t.col) 
or a nested field
-        // reference (col.field). In a correlated subquery, try the 
relation-qualified interpretation
-        // in both visible scopes first, so an inner column named "t" does not 
hide an outer alias "t".
-        if (canBindOuterScope && shouldPrioritizeRelationQualifier()
-                && unboundSlot.getNameParts().size() > 1) {
+        // reference (col.field). Try the relation-qualified interpretation in 
every visible scope
+        // first, so a nearer name "t" does not hide a farther relation alias 
"t". The visible scopes
+        // are the local ones, which HAVING, QUALIFY and ORDER BY layer from 
the select output and its
+        // child output in a clause specific order, and then the outer scope 
of a correlated subquery:
+        //   select q.v as q from t q order by q.v  -- q.v is the column of 
relation q, not alias q
+        if (shouldPrioritizeRelationQualifier() && 
unboundSlot.getNameParts().size() > 1) {
             SlotBinding localRelationBinding = 
bindSlotByRelationQualifierInThisScope(unboundSlot);
             bounded = localRelationBinding.getBoundSlots();
             foundInThisScope = !bounded.isEmpty();
-            if (!foundInThisScope) {
+            if (!foundInThisScope && canBindOuterScope) {
                 relationQualifierOccupied = 
localRelationBinding.isRelationQualifierOccupied();
-            }
-            if (!foundInThisScope && !relationQualifierOccupied) {
-                bounded = bindSlotsByRelationQualifier(unboundSlot, 
outerScope.get());
+                if (!relationQualifierOccupied) {
+                    bounded = bindSlotsByRelationQualifier(unboundSlot, 
outerScope.get());
+                }
             }
         }
 
@@ -474,34 +476,57 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
         Lambda lambda = (Lambda) unboundFunction.children().get(0);
         Expression lambdaFunction = lambda.getLambdaFunction();
         LambdaBinding binding = bindingSpec.bind(unboundFunction.getName(), 
lambda, subChildren);
-        lambdaFunction = analyzeLambdaFunction(
-                lambda, lambdaFunction, binding.getAnalysisSlots(), context);
+        lambdaFunction = analyzeLambdaFunction(lambdaFunction, 
binding.getAnalysisSlots(), context);
         Lambda lambdaClosure = binding.close(lambdaFunction);
 
         // We don't add the ArrayExpression in high order function at all
         return unboundFunction.withChildren(ImmutableList.of(lambdaClosure));
     }
 
-    private Expression analyzeLambdaFunction(Lambda lambda, Expression 
lambdaFunction,
+    private Expression analyzeLambdaFunction(Expression lambdaFunction,
             List<Slot> boundedSlots, ExpressionRewriteContext context) {
+        ExpressionAnalyzer enclosingAnalyzer = this;
         ExpressionAnalyzer lambdaAnalyzer = new 
ExpressionAnalyzer(currentPlan, new Scope(Optional.of(getScope()),
                 boundedSlots), context == null ? null : 
context.cascadesContext,
                 true, true) {
             @Override
-            protected boolean shouldPrioritizeRelationQualifier() {
-                return false;
+            public Expression visitUnboundSlot(UnboundSlot unboundSlot, 
ExpressionRewriteContext context) {
+                // The lambda arguments are the nearest lexical scope. Every 
other name is resolved by the
+                // enclosing analyzer rather than by its default scope, 
because ORDER BY, HAVING and QUALIFY
+                // layer several local scopes and a correlated subquery sees 
its outer scope:
+                //   select id from t order by array_sum(array_map(x -> x + v, 
arr))  -- v is not in the output
+                if (bindSlotByThisScope(unboundSlot).isEmpty()) {
+                    return 
enclosingAnalyzer.visitUnboundSlotOfLambdaBody(unboundSlot, context, this);
+                }
+                return super.visitUnboundSlot(unboundSlot, context);
             }
 
             @Override
-            protected void couldNotFoundColumn(UnboundSlot unboundSlot, String 
tableName) {
-                throw new AnalysisException("Unknown lambda slot '"
-                        + 
unboundSlot.getNameParts().get(unboundSlot.getNameParts().size() - 1)
-                        + " in lambda arguments" + 
lambda.getLambdaArgumentNames());
+            protected boolean shouldPrioritizeRelationQualifier() {
+                // a name that starts with a lambda argument is a field of it, 
even if a relation of the
+                // enclosing scope has the same name: array_map(x -> x.value, 
x.items) from t x
+                return false;
             }
         };
         return lambdaAnalyzer.analyze(lambdaFunction, context);
     }
 
+    /**
+     * Bind a name of a lambda body that is not a lambda argument. The 
element_at chain around the name is
+     * visited by the lambda analyzer, so whether the variant cast of the name 
is suppressed is the state of
+     * the lambda analyzer, not the state of this analyzer, which may be in a 
chain around the lambda.
+     */
+    private Expression visitUnboundSlotOfLambdaBody(UnboundSlot unboundSlot, 
ExpressionRewriteContext context,
+            ExpressionAnalyzer lambdaAnalyzer) {
+        int enclosingDepth = suppressVariantElementAtCastDepth;
+        suppressVariantElementAtCastDepth = 
lambdaAnalyzer.suppressVariantElementAtCastDepth;
+        try {
+            return visitUnboundSlot(unboundSlot, context);
+        } finally {
+            suppressVariantElementAtCastDepth = enclosingDepth;
+        }
+    }
+
     /** Whether relation-qualified columns should be resolved across scopes 
before nested fields. */
     protected boolean shouldPrioritizeRelationQualifier() {
         return true;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
index 72f1752c375..2d0a032fd9c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java
@@ -23,6 +23,7 @@ import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
 import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Lambda;
 import 
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
@@ -103,6 +104,15 @@ public class ProjectOtherJoinConditionForNestedLoopJoin 
extends OneRewriteRuleFa
     private static class AliasReplacer extends 
DefaultExpressionRewriter<ReplacerContext> {
         public static AliasReplacer INSTANCE = new AliasReplacer();
 
+        @Override
+        public Expression visitLambda(Lambda lambda, ReplacerContext ctx) {
+            // A lambda body is evaluated per array item. An expression in it 
may reference the lambda
+            // arguments, which are not input slots and which no child of the 
join outputs, so it can
+            // not be evaluated in a child Project:
+            //   array_map(x -> x + t2.b, [0]) > t1.a   -- `x + t2.b` must 
stay inside the lambda
+            return lambda;
+        }
+
         @Override
         public Expression visit(Expression expression, ReplacerContext ctx) {
             Set<Slot> input = expression.getInputSlots();
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerVariantAutoCastTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerVariantAutoCastTest.java
index cf0c16b086e..fc93df8c11b 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerVariantAutoCastTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerVariantAutoCastTest.java
@@ -19,6 +19,7 @@ package org.apache.doris.nereids.rules.analysis;
 
 import org.apache.doris.nereids.CascadesContext;
 import org.apache.doris.nereids.analyzer.Scope;
+import org.apache.doris.nereids.analyzer.UnboundFunction;
 import org.apache.doris.nereids.analyzer.UnboundSlot;
 import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.Between;
@@ -34,9 +35,11 @@ import 
org.apache.doris.nereids.trees.expressions.functions.agg.Max;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Min;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.ElementAt;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Lambda;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.TryParseToVariant;
 import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.types.ArrayType;
 import org.apache.doris.nereids.types.BigIntType;
 import org.apache.doris.nereids.types.IntegerType;
 import org.apache.doris.nereids.types.StringType;
@@ -49,6 +52,8 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.List;
+
 public class ExpressionAnalyzerVariantAutoCastTest {
 
     @AfterEach
@@ -320,6 +325,52 @@ public class ExpressionAnalyzerVariantAutoCastTest {
         assertCastElementAt(((Count) countResult).child(0));
     }
 
+    private SlotReference buildArraySlot() {
+        return new SlotReference(new ExprId(2), "arr", 
ArrayType.of(BigIntType.INSTANCE), true, ImmutableList.of());
+    }
+
+    private List<Cast> collectCastElementAt(Expression expr) {
+        return expr.collectToList(node -> node instanceof Cast && ((Cast) 
node).child() instanceof ElementAt);
+    }
+
+    @Test
+    public void testLambdaBodyElementAtChainSuppressesCastOfDottedPrefix() {
+        // array_map(x -> data.num_nested['l1']['l2'], arr)
+        // data is not a lambda argument, so the enclosing analyzer binds 
data.num_nested, but the element_at
+        // chain around it is in the lambda body: data.num_nested is a prefix 
of the path and is not cast to
+        // the type of 'num_*', same as outside a lambda
+        SlotReference data = buildVariantSlot(buildVariantType());
+        Scope scope = new Scope(ImmutableList.of(data, buildArraySlot()));
+
+        Expression chain = new ElementAt(
+                new ElementAt(new UnboundSlot("data", "num_nested"), new 
StringLiteral("l1")),
+                new StringLiteral("l2"));
+        Expression outsideLambda = analyze(chain, scope, true);
+        Assertions.assertTrue(collectCastElementAt(outsideLambda).isEmpty(), 
outsideLambda.toSql());
+
+        Lambda lambda = new Lambda(ImmutableList.of("x"), chain);
+        Expression result = analyze(
+                new UnboundFunction("array_map", ImmutableList.of(lambda, new 
UnboundSlot("arr"))), scope, true);
+        Assertions.assertTrue(collectCastElementAt(result).isEmpty(), 
result.toSql());
+    }
+
+    @Test
+    public void testLambdaBodyIsNotSuppressedByElementAtChainAroundLambda() {
+        // array_map(x -> array(data.num_a), arr)[1][1]
+        // the element_at chain is around the higher order function, 
data.num_a in the lambda body is not
+        // a part of it and is still cast
+        SlotReference data = buildVariantSlot(buildVariantType());
+        Scope scope = new Scope(ImmutableList.of(data, buildArraySlot()));
+
+        Lambda lambda = new Lambda(ImmutableList.of("x"),
+                new UnboundFunction("array", ImmutableList.of(new 
UnboundSlot("data", "num_a"))));
+        Expression arrayMap = new UnboundFunction("array_map", 
ImmutableList.of(lambda, new UnboundSlot("arr")));
+        Expression result = analyze(
+                new ElementAt(new ElementAt(arrayMap, new BigIntLiteral(1)), 
new BigIntLiteral(1)), scope, true);
+
+        Assertions.assertEquals(1, collectCastElementAt(result).size(), 
result.toSql());
+    }
+
     @Test
     public void testAutoCastDisabled() {
         VariantType variantType = buildVariantType();
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
index ba932cb5b4c..fd44b4d003e 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/TestDereference.java
@@ -68,6 +68,15 @@ public class TestDereference extends TestWithFeService {
                     "inner_variant_table", ImmutableList.of(
                             new Column("id", PrimitiveType.INT),
                             new Column("outer_alias", new VariantType())
+                    ),
+                    "shadow_table", ImmutableList.of(
+                            new Column("id", PrimitiveType.INT),
+                            new Column("v", PrimitiveType.INT),
+                            new Column("s", new StructType(new 
StructField("v", Type.INT))),
+                            new Column("arr", new ArrayType(Type.INT))
+                    ),
+                    "plain_table", ImmutableList.of(
+                            new Column("id", PrimitiveType.INT)
                     )
             )
     );
@@ -199,6 +208,147 @@ public class TestDereference extends TestWithFeService {
         
Assertions.assertTrue(getOnlyApply(plan).getCorrelationSlot().isEmpty());
     }
 
+    @Test
+    public void testOutputAliasDoesNotShadowRelationQualifier() {
+        // the select output is a nearer scope than the relation for ORDER BY, 
HAVING and QUALIFY,
+        // q.v should still be the column v of relation q rather than a field 
of the scalar output alias q
+        List<String> sqls = ImmutableList.of(
+                "select q.v as q from (select 7 as v) q order by q.v",
+                "select q.v as q from shadow_table q order by q.v",
+                "select distinct q.v as q from shadow_table q order by q.v",
+                "select q.v as p, p.id as q from shadow_table q join 
plain_table p on q.id = p.id order by q.v, p.id",
+                "select * from plain_table o where o.id in (select q.v as q 
from shadow_table q order by q.v limit 1)",
+                // db.table.column, the alias has the same name as the database
+                "select q.v as t from shadow_table q order by t.q.v",
+                // aggregate
+                "select q.id as q from shadow_table q group by q.id order by 
max(q.v)",
+                "select max(q.v) as q from shadow_table q group by q.id order 
by q.id",
+                "select q.id + 1 as q from shadow_table q group by q.id + 1 
order by q.id + 1",
+                "select q.v as q from shadow_table q having q.v > 0",
+                "select q.id + 1 as q from shadow_table q group by q.id + 1 
having q.id + 1 > 0",
+                "select max(q.id) as q from shadow_table q group by q.v having 
q.v > 0",
+                "select q.v as q from shadow_table q qualify row_number() over 
(order by q.id) = 1 and q.v > 0",
+                "select q.id + 1 as q from shadow_table q group by q.id + 1 "
+                        + "qualify row_number() over (order by q.id + 1) = 1"
+        );
+        for (String sql : sqls) {
+            Assertions.assertDoesNotThrow(() -> 
PlanChecker.from(connectContext).analyze(sql), sql);
+        }
+    }
+
+    @Test
+    public void testRelationQualifierTakesPriorityOverComplexOutputAlias() {
+        // the output alias q is a struct with field v, q.v should not 
silently become element_at(q, 'v')
+        assertBoundToColumnV("select q.s as q from shadow_table q order by 
q.v");
+        assertBoundToColumnV("select q.s as q from shadow_table q having q.v > 
0");
+        assertBoundToColumnV("select q.s as q from shadow_table q qualify 
row_number() over (order by q.v) = 1");
+    }
+
+    @Test
+    public void testOutputAliasWithoutRelationQualifierKeepsScalarFieldError() 
{
+        // q is only an output alias here, the relation is p, so q.v is a 
field of the scalar alias q
+        AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                () -> PlanChecker.from(connectContext)
+                        .analyze("select p.v as q from shadow_table p order by 
q.v"));
+        Assertions.assertTrue(exception.getMessage().contains("No such field 
'v' in 'q'"),
+                exception.getMessage());
+    }
+
+    @Test
+    public void testRelationQualifierOccupiesNestedOutputAliasPath() {
+        // q.v is the scalar column v of relation q, so q.v.b is not the path 
v.b of the struct alias q
+        AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                () -> PlanChecker.from(connectContext)
+                        .analyze("select named_struct('v', named_struct('b', 
1)) as q "
+                                + "from shadow_table q order by q.v.b"));
+        Assertions.assertTrue(exception.getMessage().contains("No such field 
'b' in 'v'"),
+                exception.getMessage());
+    }
+
+    @Test
+    public void testOutputAliasKeepsNestedFieldFallback() {
+        // no relation-qualified column matches, so the first part falls back 
to the output alias
+        assertBoundToNestedField("select q.s as a from shadow_table q order by 
a.v");
+        assertBoundToNestedField("select p.s as q from shadow_table p join 
plain_table q on p.id = q.id order by q.v");
+        assertBoundToNestedField("select p.s as q from shadow_table p join 
plain_table q on p.id = q.id "
+                + "having q.v > 0");
+    }
+
+    @Test
+    public void testLambdaBodyBindsByEnclosingClauseScopes() {
+        // a name that is not a lambda argument is resolved the same way as 
outside the lambda,
+        // ORDER BY, HAVING and QUALIFY can see the child output behind the 
select output
+        List<String> sqls = ImmutableList.of(
+                "select id from shadow_table order by array_sum(array_map(x -> 
x + v, arr))",
+                "select id from shadow_table q order by array_sum(array_map(x 
-> x + q.v, q.arr))",
+                "select q.v as q from shadow_table q order by 
array_sum(array_map(x -> x + q.v, q.arr))",
+                "select q.v as q from shadow_table q having 
array_sum(array_map(x -> x + q.v, q.arr)) > 0",
+                "select q.id as q from shadow_table q group by q.id "
+                        + "having sum(array_sum(array_map(x -> x + q.v, 
q.arr))) > 0",
+                "select q.v as q from shadow_table q "
+                        + "qualify row_number() over (order by 
array_sum(array_map(x -> x + q.v, q.arr))) = 1"
+        );
+        for (String sql : sqls) {
+            Assertions.assertDoesNotThrow(() -> 
PlanChecker.from(connectContext).analyze(sql), sql);
+        }
+
+        // a nested lambda resolves through the lambda around it
+        Assertions.assertDoesNotThrow(() -> 
PlanChecker.from(connectContext).analyze(
+                "select id from shadow_table q order by "
+                        + "array_sum(array_map(x -> array_sum(array_map(y -> y 
+ x + q.v, q.arr)), q.arr))"));
+        // the enclosing clause is a join condition, both sides are its own 
scope rather than an outer scope
+        Assertions.assertDoesNotThrow(() -> 
PlanChecker.from(connectContext).analyze(
+                "select q.id from shadow_table q join plain_table p "
+                        + "on array_sum(array_map(x -> x + p.id, q.arr)) > 
0"));
+
+        AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                () -> PlanChecker.from(connectContext)
+                        .analyze("select id from shadow_table order by 
array_map(x -> x + unknown_column, arr)"));
+        Assertions.assertTrue(exception.getMessage().contains("Unknown column 
'unknown_column'"),
+                exception.getMessage());
+    }
+
+    @Test
+    public void testLambdaBodyFollowsAmbiguityOfEnclosingClause() {
+        // id is both the output alias of q.id and the output slot p.id, 
HAVING does not pick the exact match
+        String having = "select q.id as id, p.id from shadow_table q join 
plain_table p on q.id = p.id having ";
+        for (String predicate : ImmutableList.of("id > 0", 
"array_sum(array_map(x -> x + id, q.arr)) > 0")) {
+            AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                    () -> PlanChecker.from(connectContext).analyze(having + 
predicate), predicate);
+            Assertions.assertTrue(exception.getMessage().contains("id is 
ambiguous"), exception.getMessage());
+        }
+    }
+
+    @Test
+    public void testLambdaBodyRegistersCorrelationSlot() {
+        // the enclosing analyzer of the subquery filter sees the outer scope, 
so does the lambda body
+        Plan plan = PlanChecker.from(connectContext)
+                .analyze("select o.id from plain_table o where exists ("
+                        + "select 1 from shadow_table q where 
array_sum(array_map(x -> x + o.id, q.arr)) > 0)")
+                .getPlan();
+
+        LogicalApply<?, ?> apply = getOnlyApply(plan);
+        Assertions.assertEquals(1, apply.getCorrelationSlot().size());
+        Assertions.assertEquals("id", 
apply.getCorrelationSlot().get(0).getName());
+        List<String> qualifier = 
apply.getCorrelationSlot().get(0).getQualifier();
+        Assertions.assertEquals("o", qualifier.get(qualifier.size() - 1));
+    }
+
+    private void assertBoundToColumnV(String sql) {
+        Plan plan = PlanChecker.from(connectContext).analyze(sql).getPlan();
+        Assertions.assertFalse(containsElementAt(plan), sql);
+    }
+
+    private void assertBoundToNestedField(String sql) {
+        Plan plan = PlanChecker.from(connectContext).analyze(sql).getPlan();
+        Assertions.assertTrue(containsElementAt(plan), sql);
+    }
+
+    private boolean containsElementAt(Plan plan) {
+        return plan.anyMatch(node -> ((Plan) node).getExpressions().stream()
+                .anyMatch(expression -> 
expression.containsType(ElementAt.class)));
+    }
+
     private LogicalApply<?, ?> getOnlyApply(Plan plan) {
         List<LogicalApply<?, ?>> applies = 
plan.collectToList(LogicalApply.class::isInstance);
         Assertions.assertEquals(1, applies.size());
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoinTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoinTest.java
index 960500c75f3..ee1ecef9bc7 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoinTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoinTest.java
@@ -18,10 +18,16 @@
 package org.apache.doris.nereids.rules.rewrite;
 
 import org.apache.doris.nereids.trees.expressions.Add;
+import org.apache.doris.nereids.trees.expressions.ArrayItemReference;
 import org.apache.doris.nereids.trees.expressions.EqualTo;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.LessThan;
 import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayMap;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ArraySum;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Lambda;
+import org.apache.doris.nereids.trees.expressions.literal.ArrayLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.plans.JoinType;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
@@ -32,13 +38,23 @@ import org.apache.doris.nereids.util.PlanChecker;
 import org.apache.doris.nereids.util.PlanConstructor;
 import org.apache.doris.qe.ConnectContext;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 public class ProjectOtherJoinConditionForNestedLoopJoinTest implements 
MemoPatternMatchSupported {
     private final LogicalOlapScan scan1 = 
PlanConstructor.newLogicalOlapScan(0, "t1", 0);
     private final LogicalOlapScan scan2 = 
PlanConstructor.newLogicalOlapScan(1, "t2", 0);
 
+    @AfterEach
+    public void tearDown() {
+        // the scans of the next test should not take their slot ids from the 
statement scope of this test,
+        // they would collide with the ids of the aliases the rule creates
+        ConnectContext.remove();
+    }
+
     @Test
     public void testNestedLoopJoin() {
         Slot a = scan1.getOutput().get(1);
@@ -59,6 +75,46 @@ public class ProjectOtherJoinConditionForNestedLoopJoinTest 
implements MemoPatte
                 ).printlnTree();
     }
 
+    @Test
+    public void testLambdaBodyIsNotProjected() {
+        // t1.id < array_sum(array_map(x -> x + t2.id, [0]))
+        // all input slots of the higher order function come from t2, it is 
projected as a whole with its lambda
+        Slot a = scan1.getOutput().get(0);
+        Slot b = scan2.getOutput().get(0);
+        ArrayItemReference item = new ArrayItemReference("x",
+                new ArrayLiteral(ImmutableList.of(new IntegerLiteral(0))));
+        Lambda lambda = new Lambda(ImmutableList.of("x"), new 
Add(item.toSlot(), b), ImmutableList.of(item));
+        Expression otherCondition = new LessThan(a, new ArraySum(new 
ArrayMap(lambda)));
+
+        LogicalPlan join = new LogicalPlanBuilder(scan1).join(scan2, 
JoinType.CROSS_JOIN,
+                Lists.newArrayList(), 
Lists.newArrayList(otherCondition)).build();
+        PlanChecker.from(MemoTestUtils.createConnectContext(), join)
+                .applyTopDown(new ProjectOtherJoinConditionForNestedLoopJoin())
+                .matchesFromRoot(
+                        logicalJoin(
+                                logicalOlapScan(),
+                                // proj list: id, name, array_sum(array_map(x 
-> x + id, [0])) AS alias
+                                logicalProject().when(proj -> 
proj.getProjects().size() == 3
+                                        && 
proj.getProjects().get(2).containsType(Lambda.class))
+                        ).when(j -> j.getOtherJoinConjuncts().stream()
+                                .noneMatch(conjunct -> 
conjunct.containsType(Lambda.class)))
+                );
+
+        // x + t1.id + t2.id references both sides, the lambda stays in the 
join condition as a whole.
+        // `x + t1.id` has only t1.id as input slot, but x is a lambda 
argument that no child outputs,
+        // so it can not be projected to the left child
+        Lambda mixed = new Lambda(ImmutableList.of("x"), new Add(new 
Add(item.toSlot(), a), b),
+                ImmutableList.of(item));
+        Expression mixedCondition = new LessThan(a, new ArraySum(new 
ArrayMap(mixed)));
+        LogicalPlan mixedJoin = new LogicalPlanBuilder(scan1).join(scan2, 
JoinType.CROSS_JOIN,
+                Lists.newArrayList(), 
Lists.newArrayList(mixedCondition)).build();
+        LogicalPlan rewritten = (LogicalPlan) 
PlanChecker.from(MemoTestUtils.createConnectContext(), mixedJoin)
+                .applyTopDown(new ProjectOtherJoinConditionForNestedLoopJoin())
+                .getPlan();
+        Assertions.assertTrue(rewritten.child(0) instanceof LogicalOlapScan);
+        Assertions.assertTrue(rewritten.child(1) instanceof LogicalOlapScan);
+    }
+
     @Test
     public void testHashJoin() {
         Slot id1 = scan1.getOutput().get(0);
diff --git a/regression-test/data/query_p0/test_dereference.out 
b/regression-test/data/query_p0/test_dereference.out
index 0bdaece517c..039f73c3a5c 100644
--- a/regression-test/data/query_p0/test_dereference.out
+++ b/regression-test/data/query_p0/test_dereference.out
@@ -38,3 +38,62 @@
 1
 2
 
+-- !alias_shadow_order_by_subquery_alias --
+7
+
+-- !alias_shadow_order_by --
+10
+20
+30
+
+-- !alias_shadow_having --
+20
+30
+
+-- !alias_shadow_order_by_agg_func --
+3
+2
+1
+
+-- !alias_shadow_order_by_over_agg --
+30
+20
+10
+
+-- !alias_shadow_having_group_by_expr --
+4
+
+-- !alias_shadow_qualify_group_by_expr --
+2
+
+-- !alias_shadow_struct_alias_order_by --
+3
+
+-- !alias_shadow_struct_alias_having --
+1
+
+-- !alias_shadow_keep_alias_field --
+3
+
+-- !alias_shadow_lambda_order_by --
+3
+2
+1
+
+-- !alias_shadow_lambda_having --
+20
+30
+
+-- !alias_shadow_lambda_join_on --
+1      1
+1      2
+2      1
+
+-- !alias_shadow_lambda_correlated_exists --
+1
+2
+
+-- !alias_shadow_lambda_correlated_in --
+1
+2
+
diff --git a/regression-test/suites/query_p0/test_dereference.groovy 
b/regression-test/suites/query_p0/test_dereference.groovy
index 7d1c53851b7..67a3c2330ef 100644
--- a/regression-test/suites/query_p0/test_dereference.groovy
+++ b/regression-test/suites/query_p0/test_dereference.groovy
@@ -220,6 +220,131 @@ suite("test_dereference") {
             order by t.id
             """
 
+    // An output alias is a nearer scope than the relation for ORDER BY, 
HAVING and QUALIFY.
+    // A relation-qualified column should still bind to the relation when an 
output alias reuses its name.
+    multi_sql """
+        drop table if exists test_dereference_alias_shadow;
+        create table test_dereference_alias_shadow(
+          id int,
+          v int,
+          s struct<v:int>
+        )
+        distributed by hash(id) buckets 1
+        properties(
+          'replication_num'='1'
+        );
+
+        insert into test_dereference_alias_shadow
+        values (1, 30, struct(1)), (2, 20, struct(2)), (3, 10, struct(3));
+        """
+
+    qt_alias_shadow_order_by_subquery_alias "select q.v as q from (select 7 as 
v) q order by q.v"
+
+    qt_alias_shadow_order_by "select q.v as q from 
test_dereference_alias_shadow q order by q.v"
+
+    qt_alias_shadow_having "select q.v as q from test_dereference_alias_shadow 
q having q.v > 15 order by q.v"
+
+    qt_alias_shadow_order_by_agg_func """
+            select q.id as q from test_dereference_alias_shadow q group by 
q.id order by max(q.v)
+            """
+
+    qt_alias_shadow_order_by_over_agg """
+            select max(q.v) as q from test_dereference_alias_shadow q group by 
q.id order by q.id
+            """
+
+    qt_alias_shadow_having_group_by_expr """
+            select q.id + 1 as q from test_dereference_alias_shadow q
+            group by q.id + 1 having q.id + 1 > 3
+            """
+
+    qt_alias_shadow_qualify_group_by_expr """
+            select q.id + 1 as q from test_dereference_alias_shadow q
+            group by q.id + 1 qualify row_number() over (order by q.id + 1) = 1
+            """
+
+    // the output alias q is a struct that has a field v: q.v is still the 
column v of relation q
+    qt_alias_shadow_struct_alias_order_by """
+            select id from (
+                select q.id as id, q.s as q from test_dereference_alias_shadow 
q order by q.v limit 1
+            ) x
+            """
+
+    qt_alias_shadow_struct_alias_having """
+            select id from (
+                select q.id as id, q.s as q from test_dereference_alias_shadow 
q having q.v > 25
+            ) x
+            """
+
+    // no relation-qualified column matches, fall back to the nested field of 
the output alias
+    qt_alias_shadow_keep_alias_field """
+            select id from (
+                select p.id as id, p.s as q from test_dereference_alias_shadow 
p order by q.v desc limit 1
+            ) x
+            """
+
+    // a lambda body resolves names the same way as the clause around it
+    qt_alias_shadow_lambda_order_by """
+            select id from test_dereference_alias_shadow q
+            order by array_sum(array_map(x -> x + q.v, [1]))
+            """
+
+    qt_alias_shadow_lambda_having """
+            select q.v as q from test_dereference_alias_shadow q
+            having array_sum(array_map(x -> x + q.v, [1])) > 16
+            order by array_sum(array_map(x -> x + q.v, [1]))
+            """
+
+    // a lambda body in a join condition references columns of both sides of 
the join
+    qt_alias_shadow_lambda_join_on """
+            select q.id, p.id
+            from test_dereference_alias_shadow q join 
test_dereference_alias_shadow p
+            on array_sum(array_map(x -> x + p.v + q.v, [0])) > 40
+            order by q.id, p.id
+            """
+
+    // a lambda body in a correlated subquery references a column of the outer 
query
+    qt_alias_shadow_lambda_correlated_exists """
+            select o.id from test_dereference_alias_shadow o
+            where exists (
+                select 1 from test_dereference_alias_shadow q
+                where array_sum(array_map(x -> x + o.v + q.v, [0])) > 45
+            )
+            order by o.id
+            """
+
+    qt_alias_shadow_lambda_correlated_in """
+            select o.id from test_dereference_alias_shadow o
+            where o.id in (
+                select q.id from test_dereference_alias_shadow q
+                where array_sum(array_map(x -> x + o.v, [0])) > 15
+            )
+            order by o.id
+            """
+
+    // q is only a scalar output alias here, the relation is p
+    test {
+        sql "select p.v as q from test_dereference_alias_shadow p order by q.v"
+        exception "No such field 'v' in 'q'"
+    }
+
+    // q.v is the scalar column v of relation q, so q.v.b is not the path v.b 
of the struct alias q
+    test {
+        sql """
+            select named_struct('v', named_struct('b', 1)) as q
+            from test_dereference_alias_shadow q order by q.v.b
+            """
+        exception "No such field 'b' in 'v'"
+    }
+
+    // an unknown name in a lambda body reports the error of the clause around 
it
+    test {
+        sql """
+            select id from test_dereference_alias_shadow
+            order by array_sum(array_map(x -> x + unknown_column, [1]))
+            """
+        exception "Unknown column 'unknown_column'"
+    }
+
     test {
         sql """
             select t1.id


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to