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:
@@ -53,6 +53,8 @@ const IMPORT_TARGET_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_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"),
|
||||||
@@ -87,6 +89,8 @@ const MODE_CHOICES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const MODE_VALUE: Node = Node::Choice(MODE_CHOICES);
|
const MODE_VALUE: Node = Node::Choice(MODE_CHOICES);
|
||||||
@@ -103,6 +107,8 @@ const MESSAGES_CHOICES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES);
|
const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES);
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ const TABLE_NAME_EXISTING: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Table-name slot variant that populates
|
/// Table-name slot variant that populates
|
||||||
@@ -54,6 +56,8 @@ const TABLE_NAME_INSERT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: 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_column: false,
|
||||||
writes_user_listed_column: true,
|
writes_user_listed_column: true,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
static INSERT_COMMA: Node = Node::Punct(',');
|
static INSERT_COMMA: Node = Node::Punct(',');
|
||||||
|
|
||||||
@@ -225,6 +231,8 @@ const TABLE_NAME_WRITES: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Column-name slot in `set col = …` — resolves the column's
|
/// Column-name slot in `set col = …` — resolves the column's
|
||||||
@@ -239,6 +247,8 @@ const SET_COLUMN: Node = Node::Ident {
|
|||||||
writes_column: true,
|
writes_column: true,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Value slot resolved at walk time from
|
/// Value slot resolved at walk time from
|
||||||
@@ -395,6 +405,8 @@ const SELECT_ALIAS_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
static SELECT_AS_ALIAS_NODES: &[Node] = &[
|
static SELECT_AS_ALIAS_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("as")),
|
Node::Word(Word::keyword("as")),
|
||||||
@@ -439,6 +451,8 @@ const SELECT_FROM_TABLE: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// `where <sql_expr>`.
|
/// `where <sql_expr>`.
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const TABLE_NAME_NEW_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
const TABLE_NAME_NEW: Node = Node::Hinted {
|
const TABLE_NAME_NEW: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -65,6 +67,8 @@ const TABLE_NAME_EXISTING: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMN_NAME: Node = Node::Ident {
|
const COLUMN_NAME: Node = Node::Ident {
|
||||||
@@ -76,6 +80,8 @@ const COLUMN_NAME: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
|
const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
|
||||||
@@ -87,6 +93,8 @@ const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
const COLUMN_NAME_NEW: Node = Node::Hinted {
|
const COLUMN_NAME_NEW: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -102,6 +110,8 @@ const RELATIONSHIP_NAME: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
|
const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
|
||||||
@@ -113,6 +123,8 @@ const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
const RELATIONSHIP_NAME_NEW: Node = Node::Hinted {
|
const RELATIONSHIP_NAME_NEW: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -128,6 +140,8 @@ const INDEX_NAME_EXISTING: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const INDEX_NAME_NEW_IDENT: Node = Node::Ident {
|
const INDEX_NAME_NEW_IDENT: Node = Node::Ident {
|
||||||
@@ -139,6 +153,8 @@ const INDEX_NAME_NEW_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
const INDEX_NAME_NEW: Node = Node::Hinted {
|
const INDEX_NAME_NEW: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -211,6 +227,8 @@ const DR_PARENT_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -222,6 +240,8 @@ const DR_PARENT_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const DR_PARENT: Node = Node::Seq(DR_PARENT_NODES);
|
const DR_PARENT: Node = Node::Seq(DR_PARENT_NODES);
|
||||||
@@ -236,6 +256,8 @@ const DR_CHILD_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -247,6 +269,8 @@ const DR_CHILD_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const DR_CHILD: Node = Node::Seq(DR_CHILD_NODES);
|
const DR_CHILD: Node = Node::Seq(DR_CHILD_NODES);
|
||||||
@@ -340,6 +364,8 @@ const AR_PARENT_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -351,6 +377,8 @@ const AR_PARENT_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const AR_PARENT: Node = Node::Seq(AR_PARENT_NODES);
|
const AR_PARENT: Node = Node::Seq(AR_PARENT_NODES);
|
||||||
@@ -365,6 +393,8 @@ const AR_CHILD_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -376,6 +406,8 @@ const AR_CHILD_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const AR_CHILD: Node = Node::Seq(AR_CHILD_NODES);
|
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_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
const NEW_COLUMN_NAME: Node = Node::Hinted {
|
const NEW_COLUMN_NAME: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -908,6 +942,8 @@ const COL_NAME_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
const COL_NAME: Node = Node::Hinted {
|
const COL_NAME: Node = Node::Hinted {
|
||||||
mode: NEW_NAME_HINT,
|
mode: NEW_NAME_HINT,
|
||||||
@@ -1017,6 +1053,8 @@ const COL_SPEC_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct(')'),
|
Node::Punct(')'),
|
||||||
COLUMN_CONSTRAINT_SUFFIX,
|
COLUMN_CONSTRAINT_SUFFIX,
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ const EXPR_COLUMN: Node = Node::Ident {
|
|||||||
writes_column: true,
|
writes_column: true,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Operand alternatives. The literal keywords (`null` / `true`
|
/// Operand alternatives. The literal keywords (`null` / `true`
|
||||||
|
|||||||
@@ -260,6 +260,19 @@ pub enum Node {
|
|||||||
/// follow a table-name push, or when the top frame's
|
/// follow a table-name push, or when the top frame's
|
||||||
/// `from_scope` is empty.
|
/// `from_scope` is empty.
|
||||||
writes_table_alias: bool,
|
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
|
/// 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
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ pub const TYPE_SLOT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Qualified column reference (`<Table>.<Column>`) --------------
|
// --- Qualified column reference (`<Table>.<Column>`) --------------
|
||||||
@@ -69,6 +71,8 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
@@ -80,6 +84,8 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
pub const QUALIFIED_COLUMN: Node = Node::Seq(QUALIFIED_COLUMN_NODES);
|
pub const QUALIFIED_COLUMN: Node = Node::Seq(QUALIFIED_COLUMN_NODES);
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ const EXPR_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: 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_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
static QUALIFIED_REF_TAIL_NODES: &[Node] = &[
|
static QUALIFIED_REF_TAIL_NODES: &[Node] = &[
|
||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
|
|||||||
@@ -198,9 +198,10 @@ fn table_source_bare_alias_factory(
|
|||||||
// Alias slot
|
// Alias slot
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
/// Projection-list alias slot. `writes_table_alias` stays
|
/// Projection-list alias slot. `writes_projection_alias: true`
|
||||||
/// `false` — the projection alias is not a table binding's
|
/// pushes the matched name onto the top frame's
|
||||||
/// alias. (Capture into `projection_aliases` lands in 2b-5.)
|
/// `projection_aliases` so `ORDER BY` candidates can offer it
|
||||||
|
/// (ADR-0032 §10.4).
|
||||||
const PROJECTION_BARE_ALIAS_IDENT: Node = Node::Ident {
|
const PROJECTION_BARE_ALIAS_IDENT: Node = Node::Ident {
|
||||||
source: IdentSource::NewName,
|
source: IdentSource::NewName,
|
||||||
role: "projection_alias",
|
role: "projection_alias",
|
||||||
@@ -210,6 +211,8 @@ const PROJECTION_BARE_ALIAS_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Table-source alias slot — `writes_table_alias: true` so the
|
/// 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_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: true,
|
writes_table_alias: true,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
static PROJECTION_AS_ALIAS_NODES: &[Node] = &[
|
static PROJECTION_AS_ALIAS_NODES: &[Node] = &[
|
||||||
@@ -268,6 +273,8 @@ const QUALIFIED_STAR_QUALIFIER: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
static QUALIFIED_STAR_NODES: &[Node] = &[
|
static QUALIFIED_STAR_NODES: &[Node] = &[
|
||||||
@@ -348,6 +355,8 @@ const TABLE_NAME_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
static TABLE_SOURCE_NODES: &[Node] = &[
|
static TABLE_SOURCE_NODES: &[Node] = &[
|
||||||
@@ -581,7 +590,9 @@ 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,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: true,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CTE_COLUMN_IDENT: Node = Node::Ident {
|
const CTE_COLUMN_IDENT: Node = Node::Ident {
|
||||||
@@ -593,6 +604,8 @@ const CTE_COLUMN_IDENT: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
writes_table_alias: false,
|
writes_table_alias: false,
|
||||||
|
writes_cte_name: false,
|
||||||
|
writes_projection_alias: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
static CTE_COLUMN_LIST_NODES: &[Node] = &[
|
static CTE_COLUMN_LIST_NODES: &[Node] = &[
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ fn walk_node_inner(
|
|||||||
writes_column,
|
writes_column,
|
||||||
writes_user_listed_column,
|
writes_user_listed_column,
|
||||||
writes_table_alias,
|
writes_table_alias,
|
||||||
|
writes_cte_name,
|
||||||
|
writes_projection_alias,
|
||||||
} => walk_ident(
|
} => walk_ident(
|
||||||
source,
|
source,
|
||||||
pos,
|
pos,
|
||||||
@@ -195,6 +197,8 @@ fn walk_node_inner(
|
|||||||
*writes_column,
|
*writes_column,
|
||||||
*writes_user_listed_column,
|
*writes_user_listed_column,
|
||||||
*writes_table_alias,
|
*writes_table_alias,
|
||||||
|
*writes_cte_name,
|
||||||
|
*writes_projection_alias,
|
||||||
ctx,
|
ctx,
|
||||||
path,
|
path,
|
||||||
per_byte,
|
per_byte,
|
||||||
@@ -369,6 +373,8 @@ fn walk_ident(
|
|||||||
writes_column: bool,
|
writes_column: bool,
|
||||||
writes_user_listed_column: bool,
|
writes_user_listed_column: bool,
|
||||||
writes_table_alias: bool,
|
writes_table_alias: bool,
|
||||||
|
writes_cte_name: bool,
|
||||||
|
writes_projection_alias: bool,
|
||||||
ctx: &mut WalkContext,
|
ctx: &mut WalkContext,
|
||||||
path: &mut MatchedPath,
|
path: &mut MatchedPath,
|
||||||
per_byte: &mut Vec<ByteClass>,
|
per_byte: &mut Vec<ByteClass>,
|
||||||
@@ -429,6 +435,34 @@ fn walk_ident(
|
|||||||
{
|
{
|
||||||
binding.alias = Some(text.clone());
|
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) {
|
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| {
|
||||||
cols.iter()
|
cols.iter()
|
||||||
@@ -1343,4 +1377,117 @@ mod tests {
|
|||||||
let bindings = from_scope_after_walk("select 1");
|
let bindings = from_scope_after_walk("select 1");
|
||||||
assert!(bindings.is_empty());
|
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<crate::dsl::walker::context::CteBinding>,
|
||||||
|
Vec<String>,
|
||||||
|
) {
|
||||||
|
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()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user