Files
rdbms-playground/src/dsl/command.rs
T
claude@clouddev1 757711f2bf feat: H3 help <command> per-command detail + general reference
HELP node takes an optional single-word topic (BarePath);
AppCommand::Help { topic }. note_help_topic renders the help
block(s) of every command sharing that entry word (so `help
create` covers both create forms), plus `help types` and a
friendly "no help for X" pointer for unknown topics. Full help
gains a detail-hint footer. Catalogued help.detail_hint /
help.unknown_topic; parse-error matrix updated (help now takes a
topic, so the near-miss is the multi-word case). 9 integration
tests in tests/it/help_command.rs. Mark H3 [x].
2026-06-07 13:32:18 +00:00

1047 lines
42 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 foreign key declared in an advanced-mode SQL `CREATE TABLE`.
///
/// The SQL spelling of an ADR-0013 named relationship (ADR-0035 §5,
/// sub-phase 4b). Produced by both the inline
/// `<col> … REFERENCES <parent>[(<pcol>)] …` form (always auto-named)
/// and the table-level `[CONSTRAINT <name>] FOREIGN KEY (<ccol>)
/// REFERENCES <parent>[(<pcol>)] …` form. The relationship is created
/// together with the table, in one transaction = one undo step.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SqlForeignKey {
/// `CONSTRAINT <name>` on a table-level FK; `None` for an inline
/// FK or an unnamed table FK (auto-named at execution per
/// ADR-0013).
pub name: Option<String>,
/// The column in the table being created that holds the FK.
pub child_column: String,
/// The referenced (parent) table — may be the table being created
/// (a self-referencing FK).
pub parent_table: String,
/// The referenced parent column. `None` for the bare
/// `REFERENCES <parent>` form, resolved at execution to the
/// parent's single-column primary key (ADR-0035 §4b, user-confirmed).
pub parent_column: Option<String>,
pub on_delete: ReferentialAction,
pub on_update: ReferentialAction,
}
/// A column at table-creation time: a name, a user-facing
/// type, and its column-level constraints (ADR-0029).
///
/// `PRIMARY KEY` is not represented here — it is a table-level
/// property carried separately in `Command::CreateTable`'s
/// `primary_key` list, since it may span columns.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnSpec {
pub name: String,
pub ty: Type,
/// `NOT NULL` — the column rejects `NULL` (ADR-0029).
pub not_null: bool,
/// `UNIQUE` — non-`NULL` values must be distinct (ADR-0029).
pub unique: bool,
/// `DEFAULT <literal>` — the value used when an `insert`
/// omits this column (ADR-0029). Simple-mode form.
pub default: Option<Value>,
/// `CHECK (<expr>)` — every row must satisfy this boolean
/// expression (ADR-0029). Simple-mode form (a typed `Expr`).
pub check: Option<Expr>,
/// Advanced-mode raw-SQL `DEFAULT` (ADR-0035 §4a.2): the
/// expression text captured from a SQL `CREATE TABLE`, since
/// `sql_expr` yields no `Expr`. When `Some`, it takes precedence
/// over `default` in DDL emission. `None` in simple mode.
pub default_sql: Option<String>,
/// Advanced-mode raw-SQL `CHECK` (ADR-0035 §4a.2): the inner
/// expression text (without the `CHECK ( … )` wrapper). When
/// `Some`, it takes precedence over `check`. `None` in simple mode.
pub check_sql: Option<String>,
}
impl ColumnSpec {
/// A column spec carrying no constraints — the common case
/// for callers and tests that do not exercise ADR-0029.
#[must_use]
pub fn new(name: impl Into<String>, ty: Type) -> Self {
Self {
name: name.into(),
ty,
not_null: false,
unique: false,
default: None,
check: None,
default_sql: None,
check_sql: None,
}
}
}
/// A column-level constraint with its payload (ADR-0029 §3).
///
/// Produced by `add constraint <constraint> to <T>.<col>`.
/// `Default` / `Check` carry the value / expression; `NotNull`
/// and `Unique` are payload-free.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Constraint {
NotNull,
Unique,
Default(Value),
Check(Expr),
}
impl Constraint {
/// The bare constraint kind, dropping any payload — used for
/// the `[ok]` summary line and log output.
#[must_use]
pub const fn kind(&self) -> ConstraintKind {
match self {
Self::NotNull => ConstraintKind::NotNull,
Self::Unique => ConstraintKind::Unique,
Self::Default(_) => ConstraintKind::Default,
Self::Check(_) => ConstraintKind::Check,
}
}
}
/// The kind of a column-level constraint, without a payload.
///
/// Produced by `drop constraint <kind> from <T>.<col>`
/// (ADR-0029 §3) — naming the kind is enough, since at most one
/// constraint of each kind exists per column.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintKind {
NotNull,
Unique,
Default,
Check,
}
impl ConstraintKind {
/// Upper-case SQL-style label for user-facing messages
/// (`NOT NULL`, `UNIQUE`, `DEFAULT`, `CHECK`).
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::NotNull => "NOT NULL",
Self::Unique => "UNIQUE",
Self::Default => "DEFAULT",
Self::Check => "CHECK",
}
}
}
#[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,
},
/// Advanced-mode SQL `DROP TABLE [IF EXISTS] <name>` (ADR-0035 §4,
/// sub-phase 4c). Executes through the same `do_drop_table`
/// machinery as [`Self::DropTable`] (cascade / inbound-relationship
/// refusal / metadata cleanup); `if_exists` turns an absent table
/// into a no-op-with-note rather than an error.
SqlDropTable {
name: String,
if_exists: bool,
},
/// Advanced-mode SQL `CREATE TABLE` (ADR-0035 §1, sub-phase 4a).
/// Its own command, but executed **structurally** through the
/// same `do_create_table` machinery as [`Self::CreateTable`] —
/// the columns / PK reuse `ColumnSpec`. `if_not_exists` makes an
/// already-existing table a no-op-with-note rather than an error
/// (ADR-0035 §4). 4a carries only `NOT NULL` / `UNIQUE` /
/// `PRIMARY KEY`; `DEFAULT` / `CHECK` are the 4a.2 slice.
SqlCreateTable {
name: String,
columns: Vec<ColumnSpec>,
primary_key: Vec<String>,
/// Composite (multi-column) `UNIQUE (a, b)` table constraints
/// (ADR-0035 §4a.2). Single-column table-level `UNIQUE` is
/// folded into the column's `unique` flag instead.
unique_constraints: Vec<Vec<String>>,
/// Table-level `CHECK (<expr>)` constraints, in declaration
/// order, as raw SQL text (ADR-0035 §4a.3). A multi-column
/// CHECK has no column to hang on and the engine reports no
/// CHECKs, so it round-trips via a dedicated metadata table.
check_constraints: Vec<String>,
/// Foreign keys (ADR-0035 §5, sub-phase 4b) — inline
/// `REFERENCES` and table-level `FOREIGN KEY`, each created as
/// an ADR-0013 named relationship in the same transaction as
/// the table (one undo step).
foreign_keys: Vec<SqlForeignKey>,
if_not_exists: bool,
},
/// Add a column to an existing table. The column carries
/// its constraints from the same suffix grammar as
/// `create table` (ADR-0029); `check` is `None` until the
/// CHECK grammar lands.
AddColumn {
table: String,
column: String,
ty: Type,
not_null: bool,
unique: bool,
default: Option<Value>,
check: Option<Expr>,
},
/// 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. Refused, too,
/// when an index covers the column, unless `cascade` is set
/// (the `--cascade` flag), in which case the covering
/// indexes are dropped alongside the column (ADR-0025).
DropColumn {
table: String,
column: String,
cascade: bool,
},
/// 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.
///
/// Per ADR-0017 the actual conversion model is a per-cell
/// dry-run against a curated transformer matrix; the two
/// optional flags carried in `mode` let the user opt into
/// lossy conversions or skip the client-side layer entirely.
ChangeColumnType {
table: String,
column: String,
ty: Type,
mode: ChangeColumnMode,
},
/// 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,
},
/// Create an index on one or more columns of a table
/// (ADR-0025). `name` is optional — when `None`, the
/// executor auto-generates `<Table>_<col…>_idx`.
AddIndex {
name: Option<String>,
table: String,
columns: Vec<String>,
},
/// Drop an index by name, or by positional reference to its
/// table and exact column set (ADR-0025).
DropIndex {
selector: IndexSelector,
},
/// Advanced-mode SQL `DROP INDEX [IF EXISTS] <name>` (ADR-0035 §4,
/// sub-phase 4d). Name-only (SQL has no positional column form — that
/// is the simple `drop index on T(…)`). Executes through the same
/// `do_drop_index` machinery as [`Self::DropIndex`]; `if_exists`
/// turns an absent index into a no-op-with-note rather than an error.
SqlDropIndex {
name: String,
if_exists: bool,
},
/// Advanced-mode SQL `CREATE [UNIQUE] INDEX [IF NOT EXISTS] [<name>]
/// ON <table> (<col>, …)` (ADR-0035 §4d). Executes through the same
/// `do_add_index` machinery as [`Self::AddIndex`] (the columns/
/// auto-name reuse), plus the `unique` flag (simple mode has no
/// `add unique index` — that stays deferred per ADR-0025). `name` is
/// `None` for the unnamed form (auto-named at execution);
/// `if_not_exists` makes an existing index name a no-op-with-note.
SqlCreateIndex {
name: Option<String>,
table: String,
columns: Vec<String>,
unique: bool,
if_not_exists: bool,
},
/// Advanced-mode SQL `ALTER TABLE <table> <action>` (ADR-0035 §4,
/// sub-phase 4e). `alter` is advanced-only. Each action maps to an
/// existing column executor — the runtime decomposes it to
/// `add_column` / `drop_column` / `rename_column` (one undo step
/// each). 4f/4g/4h extend [`AlterTableAction`].
SqlAlterTable {
table: String,
action: AlterTableAction,
},
/// Add a column-level constraint to an existing column
/// (ADR-0029 §2.2). Applied through the rebuild-table
/// primitive after a §5 dry-run guards populated columns.
AddConstraint {
table: String,
column: String,
constraint: Constraint,
},
/// Remove a column-level constraint from an existing column
/// (ADR-0029 §2.2). Naming the `kind` is enough — at most
/// one constraint of each kind exists per column.
DropConstraint {
table: String,
column: String,
kind: ConstraintKind,
},
/// 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,
},
/// Re-display a whole schema collection — every table,
/// relationship, or index — as a list in the output (V5). The
/// read-only "all items" sibling of `ShowTable`; pure display,
/// no schema change.
ShowList {
kind: ShowListKind,
},
/// 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.
/// An optional `where` filters rows; an optional `limit`
/// caps the row count (ADR-0026 §5). When `limit` is set the
/// query is implicitly ordered by the table's primary key,
/// so `limit n` is a stable "first n by primary key" rather
/// than an arbitrary subset.
ShowData {
name: String,
filter: Option<Expr>,
limit: Option<u64>,
},
/// Replay a sequence of DSL commands from a file. Each line
/// is parsed and dispatched through the same pipeline as
/// interactive input. Blank lines and lines whose first
/// non-whitespace character is `#` are skipped. Execution
/// stops at the first failure (parse or runtime); previously
/// applied commands are NOT rolled back — the partial state
/// is left in place because that matches the "I'm replaying
/// my history" mental model where a partial replay is a
/// recoverable state.
///
/// `path` is the literal user-typed path. The runtime
/// resolves relative paths against the active project's root
/// so that `replay history.log` works without ceremony from
/// inside a project.
Replay {
path: String,
},
/// Capture and display the query plan for an explainable
/// command without executing it (ADR-0028). The inner
/// `Command` is an ordinary parsed `ShowData` / `Update` /
/// `Delete`; the runtime recognizes the `Explain` wrapper
/// and routes it to the plan path instead of normal
/// execution. Because `EXPLAIN QUERY PLAN` never runs the
/// statement, explaining a destructive command is safe.
Explain {
query: Box<Self>,
},
/// Run a SQL `SELECT` and render its result set (ADR-0030
/// §6, ADR-0031). Advanced mode only. `sql` is the validated
/// SQL statement text: a `SELECT` changes no schema, so it is
/// carried and executed as text rather than lowered to a
/// typed command (ADR-0030 §4). The walker has already
/// confirmed it is in the supported subset.
Select {
sql: String,
},
/// Run a validated SQL `INSERT` (ADR-0033 §1, sub-phase 3b).
/// Advanced mode only. Grammar-as-text (ADR-0030 §4): `sql` is
/// the validated statement the worker executes verbatim;
/// `target_table` is extracted from the parse so the worker can
/// re-persist that table's CSV after a successful insert
/// (ADR-0030 §11) without re-parsing the SQL.
///
/// `listed_columns` is the user's explicit `(col, …)` list
/// (empty when the form omits it); `row_source` is the
/// `VALUES …` / `SELECT …` / `WITH … SELECT …` text. Both are
/// captured for sub-phase 3d's `shortid` auto-fill: when the
/// list omits a `shortid` column, the worker materialises the
/// row source, generates fresh ids, and reinserts. `returning`
/// (3g) is added by the sub-phase that reads it.
SqlInsert {
sql: String,
target_table: String,
listed_columns: Vec<String>,
row_source: String,
/// Whether a `RETURNING` clause matched (ADR-0033 §5,
/// sub-phase 3g). The worker collects the returned rows as a
/// `DataResult` when true; otherwise it surfaces the
/// affected-row count (+ auto-show) as before.
returning: bool,
/// Captured literal values per `VALUES` row, per position
/// (ADR-0036 Phase 1). `Some(v)` for a bare literal (incl. a
/// signed number); `None` for an expression position (nothing
/// static to validate). Empty when the row source is a
/// `SELECT`/`WITH` query. The worker validates these against the
/// column types before the (still verbatim) insert; the error
/// enricher reads them to show the offending value. Execution
/// itself is unchanged — these are *not* bound.
literal_rows: Vec<Vec<Option<Value>>>,
},
/// A SQL `UPDATE` validated by the walker (ADR-0033 §2,
/// advanced mode). Grammar-as-text: the worker executes `sql`
/// and re-persists `target_table`'s CSV (ADR-0030 §11).
/// `RETURNING` (3g) is added by the sub-phase that reads it.
SqlUpdate {
sql: String,
target_table: String,
/// Whether a `RETURNING` clause matched (ADR-0033 §5,
/// sub-phase 3g).
returning: bool,
/// Captured literal RHS of each top-level `SET col = <literal>`
/// assignment (ADR-0036 Phase 2). `(col, Some(v))` for a bare
/// literal (incl. a signed number); `(col, None)` for an
/// expression RHS (arithmetic, function call, scalar subquery,
/// column ref — nothing static to validate). The worker validates
/// the `Some` values against their column types before the (still
/// verbatim) update; the error enricher reads them to name the
/// offending value. Execution itself is unchanged — these are
/// *not* bound. `WHERE` is deliberately excluded (ADR-0036 §2).
set_literals: Vec<(String, Option<Value>)>,
},
/// A SQL `DELETE` validated by the walker (ADR-0033 §1/§7,
/// advanced mode). Grammar-as-text: the worker executes `sql`,
/// observes any FK cascade by row-count diffing (Amendment 2 —
/// the same mechanism the DSL `do_delete` uses), and re-persists
/// `target_table`'s CSV plus every cascade-affected child
/// (ADR-0030 §11). The worker never inspects the WHERE clause, so
/// no predicate is carried here. `RETURNING` (3g) is added by the
/// sub-phase that reads it.
SqlDelete {
sql: String,
target_table: String,
/// Whether a `RETURNING` clause matched (ADR-0033 §5,
/// sub-phase 3g). The cascade summary surfaces alongside the
/// returned rows when true.
returning: bool,
},
/// App-lifecycle command (per ADR-0003). These work in both
/// simple and advanced modes; the dispatcher branches on the
/// `Command::App(...)` variant before mode-specific routing.
/// Folded into the DSL parser so they participate in Tab
/// completion + parse-error usage templates alongside the
/// data commands.
App(AppCommand),
}
/// App-level commands surfaced through the DSL parser. These do
/// not touch the database schema or data — they affect app
/// lifecycle, mode, persistence, and verbosity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppCommand {
/// Exit cleanly. Accepts the `q` alias.
Quit,
/// Show in-app help (H3). With no `topic`, the full command
/// list + types reference; with a `topic` (a command entry
/// word like `insert` / `create` / `show`, or `types`), the
/// focused detail for that command (or command group sharing
/// the entry word).
Help {
topic: Option<String>,
},
/// Rebuild `playground.db` from `project.yaml` + data/, with
/// confirmation modal.
Rebuild,
/// Save the current project under a name (modal-driven).
Save,
/// Save the current project as a copy under a new path
/// (modal-driven).
SaveAs,
/// Close current, create a fresh temp project.
New,
/// Open the project picker modal.
Load,
/// Write a zip of project.yaml + data/. `path` is the user-
/// typed target (may be a name under the data root or an
/// absolute path). `None` opens the path prompt modal.
Export { path: Option<String> },
/// Unpack a zip into a new project and switch to it.
/// `target` overrides the project name (default: taken from
/// the zip).
Import { path: String, target: Option<String> },
/// Switch the persistent input mode.
Mode { value: ModeValue },
/// Show or set the messages verbosity.
Messages { value: Option<MessagesValue> },
/// Undo the most recent change, restoring the previous snapshot
/// after a confirmation prompt (ADR-0006 Amendment 1).
Undo,
/// Re-apply the most recently undone change, after confirmation.
Redo,
/// Copy the output panel to the system clipboard (ADR-0041).
/// `copy` / `copy all` copy the whole panel; `copy last` copies
/// the most recent command's output.
Copy { scope: CopyScope },
}
/// Which slice of the output panel `copy` targets (ADR-0041).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CopyScope {
/// The entire output buffer (`copy` bare, or `copy all`).
All,
/// From the most recent echo line to the end (`copy last`).
Last,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModeValue {
Simple,
Advanced,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessagesValue {
Short,
Verbose,
}
/// Conversion mode for `change column …` (ADR-0017 §5).
///
/// `Default` runs the per-cell dry-run and refuses on lossy or
/// incompatible cells. `ForceConversion` accepts lossy cells but
/// still refuses incompatibles. `DontConvert` skips the entire
/// client-side layer and lets the database's STRICT typing
/// decide. `ForceConversion` and `DontConvert` are mutually
/// exclusive at the grammar level.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeColumnMode {
Default,
ForceConversion,
DontConvert,
}
/// 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 {
/// Operate on rows matching this WHERE expression
/// (ADR-0026 — a full boolean expression, not just a single
/// equality).
Where(Expr),
AllRows,
}
impl RowFilter {
/// Build a `Where` filter for a single `column = value`
/// equality. The pre-ADR-0026 grammar produced exactly this
/// shape; the constructor stays as a convenience for
/// callers and tests that only need simple equality.
#[must_use]
pub fn eq(column: impl Into<String>, value: Value) -> Self {
Self::Where(Expr::Predicate(Predicate::Compare {
left: Operand::Column {
name: column.into(),
span: Operand::NO_SPAN,
},
op: CompareOp::Eq,
right: Operand::Literal {
value,
span: Operand::NO_SPAN,
},
}))
}
}
/// A complex WHERE expression (ADR-0026 §4).
///
/// Built by `grammar::expr::build_expr` from the flat
/// matched-terminal slice the walker produces for a `where`
/// clause. The recursion mirrors the stratified expression
/// grammar — `Or` / `And` are n-ary (a flat `a AND b AND c` is
/// one `And` of three children), and single-child precedence
/// tiers collapse so a bare predicate reached through the
/// `or → and → not` layers is just the `Predicate`, not three
/// wrappers.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
/// `a OR b OR …` — at least two children.
Or(Vec<Self>),
/// `a AND b AND …` — at least two children.
And(Vec<Self>),
/// `NOT <expr>`.
Not(Box<Self>),
/// A leaf comparison / match test.
Predicate(Predicate),
}
/// A single comparison or match test inside an [`Expr`]
/// (ADR-0026 §4). Operands are always a column reference or a
/// literal — never a nested expression.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Predicate {
/// `<operand> <op> <operand>` — one of the six comparisons.
Compare {
left: Operand,
op: CompareOp,
right: Operand,
},
/// `<operand> [NOT] LIKE <operand>` — `%` / `_` wildcards.
Like {
target: Operand,
pattern: Operand,
negated: bool,
},
/// `<operand> [NOT] BETWEEN <operand> AND <operand>`.
Between {
target: Operand,
low: Operand,
high: Operand,
negated: bool,
},
/// `<operand> [NOT] IN (<operand>[, …])`.
In {
target: Operand,
items: Vec<Operand>,
negated: bool,
},
/// `<operand> IS [NOT] NULL`.
IsNull { target: Operand, negated: bool },
}
/// A comparison operand — a column reference or a literal
/// (ADR-0026 §1: operands are never nested expressions).
///
/// Each operand carries the byte `span` it occupied in the
/// source. The span feeds the precise per-literal WARNING
/// highlight (ADR-0027) and is otherwise editor metadata —
/// `PartialEq` is hand-written to **ignore** it, so two
/// operands are equal when their column / value match
/// regardless of where they were typed. This keeps `Command`
/// equality whitespace- and position-independent (the bulk of
/// the `Expr` test corpus relies on it).
#[derive(Debug, Clone, Eq)]
pub enum Operand {
Column { name: String, span: (usize, usize) },
Literal { value: Value, span: (usize, usize) },
}
impl Operand {
/// Span used for operands built without a source position
/// (the [`RowFilter::eq`] convenience constructor).
pub const NO_SPAN: (usize, usize) = (0, 0);
/// The byte range this operand occupied in the source —
/// [`Operand::NO_SPAN`] for programmatically-built operands.
#[must_use]
pub const fn span(&self) -> (usize, usize) {
match self {
Self::Column { span, .. } | Self::Literal { span, .. } => *span,
}
}
}
impl PartialEq for Operand {
/// Compares column name / literal value only — the source
/// `span` is deliberately excluded (see the type docs).
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Column { name: a, .. }, Self::Column { name: b, .. }) => a == b,
(Self::Literal { value: a, .. }, Self::Literal { value: b, .. }) => {
a == b
}
_ => false,
}
}
}
/// The six comparison operators. `<>` and `!=` both parse to
/// `NotEq` — `<>` is standard SQL, `!=` the common variant
/// (ADR-0026 §1).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompareOp {
Eq,
NotEq,
Lt,
LtEq,
Gt,
GtEq,
}
/// 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}"
),
}
}
}
/// How a `drop index` command identifies the index to remove
/// (ADR-0025). Both forms are accepted; the executor resolves to
/// a single index.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IndexSelector {
Named { name: String },
Columns { table: String, columns: Vec<String> },
}
/// Which schema collection a `show <kind>` list command displays (V5).
///
/// The bare plural forms list every item of the kind across the
/// project; the singular `show table <name>` (a separate
/// `Command::ShowTable`) shows one. The singular `show
/// relationship <name>` / `show index <name>` forms are not yet
/// provided — only the list-all forms land here.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShowListKind {
/// `show tables` — every user table (internal `__rdbms_*`
/// tables excluded, as in the items panel).
Tables,
/// `show relationships` — every declared FK relationship.
Relationships,
/// `show indexes` — every index across all tables.
Indexes,
}
impl ShowListKind {
/// The full command name for the `name()` / echo surface.
#[must_use]
pub const fn command_name(self) -> &'static str {
match self {
Self::Tables => "show tables",
Self::Relationships => "show relationships",
Self::Indexes => "show indexes",
}
}
}
/// The action of an advanced-mode `ALTER TABLE` (ADR-0035 §4).
///
/// Sub-phase 4e carries the column actions; 4f adds `AlterColumnType`;
/// 4g/4h add `AddConstraint`/`AddForeignKey`/`DropConstraint`, and
/// `RenameTo`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AlterTableAction {
/// `ADD COLUMN <name> <type> [NOT NULL] [UNIQUE] [DEFAULT …]
/// [CHECK …]` — column constraints only (no PK / inline REFERENCES;
/// those are create-table / 4g). Reuses `do_add_column`. Boxed so
/// the large `ColumnSpec` doesn't bloat the enum (and `Command` /
/// `Action` that embed it) — `clippy::large_enum_variant`.
AddColumn(Box<ColumnSpec>),
/// `DROP COLUMN <name>` — reuses `do_drop_column` (cascade = false:
/// an index-covered column is refused, matching SQLite + the
/// simple-mode default; there is no `--cascade` SQL spelling).
DropColumn { column: String },
/// `RENAME COLUMN <old> TO <new>` — reuses `do_rename_column`.
RenameColumn { old: String, new: String },
/// `ALTER COLUMN <name> TYPE <type>` — reuses `do_change_column_type`
/// with `ChangeColumnMode::ForceConversion`, which is the ADR-0035 §7
/// advanced-mode policy (lossy cells are *performed* with a note, no
/// force flag; static-refused / incompatible still refuse). One undo
/// step (the executor's rebuild). ADR-0035 §4f. The ISO synonym
/// `SET DATA TYPE` (canonical) and the PostgreSQL `TYPE` shorthand
/// both build this action (ADR-0035 Amendment 2).
AlterColumnType { column: String, ty: Type },
/// `ALTER COLUMN <name> SET NOT NULL` (ADR-0035 Amendment 2) — a
/// documented PostgreSQL extension (ISO has no in-place NOT-NULL
/// verb). Decomposes to `do_add_constraint(NotNull)` (ADR-0029).
SetColumnNotNull { column: String },
/// `ALTER COLUMN <name> DROP NOT NULL` (ADR-0035 Amendment 2).
/// Decomposes to `do_drop_constraint(NotNull)`.
DropColumnNotNull { column: String },
/// `ALTER COLUMN <name> SET DEFAULT <expr>` (ADR-0035 Amendment 2,
/// ISO). `default_sql` is the raw `sql_expr` text (the §4a.2 / §4e
/// mechanism — `sql_expr` builds no AST, so the default cannot be a
/// typed `Value`). Decomposes to the dedicated `do_set_column_default`
/// executor (not `do_add_constraint`, which is `Value`-based).
SetColumnDefault { column: String, default_sql: String },
/// `ALTER COLUMN <name> DROP DEFAULT` (ADR-0035 Amendment 2, ISO).
/// Decomposes to `do_drop_constraint(Default)`.
DropColumnDefault { column: String },
/// `ADD [CONSTRAINT <name>] (CHECK (…) | UNIQUE (…) | FOREIGN KEY
/// (…) REFERENCES …)` — a table-level constraint (ADR-0035 §4g). The
/// `name` is the `CONSTRAINT <name>` prefix (the FK carries its own
/// `SqlForeignKey::name`, set from this prefix at build time). CHECK
/// and FOREIGN KEY may be named; UNIQUE may not (composite UNIQUE is
/// anonymous in our model — §4g). Boxed: the FK payload is sizeable
/// (`clippy::large_enum_variant`).
AddTableConstraint {
name: Option<String>,
constraint: Box<TableConstraint>,
},
/// `DROP CONSTRAINT <name>` — drops a named table-level CHECK or a
/// named FK (relationship), resolved by name (ADR-0035 §4g).
DropConstraint { name: String },
/// `RENAME TO <new>` — rename the table (ADR-0035 §6, sub-phase 4h).
/// The one genuinely new low-level op in Phase 4: a native table
/// rename plus reconciliation of the CSV file name and every metadata
/// row that names the table (columns, both relationship ends, table
/// CHECKs, and any table-qualified CHECK *text*). Advanced-mode only.
RenameTable { new: String },
}
/// A table-level constraint added via `ALTER TABLE … ADD [CONSTRAINT
/// <name>] …` (ADR-0035 §4g).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TableConstraint {
/// `CHECK (<expr>)` — the expression as **raw SQL text** (the
/// `sql_expr` grammar is validate-only; the builder captures the
/// matched span — the 4a.2 / 4e mechanism).
Check { expr_sql: String },
/// `UNIQUE (<col>, …)` — a composite UNIQUE constraint.
Unique { columns: Vec<String> },
/// `FOREIGN KEY (<col>) REFERENCES <P>[(<col>)] [ON …]` — reuses the
/// 4b `SqlForeignKey` shape; decomposed to `add_relationship`.
ForeignKey(SqlForeignKey),
}
impl std::fmt::Display for IndexSelector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Named { name } => write!(f, "{name}"),
Self::Columns { table, columns } => {
write!(f, "on {table} ({})", columns.join(", "))
}
}
}
}
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::SqlCreateTable { .. } => "create table",
Self::DropTable { .. } => "drop table",
Self::SqlDropTable { .. } => "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::AddIndex { .. } => "add index",
Self::DropIndex { .. } => "drop index",
Self::SqlDropIndex { .. } => "drop index",
Self::SqlCreateIndex { .. } => "create index",
Self::SqlAlterTable { .. } => "alter table",
Self::AddConstraint { .. } => "add constraint",
Self::DropConstraint { .. } => "drop constraint",
Self::ShowTable { .. } => "show table",
Self::ShowList { kind } => kind.command_name(),
Self::Insert { .. } => "insert into",
Self::Update { .. } => "update",
Self::Delete { .. } => "delete from",
Self::ShowData { .. } => "show data",
Self::Replay { .. } => "replay",
Self::Explain { .. } => "explain",
Self::Select { .. } => "select",
Self::SqlInsert { .. } => "insert into",
Self::SqlUpdate { .. } => "update",
Self::SqlDelete { .. } => "delete from",
Self::App(app) => match app {
AppCommand::Quit => "quit",
AppCommand::Help { .. } => "help",
AppCommand::Rebuild => "rebuild",
AppCommand::Save => "save",
AppCommand::SaveAs => "save as",
AppCommand::New => "new",
AppCommand::Load => "load",
AppCommand::Export { .. } => "export",
AppCommand::Import { .. } => "import",
AppCommand::Mode { .. } => "mode",
AppCommand::Messages { .. } => "messages",
AppCommand::Undo => "undo",
AppCommand::Redo => "redo",
AppCommand::Copy { .. } => "copy",
},
}
}
/// 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::SqlCreateTable { name, .. }
| Self::DropTable { name }
| Self::SqlDropTable { name, .. }
| Self::ShowTable { name }
| Self::ShowData { name, .. } => name,
Self::AddColumn { table, .. }
| Self::DropColumn { table, .. }
| Self::RenameColumn { table, .. }
| Self::ChangeColumnType { table, .. }
| Self::AddConstraint { table, .. }
| Self::DropConstraint { 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,
},
Self::AddIndex { table, .. } => table,
Self::DropIndex { selector } => match selector {
IndexSelector::Columns { table, .. } => table,
// A named drop doesn't name the table until the
// executor resolves it; the index name is a
// sensible fallback for logging.
IndexSelector::Named { name } => name,
},
// The SQL drop is name-only; the index name identifies it
// until the executor resolves the table (mirrors the named
// `DropIndex` / `SqlDropTable` fallback).
Self::SqlDropIndex { name, .. } => name,
Self::SqlCreateIndex { table, .. } => table,
Self::SqlAlterTable { table, .. } => table,
// Replay isn't tied to a single table; the path is
// the most identifying thing for log output.
Self::Replay { path } => path,
// Explain forwards to the wrapped query — the table
// the plan is about is the inner command's table.
Self::Explain { query } => query.target_table(),
// A SQL `SELECT` may read several tables (or none);
// there is no single structure-target table. The
// result renders as a data view, not a structure
// view, so an empty target is correct here.
Self::Select { .. } => "",
// A `show <kind>` list spans every table (or none) —
// there is no single structure-target table; it renders
// as a list, not a structure view.
Self::ShowList { .. } => "",
// A SQL `INSERT` carries its parsed target table (for
// CSV re-persistence and ok-summary subject).
Self::SqlInsert { target_table, .. }
| Self::SqlUpdate { target_table, .. }
| Self::SqlDelete { target_table, .. } => target_table,
// App commands aren't tied to schema entities — the
// verb is the most identifying thing. The
// display_subject override below provides a richer
// form when one exists.
Self::App(_) => "",
}
}
/// 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}"
),
},
// A constraint command's subject is the dotted
// `<table>.<column>` it acts on (ADR-0029 §2.2).
Self::AddConstraint { table, column, .. }
| Self::DropConstraint { table, column, .. } => {
format!("{table}.{column}")
}
_ => self.target_table().to_string(),
}
}
}