//! 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 /// ` … REFERENCES [()] …` form (always auto-named) /// and the table-level `[CONSTRAINT ] FOREIGN KEY () /// REFERENCES [()] …` form. The relationship is created /// together with the table, in one transaction = one undo step. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SqlForeignKey { /// `CONSTRAINT ` 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, /// 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 ` form, resolved at execution to the /// parent's single-column primary key (ADR-0035 §4b, user-confirmed). pub parent_column: Option, 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 ` — the value used when an `insert` /// omits this column (ADR-0029). Simple-mode form. pub default: Option, /// `CHECK ()` — every row must satisfy this boolean /// expression (ADR-0029). Simple-mode form (a typed `Expr`). pub check: Option, /// 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, /// 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, } 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, 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 to .`. /// `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 from .` /// (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, /// 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, }, DropTable { name: String, }, /// Advanced-mode SQL `DROP TABLE [IF EXISTS] ` (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, primary_key: Vec, /// 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>, /// Table-level `CHECK ()` 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, /// 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, 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, check: Option, }, /// 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 (`__to_`). /// `create_fk` requests the child column be created /// automatically with the appropriate type if it is missing. AddRelationship { name: Option, 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 `__idx`. AddIndex { name: Option, table: String, columns: Vec, }, /// 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] ` (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] [] /// ON
(, …)` (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, table: String, columns: Vec, unique: bool, if_not_exists: bool, }, /// Advanced-mode SQL `ALTER TABLE
` (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>, values: Vec, }, /// 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, limit: Option, }, /// 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, }, /// 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, 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>>, }, /// 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 = ` /// 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)>, }, /// 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, }, /// 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 }, /// 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 }, /// Switch the persistent input mode. Mode { value: ModeValue }, /// Show or set the messages verbosity. Messages { value: Option }, /// 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, 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), /// `a AND b AND …` — at least two children. And(Vec), /// `NOT `. Not(Box), /// 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 { /// ` ` — one of the six comparisons. Compare { left: Operand, op: CompareOp, right: Operand, }, /// ` [NOT] LIKE ` — `%` / `_` wildcards. Like { target: Operand, pattern: Operand, negated: bool, }, /// ` [NOT] BETWEEN AND `. Between { target: Operand, low: Operand, high: Operand, negated: bool, }, /// ` [NOT] IN ([, …])`. In { target: Operand, items: Vec, negated: bool, }, /// ` 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 }, } /// Which schema collection a `show ` list command displays (V5). /// /// The bare plural forms list every item of the kind across the /// project; the singular `show table ` (a separate /// `Command::ShowTable`) shows one. The singular `show /// relationship ` / `show index ` 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 [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), /// `DROP COLUMN ` — 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 TO ` — reuses `do_rename_column`. RenameColumn { old: String, new: String }, /// `ALTER COLUMN 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 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 DROP NOT NULL` (ADR-0035 Amendment 2). /// Decomposes to `do_drop_constraint(NotNull)`. DropColumnNotNull { column: String }, /// `ALTER COLUMN SET DEFAULT ` (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 DROP DEFAULT` (ADR-0035 Amendment 2, ISO). /// Decomposes to `do_drop_constraint(Default)`. DropColumnDefault { column: String }, /// `ADD [CONSTRAINT ] (CHECK (…) | UNIQUE (…) | FOREIGN KEY /// (…) REFERENCES …)` — a table-level constraint (ADR-0035 §4g). The /// `name` is the `CONSTRAINT ` 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, constraint: Box, }, /// `DROP CONSTRAINT ` — drops a named table-level CHECK or a /// named FK (relationship), resolved by name (ADR-0035 §4g). DropConstraint { name: String }, /// `RENAME TO ` — 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 /// ] …` (ADR-0035 §4g). #[derive(Debug, Clone, PartialEq, Eq)] pub enum TableConstraint { /// `CHECK ()` — 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 (, …)` — a composite UNIQUE constraint. Unique { columns: Vec }, /// `FOREIGN KEY () REFERENCES

[(

)] [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 ` 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] ` /// 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 // `
.` it acts on (ADR-0029 §2.2). Self::AddConstraint { table, column, .. } | Self::DropConstraint { table, column, .. } => { format!("{table}.{column}") } _ => self.target_table().to_string(), } } }