Skip to content

Commit e8554d2

Browse files
committed
fix: keep LATERAL ON clauses on alias output names
requalify_filter mapped both the pulled-up correlation predicate and the user's ON clause through the pre-alias schema. ON is already bound to the alias output. When an inner column shares that qualifier, the map sends ON s.id to a later id:1. Pull-up only appends columns, and those are the only names unique_field_aliases suffixes, so the planner's ON names stay valid. EXISTS, IN, and scalar decorrelation only positionally rewrite correlation columns, not a user predicate that already uses output names. Assisted by Cursor (Grok).
1 parent e8be006 commit e8554d2

3 files changed

Lines changed: 125 additions & 8 deletions

File tree

‎datafusion/optimizer/src/decorrelate_lateral_join.rs‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,25 +135,27 @@ fn rewrite_internal(join: Join) -> Result<Transformed<LogicalPlan>> {
135135
.cloned();
136136

137137
// Re-wrap in SubqueryAlias if the original had one, preserving the alias name.
138-
// The SubqueryAlias re-qualifies all columns with the alias, so we must also
139-
// rewrite column references in both the correlation and ON-clause filters.
138+
// The correlation filter still names the inner tables. `try_new` may suffix
139+
// a duplicate, so those columns are mapped by position.
140+
//
141+
// The user's ON clause already names the alias output. Pull-up only appends
142+
// columns, and `unique_field_aliases` only suffixes those new columns, so
143+
// the names ON uses do not change. Mapping ON through the pre-alias schema
144+
// is wrong when an inner column shares the alias qualifier (`l AS s` inside
145+
// `LATERAL (...) s`): `s.id` would move from the first output `id` to a
146+
// later `id:1`.
140147
let (right_plan, correlation_filter, original_join_filter) =
141148
if let Some(ref alias) = alias {
142149
let input_schema = Arc::clone(rewritten_subquery.schema());
143150
let right = LogicalPlan::SubqueryAlias(SubqueryAlias::try_new(
144151
Arc::new(rewritten_subquery),
145152
alias.clone(),
146153
)?);
147-
// `try_new` may suffix a duplicate (`id`, then `id:1`). Map from
148-
// the schema it consumed to the schema it produced.
149154
let output_schema = Arc::clone(right.schema());
150155
let corr = correlation_filter
151156
.map(|f| requalify_filter(f, &input_schema, &output_schema))
152157
.transpose()?;
153-
let on = original_join_filter
154-
.map(|f| requalify_filter(f, &input_schema, &output_schema))
155-
.transpose()?;
156-
(right, corr, on)
158+
(right, corr, original_join_filter)
157159
} else {
158160
(rewritten_subquery, correlation_filter, original_join_filter)
159161
};

‎datafusion/sqllogictest/test_files/lateral_join.slt‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,57 @@ DROP TABLE l;
10931093
statement ok
10941094
DROP TABLE m;
10951095

1096+
# The user's ON clause names the alias output (`s.id` is decoy.id, the
1097+
# first id). The inner table is also `s`, so a positional map of that
1098+
# ON predicate follows the later field and filters `l` instead.
1099+
statement ok
1100+
CREATE TABLE o(k INT) AS VALUES (1), (100);
1101+
1102+
statement ok
1103+
CREATE TABLE l(id INT, v INT) AS VALUES (1, 10), (100, 99);
1104+
1105+
statement ok
1106+
CREATE TABLE decoy(id INT) AS VALUES (100);
1107+
1108+
query IIII
1109+
SELECT o.k, s.id, s."id:1", s.v
1110+
FROM o
1111+
JOIN LATERAL (
1112+
SELECT decoy.id, s.id, s.v
1113+
FROM l AS s, decoy
1114+
WHERE s.id = o.k
1115+
) s
1116+
ON s.id = 100
1117+
ORDER BY o.k, s.v;
1118+
----
1119+
1 100 1 10
1120+
100 100 100 99
1121+
1122+
# `s.id` in ON is decoy.id, which is 100, so both correlated rows pass.
1123+
# Remapping that ON onto l.id turns the k=1 row into NULLs.
1124+
query IIII
1125+
SELECT o.k, s.id, s."id:1", s.v
1126+
FROM o
1127+
LEFT JOIN LATERAL (
1128+
SELECT decoy.id, s.id, s.v
1129+
FROM l AS s, decoy
1130+
WHERE s.id = o.k
1131+
) s
1132+
ON s.id = 100
1133+
ORDER BY o.k, s.v;
1134+
----
1135+
1 100 1 10
1136+
100 100 100 99
1137+
1138+
statement ok
1139+
DROP TABLE o;
1140+
1141+
statement ok
1142+
DROP TABLE l;
1143+
1144+
statement ok
1145+
DROP TABLE decoy;
1146+
10961147
###########################################################
10971148
# Cleanup
10981149
###########################################################

‎datafusion/sqllogictest/test_files/subquery.slt‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3407,3 +3407,67 @@ DROP TABLE l;
34073407

34083408
statement ok
34093409
DROP TABLE decoy;
3410+
3411+
# The IN value is decoy_hit.id (1). The correlation is l_miss.id (99),
3412+
# which matches none of 1, 2, 3, so IN is empty and NOT IN / NOT EXISTS
3413+
# keep every outer row.
3414+
statement ok
3415+
CREATE TABLE o(k INT) AS VALUES (1), (2), (3);
3416+
3417+
statement ok
3418+
CREATE TABLE l_miss(id INT) AS VALUES (99);
3419+
3420+
statement ok
3421+
CREATE TABLE decoy_hit(id INT) AS VALUES (1);
3422+
3423+
query I
3424+
SELECT o.k FROM o WHERE o.k IN (
3425+
SELECT decoy_hit.id FROM l_miss, decoy_hit WHERE l_miss.id = o.k
3426+
) ORDER BY o.k;
3427+
----
3428+
3429+
query I
3430+
SELECT o.k FROM o WHERE o.k NOT IN (
3431+
SELECT decoy_hit.id FROM l_miss, decoy_hit WHERE l_miss.id = o.k
3432+
) ORDER BY o.k;
3433+
----
3434+
1
3435+
2
3436+
3
3437+
3438+
query I
3439+
SELECT o.k FROM o WHERE NOT EXISTS (
3440+
SELECT decoy_hit.id FROM l_miss, decoy_hit WHERE l_miss.id = o.k
3441+
) ORDER BY o.k;
3442+
----
3443+
1
3444+
2
3445+
3
3446+
3447+
statement ok
3448+
DROP TABLE l_miss;
3449+
3450+
statement ok
3451+
DROP TABLE decoy_hit;
3452+
3453+
statement ok
3454+
CREATE TABLE l(id INT, v INT) AS VALUES (1, 10), (2, 20);
3455+
3456+
statement ok
3457+
CREATE TABLE decoy(id INT) AS VALUES (100);
3458+
3459+
query I
3460+
SELECT o.k FROM o WHERE NOT EXISTS (
3461+
SELECT decoy.id, l.v FROM l, decoy WHERE l.id = o.k
3462+
) ORDER BY o.k;
3463+
----
3464+
3
3465+
3466+
statement ok
3467+
DROP TABLE o;
3468+
3469+
statement ok
3470+
DROP TABLE l;
3471+
3472+
statement ok
3473+
DROP TABLE decoy;

0 commit comments

Comments
 (0)