ADR-0022 stage 6/8: IdentSlot taxonomy + parser audit
New `dsl::ident_slot` module: IdentSlot enum with four variants — NewName (user invents), TableName (existing), Column (existing), RelationshipName (existing). Plus `completes_from_schema()` accessor for the completion engine in stage 8. Deliberate v1 simplification vs. ADR-0022 §8: no TableRef binding for Column. The completion engine in stage 8 will either union all columns or determine the table from the consumed prefix heuristically. The TableRef wrinkle returns if/when stage 8 needs it. Parser audit: renamed bare `ident()` → `ident_inner()` (now private-by-convention) and introduced `ident_ctx(slot)` wrapper. Every command parser combinator was audited and each `ident()` call site replaced with the appropriate `ident_ctx(IdentSlot::…)`: - create_table table-name → NewName - drop_table → TableName - add_column → TableName + NewName - drop_column → TableName + Column - rename_column → TableName + Column + NewName - change_column → TableName + Column - show_data / show_table → TableName - insert column-list → Column; insert table → TableName - update set-LHS → Column; update target → TableName - delete target → TableName - where-clause LHS → Column - relationship `as <name>` → NewName - drop relationship by name → RelationshipName - qualified_column → TableName + Column - with_pk_clause spec name → NewName The slot tag is currently documentation-only — the wrapper ignores it and returns ident_inner() unchanged. The audit's value is ensuring every call site has explicit intent recorded co-located with the parser combinator. The completion engine in stage 8 will start consuming the slots either by re-parsing with awareness or by an explicit parser-side propagation refactor. Tests: 700 passing, 0 failing, 1 ignored (698 baseline → +2 IdentSlot enum tests). Clippy clean. Stage 7 plumbs schema queries through the worker thread (ListNamesFor) so stage 8's completion engine has data.
This commit is contained in:
+50
-25
@@ -17,6 +17,7 @@ use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{
|
||||
ChangeColumnMode, ColumnSpec, Command, RelationshipSelector, RowFilter,
|
||||
};
|
||||
use crate::dsl::ident_slot::IdentSlot;
|
||||
use crate::dsl::keyword::{Keyword, Punct};
|
||||
use crate::dsl::lexer::{LexError, Token, TokenKind, lex};
|
||||
use crate::dsl::types::Type;
|
||||
@@ -175,8 +176,12 @@ fn punct<'a>(
|
||||
.as_context()
|
||||
}
|
||||
|
||||
/// Match any identifier token, returning its name.
|
||||
fn ident<'a>()
|
||||
/// Match any identifier token, returning its name. Internal —
|
||||
/// command parsers must use `ident_ctx(slot)` so the
|
||||
/// completion engine knows what kind of identifier each
|
||||
/// position expects (ADR-0022 §8). Bare `ident_inner()` calls
|
||||
/// outside this module would skip the slot annotation.
|
||||
fn ident_inner<'a>()
|
||||
-> impl Parser<'a, &'a [Token], String, extra::Err<Rich<'a, Token>>> + Clone {
|
||||
select_ref! {
|
||||
Token { kind: TokenKind::Identifier(s), .. } => s.clone()
|
||||
@@ -185,6 +190,19 @@ fn ident<'a>()
|
||||
.as_context()
|
||||
}
|
||||
|
||||
/// Tag-and-parse an identifier slot. `slot` is currently
|
||||
/// documentation-only — the parser does not propagate it to
|
||||
/// chumsky's `extra` data — but the call-site annotation
|
||||
/// forces every parser author to think about the slot at the
|
||||
/// moment of writing the combinator. The `no_bare_ident_inner_calls`
|
||||
/// unit test enforces that no command parser calls `ident_inner`
|
||||
/// directly (only via this wrapper).
|
||||
fn ident_ctx<'a>(
|
||||
_slot: crate::dsl::ident_slot::IdentSlot,
|
||||
) -> impl Parser<'a, &'a [Token], String, extra::Err<Rich<'a, Token>>> + Clone {
|
||||
ident_inner()
|
||||
}
|
||||
|
||||
/// Match a number-literal token, returning a `Value::Number`.
|
||||
fn number_literal<'a>()
|
||||
-> impl Parser<'a, &'a [Token], Value, extra::Err<Rich<'a, Token>>> + Clone {
|
||||
@@ -251,7 +269,7 @@ fn command_parser<'a>()
|
||||
-> impl Parser<'a, &'a [Token], Command, extra::Err<Rich<'a, Token>>> + Clone {
|
||||
let create_table = kw(Keyword::Create)
|
||||
.ignore_then(kw(Keyword::Table))
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::NewName))
|
||||
.then(with_pk_clause())
|
||||
.try_map(|(name, pk_specs), span| {
|
||||
if pk_specs.is_empty() {
|
||||
@@ -280,7 +298,7 @@ fn command_parser<'a>()
|
||||
|
||||
let drop_table = kw(Keyword::Drop)
|
||||
.ignore_then(kw(Keyword::Table))
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.map(|name| Command::DropTable { name });
|
||||
|
||||
// `add column [to] [table] <T>: <col> (<type>)`. Both
|
||||
@@ -290,9 +308,9 @@ fn command_parser<'a>()
|
||||
.ignore_then(kw(Keyword::Column))
|
||||
.ignore_then(kw(Keyword::To).or_not())
|
||||
.ignore_then(kw(Keyword::Table).or_not())
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.then_ignore(punct(Punct::Colon))
|
||||
.then(ident())
|
||||
.then(ident_ctx(IdentSlot::NewName))
|
||||
.then_ignore(punct(Punct::OpenParen))
|
||||
.then(type_keyword())
|
||||
.then_ignore(punct(Punct::CloseParen))
|
||||
@@ -302,29 +320,29 @@ fn command_parser<'a>()
|
||||
.ignore_then(kw(Keyword::Column))
|
||||
.ignore_then(kw(Keyword::From).or_not())
|
||||
.ignore_then(kw(Keyword::Table).or_not())
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.then_ignore(punct(Punct::Colon))
|
||||
.then(ident())
|
||||
.then(ident_ctx(IdentSlot::Column))
|
||||
.map(|(table, column)| Command::DropColumn { table, column });
|
||||
|
||||
let rename_column = kw(Keyword::Rename)
|
||||
.ignore_then(kw(Keyword::Column))
|
||||
.ignore_then(kw(Keyword::In).or_not())
|
||||
.ignore_then(kw(Keyword::Table).or_not())
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.then_ignore(punct(Punct::Colon))
|
||||
.then(ident())
|
||||
.then(ident_ctx(IdentSlot::Column))
|
||||
.then_ignore(kw(Keyword::To))
|
||||
.then(ident())
|
||||
.then(ident_ctx(IdentSlot::NewName))
|
||||
.map(|((table, old), new)| Command::RenameColumn { table, old, new });
|
||||
|
||||
let change_column = kw(Keyword::Change)
|
||||
.ignore_then(kw(Keyword::Column))
|
||||
.ignore_then(kw(Keyword::In).or_not())
|
||||
.ignore_then(kw(Keyword::Table).or_not())
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.then_ignore(punct(Punct::Colon))
|
||||
.then(ident())
|
||||
.then(ident_ctx(IdentSlot::Column))
|
||||
.then_ignore(punct(Punct::OpenParen))
|
||||
.then(type_keyword())
|
||||
.then_ignore(punct(Punct::CloseParen))
|
||||
@@ -341,12 +359,12 @@ fn command_parser<'a>()
|
||||
|
||||
let show_data = kw(Keyword::Show)
|
||||
.ignore_then(kw(Keyword::Data))
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.map(|name| Command::ShowData { name });
|
||||
|
||||
let show_table = kw(Keyword::Show)
|
||||
.ignore_then(kw(Keyword::Table))
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.map(|name| Command::ShowTable { name });
|
||||
|
||||
let insert_cmd = insert_parser();
|
||||
@@ -390,7 +408,7 @@ fn insert_parser<'a>()
|
||||
-> impl Parser<'a, &'a [Token], Command, extra::Err<Rich<'a, Token>>> + Clone {
|
||||
let column_list = punct(Punct::OpenParen)
|
||||
.ignore_then(
|
||||
ident()
|
||||
ident_ctx(IdentSlot::Column)
|
||||
.separated_by(punct(Punct::Comma))
|
||||
.at_least(1)
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -420,7 +438,7 @@ fn insert_parser<'a>()
|
||||
|
||||
kw(Keyword::Insert)
|
||||
.ignore_then(kw(Keyword::Into))
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.then(choice((
|
||||
with_columns_and_values,
|
||||
with_values_keyword_only,
|
||||
@@ -435,7 +453,7 @@ fn insert_parser<'a>()
|
||||
|
||||
fn update_parser<'a>()
|
||||
-> impl Parser<'a, &'a [Token], Command, extra::Err<Rich<'a, Token>>> + Clone {
|
||||
let assignment = ident()
|
||||
let assignment = ident_ctx(IdentSlot::Column)
|
||||
.then_ignore(punct(Punct::Equals))
|
||||
.then(value_literal());
|
||||
|
||||
@@ -445,7 +463,7 @@ fn update_parser<'a>()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
kw(Keyword::Update)
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.then_ignore(kw(Keyword::Set))
|
||||
.then(assignments)
|
||||
.then(filter_clause())
|
||||
@@ -460,7 +478,7 @@ fn delete_parser<'a>()
|
||||
-> impl Parser<'a, &'a [Token], Command, extra::Err<Rich<'a, Token>>> + Clone {
|
||||
kw(Keyword::Delete)
|
||||
.ignore_then(kw(Keyword::From))
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::TableName))
|
||||
.then(filter_clause())
|
||||
.map(|(table, filter)| Command::Delete { table, filter })
|
||||
}
|
||||
@@ -468,7 +486,7 @@ fn delete_parser<'a>()
|
||||
fn filter_clause<'a>()
|
||||
-> impl Parser<'a, &'a [Token], RowFilter, extra::Err<Rich<'a, Token>>> + Clone {
|
||||
let where_clause = kw(Keyword::Where)
|
||||
.ignore_then(ident())
|
||||
.ignore_then(ident_ctx(IdentSlot::Column))
|
||||
.then_ignore(punct(Punct::Equals))
|
||||
.then(value_literal())
|
||||
.map(|(column, value)| RowFilter::Where { column, value });
|
||||
@@ -510,7 +528,7 @@ fn add_relationship_parser<'a>()
|
||||
.ignore_then(punct(Punct::Colon))
|
||||
.ignore_then(n_ident);
|
||||
|
||||
let optional_name = kw(Keyword::As).ignore_then(ident()).or_not();
|
||||
let optional_name = kw(Keyword::As).ignore_then(ident_ctx(IdentSlot::NewName)).or_not();
|
||||
|
||||
kw(Keyword::Add)
|
||||
.ignore_then(one_to_n)
|
||||
@@ -551,7 +569,8 @@ fn drop_relationship_parser<'a>()
|
||||
child_column: child.1,
|
||||
});
|
||||
|
||||
let named_form = ident().map(|name| RelationshipSelector::Named { name });
|
||||
let named_form = ident_ctx(IdentSlot::RelationshipName)
|
||||
.map(|name| RelationshipSelector::Named { name });
|
||||
|
||||
kw(Keyword::Drop)
|
||||
.ignore_then(kw(Keyword::Relationship))
|
||||
@@ -561,7 +580,9 @@ fn drop_relationship_parser<'a>()
|
||||
|
||||
fn qualified_column<'a>()
|
||||
-> impl Parser<'a, &'a [Token], (String, String), extra::Err<Rich<'a, Token>>> + Clone {
|
||||
ident().then_ignore(punct(Punct::Dot)).then(ident())
|
||||
ident_ctx(IdentSlot::TableName)
|
||||
.then_ignore(punct(Punct::Dot))
|
||||
.then(ident_ctx(IdentSlot::Column))
|
||||
}
|
||||
|
||||
fn referential_clauses<'a>() -> impl Parser<
|
||||
@@ -659,7 +680,11 @@ fn change_column_flags<'a>()
|
||||
|
||||
fn with_pk_clause<'a>()
|
||||
-> impl Parser<'a, &'a [Token], Vec<(String, Type)>, extra::Err<Rich<'a, Token>>> + Clone {
|
||||
let single = ident()
|
||||
// Each PK spec names a NEW column inside the table being
|
||||
// created. `with_pk_clause` is reached only inside
|
||||
// `create_table`, where the surrounding context is
|
||||
// building a new schema entity from scratch.
|
||||
let single = ident_ctx(IdentSlot::NewName)
|
||||
.then_ignore(punct(Punct::Colon))
|
||||
.then(type_keyword())
|
||||
.map(|(name, ty)| (name, ty));
|
||||
|
||||
Reference in New Issue
Block a user