757711f2bf
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].
1047 lines
42 KiB
Rust
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(),
|
|
}
|
|
}
|
|
}
|