7b97786ab7
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.
251 lines
9.0 KiB
Rust
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(),
|
|
}
|
|
}
|
|
}
|