Skip to content

Commit 97f499c

Browse files
committed
ast: Preserve FROM clause ordering for mixed join/comma syntax
Fix table expression ordering when parsing mixed join/comma syntax like `FROM t1 INNER JOIN t2, t3`. Previously, Readyset split table expressions into separate `tables` and `join` fields without preserving left-to-right order, breaking correlated subqueries (e.g., LATERAL) that depend on earlier table expressions. Solution: After encountering the first explicit join, convert subsequent comma-separated tables to CROSS JOIN operators, preserving the left-to-right evaluation order required by SQL semantics. Example that now works: FROM t1 INNER JOIN t2 AS TABLE2, LATERAL (SELECT ... FROM TABLE2) Previously, LATERAL couldn't find TABLE2 because it was processed before the joined table. Now the order is preserved: t1 → t2 → LATERAL. Adds comprehensive logictests in `sqlparser/psql/from_clause_ordering.test` to prevent regression. Release-Note-Core: Correlated subquery resolution in mixed join/comma syntax. AI-Use: level:3 Change-Id: I7cb724b5451c371840647ca9c689883afac76f1b Reviewed-on: https://gerrit.readyset.name/c/readyset/+/11063 Reviewed-by: Vassili Zarouba <vassili@readyset.io> Tested-by: Buildkite CI
1 parent ee6f2d6 commit 97f499c

File tree

2 files changed

+66
-2
lines changed

2 files changed

+66
-2
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Tests for FROM clause ordering with mixed join/comma syntax
2+
# These tests ensure that sqlparser's FROM clause parsing maintains correct
3+
# left-to-right ordering when converting to ReadySet's AST
4+
5+
statement ok
6+
create table t1 (id int, x int)
7+
8+
statement ok
9+
create table t2 (id int, y int)
10+
11+
statement ok
12+
create table t3 (id int, z int)
13+
14+
statement ok
15+
insert into t1 (id, x) values (1, 10), (2, 20)
16+
17+
statement ok
18+
insert into t2 (id, y) values (1, 100), (2, 200)
19+
20+
statement ok
21+
insert into t3 (id, z) values (1, 1000), (2, 2000)
22+
23+
# Test the original bug: mixed join/comma syntax with LATERAL correlated subquery
24+
# This ensures that table ordering is preserved so LATERAL can reference earlier tables
25+
query I rowsort
26+
SELECT count(*)
27+
FROM t1 AS T1
28+
INNER JOIN t2 AS TABLE2 ON T1.id = TABLE2.id,
29+
LATERAL (SELECT T3.z FROM t3 T3 WHERE T3.id = TABLE2.id LIMIT 1) TT
30+
WHERE T1.id = 1;
31+
----
32+
1
33+
34+
# Test that comma-after-join preserves correct ordering (non-LATERAL case)
35+
query III rowsort
36+
SELECT t1.id, t2.y, t3.z
37+
FROM t1
38+
INNER JOIN t2 ON t1.id = t2.id,
39+
t3
40+
WHERE t1.id = t3.id AND t1.id = 1;
41+
----
42+
1
43+
100
44+
1000
45+

readyset-sql/src/ast/select.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,27 @@ impl TryFromDialect<sqlparser::ast::Select> for SelectStatement {
509509
let (new_tables, new_joins) = from_clause
510510
.into_tables_and_joins()
511511
.map_err(|e| failed_err!("couldn't convert FROM clause: {e}"))?;
512-
tables.extend(new_tables);
513-
join.extend(new_joins);
512+
513+
// After we've encountered the first explicit join, all subsequent table expressions
514+
// should be added as CROSS JOINs to preserve left-to-right ordering.
515+
// This is important for correlated subqueries (e.g., LATERAL) that depend on
516+
// earlier table expressions.
517+
if join.is_empty() {
518+
// No joins yet, add tables normally
519+
tables.extend(new_tables);
520+
join.extend(new_joins);
521+
} else {
522+
// We've already seen a join, so convert remaining tables to CROSS JOINs
523+
debug_assert!(!tables.is_empty());
524+
for table in new_tables {
525+
join.push(JoinClause {
526+
operator: JoinOperator::CrossJoin,
527+
right: JoinRightSide::Table(table),
528+
constraint: JoinConstraint::Empty,
529+
});
530+
}
531+
join.extend(new_joins);
532+
}
514533
}
515534
Ok(SelectStatement {
516535
distinct: matches!(value.distinct, Some(sqlparser::ast::Distinct::Distinct)),

0 commit comments

Comments
 (0)