Files
rdbms-playground/docs/adr
claude@clouddev1 19d3cd3306 docs: ADR-0035 — record two /runda refinements (IF [NOT] EXISTS, INTEGER PRIMARY KEY)
Pre-implementation /runda round settled two open micro-calls before 4a,
both user-confirmed:

- IF [NOT] EXISTS admitted (no-op-that-succeeds-with-a-note), not
  refused — a near-universal cross-vendor idiom (PostgreSQL, MySQL,
  SQLite, Oracle 23ai), reclassified into scope rather than treated as
  an engine-specific spelling. Touches §3/§4/§12/§13 (4a, 4c).
- INTEGER PRIMARY KEY maps to a plain int PK, not auto-increment;
  serial stays the sole auto-increment type (§3).

README index updated in the same edit per the lockstep rule.
2026-05-24 22:31:44 +00:00
..

Architecture Decision Records

This directory contains the project's ADRs, recorded per ADR-0000.

Index

  • ADR-0000 — Record architecture decisions
  • ADR-0001 — Language and TUI framework
  • ADR-0002 — Database engine
  • ADR-0003 — Input modes and command dispatch
  • ADR-0004 — Project file format
  • ADR-0005 — Column type vocabulary
  • ADR-0006 — Undo snapshots and replay logAccepted. The replay/journal half (U3/U4) shipped via ADR-0034; the undo/snapshot half (U1/U2) is settled by Amendment 1 (2026-05-24) and implemented 2026-05-24 (plan: docs/plans/20260524-adr-0006-undo-snapshots.md; ring in src/undo.rs, worker hook in src/db.rs). Amendment 1 supersedes the original "snapshots only before destructive operations" model: a snapshot is taken before every data/schema mutation (DSL + SQL) for familiar single-step (Ctrl-Z) undo — so the confirmation collapses to naming the one command being undone (no db-diff). Snapshot is a hybrid whole-project copy — database via the online backup API plus project.yaml/data/*.csv as files — reconciling this ADR with ADR-0015's "text is authoritative, db is derived"; undo restores all three directly. Staged before the mutation's transaction, finalised after the db commit (preserves ADR-0015 §6 commit-db-last); rolled-back ops leave no snapshot. Persisted ring under .snapshots/, N = 50 (raised from 10), git-ignored + export-excluded + temp-cleanup-aware. redo supported, redo stack discarded on new work. Batch ops record one undo step (replay + future batch via a Begin/EndBatch worker primitive); import is outside undo (it switches projects per ADR-0015 §11, leaving the current project untouched). A --no-undo CLI flag disables snapshotting (hardware escape hatch). Adds the backup feature to rusqlite
  • ADR-0007 — Sharing and export
  • ADR-0008 — Testing approach
  • ADR-0009 — DSL command syntax conventions
  • ADR-0010 — Database access via a dedicated worker thread
  • ADR-0011 — Foreign-key column type compatibility
  • ADR-0012 — Internal metadata for user-facing column types
  • ADR-0013 — Relationships, naming, and the rebuild-table strategy
  • ADR-0014 — Data operations, value literals, and the auto-show pattern
  • ADR-0015 — Project storage runtime
  • ADR-0016 — Pretty table rendering for data and structure views
  • ADR-0017 — Column type-change compatibility
  • ADR-0018 — Auto-fill contracts for serial and shortid columns
  • ADR-0019 — Friendly error layer (H1) and i18n message catalog
  • ADR-0020 — Tokenization layer for the DSL parser
  • ADR-0021 — Parser-as-source-of-truth for H1a (per-command usage in parse errors)
  • ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)Amendment 1 supersedes §12's simple-mode-only carve-out: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. ambient_hint_in_mode + hint_resolution_at_input_in_mode + expected_for_hint_snapshot thread Mode; render_hint_panel calls ambient for all modes (no more advanced-mode None); the one-shot : sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; Amendment 2 reverses the handoff-14 keywords-first candidate ordering — schema identifiers (table/column/relationship names) now sort before keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the tok_identifier/tok_keyword colour split marks the boundary); shipped with a walk_repeated fix that surfaces a list item's trailing optionals at a clean boundary (order by Name asc/desc, select Name as, create table … Code(text) not/unique/default/check; the , separator deliberately not surfaced); records a deferred two-line hint box for growing lists
  • ADR-0023 — Unified declarative grammar tree — direction (superseded for execution detail by ADR-0024)
  • ADR-0024 — Unified grammar tree: execution planAccepted, the executable spec — implemented (Phases AF; Phase F shipped "minimal", parser.rs retained as the router — see the ADR's Phase F implementation note)
  • ADR-0025 — IndexesAccepted, add index / drop index, persistence, rebuild-table preservation, and items-list display (C3 index portion + S2)
  • ADR-0026 — Complex WHERE expressionsAccepted, stratified recursive expression grammar (AND/OR/NOT, comparisons, LIKE, IS NULL, IN, BETWEEN) for update / delete / show data filters; show data gains where + limit; adds the Subgrammar node and a recursive Expr AST (C5a)
  • ADR-0027 — Input-field validity indicatorAccepted, a debounced [ERR] / [WRN] marker at the input row's right edge, backed by a walker diagnostics-severity model (parse-outcome + schema-existence); advisory, never blocks submission (S6); Amendment 1 adds a LIKE-on-numeric-column WARNING
  • ADR-0028 — Query plans (EXPLAIN QUERY PLAN)Accepted, an explain prefix command over show data / update / delete; an annotated, span-styled plan tree; introduces the OutputLine styled-runs mechanism (ADR-0016's deferred per-span styling) (QA1 / QA2)
  • ADR-0029 — Column constraints (NOT NULL / UNIQUE / CHECK / DEFAULT)Accepted, the four column-level constraints declared in the column-spec suffix (create table / add column) and modified on existing columns via add constraint … / drop constraint …; a pre-flight dry-run guards populated columns; CHECK reuses the ADR-0026 expression grammar via Subgrammar (C3)
  • ADR-0030 — Advanced mode: the standard-SQL surfaceAccepted, SQL added as grammar within the unified grammar tree (ADR-0024), not a separate batch parser — so SQL gets the same completion / highlighting / hints / parse-errors as the DSL; mode gates the SQL forms; DDL routes through the typed Command executor (metadata + type vocabulary preserved), DML and SELECT execute as validated SQL; engine-neutral posture, the DSL→SQL teaching echo; supersedes ADR-0001's sqlparser-rs reservation; phased plan (Q1 / Q2 / Q4)
  • ADR-0031 — The SQL expression grammarAccepted, the stratified SQL expression grammar fragment commissioned by ADR-0030 §3: a single precedence ladder (OR/AND/NOT, the comparison/LIKE/IN/BETWEEN/IS NULL predicate set, arithmetic incl. ||, function calls, CASE) — the superset of ADR-0026's DSL WHERE grammar, authored as a parallel fragment so simple mode is untouched; pure validation, builds no AST (consumers run/store SQL as text per ADR-0030 §4/§6); reuses ADR-0026's Subgrammar recursion + depth cap unchanged; subquery expressions and qualified column refs deferred to ADR-0030 Phase 2
  • ADR-0032 — The full SQL SELECT grammarAccepted, the Phase-2 grammar commissioned by ADR-0030 §3: full SELECT with INNER/LEFT/RIGHT/FULL OUTER/CROSS joins, GROUP BY/HAVING, all four set ops (UNION/UNION ALL/INTERSECT/EXCEPT), WITH and WITH RECURSIVE CTEs, LIMIT … OFFSET, DISTINCT, t.*, and bare-alias projection (lifting Phase-1 §4.2); additive extensions to ADR-0031's sql_expr for scalar subqueries, IN (SELECT …), [NOT] EXISTS, and qualified column refs (redeeming ADR-0031 §7 OOS-1/OOS-2); grammar-recursion via Subgrammar(&SQL_SELECT_COMPOUND) reuses ADR-0026's MAX_SUBGRAMMAR_DEPTH = 64 cap unchanged; softens ADR-0030 §8's "ambient assistance comes for free" claim: completion scope needs new WalkContext accumulators (a from_scope_stack of ScopeFrames holding from_scope / cte_bindings / projection_aliases), a new walker node variant Node::ScopedSubgrammar(&Node) as the push/pop trigger (existing Node::Subgrammar unchanged so DSL Expr and sql_expr recursion are unaffected), qualified-prefix completion narrowing, body-projection-derived CTE column resolution (so SELECT * and explicit-projection CTE bodies both yield real column completion past cte_alias.|), and a post-walk fixup pass that re-resolves projection-list identifier highlighting/validity once FROM is parsed (the projection-before-FROM problem); classifies every Phase-2 validation case against ADR-0027's ERROR/WARNING guideline (§11): five new diagnostic.* keys for parse-time-detectable cases (unknown qualifier, ambiguous column, projection-alias misplaced, CTE/compound arity mismatch) plus eight engine.* translation keys; a MatchedPath-walking predicate-warnings variant that closes the Phase-1 gap where SQL WHERE expressions emitted no LIKE-on-numeric / = NULL / type-mismatch warnings (ADR-0027 Amendment 1 finally extends to the SQL surface); adds a worker-side post-prepare type-resolution pass via engine column-origin metadata so bare column refs recover their playground type (partially lifting Phase-1 §4.5, the bool→0/1 case) — Cargo.toml gains column_metadata to rusqlite features (verified against pinned 0.39.0); __rdbms_* rejection extended to every new table-source slot; Amendment 1 narrows §12's resolution rule from a grammar-side structural classification to "trust the engine's column-origin metadata verbatim" after an empirical probe showed origin metadata follows through non-recursive CTEs, scalar subqueries, derived tables, set ops, and joins — the one structural exception is recursive CTE result columns, which return None and stay typeless; Amendment 2 records that §10.6's "rewrite the highlight class" prescription is realised via the two-pass schema-existence diagnostic + the renderer's diagnostic-overlay path (no separate per-byte rewrite step needed; no new HighlightClass variant), and that the projection-before-FROM completion narrowing has been improved by an src/completion.rs look-ahead probe when the leading walk's from_scope is empty but the full input parses
  • ADR-0033 — The full SQL DML grammar (INSERT / UPDATE / DELETE)Accepted (implemented + verified through sub-phase 3k, 2026-05-23; phase-exit report docs/handoff/20260523-phase-3-verification.md), the Phase-3 grammar commissioned by ADR-0030 §3: single- and multi-row INSERT (incl. INSERT … SELECT recursing through ADR-0032's SQL_SELECT_COMPOUND), UPDATE with SET assignment list, DELETE, all three optionally followed by RETURNING projection_list, plus full ON CONFLICT … DO NOTHING / DO UPDATE UPSERT on INSERT; fixes the DSL-vs-SQL dispatch architecture for shared entry words (insert/update/delete): SQL-first / DSL-fallback in Advanced mode via a Choice(SQL_shape, DSL_shape) per shape, gated by a new walker capability Node::Guard(fn) — a zero-byte-consumption gating node that fails the enclosing Seq with a ValidationError; carries Command::SqlInsert / SqlUpdate / SqlDelete variants and do_sql_* worker handlers each of which knows the target table (for re-persistence) and the returning: bool flag (for DataResult routing); shortid auto-fill mirrors the DSL do_insert mechanism via worker post-fill; SQL DELETE produces the same per-relationship cascade summary the DSL DELETE does (ADR-0014 parity); three new walker diagnostics (insert_arity_mismatch ERROR, auto_column_overridden WARNING, not_null_missing WARNING) with positive + negative tests each; OOS list explicitly carves out DEFAULT VALUES (the project's planned seed feature), SQLite-specific OR REPLACE / OR IGNORE / OR ABORT / OR FAIL / OR ROLLBACK prefixes, UPDATE FROM multi-table updates, and WITH-prefixed DML; the excluded keyword inside ON CONFLICT DO UPDATE is a deliberate carve-out from ADR-0030 §7's engine-neutral posture (no standard-SQL UPSERT spelling exists that SQLite and PostgreSQL share); eleven phased sub-phases each with explicit exit gates + written DA gate, opening with the dispatch mechanism before any DML grammar lands; initial DA review recorded seven critiques that were resolved before status moved to Proposed; Amendment 1 supersedes §2's dispatch mechanism: the originally-chosen Node::Guard(fn) + Choice(SQL_shape, DSL_shape) was found during 3a to be unworkable as framed (any guard-in-Choice mechanism forces a walk_choice change — walk_choice only falls through on NoMatch, so Simple-mode valid-DSL would wrongly surface "this is SQL", and walk_seq treats a NoMatch past idx 0 as a hard Failed, breaking Advanced-mode DSL fall-through); replaced by category-grouped, mode-aware dispatch in walker::walk (each REGISTRY entry tagged CommandCategory::{Simple, Advanced}, generalising the existing whole-command is_advanced_only gate), shared entry words carrying a node in both groups, no Node::Guard and no walk_choice/walk_seq change, advanced-mode completion SQL-first with DSL as a full-line fallback; Amendment 2 (sub-phase 3f) supersedes §7's cascade mechanism: the WHERE-injected per-child pre-count rested on a premise that was factually wrong about the DSL handler (which detects cascades by before/after row-count diffing inside a transaction, not by Expr-derived pre-count subqueries) and would have broken the §2 parity promise by reporting SET NULL the DSL path doesn't; replaced by mirroring do_delete's count-diff exactly (verbatim DELETE executes, child-count diff observes the cascade — ON DELETE CASCADE row removals only, SET NULL deferred for both paths to preserve parity), which shares the render-layer formatter for free via CommandOutcome::Delete and withdraws risk R2 (no WHERE-byte extraction, no N+1 subquery); Amendment 3 (sub-phase 3j) records the command-identity model and defers the execution-mode side-channel: a command is the typed outcome of a mode-rooted grammar path and its identity is intrinsic (Advanced mode tries SQL first, falls back to the Simple DSL command when no SQL branch matches a token, e.g. delete … --all-rows; note update … --all-rows does not fall back — the SQL SET expression eats --all-rows, harmless since the engine treats it as a comment); Simple mode commits the DSL candidate for shared words so the real DSL error surfaces, and when that line would also run in advanced mode the rendering layer combines them — DSL error plus an advanced_mode.also_valid_sql pointer ("… (valid as SQL in advanced mode)") — keeping the actionable DSL fix while pointing at advanced mode; bare "this is SQL" is reserved for entry words with no DSL form (select/with); a fully-overlapping input (insert … values …) legitimately yields two distinct commands (Command::Insert typed-AST vs Command::SqlInsert validated-text) that do the same thing but execute differently (ADR-0030 §4), so each is tested in the mode that produces it; corrects the plan's 3j exit-gate premise that the DSL DML tests run in Simple mode (they call parse_command, which defaults to Advanced) — the real invariant is "Simple-mode behaviour unchanged, Advanced mode SQL-first, DSL grammar tested in Simple mode, both variants tested in their producing mode", with §6/§7 parity keeping the paths observably equivalent; and defers to its own future ADR the execution-time mode side-channel (three-way Mode: simple/advanced/advanced-one-shot threaded through Action→worker, for mode-dependent output like echoing generated SQL) — today only the rendering side-channel OutputLine.mode_at_submission exists, and the three-way distinction is not required for Phase 3 dispatch correctness
  • ADR-0034 — history.log as a complete command journal; replay reads success-onlyAccepted, resolves a three-way tension in history.log's roles found while implementing ADR-0033 3f: (1) the persistent log is success-only while the in-memory Up/Down recall ring records every submission (success or failure, "so users can recall and edit typo'd commands"), and the ring is re-seeded from the log on project open — so failed commands are recallable within a session but silently lost across sessions; (2) replay wants the state-building (successful) commands while recall wants everything typed, which one success-only file cannot serve; (3) replay history.log never actually worked — run_replay parses each whole line through the DSL parser with no understanding of the <ts>|<status>|<source> record shape, so a real log fails on line 1, and no test ever fed the pipe format to replay (the replay_history_log_records_subcommands_only test only checks what replay writes, never replays the log as input). Decision: history.log becomes a complete journal — every submission recorded, tagged ok/err via the status field the format already reserved (ADR-0015 §5) — and each consumer filters: hydration reads all records (cross-session recall matches in-session), replay reads ok only (and learns the journal format, while still accepting bare-command .commands scripts; detection by the leading timestamp+status prefix so a | inside a bare command isn't misread). Successful commands stay journalled transactionally by the worker; failed commands are journalled err best-effort from the runtime/app error path (a parse failure never reaches the worker). Amends ADR-0006's "successfully executed" wording and ADR-0015 §5 ("status always ok") / §12 (hydration). Code deferred to two tracked test-first sub-tasks (journal-failures+filtering; replay-parses-journal-format); existing all-ok logs need no migration; implemented 2026-05-24 (plan docs/plans/20260524-adr-0034-history-journal.md); Amendment 1 (2026-05-24): replay filters out app-lifecycle commands — a working replay history.log (the §3 fix) exposed that the journal also records save as/load/new/export/import/rebuild/mode (which would panic the worker dispatch or abort the replay), so replay now re-applies only schema/data write commands and skips every Command::App + nested Command::Replay; all skips continue (never abort — reversing the prior nested-replay refusal, so a journal containing a once-run replay needs no hand-editing, and the infinite-loop footgun is closed by construction), with a [skip] warning on import and nested-replay skips (their omission can leave replayed state incomplete) and silent skips for the rest; replay.error_nested removed, replay.skipped_import/replay.skipped_replay added, ReplayCompleted carries warnings
  • ADR-0035 — Advanced-mode SQL DDLProposed (design agreed 2026-05-24; implementation phased + pending), Phase 4 of the ADR-0030 roadmap (peer of 0031/0032/0033) and clarifies ADR-0030 §4. Advanced-mode CREATE/DROP/ALTER TABLE + CREATE/DROP INDEX get their own per-statement commands (SqlCreateTable/SqlAlterTable/SqlDropTable/SqlCreateIndex/SqlDropIndex), like DML's Sql* set — but unlike DML they execute structurally, not verbatim (raw execution would lose the playground's types, named relationships, and STRICT; "verbatim" was a DML convenience, not a rule). Handlers reuse the low-level schema/metadata helpers where the operation matches simple mode and stand alone where the SQL surface is richer (clarity over forced refactoring); simple mode is untouched (additive). Dispatch: create/drop reuse ADR-0033 Amendment 1's category-grouped mode-aware dispatch (SQL-first, simple fallback); alter is a new advanced-only entry word. Full surface (no pre-emptive cuts, Q4): CREATE TABLE with column + table constraints, single/compound PRIMARY KEY, inline + table-level FOREIGN KEYnamed relationships (one statement = one command = one undo step, ADR-0006); ALTER TABLE add/drop/rename column, ALTER COLUMN TYPE, add/drop constraint, add FK, RENAME TO (advanced-only table rename — new low-level op renaming the table + its CSV + the relationship metadata, closing the rename half of C1); CREATE [UNIQUE] INDEX / DROP INDEX. Type slot accepts the ten playground keywords and standard-SQL aliases (integerint, varchartext, timestampdatetime, …; length args accepted-and-ignored; no engine type names in/out — ADR-0030 §5). CHECK/DEFAULT reuse ADR-0031 sql_expr. Pre-implementation /runda refinements (2026-05-24, user-confirmed): CREATE TABLE/DROP TABLE admit IF [NOT] EXISTS (no-op-that-succeeds-with-a-note — a near-universal cross-vendor idiom, reclassified into scope, not engine-specific); INTEGER PRIMARY KEY maps to a plain int PK, not auto-increment (serial stays the sole auto-increment type). Column-type-conversion is unified (ADR-0017 engine, mode-appropriate policy): clean auto-converts and incompatible/own-type-static cases refuse in both modes, but a lossy change refuses-by-default in simple mode (--force-conversion opts in) while advanced mode performs it with a loss note and relies on undo as the safety net — no force flag, no dropping to simple mode (a payoff of shipping ADR-0006 first). OOS: views/triggers/txn-control/PRAGMA/etc. (ADR-0030 §3), the Postgres USING clause, and the DSL→SQL teaching echo (ADR-0030 Phase 5). Nine sub-phases (4a4i), each with exit + DA gates