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]