grammar: admit WITH inside subqueries / CTE bodies (ADR-0032 §10.3)

ADR-0032 §10.3 says cte_bindings lives on the scope frame, with
inner subqueries free to declare their own CTEs that shadow outer
ones. The grammar didn't actually admit nested WITH inside
SQL_SELECT_COMPOUND — a real ADR-vs-implementation gap.

Closes the gap by making SQL_SELECT_COMPOUND a Choice between a
WITH-prefixed form and a plain form. The naive Optional-prefix
approach silently broke the paren-vs-subquery dispatch in
sql_expr.rs's PAREN_GROUP: Optional matches 0 bytes, committing
the Seq, so SELECT_CORE's NoMatch on `(a + b)` became Failed and
the Choice couldn't fall through to or_expr. The Choice-fronted
form keeps the fast NoMatch on non-WITH non-SELECT first tokens.

Side effect: scalar subquery / IN / EXISTS / derived-table
bodies now admit a leading WITH too, which matches standard SQL.

Updated two tests that were guarding the old `(WITH …)` rejection
behavior. Added one new harvest test exercising nested-WITH inside
a CTE body — the harvest's `expand_binding` mechanism already
handled the data correctly; the grammar gap was the sole blocker.

Test totals: 1414 → 1415 passing (+1 nested-with-in-cte test).
Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-20 20:34:42 +00:00
parent dd37a1cbfc
commit fd259048da
3 changed files with 78 additions and 17 deletions
+30
View File
@@ -2151,6 +2151,36 @@ mod tests {
assert_eq!(ctes[0].columns[0].name.as_deref(), Some("id"));
}
#[test]
fn cte_harvest_nested_with_in_cte_body() {
// Nested WITH inside a CTE body now parses (ADR-0032
// §10.3 — inner subqueries may declare their own CTEs).
// The outer CTE's body has its own scope and its own
// CTE inside it. The outer's `*` projects from its
// body's FROM, which references the inner CTE; the
// inner CTE's columns flow through `expand_binding`.
let schema = schema_users();
let ctes = cte_bindings_after_walk_with_schema(
"with outer_cte as (with inner_cte as (select id, name from users) select * from inner_cte) select * from outer_cte",
&schema,
);
let outer = ctes
.iter()
.find(|c| c.name == "outer_cte")
.expect("outer_cte binding");
assert_eq!(outer.columns.len(), 2);
assert_eq!(outer.columns[0].name.as_deref(), Some("id"));
assert_eq!(
outer.columns[0].type_,
Some(crate::dsl::types::Type::Int),
);
assert_eq!(outer.columns[1].name.as_deref(), Some("name"));
assert_eq!(
outer.columns[1].type_,
Some(crate::dsl::types::Type::Text),
);
}
#[test]
fn cte_harvest_sibling_b_sees_a_columns() {
// Sibling CTEs at the same level. When `b`'s body