feat: ADR-0035 4h — ALTER TABLE … RENAME TO
The one genuinely new low-level op in Phase 4: a native engine RENAME TO
plus one-transaction reconciliation (commit-db-last) of everything the
engine does not track —
- every metadata row naming the table: __rdbms_playground_columns, both
ends of __rdbms_playground_relationships (FK parent, child, and
self-referential), and __rdbms_playground_table_checks;
- the CSV file, via the existing persistence rewrite+delete path
(rewritten_tables=[new], deleted_tables=[old]) — no new method;
- CHECK text that qualifies a column with the old table name
(T.age → U.age, column- and table-level): the engine rewrites the live
CHECK but the stored text would drift and break a fresh rebuild (a
planning-/runda finding); rewrite_check_table_qualifier keeps them in
step. Bounded — a CHECK references only its own table.
Grammar: a fifth AlterTableAction (RenameTable { new }), added by
splitting the `rename` verb into one branch with an inner Choice on a
distinct second keyword (column vs to); the new-name slot mirrors the
CREATE TABLE name slot (NewName + reject_internal_table validator).
Refusals are engine-neutral and case-insensitive (the engine matches
names that way): same-name, case-only, existing-target, __rdbms_*, and
non-existent source. Auto-named indexes and relationships keep their
stale names (only table-name columns update — §6 scope). One undo step;
advanced-mode only; closes the rename half of C1.
Tests: 8 Tier-3 e2e + rewrite-helper unit tests + parse-dispatch tests.
Full suite 1903 passing / 0 failing / 1 ignored; clippy clean.
This commit is contained in:
+88
-5
@@ -1904,14 +1904,49 @@ const AT_ADD_COLUMN_TAIL: Node = Node::Seq(AT_ADD_COLUMN_TAIL_NODES);
|
||||
static AT_DROP_COLUMN_TAIL_NODES: &[Node] = &[Node::Word(Word::keyword("column")), COLUMN_NAME];
|
||||
const AT_DROP_COLUMN_TAIL: Node = Node::Seq(AT_DROP_COLUMN_TAIL_NODES);
|
||||
|
||||
static AT_RENAME_COLUMN_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("rename")),
|
||||
// New-table-name slot for `RENAME TO <new>` (ADR-0035 §6, sub-phase 4h).
|
||||
// Mirrors the `CREATE TABLE` name slot: `IdentSource::NewName` (a name
|
||||
// being introduced, not completed from existing tables) + the same
|
||||
// `reject_internal_table` parse-time validator, so an `__rdbms_*` target
|
||||
// is refused before submit. Wrapped in `NEW_NAME_HINT` like
|
||||
// `NEW_COLUMN_NAME`. `writes_table: false` — nothing downstream of
|
||||
// `rename to <new>` references the schema cache.
|
||||
const NEW_TABLE_NAME_IDENT: Node = Node::Ident {
|
||||
source: IdentSource::NewName,
|
||||
role: "new_table_name",
|
||||
validator: Some(super::sql_select::reject_internal_table),
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const NEW_TABLE_NAME: Node = Node::Hinted {
|
||||
mode: NEW_NAME_HINT,
|
||||
inner: &NEW_TABLE_NAME_IDENT,
|
||||
};
|
||||
|
||||
// The `rename` verb fans out (like `add`/`drop`, §6.1) to an inner
|
||||
// `Choice` whose two tails lead on DISTINCT second keywords: `column`
|
||||
// (rename column) and `to` (rename table — 4h). The walker `Choice`
|
||||
// selects by the leading token and never backtracks between branches, so
|
||||
// the distinct keywords keep them apart.
|
||||
static AT_RENAME_COLUMN_TAIL_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("column")),
|
||||
COLUMN_NAME,
|
||||
Node::Word(Word::keyword("to")),
|
||||
NEW_COLUMN_NAME,
|
||||
];
|
||||
const AT_RENAME_COLUMN: Node = Node::Seq(AT_RENAME_COLUMN_NODES);
|
||||
const AT_RENAME_COLUMN_TAIL: Node = Node::Seq(AT_RENAME_COLUMN_TAIL_NODES);
|
||||
static AT_RENAME_TABLE_TAIL_NODES: &[Node] =
|
||||
&[Node::Word(Word::keyword("to")), NEW_TABLE_NAME];
|
||||
const AT_RENAME_TABLE_TAIL: Node = Node::Seq(AT_RENAME_TABLE_TAIL_NODES);
|
||||
static AT_RENAME_TAIL_CHOICES: &[Node] = &[AT_RENAME_COLUMN_TAIL, AT_RENAME_TABLE_TAIL];
|
||||
const AT_RENAME_TAIL: Node = Node::Choice(AT_RENAME_TAIL_CHOICES);
|
||||
static AT_RENAME_NODES: &[Node] = &[Node::Word(Word::keyword("rename")), AT_RENAME_TAIL];
|
||||
const AT_RENAME: Node = Node::Seq(AT_RENAME_NODES);
|
||||
|
||||
// `ALTER COLUMN <col> TYPE <type>` (ADR-0035 §4f). The type slot reuses
|
||||
// SQL_TYPE (the same alias map + `double precision` pair the CREATE
|
||||
@@ -1995,7 +2030,7 @@ const AT_DROP: Node = Node::Seq(AT_DROP_NODES);
|
||||
// concrete keywords, trap-safe. (The branch's `alter` is the action
|
||||
// word; the entry-word `alter` was already consumed by dispatch.) The
|
||||
// second-keyword fan-out happens in `AT_ADD` / `AT_DROP`'s inner Choice.
|
||||
static AT_ACTION_CHOICES: &[Node] = &[AT_ADD, AT_DROP, AT_RENAME_COLUMN, AT_ALTER_COLUMN];
|
||||
static AT_ACTION_CHOICES: &[Node] = &[AT_ADD, AT_DROP, AT_RENAME, AT_ALTER_COLUMN];
|
||||
const AT_ACTION: Node = Node::Choice(AT_ACTION_CHOICES);
|
||||
|
||||
static SQL_ALTER_TABLE_SHAPE_NODES: &[Node] = &[
|
||||
@@ -2127,7 +2162,12 @@ fn build_alter_column_type(path: &MatchedPath) -> Result<AlterTableAction, Valid
|
||||
/// still routes to AddColumn.
|
||||
/// 3. **`add`** — a table-level constraint (CHECK / UNIQUE / FK / the
|
||||
/// refused PRIMARY KEY).
|
||||
/// 4. else **`drop`** — `drop constraint <name>`.
|
||||
/// 4. **`rename`** — `rename to <new>` (table rename, 4h). Reached only
|
||||
/// when `column` is absent (caught by step 2), so a lone `rename`
|
||||
/// means the table form. The new name binds a *distinct* role
|
||||
/// (`new_table_name`), so it never collides with the `table_name`
|
||||
/// target slot.
|
||||
/// 5. else **`drop`** — `drop constraint <name>`.
|
||||
fn build_sql_alter_table(path: &MatchedPath, source: &str) -> Result<Command, ValidationError> {
|
||||
let table = require_ident(path, "table_name")?;
|
||||
let action = if path.contains_word("type") {
|
||||
@@ -2147,6 +2187,10 @@ fn build_sql_alter_table(path: &MatchedPath, source: &str) -> Result<Command, Va
|
||||
}
|
||||
} else if path.contains_word("add") {
|
||||
build_alter_add_table_constraint(path, source)?
|
||||
} else if path.contains_word("rename") {
|
||||
AlterTableAction::RenameTable {
|
||||
new: require_ident(path, "new_table_name")?,
|
||||
}
|
||||
} else {
|
||||
AlterTableAction::DropConstraint {
|
||||
name: require_ident(path, "constraint_name")?,
|
||||
@@ -2791,6 +2835,45 @@ mod sql_alter_table_tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_table() {
|
||||
// ADR-0035 §6 / 4h: `rename to <new>` — the `rename` verb fans out
|
||||
// on a distinct second keyword (`to` vs `column`).
|
||||
let (table, action) = alter("alter table Orders rename to Purchases");
|
||||
assert_eq!(table, "Orders");
|
||||
match action {
|
||||
AlterTableAction::RenameTable { new } => assert_eq!(new, "Purchases"),
|
||||
other => panic!("expected RenameTable, got {other:?}"),
|
||||
}
|
||||
// trailing semicolon tolerated
|
||||
assert!(matches!(
|
||||
alter("alter table Orders rename to Purchases;").1,
|
||||
AlterTableAction::RenameTable { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_table_does_not_steal_rename_column() {
|
||||
// The two `rename` tails coexist: `rename to` → table,
|
||||
// `rename column … to …` → column. Neither misroutes.
|
||||
assert!(matches!(
|
||||
alter("alter table T rename to U").1,
|
||||
AlterTableAction::RenameTable { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
alter("alter table T rename column a to b").1,
|
||||
AlterTableAction::RenameColumn { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_to_internal_target_refused_at_parse() {
|
||||
// The target slot carries the `reject_internal_table` validator
|
||||
// (mirroring CREATE TABLE), so an `__rdbms_*` target is refused
|
||||
// before submit — engine-neutral, not a raw engine error.
|
||||
assert!(parse_command_in_mode("alter table T rename to __rdbms_evil", Mode::Advanced).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alter_column_type_parses() {
|
||||
// ADR-0035 §4f: the fourth action, discriminated by the `type`
|
||||
|
||||
Reference in New Issue
Block a user