ADR-0024 Phase C: create table with column-list value literals
Migrate `create table <Name> [with pk [<col>:<type>[, ...]]]`
to the walker. Exercises Repeated{separator: Some(Punct(','))}
for the first time — the with-pk column-spec list.
Walker behaviour changes:
- Optional now backtracks on partial-match failure (Incomplete
or Failed-Mismatch from a Seq mid-shape). Path / per-byte
state rolls back to before the partial attempt; the inner's
expected-set propagates as `skipped` so callers see "what
would have completed it". Matches chumsky's `or_not`
semantics. ValidationFailed (content errors) does NOT
backtrack — the user means to fix those.
- Bridge: ValidationFailed errors now classify as
`at_eof = true`, mirroring the chumsky-side custom-error
convention. This is what lets `create table Customers`
classify as IncompleteAtEof rather than DefiniteErrorAt
(the user can still continue typing `with pk …`).
Grammar:
- src/dsl/grammar/ddl.rs gains CREATE: shape is
Seq(Word("table"), Ident{NewName,table_name}, Optional(WITH_PK))
where WITH_PK = Seq(Word("with"), Word("pk"),
Optional(Repeated{COL_SPEC, separator: Punct(','), min:1})).
AST builder enforces `with pk needs at least one column`
with the existing parse.custom.create_table_needs_pk catalog
wording; `with pk` alone defaults to id:serial.
Tests:
- 6 new walker-specific tests for create_table: with-pk
default, named typed PK, compound PK, whitespace tolerance
around `:` and `,`, bare-create-table-errors-with-with-pk-
hint, case-insensitive keywords.
- Total: 825 passed, 0 failed, 1 ignored (was 819 / 1).
- cargo clippy --all-targets -- -D warnings clean.
This commit is contained in:
+124
-8
@@ -12,11 +12,12 @@
|
||||
//! `parent_table` vs `child_table` for the endpoints clause).
|
||||
|
||||
use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{ChangeColumnMode, Command, RelationshipSelector};
|
||||
use crate::dsl::command::{ChangeColumnMode, ColumnSpec, Command, RelationshipSelector};
|
||||
use crate::dsl::grammar::{
|
||||
CommandNode, IdentSource, Node, ValidationError, Word,
|
||||
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT},
|
||||
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR},
|
||||
};
|
||||
use crate::dsl::types::Type;
|
||||
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
|
||||
|
||||
// =================================================================
|
||||
@@ -579,9 +580,124 @@ pub static CHANGE: CommandNode = CommandNode {
|
||||
hint_mode: None,
|
||||
};
|
||||
|
||||
// `TABLE_NAME_NEW` is currently unused (Phase C will bring
|
||||
// it back when `create table` migrates). Keeping the
|
||||
// declaration here keeps the per-source-of-truth convention
|
||||
// consistent.
|
||||
#[allow(dead_code)]
|
||||
const _UNUSED: Node = TABLE_NAME_NEW;
|
||||
// =================================================================
|
||||
// create_table — `create table <Name> [with pk [<col>:<type>[, ...]]]`
|
||||
// (Phase C)
|
||||
// =================================================================
|
||||
|
||||
const COL_SPEC_NODES: &[Node] = &[
|
||||
Node::Ident {
|
||||
source: IdentSource::NewName,
|
||||
role: "col_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
},
|
||||
Node::Punct(':'),
|
||||
Node::Ident {
|
||||
source: IdentSource::Types,
|
||||
role: "col_type",
|
||||
validator: Some(TYPE_VALIDATOR),
|
||||
highlight_override: None,
|
||||
},
|
||||
];
|
||||
const COL_SPEC: Node = Node::Seq(COL_SPEC_NODES);
|
||||
|
||||
const SPEC_LIST: Node = Node::Repeated {
|
||||
inner: &COL_SPEC,
|
||||
separator: Some(&Node::Punct(',')),
|
||||
min: 1,
|
||||
};
|
||||
const SPEC_LIST_OPT: Node = Node::Optional(&SPEC_LIST);
|
||||
|
||||
const WITH_PK_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("with")),
|
||||
Node::Word(Word::keyword("pk")),
|
||||
SPEC_LIST_OPT,
|
||||
];
|
||||
const WITH_PK: Node = Node::Seq(WITH_PK_NODES);
|
||||
const WITH_PK_OPT: Node = Node::Optional(&WITH_PK);
|
||||
|
||||
const CREATE_TABLE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("table")),
|
||||
TABLE_NAME_NEW,
|
||||
WITH_PK_OPT,
|
||||
];
|
||||
const CREATE_TABLE: Node = Node::Seq(CREATE_TABLE_NODES);
|
||||
|
||||
fn build_create_table(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
let name = require_ident(path, "table_name")?;
|
||||
|
||||
// Collect column specs by pairing alternating col_name /
|
||||
// col_type ident matches. They always appear in declaration
|
||||
// order so a simple zip is correct.
|
||||
let names: Vec<String> = path
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|i| match &i.kind {
|
||||
MatchedKind::Ident { role: "col_name" } => Some(i.text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let types_raw: Vec<&str> = path
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|i| match &i.kind {
|
||||
MatchedKind::Ident { role: "col_type" } => Some(i.text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// No PK clause OR `with pk` alone (no specs): if `with` was
|
||||
// matched, default to id:serial; otherwise reject with the
|
||||
// "tables need at least one column" friendly wording.
|
||||
let saw_with = path
|
||||
.items
|
||||
.iter()
|
||||
.any(|i| matches!(i.kind, MatchedKind::Word("with")));
|
||||
|
||||
let pk_specs: Vec<(String, Type)> = if names.is_empty() {
|
||||
if saw_with {
|
||||
// `with pk` alone — default to id:serial.
|
||||
vec![("id".to_string(), Type::Serial)]
|
||||
} else {
|
||||
return Err(ValidationError {
|
||||
message_key: "parse.custom.create_table_needs_pk",
|
||||
args: vec![],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let mut out = Vec::with_capacity(names.len());
|
||||
for (n, t_str) in names.iter().zip(types_raw.iter()) {
|
||||
let ty = t_str.parse::<Type>().map_err(|_| ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown type".to_string())],
|
||||
})?;
|
||||
out.push((n.clone(), ty));
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
let columns = pk_specs
|
||||
.iter()
|
||||
.map(|(n, t)| ColumnSpec {
|
||||
name: n.clone(),
|
||||
ty: *t,
|
||||
})
|
||||
.collect();
|
||||
let primary_key = pk_specs.into_iter().map(|(n, _)| n).collect();
|
||||
|
||||
Ok(Command::CreateTable {
|
||||
name,
|
||||
columns,
|
||||
primary_key,
|
||||
})
|
||||
}
|
||||
|
||||
pub static CREATE: CommandNode = CommandNode {
|
||||
entry: Word::keyword("create"),
|
||||
shape: CREATE_TABLE,
|
||||
ast_builder: build_create_table,
|
||||
help_id: Some("ddl.create"),
|
||||
usage_id: Some("parse.usage.create_table"),
|
||||
hint_mode: None,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user