Files
rdbms-playground/src/dsl/command.rs
T
claude@clouddev1 7b97786ab7 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.
2026-05-08 10:09:24 +00:00

251 lines
9.0 KiB
Rust

//! The Command AST.
//!
//! `Command` is the parser's output and the database worker's
//! input. Each variant carries fully validated data — the parser
//! is responsible for shape, the database worker for semantics
//! (e.g. "table does not exist").
//!
//! The shape supports compound primary keys natively even though
//! only the dedicated `with pk a:int,b:int` grammar exposes them
//! today. Future grammar extensions (inline column specs, `set
//! primary key`, junction-table convenience commands) emit into
//! the same shape.
use crate::dsl::action::ReferentialAction;
use crate::dsl::types::Type;
use crate::dsl::value::Value;
/// A column at table-creation time: a name and a user-facing
/// type. Constraints beyond `PRIMARY KEY` (NOT NULL, UNIQUE,
/// CHECK, DEFAULT) come in later iterations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnSpec {
pub name: String,
pub ty: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
CreateTable {
name: String,
/// Columns to create, in declaration order.
columns: Vec<ColumnSpec>,
/// Names of columns forming the primary key. Length 1 is
/// a single PK; length >= 2 is a compound PK; length 0
/// indicates no primary key (a future grammar option,
/// not produced by today's parser).
primary_key: Vec<String>,
},
DropTable {
name: String,
},
AddColumn {
table: String,
column: String,
ty: Type,
},
/// Remove a column from a table. Refused if the column is
/// part of the primary key or is involved in a declared
/// relationship — drop the relationship first.
DropColumn {
table: String,
column: String,
},
/// Rename a column. SQLite handles cascading renames in
/// FK references on other tables; the executor mirrors
/// the change into our `__rdbms_playground_columns` and
/// `__rdbms_playground_relationships` metadata tables in
/// the same transaction.
RenameColumn {
table: String,
old: String,
new: String,
},
/// Change a column's type. Implemented via the
/// rebuild-table primitive (ADR-0013) since SQLite's
/// ALTER TABLE does not support type changes. Refused
/// for PK columns and for columns involved in a declared
/// relationship — those would require cascading FK
/// type updates the v1 surface deliberately doesn't
/// expose.
ChangeColumnType {
table: String,
column: String,
ty: Type,
},
/// Establish a 1:n relationship: parent_table.parent_column
/// is the primary-key side; child_table.child_column is the
/// foreign-key side. `name` is optional — when `None`, the
/// executor auto-generates one (`<Child>_<column>_to_<Parent>`).
/// `create_fk` requests the child column be created
/// automatically with the appropriate type if it is missing.
AddRelationship {
name: Option<String>,
parent_table: String,
parent_column: String,
child_table: String,
child_column: String,
on_delete: ReferentialAction,
on_update: ReferentialAction,
create_fk: bool,
},
/// Drop a relationship by either user-given/auto-generated
/// name, or by positional reference to the FK endpoints.
DropRelationship {
selector: RelationshipSelector,
},
/// Re-display a table's structure in the output. Doesn't
/// change schema; useful when the user wants to look at a
/// table they aren't currently DDL'ing on.
ShowTable {
name: String,
},
/// Insert a single row. `columns` is `None` for the natural-
/// order short form (`insert into T values (...)`); the
/// executor fills in the column list by walking the schema.
Insert {
table: String,
columns: Option<Vec<String>>,
values: Vec<Value>,
},
/// Update rows matching the WHERE clause (or all rows when
/// `all_rows` is set, per ADR-0009 opt-in convention).
Update {
table: String,
assignments: Vec<(String, Value)>,
filter: RowFilter,
},
Delete {
table: String,
filter: RowFilter,
},
/// Render the rows of a table as a data view in the output.
ShowData {
name: String,
},
}
/// How an UPDATE / DELETE selects which rows to operate on.
/// `Where` is the default safe form. `AllRows` is the explicit
/// `--all-rows` flag opt-in for unfiltered operations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RowFilter {
Where { column: String, value: Value },
AllRows,
}
/// How a `drop relationship` command identifies the relationship
/// to remove. Both forms are accepted; the executor resolves to
/// a single row in the metadata table.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RelationshipSelector {
Named { name: String },
Endpoints {
parent_table: String,
parent_column: String,
child_table: String,
child_column: String,
},
}
impl std::fmt::Display for RelationshipSelector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Named { name } => write!(f, "{name}"),
Self::Endpoints {
parent_table,
parent_column,
child_table,
child_column,
} => write!(
f,
"from {parent_table}.{parent_column} to {child_table}.{child_column}"
),
}
}
}
impl Command {
/// Short label for log output and result rendering.
#[must_use]
pub const fn verb(&self) -> &'static str {
match self {
Self::CreateTable { .. } => "create table",
Self::DropTable { .. } => "drop table",
Self::AddColumn { .. } => "add column",
Self::DropColumn { .. } => "drop column",
Self::RenameColumn { .. } => "rename column",
Self::ChangeColumnType { .. } => "change column",
Self::AddRelationship { .. } => "add relationship",
Self::DropRelationship { .. } => "drop relationship",
Self::ShowTable { .. } => "show table",
Self::Insert { .. } => "insert into",
Self::Update { .. } => "update",
Self::Delete { .. } => "delete from",
Self::ShowData { .. } => "show data",
}
}
/// The table whose structure most directly reflects the
/// outcome of this command. For relationships this is the
/// child table, since the FK constraint physically belongs
/// there and our describe view shows both sides anyway.
#[must_use]
pub fn target_table(&self) -> &str {
match self {
Self::CreateTable { name, .. }
| Self::DropTable { name }
| Self::ShowTable { name }
| Self::ShowData { name } => name,
Self::AddColumn { table, .. }
| Self::DropColumn { table, .. }
| Self::RenameColumn { table, .. }
| Self::ChangeColumnType { table, .. }
| Self::Insert { table, .. }
| Self::Update { table, .. }
| Self::Delete { table, .. } => table,
// For relationships we focus on the parent (1-side):
// the structure rendering after add/drop shows that
// table's "Referenced by" entry, which is what the
// user looks at to confirm the relationship.
Self::AddRelationship { parent_table, .. } => parent_table,
Self::DropRelationship { selector } => match selector {
RelationshipSelector::Endpoints { parent_table, .. } => parent_table,
// For a named drop we don't know the parent table
// until the executor resolves it; the name itself
// is a sensible fallback for logging.
RelationshipSelector::Named { name } => name,
},
}
}
/// Human-readable subject for the `[ok] <verb> <subject>`
/// summary line. Most commands target a single table, but
/// relationship commands are better described by their
/// endpoints than by either side alone.
#[must_use]
pub fn display_subject(&self) -> String {
match self {
Self::AddRelationship {
parent_table,
parent_column,
child_table,
child_column,
..
} => format!("from {parent_table}.{parent_column} to {child_table}.{child_column}"),
Self::DropRelationship { selector } => match selector {
RelationshipSelector::Named { name } => name.clone(),
RelationshipSelector::Endpoints {
parent_table,
parent_column,
child_table,
child_column,
} => format!(
"from {parent_table}.{parent_column} to {child_table}.{child_column}"
),
},
_ => self.target_table().to_string(),
}
}
}