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:
@@ -52,6 +52,7 @@ const IMPORT_TARGET_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
const IMPORT_TARGET: Node = Node::Hinted {
|
const IMPORT_TARGET: Node = Node::Hinted {
|
||||||
mode: HintMode::ForceProse("hint.ambient_typing_name"),
|
mode: HintMode::ForceProse("hint.ambient_typing_name"),
|
||||||
@@ -85,6 +86,7 @@ const MODE_CHOICES: &[Node] = &[
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const MODE_VALUE: Node = Node::Choice(MODE_CHOICES);
|
const MODE_VALUE: Node = Node::Choice(MODE_CHOICES);
|
||||||
@@ -100,6 +102,7 @@ const MESSAGES_CHOICES: &[Node] = &[
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES);
|
const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const TABLE_NAME_EXISTING: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Table-name slot variant that populates
|
/// Table-name slot variant that populates
|
||||||
@@ -52,6 +53,7 @@ const TABLE_NAME_INSERT: Node = Node::Ident {
|
|||||||
writes_table: true,
|
writes_table: true,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -110,6 +112,7 @@ static FORM_A_COLUMN: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: true,
|
writes_user_listed_column: true,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
static INSERT_COMMA: Node = Node::Punct(',');
|
static INSERT_COMMA: Node = Node::Punct(',');
|
||||||
|
|
||||||
@@ -221,6 +224,7 @@ const TABLE_NAME_WRITES: Node = Node::Ident {
|
|||||||
writes_table: true,
|
writes_table: true,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Column-name slot in `set col = …` — resolves the column's
|
/// Column-name slot in `set col = …` — resolves the column's
|
||||||
@@ -234,6 +238,7 @@ const SET_COLUMN: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: true,
|
writes_column: true,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Value slot resolved at walk time from
|
/// Value slot resolved at walk time from
|
||||||
@@ -389,6 +394,7 @@ const SELECT_ALIAS_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
static SELECT_AS_ALIAS_NODES: &[Node] = &[
|
static SELECT_AS_ALIAS_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("as")),
|
Node::Word(Word::keyword("as")),
|
||||||
@@ -432,6 +438,7 @@ const SELECT_FROM_TABLE: Node = Node::Ident {
|
|||||||
writes_table: true,
|
writes_table: true,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// `where <sql_expr>`.
|
/// `where <sql_expr>`.
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const TABLE_NAME_NEW_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
const TABLE_NAME_NEW: Node = Node::Hinted {
|
const TABLE_NAME_NEW: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -63,6 +64,7 @@ const TABLE_NAME_EXISTING: Node = Node::Ident {
|
|||||||
writes_table: true,
|
writes_table: true,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMN_NAME: Node = Node::Ident {
|
const COLUMN_NAME: Node = Node::Ident {
|
||||||
@@ -73,6 +75,7 @@ const COLUMN_NAME: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
|
const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
|
||||||
@@ -83,6 +86,7 @@ const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
const COLUMN_NAME_NEW: Node = Node::Hinted {
|
const COLUMN_NAME_NEW: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -97,6 +101,7 @@ const RELATIONSHIP_NAME: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
|
const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
|
||||||
@@ -107,6 +112,7 @@ const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
const RELATIONSHIP_NAME_NEW: Node = Node::Hinted {
|
const RELATIONSHIP_NAME_NEW: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -121,6 +127,7 @@ const INDEX_NAME_EXISTING: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const INDEX_NAME_NEW_IDENT: Node = Node::Ident {
|
const INDEX_NAME_NEW_IDENT: Node = Node::Ident {
|
||||||
@@ -131,6 +138,7 @@ const INDEX_NAME_NEW_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
const INDEX_NAME_NEW: Node = Node::Hinted {
|
const INDEX_NAME_NEW: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -202,6 +210,7 @@ const DR_PARENT_NODES: &[Node] = &[
|
|||||||
writes_table: true,
|
writes_table: true,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -212,6 +221,7 @@ const DR_PARENT_NODES: &[Node] = &[
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const DR_PARENT: Node = Node::Seq(DR_PARENT_NODES);
|
const DR_PARENT: Node = Node::Seq(DR_PARENT_NODES);
|
||||||
@@ -225,6 +235,7 @@ const DR_CHILD_NODES: &[Node] = &[
|
|||||||
writes_table: true,
|
writes_table: true,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -235,6 +246,7 @@ const DR_CHILD_NODES: &[Node] = &[
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const DR_CHILD: Node = Node::Seq(DR_CHILD_NODES);
|
const DR_CHILD: Node = Node::Seq(DR_CHILD_NODES);
|
||||||
@@ -327,6 +339,7 @@ const AR_PARENT_NODES: &[Node] = &[
|
|||||||
writes_table: true,
|
writes_table: true,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -337,6 +350,7 @@ const AR_PARENT_NODES: &[Node] = &[
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const AR_PARENT: Node = Node::Seq(AR_PARENT_NODES);
|
const AR_PARENT: Node = Node::Seq(AR_PARENT_NODES);
|
||||||
@@ -350,6 +364,7 @@ const AR_CHILD_NODES: &[Node] = &[
|
|||||||
writes_table: true,
|
writes_table: true,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -360,6 +375,7 @@ const AR_CHILD_NODES: &[Node] = &[
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const AR_CHILD: Node = Node::Seq(AR_CHILD_NODES);
|
const AR_CHILD: Node = Node::Seq(AR_CHILD_NODES);
|
||||||
@@ -427,6 +443,7 @@ const NEW_COLUMN_NAME_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
const NEW_COLUMN_NAME: Node = Node::Hinted {
|
const NEW_COLUMN_NAME: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -890,6 +907,7 @@ const COL_NAME_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
const COL_NAME: Node = Node::Hinted {
|
const COL_NAME: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -998,6 +1016,7 @@ const COL_SPEC_NODES: &[Node] = &[
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct(')'),
|
Node::Punct(')'),
|
||||||
COLUMN_CONSTRAINT_SUFFIX,
|
COLUMN_CONSTRAINT_SUFFIX,
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ const EXPR_COLUMN: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: true,
|
writes_column: true,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Operand alternatives. The literal keywords (`null` / `true`
|
/// Operand alternatives. The literal keywords (`null` / `true`
|
||||||
|
|||||||
@@ -251,6 +251,15 @@ pub enum Node {
|
|||||||
/// user's explicit selection instead of the
|
/// user's explicit selection instead of the
|
||||||
/// auto-filtered schema default.
|
/// auto-filtered schema default.
|
||||||
writes_user_listed_column: bool,
|
writes_user_listed_column: bool,
|
||||||
|
/// Set the matched text as the alias of the most-
|
||||||
|
/// recently-pushed `TableBinding` on the top
|
||||||
|
/// `ScopeFrame`'s `from_scope` (ADR-0032 §10.1). Used by
|
||||||
|
/// the `[ AS ] alias` slot on `from_clause` /
|
||||||
|
/// `join_clause` table sources in `sql_select.rs`; a
|
||||||
|
/// no-op on `IdentSource::NewName` slots that do not
|
||||||
|
/// follow a table-name push, or when the top frame's
|
||||||
|
/// `from_scope` is empty.
|
||||||
|
writes_table_alias: bool,
|
||||||
},
|
},
|
||||||
/// A number literal. The optional `validator` runs against
|
/// A number literal. The optional `validator` runs against
|
||||||
/// the matched text (used by Phase D value slots to enforce
|
/// the matched text (used by Phase D value slots to enforce
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ pub const TYPE_SLOT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Qualified column reference (`<Table>.<Column>`) --------------
|
// --- Qualified column reference (`<Table>.<Column>`) --------------
|
||||||
@@ -67,6 +68,7 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -77,6 +79,7 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
pub const QUALIFIED_COLUMN: Node = Node::Seq(QUALIFIED_COLUMN_NODES);
|
pub const QUALIFIED_COLUMN: Node = Node::Seq(QUALIFIED_COLUMN_NODES);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const EXPR_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -464,6 +465,7 @@ const QUALIFIED_REF_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
static QUALIFIED_REF_TAIL_NODES: &[Node] = &[
|
static QUALIFIED_REF_TAIL_NODES: &[Node] = &[
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ fn projection_bare_alias_factory(
|
|||||||
{
|
{
|
||||||
Node::Subgrammar(&EMPTY_NOMATCH)
|
Node::Subgrammar(&EMPTY_NOMATCH)
|
||||||
}
|
}
|
||||||
Some(_) => BARE_ALIAS_IDENT,
|
Some(_) => PROJECTION_BARE_ALIAS_IDENT,
|
||||||
None => Node::Subgrammar(&EMPTY_NOMATCH),
|
None => Node::Subgrammar(&EMPTY_NOMATCH),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ fn table_source_bare_alias_factory(
|
|||||||
{
|
{
|
||||||
Node::Subgrammar(&EMPTY_NOMATCH)
|
Node::Subgrammar(&EMPTY_NOMATCH)
|
||||||
}
|
}
|
||||||
Some(_) => BARE_ALIAS_IDENT,
|
Some(_) => TABLE_SOURCE_BARE_ALIAS_IDENT,
|
||||||
None => Node::Subgrammar(&EMPTY_NOMATCH),
|
None => Node::Subgrammar(&EMPTY_NOMATCH),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,24 +198,48 @@ fn table_source_bare_alias_factory(
|
|||||||
// Alias slot
|
// 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,
|
source: IdentSource::NewName,
|
||||||
role: "select_alias",
|
role: "projection_alias",
|
||||||
validator: None,
|
validator: None,
|
||||||
highlight_override: None,
|
highlight_override: None,
|
||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_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")),
|
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] = &[
|
static PROJECTION_ALIAS_CHOICES: &[Node] = &[
|
||||||
Node::Subgrammar(&AS_ALIAS_EXPLICIT),
|
Node::Subgrammar(&PROJECTION_AS_ALIAS),
|
||||||
Node::Lookahead(projection_bare_alias_factory),
|
Node::Lookahead(projection_bare_alias_factory),
|
||||||
];
|
];
|
||||||
static PROJECTION_ALIAS_CHOICE: Node = Node::Choice(PROJECTION_ALIAS_CHOICES);
|
static PROJECTION_ALIAS_CHOICE: Node = Node::Choice(PROJECTION_ALIAS_CHOICES);
|
||||||
@@ -223,7 +247,7 @@ static PROJECTION_ALIAS_OPTIONAL: Node =
|
|||||||
Node::Optional(&PROJECTION_ALIAS_CHOICE);
|
Node::Optional(&PROJECTION_ALIAS_CHOICE);
|
||||||
|
|
||||||
static TABLE_SOURCE_ALIAS_CHOICES: &[Node] = &[
|
static TABLE_SOURCE_ALIAS_CHOICES: &[Node] = &[
|
||||||
Node::Subgrammar(&AS_ALIAS_EXPLICIT),
|
Node::Subgrammar(&TABLE_SOURCE_AS_ALIAS),
|
||||||
Node::Lookahead(table_source_bare_alias_factory),
|
Node::Lookahead(table_source_bare_alias_factory),
|
||||||
];
|
];
|
||||||
static TABLE_SOURCE_ALIAS_CHOICE: Node =
|
static TABLE_SOURCE_ALIAS_CHOICE: Node =
|
||||||
@@ -243,6 +267,7 @@ const QUALIFIED_STAR_QUALIFIER: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
static QUALIFIED_STAR_NODES: &[Node] = &[
|
static QUALIFIED_STAR_NODES: &[Node] = &[
|
||||||
@@ -319,9 +344,10 @@ const TABLE_NAME_IDENT: Node = Node::Ident {
|
|||||||
role: "table_name",
|
role: "table_name",
|
||||||
validator: Some(reject_internal_table),
|
validator: Some(reject_internal_table),
|
||||||
highlight_override: None,
|
highlight_override: None,
|
||||||
writes_table: false,
|
writes_table: true,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
static TABLE_SOURCE_NODES: &[Node] = &[
|
static TABLE_SOURCE_NODES: &[Node] = &[
|
||||||
@@ -555,6 +581,7 @@ const CTE_NAME_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CTE_COLUMN_IDENT: Node = Node::Ident {
|
const CTE_COLUMN_IDENT: Node = Node::Ident {
|
||||||
@@ -565,6 +592,7 @@ const CTE_COLUMN_IDENT: Node = Node::Ident {
|
|||||||
writes_table: false,
|
writes_table: false,
|
||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
|
writes_table_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
static CTE_COLUMN_LIST_NODES: &[Node] = &[
|
static CTE_COLUMN_LIST_NODES: &[Node] = &[
|
||||||
|
|||||||
+158
-9
@@ -184,6 +184,7 @@ fn walk_node_inner(
|
|||||||
writes_table,
|
writes_table,
|
||||||
writes_column,
|
writes_column,
|
||||||
writes_user_listed_column,
|
writes_user_listed_column,
|
||||||
|
writes_table_alias,
|
||||||
} => walk_ident(
|
} => walk_ident(
|
||||||
source,
|
source,
|
||||||
pos,
|
pos,
|
||||||
@@ -193,6 +194,7 @@ fn walk_node_inner(
|
|||||||
*writes_table,
|
*writes_table,
|
||||||
*writes_column,
|
*writes_column,
|
||||||
*writes_user_listed_column,
|
*writes_user_listed_column,
|
||||||
|
*writes_table_alias,
|
||||||
ctx,
|
ctx,
|
||||||
path,
|
path,
|
||||||
per_byte,
|
per_byte,
|
||||||
@@ -366,6 +368,7 @@ fn walk_ident(
|
|||||||
writes_table: bool,
|
writes_table: bool,
|
||||||
writes_column: bool,
|
writes_column: bool,
|
||||||
writes_user_listed_column: bool,
|
writes_user_listed_column: bool,
|
||||||
|
writes_table_alias: bool,
|
||||||
ctx: &mut WalkContext,
|
ctx: &mut WalkContext,
|
||||||
path: &mut MatchedPath,
|
path: &mut MatchedPath,
|
||||||
per_byte: &mut Vec<ByteClass>,
|
per_byte: &mut Vec<ByteClass>,
|
||||||
@@ -385,17 +388,46 @@ fn walk_ident(
|
|||||||
kind: FailureKind::Validation(err),
|
kind: FailureKind::Validation(err),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// ADR-0024 §Phase D: schema-aware writes. When the ident is
|
// ADR-0024 §Phase D / ADR-0032 §10.1: schema-aware writes.
|
||||||
// a Tables source with `writes_table`, resolve the matched
|
// When the ident is a `Tables` source with `writes_table`,
|
||||||
// name against the schema cache and populate current_table /
|
// resolve the matched name against the schema cache and:
|
||||||
// current_table_columns so subsequent dynamic sub-grammars
|
// 1. populate `current_table` / `current_table_columns`
|
||||||
// can read them. `writes_column` resolves against the
|
// (preserved for DSL paths that read those fields
|
||||||
// already-populated `current_table_columns`.
|
// directly);
|
||||||
|
// 2. push a `TableBinding` onto the top `ScopeFrame`'s
|
||||||
|
// `from_scope` (ADR-0032 §10.1 — for SQL multi-table
|
||||||
|
// contexts).
|
||||||
if writes_table && matches!(src, crate::dsl::grammar::IdentSource::Tables) {
|
if writes_table && matches!(src, crate::dsl::grammar::IdentSource::Tables) {
|
||||||
ctx.current_table = Some(text.clone());
|
let resolved_columns: Vec<crate::completion::TableColumn> = ctx
|
||||||
ctx.current_table_columns = ctx
|
|
||||||
.schema
|
.schema
|
||||||
.and_then(|s| s.columns_for_table(&text).map(<[_]>::to_vec));
|
.and_then(|s| s.columns_for_table(&text).map(<[_]>::to_vec))
|
||||||
|
.unwrap_or_default();
|
||||||
|
ctx.current_table = Some(text.clone());
|
||||||
|
ctx.current_table_columns = if resolved_columns.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(resolved_columns.clone())
|
||||||
|
};
|
||||||
|
if let Some(frame) = ctx.from_scope_stack.last_mut() {
|
||||||
|
frame
|
||||||
|
.from_scope
|
||||||
|
.push(crate::dsl::walker::context::TableBinding {
|
||||||
|
table: text.clone(),
|
||||||
|
alias: None,
|
||||||
|
columns: resolved_columns,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ADR-0032 §10.1: the optional `[ AS ] alias` slot on a
|
||||||
|
// `from_clause` / `join_clause` table source. The flag is
|
||||||
|
// expected on `IdentSource::NewName` slots; the just-pushed
|
||||||
|
// binding (the most recent entry in the top frame's
|
||||||
|
// `from_scope`) gets its alias set.
|
||||||
|
if writes_table_alias
|
||||||
|
&& let Some(frame) = ctx.from_scope_stack.last_mut()
|
||||||
|
&& let Some(binding) = frame.from_scope.last_mut()
|
||||||
|
{
|
||||||
|
binding.alias = Some(text.clone());
|
||||||
}
|
}
|
||||||
if writes_column && matches!(src, crate::dsl::grammar::IdentSource::Columns) {
|
if writes_column && matches!(src, crate::dsl::grammar::IdentSource::Columns) {
|
||||||
ctx.current_column = ctx.current_table_columns.as_ref().and_then(|cols| {
|
ctx.current_column = ctx.current_table_columns.as_ref().and_then(|cols| {
|
||||||
@@ -1194,4 +1226,121 @@ mod tests {
|
|||||||
"WalkContext::new should seed exactly one bottom frame",
|
"WalkContext::new should seed exactly one bottom frame",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- from_scope binding population (ADR-0032 §10.1) ----
|
||||||
|
|
||||||
|
/// Walk a top-level SQL SELECT and return the bottom frame's
|
||||||
|
/// `from_scope` after the walk completes. Used to verify that
|
||||||
|
/// `writes_table` / `writes_table_alias` populate bindings.
|
||||||
|
fn from_scope_after_walk(
|
||||||
|
input: &str,
|
||||||
|
) -> Vec<crate::dsl::walker::context::TableBinding> {
|
||||||
|
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:?}"
|
||||||
|
);
|
||||||
|
// The bottom frame survives the walk; any ScopedSubgrammar
|
||||||
|
// frames have been popped by now.
|
||||||
|
ctx.from_scope_stack[0].from_scope.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_from_table_pushes_one_binding() {
|
||||||
|
let bindings = from_scope_after_walk("select * from users");
|
||||||
|
assert_eq!(bindings.len(), 1);
|
||||||
|
assert_eq!(bindings[0].table, "users");
|
||||||
|
assert_eq!(bindings[0].alias, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn as_alias_on_from_table_is_captured() {
|
||||||
|
let bindings = from_scope_after_walk("select * from users as u");
|
||||||
|
assert_eq!(bindings.len(), 1);
|
||||||
|
assert_eq!(bindings[0].table, "users");
|
||||||
|
assert_eq!(bindings[0].alias, Some("u".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bare_alias_on_from_table_is_captured() {
|
||||||
|
let bindings = from_scope_after_walk("select * from users u");
|
||||||
|
assert_eq!(bindings.len(), 1);
|
||||||
|
assert_eq!(bindings[0].table, "users");
|
||||||
|
assert_eq!(bindings[0].alias, Some("u".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn join_pushes_a_second_binding() {
|
||||||
|
let bindings = from_scope_after_walk(
|
||||||
|
"select * from a join b on x = y",
|
||||||
|
);
|
||||||
|
assert_eq!(bindings.len(), 2);
|
||||||
|
assert_eq!(bindings[0].table, "a");
|
||||||
|
assert_eq!(bindings[1].table, "b");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn join_with_aliases() {
|
||||||
|
let bindings = from_scope_after_walk(
|
||||||
|
"select * from a as x join b as y on x.id = y.id",
|
||||||
|
);
|
||||||
|
assert_eq!(bindings.len(), 2);
|
||||||
|
assert_eq!(bindings[0].table, "a");
|
||||||
|
assert_eq!(bindings[0].alias, Some("x".to_string()));
|
||||||
|
assert_eq!(bindings[1].table, "b");
|
||||||
|
assert_eq!(bindings[1].alias, Some("y".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn three_way_join_pushes_three_bindings() {
|
||||||
|
let bindings = from_scope_after_walk(
|
||||||
|
"select * from a join b on x = y left join c on y = z",
|
||||||
|
);
|
||||||
|
assert_eq!(bindings.len(), 3);
|
||||||
|
assert_eq!(bindings[0].table, "a");
|
||||||
|
assert_eq!(bindings[1].table, "b");
|
||||||
|
assert_eq!(bindings[2].table, "c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subquery_bindings_do_not_leak_to_outer_scope() {
|
||||||
|
// The inner `(SELECT id FROM inner_t)` pushes its
|
||||||
|
// binding into the inner scope frame; on exit, the frame
|
||||||
|
// pops and the inner binding is gone. The outer scope's
|
||||||
|
// from_scope still contains only `outer_t`.
|
||||||
|
let bindings = from_scope_after_walk(
|
||||||
|
"select * from outer_t where id in (select id from inner_t)",
|
||||||
|
);
|
||||||
|
assert_eq!(bindings.len(), 1);
|
||||||
|
assert_eq!(bindings[0].table, "outer_t");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cte_body_bindings_do_not_leak_to_outer_scope() {
|
||||||
|
// The CTE body's `from base_table` pushes into the CTE
|
||||||
|
// body's scope frame; on body-frame exit, the inner
|
||||||
|
// binding goes away. The outer scope contains only
|
||||||
|
// the CTE-name reference `cte_x`.
|
||||||
|
let bindings = from_scope_after_walk(
|
||||||
|
"with cte_x as (select * from base_table) select * from cte_x",
|
||||||
|
);
|
||||||
|
assert_eq!(bindings.len(), 1);
|
||||||
|
assert_eq!(bindings[0].table, "cte_x");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_scope_empty_for_select_without_from() {
|
||||||
|
let bindings = from_scope_after_walk("select 1");
|
||||||
|
assert!(bindings.is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user