walker: populate from_scope table bindings (ADR-0032 §10.1)

Sub-phase 2b checkpoint 3 — the `writes_table` / `writes_table_alias`
flags now drive the multi-binding `from_scope` accumulator on
the top `ScopeFrame`.

Node::Ident gains `writes_table_alias: bool`. When set on an
ident-name slot, the matched name lands on the most-recently-
pushed `TableBinding`'s `alias`. All 46 existing Ident sites
across the codebase are updated to `writes_table_alias: false`
(mechanical — no behavioral change for DSL paths).

walk_ident's `writes_table` semantics extend:

- `IdentSource::Tables` matches with `writes_table: true` still
  populate `current_table` / `current_table_columns` as before
  (preserved for DSL paths that read those fields directly via
  the dynamic-subgrammar / column-writes machinery), AND now
  also push a fresh `TableBinding` onto the top ScopeFrame's
  `from_scope`. The two mechanisms coexist additively —
  current_table reflects the most-recent `writes_table` write
  (single-binding view, as before); from_scope is the
  authoritative multi-binding accumulator that SQL JOINs,
  subqueries, and CTE bodies use.

sql_select.rs splits the alias slot into two ident variants:

- `PROJECTION_BARE_ALIAS_IDENT` (role `projection_alias`) —
  no scope writes; capture into `projection_aliases` is 2b-5.
- `TABLE_SOURCE_BARE_ALIAS_IDENT` (role `table_alias`,
  `writes_table_alias: true`) — sets the top binding's alias.

The `AS alias` form likewise splits into PROJECTION_AS_ALIAS
and TABLE_SOURCE_AS_ALIAS so each path threads through the
correct ident. The bare-alias lookahead factories return the
projection or table-source ident accordingly.

`TABLE_NAME_IDENT` in sql_select.rs gets `writes_table: true`
so each FROM / JOIN table source pushes a binding. The
schema-resolved columns are stored on the TableBinding for
later use by qualified-prefix completion (2e) and the
schema-existence diagnostic (2d).

Tests (9 new, all green):

- single from-table → one binding
- AS alias / bare alias on from-table → alias captured
- two-way JOIN → two bindings, correct order
- two-way JOIN with both aliased → two bindings with aliases
- three-way JOIN (left + bare) → three bindings in order
- subquery from_scope does not leak to outer scope (the
  ScopedSubgrammar push/pop discipline at work)
- CTE body from_scope does not leak to outer scope (the outer
  scope sees only the CTE-name reference, not the body's
  internals)
- SELECT without FROM → empty from_scope

All 1351 previous tests still pass — DSL paths untouched.
Test totals: 1358 passing, 0 failed, 1 ignored. Clippy clean.

Frame is_cte_body marker, body-projection harvest, and
projection_aliases population are the remaining 2b work
(2b-4 and 2b-5).
This commit is contained in:
claude@clouddev1
2026-05-20 15:25:10 +00:00
parent 98a74b23d3
commit b522d09f5a
9 changed files with 240 additions and 19 deletions
+38 -10
View File
@@ -173,7 +173,7 @@ fn projection_bare_alias_factory(
{
Node::Subgrammar(&EMPTY_NOMATCH)
}
Some(_) => BARE_ALIAS_IDENT,
Some(_) => PROJECTION_BARE_ALIAS_IDENT,
None => Node::Subgrammar(&EMPTY_NOMATCH),
}
}
@@ -189,7 +189,7 @@ fn table_source_bare_alias_factory(
{
Node::Subgrammar(&EMPTY_NOMATCH)
}
Some(_) => BARE_ALIAS_IDENT,
Some(_) => TABLE_SOURCE_BARE_ALIAS_IDENT,
None => Node::Subgrammar(&EMPTY_NOMATCH),
}
}
@@ -198,24 +198,48 @@ fn table_source_bare_alias_factory(
// Alias slot
// =================================================================
const BARE_ALIAS_IDENT: Node = Node::Ident {
/// 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.)
const PROJECTION_BARE_ALIAS_IDENT: Node = Node::Ident {
source: IdentSource::NewName,
role: "select_alias",
role: "projection_alias",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
writes_table_alias: false,
};
static AS_ALIAS_NODES: &[Node] = &[
/// Table-source alias slot — `writes_table_alias: true` so the
/// matched name lands on the most-recently-pushed
/// `TableBinding`'s `alias` (ADR-0032 §10.1).
const TABLE_SOURCE_BARE_ALIAS_IDENT: Node = Node::Ident {
source: IdentSource::NewName,
role: "table_alias",
validator: None,
highlight_override: None,
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
writes_table_alias: true,
};
static PROJECTION_AS_ALIAS_NODES: &[Node] = &[
Node::Word(Word::keyword("as")),
BARE_ALIAS_IDENT,
PROJECTION_BARE_ALIAS_IDENT,
];
static AS_ALIAS_EXPLICIT: Node = Node::Seq(AS_ALIAS_NODES);
static PROJECTION_AS_ALIAS: Node = Node::Seq(PROJECTION_AS_ALIAS_NODES);
static TABLE_SOURCE_AS_ALIAS_NODES: &[Node] = &[
Node::Word(Word::keyword("as")),
TABLE_SOURCE_BARE_ALIAS_IDENT,
];
static TABLE_SOURCE_AS_ALIAS: Node = Node::Seq(TABLE_SOURCE_AS_ALIAS_NODES);
static PROJECTION_ALIAS_CHOICES: &[Node] = &[
Node::Subgrammar(&AS_ALIAS_EXPLICIT),
Node::Subgrammar(&PROJECTION_AS_ALIAS),
Node::Lookahead(projection_bare_alias_factory),
];
static PROJECTION_ALIAS_CHOICE: Node = Node::Choice(PROJECTION_ALIAS_CHOICES);
@@ -223,7 +247,7 @@ static PROJECTION_ALIAS_OPTIONAL: Node =
Node::Optional(&PROJECTION_ALIAS_CHOICE);
static TABLE_SOURCE_ALIAS_CHOICES: &[Node] = &[
Node::Subgrammar(&AS_ALIAS_EXPLICIT),
Node::Subgrammar(&TABLE_SOURCE_AS_ALIAS),
Node::Lookahead(table_source_bare_alias_factory),
];
static TABLE_SOURCE_ALIAS_CHOICE: Node =
@@ -243,6 +267,7 @@ const QUALIFIED_STAR_QUALIFIER: Node = Node::Ident {
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
writes_table_alias: false,
};
static QUALIFIED_STAR_NODES: &[Node] = &[
@@ -319,9 +344,10 @@ const TABLE_NAME_IDENT: Node = Node::Ident {
role: "table_name",
validator: Some(reject_internal_table),
highlight_override: None,
writes_table: false,
writes_table: true,
writes_column: false,
writes_user_listed_column: false,
writes_table_alias: false,
};
static TABLE_SOURCE_NODES: &[Node] = &[
@@ -555,6 +581,7 @@ const CTE_NAME_IDENT: Node = Node::Ident {
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
writes_table_alias: false,
};
const CTE_COLUMN_IDENT: Node = Node::Ident {
@@ -565,6 +592,7 @@ const CTE_COLUMN_IDENT: Node = Node::Ident {
writes_table: false,
writes_column: false,
writes_user_listed_column: false,
writes_table_alias: false,
};
static CTE_COLUMN_LIST_NODES: &[Node] = &[