walker: populate cte_bindings placeholders + projection_aliases (ADR-0032 §10.3 stage 1 / §10.4)

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).
This commit is contained in:
claude@clouddev1
2026-05-20 15:29:08 +00:00
parent b522d09f5a
commit 4ff054ca75
9 changed files with 247 additions and 4 deletions
+6
View File
@@ -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);
+14
View File
@@ -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 <sql_expr>`.
+38
View File
@@ -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,
+2
View File
@@ -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`
+13
View File
@@ -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
+6
View File
@@ -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 (`<Table>.<Column>`) --------------
@@ -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);
+4
View File
@@ -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('.'),
+17 -4
View File
@@ -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] = &[