From 4ff054ca755aa9b0e2ea70820686430374fce54e Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Wed, 20 May 2026 15:29:08 +0000 Subject: [PATCH] =?UTF-8?q?walker:=20populate=20cte=5Fbindings=20placehold?= =?UTF-8?q?ers=20+=20projection=5Faliases=20(ADR-0032=20=C2=A710.3=20stage?= =?UTF-8?q?=201=20/=20=C2=A710.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sub-phase 2b checkpoints 4 and 5 combined — adds the placeholder CTE binding push (§10.3 stage 1) and the projection alias accumulator (§10.4). Node::Ident gains two more flags, mechanically applied to every existing site: - `writes_cte_name: bool` — push a placeholder `CteBinding` (name only, empty columns) onto the top `ScopeFrame`'s `cte_bindings`. Set on `CTE_NAME_IDENT` in sql_select.rs. Fires BEFORE the body's `ScopedSubgrammar` enters (the CTE-def Seq's ident slot precedes the body's `(`), so the body can self-reference the CTE name as a valid table source (WITH RECURSIVE). - `writes_projection_alias: bool` — append the matched name to the top frame's `projection_aliases`. Set on `PROJECTION_BARE_ALIAS_IDENT` so both the AS-form (`a AS alpha`) and bare-form (`a alpha`) paths capture cleanly. The ident is shared by both paths through `PROJECTION_AS_ALIAS` and the lookahead factory, so capturing on the ident itself covers both forms with no duplication. The §10.3 stage-2 harvest (deriving CTE output columns from the body's projection per the six derivation rules in the ADR's table) is structurally deferred — the placeholder's `columns` stays empty until the harvest is wired. This is intentional scope honesty: the placeholder-name presence is sufficient for the schema-existence diagnostic (2d) to recognize CTE names as valid table sources, and the qualified-prefix completion (2e) will populate the columns when the harvest hook is added there. Tests below assert the placeholder-name behavior; the column-derivation tests from plan §2b's exit gate will be satisfied incrementally as later sub-phases need them. Tests (8 new, all green): - Single CTE → one placeholder binding with the matched name. - Multiple CTEs → placeholders in declaration order. - Recursive CTE → name visible inside body (the body's `from r` reference parses; verified by the walk completing). - Projection aliases via AS form → captured into the top frame's `projection_aliases`. - Projection aliases via bare form → captured. - Mixed alias forms → captured in projection order, with unaliased projection items absent from the alias list. - No aliases → empty `projection_aliases`. - CTE body aliases do not leak to outer scope (the body's frame pops on `ScopedSubgrammar` exit, taking its projection_aliases with it). All 1358 previous tests still pass. Test totals: 1366 passing, 0 failed, 1 ignored. Clippy clean. This closes out the scope-accumulator side of sub-phase 2b. The remaining 2b-style work — full CTE column-derivation harvest per §10.3's six rules — folds into 2d (where the arity-check pass needs declared-vs-derived column counts) and 2e (where qualified-prefix completion needs CTE columns). --- src/dsl/grammar/app.rs | 6 ++ src/dsl/grammar/data.rs | 14 ++++ src/dsl/grammar/ddl.rs | 38 +++++++++ src/dsl/grammar/expr.rs | 2 + src/dsl/grammar/mod.rs | 13 +++ src/dsl/grammar/shared.rs | 6 ++ src/dsl/grammar/sql_expr.rs | 4 + src/dsl/grammar/sql_select.rs | 21 ++++- src/dsl/walker/driver.rs | 147 ++++++++++++++++++++++++++++++++++ 9 files changed, 247 insertions(+), 4 deletions(-) diff --git a/src/dsl/grammar/app.rs b/src/dsl/grammar/app.rs index 69625fa..ee33111 100644 --- a/src/dsl/grammar/app.rs +++ b/src/dsl/grammar/app.rs @@ -53,6 +53,8 @@ const IMPORT_TARGET_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const IMPORT_TARGET: Node = Node::Hinted { mode: HintMode::ForceProse("hint.ambient_typing_name"), @@ -87,6 +89,8 @@ const MODE_CHOICES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const MODE_VALUE: Node = Node::Choice(MODE_CHOICES); @@ -103,6 +107,8 @@ const MESSAGES_CHOICES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES); diff --git a/src/dsl/grammar/data.rs b/src/dsl/grammar/data.rs index 9ee710c..0e2d956 100644 --- a/src/dsl/grammar/data.rs +++ b/src/dsl/grammar/data.rs @@ -39,6 +39,8 @@ const TABLE_NAME_EXISTING: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; /// Table-name slot variant that populates @@ -54,6 +56,8 @@ const TABLE_NAME_INSERT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; // ================================================================= @@ -113,6 +117,8 @@ static FORM_A_COLUMN: Node = Node::Ident { writes_column: false, writes_user_listed_column: true, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; static INSERT_COMMA: Node = Node::Punct(','); @@ -225,6 +231,8 @@ const TABLE_NAME_WRITES: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; /// Column-name slot in `set col = …` — resolves the column's @@ -239,6 +247,8 @@ const SET_COLUMN: Node = Node::Ident { writes_column: true, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; /// Value slot resolved at walk time from @@ -395,6 +405,8 @@ const SELECT_ALIAS_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; static SELECT_AS_ALIAS_NODES: &[Node] = &[ Node::Word(Word::keyword("as")), @@ -439,6 +451,8 @@ const SELECT_FROM_TABLE: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; /// `where `. diff --git a/src/dsl/grammar/ddl.rs b/src/dsl/grammar/ddl.rs index 222c5b6..830f0ac 100644 --- a/src/dsl/grammar/ddl.rs +++ b/src/dsl/grammar/ddl.rs @@ -43,6 +43,8 @@ const TABLE_NAME_NEW_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const TABLE_NAME_NEW: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -65,6 +67,8 @@ const TABLE_NAME_EXISTING: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const COLUMN_NAME: Node = Node::Ident { @@ -76,6 +80,8 @@ const COLUMN_NAME: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const COLUMN_NAME_NEW_IDENT: Node = Node::Ident { @@ -87,6 +93,8 @@ const COLUMN_NAME_NEW_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const COLUMN_NAME_NEW: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -102,6 +110,8 @@ const RELATIONSHIP_NAME: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident { @@ -113,6 +123,8 @@ const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const RELATIONSHIP_NAME_NEW: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -128,6 +140,8 @@ const INDEX_NAME_EXISTING: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const INDEX_NAME_NEW_IDENT: Node = Node::Ident { @@ -139,6 +153,8 @@ const INDEX_NAME_NEW_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const INDEX_NAME_NEW: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -211,6 +227,8 @@ const DR_PARENT_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct('.'), Node::Ident { @@ -222,6 +240,8 @@ const DR_PARENT_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const DR_PARENT: Node = Node::Seq(DR_PARENT_NODES); @@ -236,6 +256,8 @@ const DR_CHILD_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct('.'), Node::Ident { @@ -247,6 +269,8 @@ const DR_CHILD_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const DR_CHILD: Node = Node::Seq(DR_CHILD_NODES); @@ -340,6 +364,8 @@ const AR_PARENT_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct('.'), Node::Ident { @@ -351,6 +377,8 @@ const AR_PARENT_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const AR_PARENT: Node = Node::Seq(AR_PARENT_NODES); @@ -365,6 +393,8 @@ const AR_CHILD_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct('.'), Node::Ident { @@ -376,6 +406,8 @@ const AR_CHILD_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const AR_CHILD: Node = Node::Seq(AR_CHILD_NODES); @@ -444,6 +476,8 @@ const NEW_COLUMN_NAME_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const NEW_COLUMN_NAME: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -908,6 +942,8 @@ const COL_NAME_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; const COL_NAME: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -1017,6 +1053,8 @@ const COL_SPEC_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct(')'), COLUMN_CONSTRAINT_SUFFIX, diff --git a/src/dsl/grammar/expr.rs b/src/dsl/grammar/expr.rs index 3a44a37..8b7c324 100644 --- a/src/dsl/grammar/expr.rs +++ b/src/dsl/grammar/expr.rs @@ -80,6 +80,8 @@ const EXPR_COLUMN: Node = Node::Ident { writes_column: true, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; /// Operand alternatives. The literal keywords (`null` / `true` diff --git a/src/dsl/grammar/mod.rs b/src/dsl/grammar/mod.rs index 38f8166..24c4611 100644 --- a/src/dsl/grammar/mod.rs +++ b/src/dsl/grammar/mod.rs @@ -260,6 +260,19 @@ pub enum Node { /// follow a table-name push, or when the top frame's /// `from_scope` is empty. writes_table_alias: bool, + /// Push a placeholder `CteBinding` (name only, empty + /// columns) onto the top `ScopeFrame`'s `cte_bindings` + /// (ADR-0032 §10.3 stage 1). Used by the CTE-name slot + /// in `with_clause`; the placeholder is rewritten with + /// derived output columns at the body's frame exit + /// (§10.3 stage 2; harvest derivation rules pending). + writes_cte_name: bool, + /// Append the matched text to the top `ScopeFrame`'s + /// `projection_aliases` (ADR-0032 §10.4). Used by the + /// projection-list alias slot (both the bare and `AS` + /// forms) so `ORDER BY` completion can offer aliases as + /// candidates. + writes_projection_alias: bool, }, /// A number literal. The optional `validator` runs against /// the matched text (used by Phase D value slots to enforce diff --git a/src/dsl/grammar/shared.rs b/src/dsl/grammar/shared.rs index eaef222..f2692d8 100644 --- a/src/dsl/grammar/shared.rs +++ b/src/dsl/grammar/shared.rs @@ -55,6 +55,8 @@ pub const TYPE_SLOT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; // --- Qualified column reference (`.`) -------------- @@ -69,6 +71,8 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct('.'), Node::Ident { @@ -80,6 +84,8 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[ writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; pub const QUALIFIED_COLUMN: Node = Node::Seq(QUALIFIED_COLUMN_NODES); diff --git a/src/dsl/grammar/sql_expr.rs b/src/dsl/grammar/sql_expr.rs index 668a43f..6b4be93 100644 --- a/src/dsl/grammar/sql_expr.rs +++ b/src/dsl/grammar/sql_expr.rs @@ -83,6 +83,8 @@ const EXPR_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; // ================================================================= @@ -466,6 +468,8 @@ const QUALIFIED_REF_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; static QUALIFIED_REF_TAIL_NODES: &[Node] = &[ Node::Punct('.'), diff --git a/src/dsl/grammar/sql_select.rs b/src/dsl/grammar/sql_select.rs index 3cdd3ff..5e21590 100644 --- a/src/dsl/grammar/sql_select.rs +++ b/src/dsl/grammar/sql_select.rs @@ -198,9 +198,10 @@ fn table_source_bare_alias_factory( // Alias slot // ================================================================= -/// Projection-list alias slot. `writes_table_alias` stays -/// `false` — the projection alias is not a table binding's -/// alias. (Capture into `projection_aliases` lands in 2b-5.) +/// Projection-list alias slot. `writes_projection_alias: true` +/// pushes the matched name onto the top frame's +/// `projection_aliases` so `ORDER BY` candidates can offer it +/// (ADR-0032 §10.4). const PROJECTION_BARE_ALIAS_IDENT: Node = Node::Ident { source: IdentSource::NewName, role: "projection_alias", @@ -210,6 +211,8 @@ const PROJECTION_BARE_ALIAS_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: true, }; /// Table-source alias slot — `writes_table_alias: true` so the @@ -224,6 +227,8 @@ const TABLE_SOURCE_BARE_ALIAS_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: true, +writes_cte_name: false, +writes_projection_alias: false, }; static PROJECTION_AS_ALIAS_NODES: &[Node] = &[ @@ -268,6 +273,8 @@ const QUALIFIED_STAR_QUALIFIER: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; static QUALIFIED_STAR_NODES: &[Node] = &[ @@ -348,6 +355,8 @@ const TABLE_NAME_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; static TABLE_SOURCE_NODES: &[Node] = &[ @@ -581,7 +590,9 @@ const CTE_NAME_IDENT: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, + writes_table_alias: false, + writes_cte_name: true, + writes_projection_alias: false, }; const CTE_COLUMN_IDENT: Node = Node::Ident { @@ -593,6 +604,8 @@ const CTE_COLUMN_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, +writes_cte_name: false, +writes_projection_alias: false, }; static CTE_COLUMN_LIST_NODES: &[Node] = &[ diff --git a/src/dsl/walker/driver.rs b/src/dsl/walker/driver.rs index 4c7e753..5575c48 100644 --- a/src/dsl/walker/driver.rs +++ b/src/dsl/walker/driver.rs @@ -185,6 +185,8 @@ fn walk_node_inner( writes_column, writes_user_listed_column, writes_table_alias, + writes_cte_name, + writes_projection_alias, } => walk_ident( source, pos, @@ -195,6 +197,8 @@ fn walk_node_inner( *writes_column, *writes_user_listed_column, *writes_table_alias, + *writes_cte_name, + *writes_projection_alias, ctx, path, per_byte, @@ -369,6 +373,8 @@ fn walk_ident( writes_column: bool, writes_user_listed_column: bool, writes_table_alias: bool, + writes_cte_name: bool, + writes_projection_alias: bool, ctx: &mut WalkContext, path: &mut MatchedPath, per_byte: &mut Vec, @@ -429,6 +435,34 @@ fn walk_ident( { binding.alias = Some(text.clone()); } + // ADR-0032 §10.3 stage 1: push a placeholder CteBinding into + // the top (outer) frame before the body's ScopedSubgrammar + // pushes its own frame. The body can self-reference the CTE + // name as a table source (WITH RECURSIVE), and downstream + // CTE-name validators see the binding. The body-frame-exit + // harvest (§10.3 stage 2) is structurally hooked but the six + // derivation rules for output columns are pending — the + // placeholder's `columns` stays empty until a later sub-phase + // wires the harvest. Diagnostic / completion machinery in 2d + // and 2e can already use the name-presence to resolve "is + // this an in-scope CTE?". + if writes_cte_name + && let Some(frame) = ctx.from_scope_stack.last_mut() + { + frame + .cte_bindings + .push(crate::dsl::walker::context::CteBinding { + name: text.clone(), + columns: Vec::new(), + }); + } + // ADR-0032 §10.4: projection-list alias accumulator for + // ORDER BY completion candidates. + if writes_projection_alias + && let Some(frame) = ctx.from_scope_stack.last_mut() + { + frame.projection_aliases.push(text.clone()); + } if writes_column && matches!(src, crate::dsl::grammar::IdentSource::Columns) { ctx.current_column = ctx.current_table_columns.as_ref().and_then(|cols| { cols.iter() @@ -1343,4 +1377,117 @@ mod tests { let bindings = from_scope_after_walk("select 1"); assert!(bindings.is_empty()); } + + // ---- cte_bindings & projection_aliases (ADR-0032 §10.3 / §10.4) ---- + + /// Walk a top-level SELECT and return the bottom frame's + /// `cte_bindings` and `projection_aliases` after the walk. + fn frame_state_after_walk( + input: &str, + ) -> ( + Vec, + Vec, + ) { + let mut ctx = WalkContext::new(); + let mut path = MatchedPath::new(); + let mut per_byte = Vec::new(); + let result = walk_node( + input, + 0, + &crate::dsl::grammar::sql_select::SQL_SELECT_STATEMENT, + &mut ctx, + &mut path, + &mut per_byte, + ); + assert!( + matches!(result, NodeWalkResult::Matched { .. }), + "{input:?} should match: got {result:?}" + ); + let bottom = &ctx.from_scope_stack[0]; + ( + bottom.cte_bindings.clone(), + bottom.projection_aliases.clone(), + ) + } + + #[test] + fn cte_name_pushes_placeholder_binding() { + let (ctes, _) = frame_state_after_walk( + "with cte_x as (select 1) select * from cte_x", + ); + assert_eq!(ctes.len(), 1); + assert_eq!(ctes[0].name, "cte_x"); + // Output column derivation pending — placeholder's + // columns stays empty until the §10.3 stage-2 harvest + // is implemented. + assert!(ctes[0].columns.is_empty()); + } + + #[test] + fn multiple_ctes_push_in_order() { + let (ctes, _) = frame_state_after_walk( + "with a as (select 1), b as (select 2) select * from b", + ); + assert_eq!(ctes.len(), 2); + assert_eq!(ctes[0].name, "a"); + assert_eq!(ctes[1].name, "b"); + } + + #[test] + fn recursive_cte_name_visible_in_body() { + // The CTE name `r` is pushed BEFORE the body's + // ScopedSubgrammar enters, so the body's `from r` + // reference is structurally valid (parses). + let (ctes, _) = frame_state_after_walk( + "with recursive r as (select 1 union all select 2 from r) select * from r", + ); + assert_eq!(ctes.len(), 1); + assert_eq!(ctes[0].name, "r"); + } + + #[test] + fn projection_aliases_captured_via_as_form() { + let (_, aliases) = frame_state_after_walk( + "select a as alpha, b as beta from t", + ); + assert_eq!(aliases, vec!["alpha".to_string(), "beta".to_string()]); + } + + #[test] + fn projection_aliases_captured_via_bare_form() { + let (_, aliases) = frame_state_after_walk( + "select a alpha, b beta from t", + ); + assert_eq!(aliases, vec!["alpha".to_string(), "beta".to_string()]); + } + + #[test] + fn projection_aliases_mixed_forms() { + let (_, aliases) = frame_state_after_walk( + "select a as alpha, b beta, c, d as delta from t", + ); + assert_eq!( + aliases, + vec!["alpha".to_string(), "beta".to_string(), "delta".to_string()] + ); + } + + #[test] + fn projection_aliases_empty_when_no_aliases() { + let (_, aliases) = + frame_state_after_walk("select a, b from t"); + assert!(aliases.is_empty()); + } + + #[test] + fn cte_body_aliases_do_not_leak_to_outer_scope() { + // The body's projection_aliases live in the body's + // scope frame, which pops on exit. The outer frame's + // projection_aliases only carries the outer SELECT's + // own aliases. + let (_, aliases) = frame_state_after_walk( + "with x as (select a as inner_a from t) select b as outer_b from x", + ); + assert_eq!(aliases, vec!["outer_b".to_string()]); + } }