B2/C2: column drop / rename / change-type DSL commands
Closes B2 (rebuild-table reused outside relationships) and
C2 (full add/drop/rename/change-type column operations).
* drop column [from] [table] <T>: <col>
- ALTER TABLE DROP COLUMN (SQLite 3.35+) + metadata
cleanup in __rdbms_playground_columns.
- Refuses PK columns and columns involved in a declared
relationship (drop the relationship first).
* rename column [in] [table] <T>: <old> to <new>
- ALTER TABLE RENAME COLUMN (SQLite 3.25+); SQLite
cascades the rename through FK declarations on other
tables.
- Mirrors the new name into both metadata tables
(__rdbms_playground_columns, __rdbms_playground_relationships)
so describes stay accurate after a rename.
- Refuses identity rename and name collisions.
* change column [in] [table] <T>: <col> (<newtype>)
- Routes through the rebuild_table primitive (ADR-0013)
since SQLite ALTER doesn't support type changes.
INSERT INTO new SELECT FROM old; STRICT typing enforces
cell compatibility, transaction rolls back on mismatch.
- Refuses PK columns, relationship-involved columns,
`serial` target, and no-op same-type changes.
Adds 20 tests (parser + db layer); updates the in-app help
listing. Both prepositions independently optional in each
new command, matching `add column`'s grammar shape.
Total: 449 passing, 0 failing, 0 skipped (up from 429).
Clippy clean.
Known spec gap: column-type-change conversion compatibility
is not yet documented (currently relies on SQLite STRICT
errors); follow-up will close this.
This commit is contained in:
+171
-5
@@ -136,12 +136,10 @@ fn command_parser<'a>()
|
||||
// and `add column T: c (text)` all parse identically.
|
||||
// Matches the convention elsewhere in the DSL where bare
|
||||
// identifiers are accepted in unambiguous positions.
|
||||
let optional_to = keyword_ci("to").or_not();
|
||||
let optional_table = keyword_ci("table").or_not();
|
||||
let add_column = keyword_ci("add")
|
||||
.ignore_then(keyword_ci("column"))
|
||||
.ignore_then(optional_to)
|
||||
.ignore_then(optional_table)
|
||||
.ignore_then(optional_keyword("to"))
|
||||
.ignore_then(optional_keyword("table"))
|
||||
.ignore_then(identifier())
|
||||
.then_ignore(just(':').padded())
|
||||
.then(identifier())
|
||||
@@ -150,6 +148,45 @@ fn command_parser<'a>()
|
||||
.then_ignore(just(')').padded())
|
||||
.map(|((table, column), ty)| Command::AddColumn { table, column, ty });
|
||||
|
||||
// `drop column [from] [table] <T>: <col>`. Both
|
||||
// prepositions independently optional, matching the
|
||||
// `add column` shape for symmetry.
|
||||
let drop_column = keyword_ci("drop")
|
||||
.ignore_then(keyword_ci("column"))
|
||||
.ignore_then(optional_keyword("from"))
|
||||
.ignore_then(optional_keyword("table"))
|
||||
.ignore_then(identifier())
|
||||
.then_ignore(just(':').padded())
|
||||
.then(identifier())
|
||||
.map(|(table, column)| Command::DropColumn { table, column });
|
||||
|
||||
// `rename column [in] [table] <T>: <old> to <new>`.
|
||||
let rename_column = keyword_ci("rename")
|
||||
.ignore_then(keyword_ci("column"))
|
||||
.ignore_then(optional_keyword("in"))
|
||||
.ignore_then(optional_keyword("table"))
|
||||
.ignore_then(identifier())
|
||||
.then_ignore(just(':').padded())
|
||||
.then(identifier())
|
||||
.then_ignore(keyword_ci("to"))
|
||||
.then(identifier())
|
||||
.map(|((table, old), new)| Command::RenameColumn { table, old, new });
|
||||
|
||||
// `change column [in] [table] <T>: <col> (<newtype>)`.
|
||||
// Same shape as `add column` — the column-and-type
|
||||
// tuple in parens — but the verb is `change`.
|
||||
let change_column = keyword_ci("change")
|
||||
.ignore_then(keyword_ci("column"))
|
||||
.ignore_then(optional_keyword("in"))
|
||||
.ignore_then(optional_keyword("table"))
|
||||
.ignore_then(identifier())
|
||||
.then_ignore(just(':').padded())
|
||||
.then(identifier())
|
||||
.then_ignore(just('(').padded())
|
||||
.then(type_keyword())
|
||||
.then_ignore(just(')').padded())
|
||||
.map(|((table, column), ty)| Command::ChangeColumnType { table, column, ty });
|
||||
|
||||
let add_relationship = add_relationship_parser();
|
||||
let drop_relationship = drop_relationship_parser();
|
||||
|
||||
@@ -169,10 +206,16 @@ fn command_parser<'a>()
|
||||
|
||||
choice((
|
||||
create_table,
|
||||
// `drop column` and `drop relationship` come before
|
||||
// `drop table` because both are more specific —
|
||||
// chumsky's `choice` tries each in order.
|
||||
drop_column,
|
||||
drop_relationship,
|
||||
drop_table,
|
||||
add_column,
|
||||
add_relationship,
|
||||
drop_relationship,
|
||||
rename_column,
|
||||
change_column,
|
||||
// Order: `show data` before `show table` because both
|
||||
// start with `show` and the longer keyword is checked
|
||||
// first via this ordering.
|
||||
@@ -582,6 +625,13 @@ fn type_keyword<'a>()
|
||||
})
|
||||
}
|
||||
|
||||
/// `keyword_ci(kw).or_not()` packaged for readability.
|
||||
fn optional_keyword<'a>(
|
||||
kw: &'static str,
|
||||
) -> impl Parser<'a, &'a str, (), extra::Err<Rich<'a, char>>> + Clone {
|
||||
keyword_ci(kw).or_not().map(|_| ())
|
||||
}
|
||||
|
||||
/// Case-insensitive keyword matcher. Consumes leading and
|
||||
/// trailing whitespace and, importantly, requires a word
|
||||
/// boundary so `create` does not match a prefix of `created`.
|
||||
@@ -732,6 +782,122 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// --- drop column / rename column / change column ---
|
||||
|
||||
#[test]
|
||||
fn drop_column_simple() {
|
||||
assert_eq!(
|
||||
ok("drop column from table Customers: Email"),
|
||||
Command::DropColumn {
|
||||
table: "Customers".to_string(),
|
||||
column: "Email".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_column_accepts_bare_identifiers() {
|
||||
// Both prepositions independently optional, matching
|
||||
// `add column`'s shape.
|
||||
assert_eq!(
|
||||
ok("drop column Customers: Email"),
|
||||
Command::DropColumn {
|
||||
table: "Customers".to_string(),
|
||||
column: "Email".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
ok("drop column from Customers: Email"),
|
||||
Command::DropColumn {
|
||||
table: "Customers".to_string(),
|
||||
column: "Email".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
ok("drop column table Customers: Email"),
|
||||
Command::DropColumn {
|
||||
table: "Customers".to_string(),
|
||||
column: "Email".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_column_simple() {
|
||||
assert_eq!(
|
||||
ok("rename column in table Customers: OldName to NewName"),
|
||||
Command::RenameColumn {
|
||||
table: "Customers".to_string(),
|
||||
old: "OldName".to_string(),
|
||||
new: "NewName".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_column_accepts_bare_identifiers() {
|
||||
assert_eq!(
|
||||
ok("rename column Customers: A to B"),
|
||||
Command::RenameColumn {
|
||||
table: "Customers".to_string(),
|
||||
old: "A".to_string(),
|
||||
new: "B".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
ok("rename column in Customers: A to B"),
|
||||
Command::RenameColumn {
|
||||
table: "Customers".to_string(),
|
||||
old: "A".to_string(),
|
||||
new: "B".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
ok("rename column table Customers: A to B"),
|
||||
Command::RenameColumn {
|
||||
table: "Customers".to_string(),
|
||||
old: "A".to_string(),
|
||||
new: "B".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_column_simple() {
|
||||
assert_eq!(
|
||||
ok("change column in table Customers: Score (int)"),
|
||||
Command::ChangeColumnType {
|
||||
table: "Customers".to_string(),
|
||||
column: "Score".to_string(),
|
||||
ty: Type::Int,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_column_accepts_bare_identifiers() {
|
||||
assert_eq!(
|
||||
ok("change column Customers: Score (real)"),
|
||||
Command::ChangeColumnType {
|
||||
table: "Customers".to_string(),
|
||||
column: "Score".to_string(),
|
||||
ty: Type::Real,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_column_keywords_are_case_insensitive() {
|
||||
assert_eq!(
|
||||
ok("CHANGE COLUMN IN TABLE Customers: Score (TEXT)"),
|
||||
Command::ChangeColumnType {
|
||||
table: "Customers".to_string(),
|
||||
column: "Score".to_string(),
|
||||
ty: Type::Text,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_table_simple() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user