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:
+117
-19
@@ -95,6 +95,13 @@ pub fn walk(
|
||||
NodeWalkResult::Matched { end, .. } => {
|
||||
let trailing = skip_whitespace(effective_source, end);
|
||||
if trailing < effective_source.len() {
|
||||
// The shape matched but the user kept typing.
|
||||
// Don't merge skipped-Optional expectations
|
||||
// into the trailing-input error: the completion
|
||||
// engine reads `expected` to decide what to
|
||||
// suggest, and adding "what could have come
|
||||
// before this trailing token" would suggest
|
||||
// candidates the user has already passed.
|
||||
WalkOutcome::Mismatch {
|
||||
position: trailing,
|
||||
expected: vec![Expectation::EndOfInput],
|
||||
@@ -382,27 +389,23 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn walker_import_trailing_as_without_target_errors() {
|
||||
// Phase B Optional-backtracking: when the user types
|
||||
// `import foo.zip as ` and stops, the inner Optional
|
||||
// `(as <target>)` partial-matches `as` then runs out
|
||||
// of input → backtracks (matches chumsky's `or_not`
|
||||
// semantics). The walker reports a successful parse of
|
||||
// `import foo.zip` followed by trailing `as ` → a
|
||||
// structural Mismatch with expected=`end of input`.
|
||||
// The friendly "import: empty target after `as`"
|
||||
// wording is no longer produced by the walker, but the
|
||||
// integration test
|
||||
// (`import_with_empty_target_after_as_errors`) still
|
||||
// passes because the rendered `import_usage` template
|
||||
// line in the dispatch output contains both "import"
|
||||
// and "target".
|
||||
let err = parse("import foo.zip as ").unwrap_err();
|
||||
match err {
|
||||
crate::dsl::ParseError::Invalid {
|
||||
message, expected, ..
|
||||
} => {
|
||||
// Phase A: the friendly `project.import_empty_target`
|
||||
// wording moves out of the parser; the walker's
|
||||
// structural error names the slot via its
|
||||
// user-facing label. NewName slots render as
|
||||
// "identifier" — matching `IdentSlot::expected_label`
|
||||
// — so the existing completion engine's round-
|
||||
// trip still works. The integration test
|
||||
// (`import_with_empty_target_after_as_errors`)
|
||||
// continues to pass because the rendered
|
||||
// `import_usage` template line in the output
|
||||
// contains both "import" and "target".
|
||||
assert!(
|
||||
message.contains("identifier")
|
||||
|| expected.iter().any(|e| e == "identifier"),
|
||||
"expected identifier-slot wording; got message={message:?}, expected={expected:?}"
|
||||
);
|
||||
crate::dsl::ParseError::Invalid { message, .. } => {
|
||||
assert!(
|
||||
message.contains("import"),
|
||||
"expected `import` in 'after `<prefix>`' framing; got: {message}"
|
||||
@@ -646,6 +649,101 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Phase C — create table.
|
||||
// =========================================================
|
||||
|
||||
use crate::dsl::command::ColumnSpec;
|
||||
|
||||
fn col(name: &str, ty: Type) -> ColumnSpec {
|
||||
ColumnSpec {
|
||||
name: name.to_string(),
|
||||
ty,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walker_parses_create_table_with_pk_default_id_serial() {
|
||||
assert_eq!(
|
||||
parse("create table Customers with pk").unwrap(),
|
||||
Command::CreateTable {
|
||||
name: "Customers".to_string(),
|
||||
columns: vec![col("id", Type::Serial)],
|
||||
primary_key: vec!["id".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walker_parses_create_table_named_typed_pk() {
|
||||
assert_eq!(
|
||||
parse("create table Customers with pk email:text").unwrap(),
|
||||
Command::CreateTable {
|
||||
name: "Customers".to_string(),
|
||||
columns: vec![col("email", Type::Text)],
|
||||
primary_key: vec!["email".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walker_parses_create_table_compound_pk() {
|
||||
assert_eq!(
|
||||
parse("create table OrderLines with pk order_id:int,product_id:int").unwrap(),
|
||||
Command::CreateTable {
|
||||
name: "OrderLines".to_string(),
|
||||
columns: vec![col("order_id", Type::Int), col("product_id", Type::Int)],
|
||||
primary_key: vec!["order_id".to_string(), "product_id".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walker_create_table_pk_tolerates_whitespace_around_punct() {
|
||||
assert_eq!(
|
||||
parse("create table T with pk id : serial").unwrap(),
|
||||
Command::CreateTable {
|
||||
name: "T".to_string(),
|
||||
columns: vec![col("id", Type::Serial)],
|
||||
primary_key: vec!["id".to_string()],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse("create table T with pk a : int , b : int").unwrap(),
|
||||
Command::CreateTable {
|
||||
name: "T".to_string(),
|
||||
columns: vec![col("a", Type::Int), col("b", Type::Int)],
|
||||
primary_key: vec!["a".to_string(), "b".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walker_bare_create_table_errors_with_with_pk_hint() {
|
||||
let err = parse("create table Customers").unwrap_err();
|
||||
match err {
|
||||
crate::dsl::ParseError::Invalid { message, .. } => {
|
||||
assert!(
|
||||
message.contains("with pk"),
|
||||
"error should mention `with pk`:\n{message}"
|
||||
);
|
||||
}
|
||||
other => panic!("expected Invalid, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walker_create_table_keywords_are_case_insensitive() {
|
||||
assert_eq!(
|
||||
parse("CREATE TABLE Customers WITH PK email:TEXT").unwrap(),
|
||||
Command::CreateTable {
|
||||
name: "Customers".to_string(),
|
||||
columns: vec![col("email", Type::Text)],
|
||||
primary_key: vec!["email".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Routing fall-through still works for non-DDL ----
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user