CalvinKirs opened a new pull request, #68260:
URL: https://github.com/apache/doris/pull/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)
   ```
   
   ## The fix
   
   Both changes are in `ExpressionAnalyzer`, 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.
   
   ## 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 GROUP BY, 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.
   - 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 (HAVING reports it as ambiguous, as it 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 in `TestDereference` (analysis), `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'` | ✓ | |
   | 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 | ✓ | |
   | Lambda in a correlated subquery referencing an outer column (correlated 
slot registered) | ✓ | |
   | 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 new unit tests fail without the fix (`No such field 'v' in 'q'`, or an 
`element_at` binding for the struct alias) and pass with it. All FE unit tests 
under `nereids/rules/analysis` plus the lambda unit tests pass (39 classes, 383 
tests).
   - 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 whole `test_dereference` suite has not run end to 
end locally, because the existing `parse_to_variant` cases need a master BE, so 
CI is its first full run.
   
   Not covered, left for a follow-up:
   
   - Execution (not just analysis) of a lambda in a correlated subquery that 
references an outer column, for EXISTS / IN / scalar subqueries. This used to 
be an analysis error, so there is no previously-correct result to regress.
   - 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.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to