ae57c6fc82
The output tag was tinted by submission mode for every line kind, so a [system] line and an [error] line rendered with an identical leftmost tag — distinguishable only by body colour. And flooding the whole error body in red made long messages hard to read. Colour the tag by message status instead (its OutputKind): [system] → green, [error] → red; the echo tag keeps the mode tint (ADR-0037's actual purpose — per-command success rides the ✓/✗ marker). Bodies go neutral; the error body stays bold for weight (rustc-style: severity- coloured label, readable bold message). Yields a status traffic-light matching the ✓/✗ palette. Narrows ADR-0037's mode side-channel to the echo line it was always for. ADR-0037 Amendment 1; closes the tag-colour gap ADR-0040 flagged as OOS.
50 KiB
50 KiB
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 log — Accepted. 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 insrc/undo.rs, worker hook insrc/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 plusproject.yaml/data/*.csvas 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.redosupported, redo stack discarded on new work. Batch ops record one undo step (replay+ future batch via a Begin/EndBatch worker primitive);importis outside undo (it switches projects per ADR-0015 §11, leaving the current project untouched). A--no-undoCLI flag disables snapshotting (hardware escape hatch). Adds thebackupfeature torusqlite - 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
serialandshortidcolumns - 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_snapshotthreadMode;render_hint_panelcalls ambient for all modes (no more advanced-modeNone); 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; thetok_identifier/tok_keywordcolour split marks the boundary); shipped with awalk_repeatedfix 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; Amendment 3 makes the ambient-hint fallback rung schema-aware — Amendment 1's bottom-rungparse_command_in_modewas schemaless while every earlier rung was not, so between-values insert hints pointed at)(type-blind close) instead of,and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now usesparse_command_with_schema_in_mode, no extra walk, with the friendly arity diagnostic still winning at its higher rung; Amendment 4 gives column types a dedicated highlight class — bothNode::Ident.highlight_overrideand theWord.highlight_overridefield were dead (driver destructured the former to_,walk_wordhardcodedKeyword); now both wired through, with a newHighlightClass::Type+ eighthThemefieldtok_type(a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the threeIdentSource::Typesslots opt in viaSome(Type)(advanced-mode single-word SQL aliases —float,varchar, … per ADR-0035 §3 — ride along for free), and the two-worddouble precisionalias opts in via the newWord::type_keywordconstructor so it matches its synonyms; Amendment 5 lets the hint panel grow for long prose hints — a fixed one-row panel clipped long field-value/usage hints past the first line (issue #12);resolve_hint_linesnow pre-wraps prose andrender_right_columnsizes the panel to the line count (1 row default, up toMAX_HINT_ROWS=3, reclaimed when short) with aclamp_wrappedellipsis backstop; the candidate list still scrolls horizontally on one row (Amendment 2's deferred two-line candidate box stays deferred); also shortens the 299-charparse.usage.sql_create_tablesynopsis to a terse one-liner (full grammar remains inhelp.ddl.sql_create_table); Amendment 6 adds a curated SQL function-name list (src/dsl/sql_functions.rs,KNOWN_SQL_FUNCTIONS— aggregates + common + broader scalars;castdeliberately excluded as itsCAST(x AS type)syntax isn't a plain-call shape) as the single source of truth shared by two consumers at thesql_expr_identslot (ADR-0031 §1): issue #15 offers the functions as Tab candidates under a newCandidateKind::Function+ ninthThemecolourtok_function(a blue distinct from keyword/identifier/type, parallel to Amendment 4'stok_type) so a learner discoverssum/upper/…; issue #16 restores the typing-time column-typo flag the issue-#6 fix had dropped wholesale at this slot —invalid_ident_at_cursornow bails only when the partial prefix-matches a known function, else falls through to the schema-column check, soselect Agxwarns again at typing time whileselect sumdoes not (the issue-#6 lockdown tests + the submit-timeunknown_columndiagnostic path are untouched, and the no-validation-allowlist posture stands); see ADR-0031's status note for the grammar-side anchor - ADR-0023 — Unified declarative grammar tree — direction (superseded for execution detail by ADR-0024)
- ADR-0024 — Unified grammar tree: execution plan — Accepted, the executable spec — implemented (Phases A–F; Phase F shipped "minimal",
parser.rsretained as the router — see the ADR's Phase F implementation note) - ADR-0025 — Indexes — Accepted (Amendment 1, 2026-05-25: UNIQUE indexes admitted on the advanced-mode surface via
CREATE UNIQUE INDEX— ADR-0035 §4d; theIndexSchema.uniqueflag round-trips throughproject.yamlwith no new metadata table since the engine reports uniqueness natively; simple-modeadd unique indexstays deferred),add index/drop index, persistence, rebuild-table preservation, and items-list display (C3index portion +S2) - ADR-0026 — Complex WHERE expressions — Accepted, stratified recursive expression grammar (
AND/OR/NOT, comparisons,LIKE,IS NULL,IN,BETWEEN) forupdate/delete/show datafilters;show datagainswhere+limit; adds theSubgrammarnode and a recursiveExprAST (C5a) - ADR-0027 — Input-field validity indicator — Accepted, 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 aLIKE-on-numeric-column WARNING - ADR-0028 — Query plans (
EXPLAIN QUERY PLAN) — Accepted, anexplainprefix command overshow data/update/delete; an annotated, span-styled plan tree; introduces theOutputLinestyled-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 viaadd constraint …/drop constraint …; a pre-flight dry-run guards populated columns;CHECKreuses the ADR-0026 expression grammar viaSubgrammar(C3) - ADR-0030 — Advanced mode: the standard-SQL surface — Accepted, 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
Commandexecutor (metadata + type vocabulary preserved), DML andSELECTexecute as validated SQL; engine-neutral posture, the DSL→SQL teaching echo; supersedes ADR-0001'ssqlparser-rsreservation; phased plan (Q1/Q2/Q4); §13 OOS-2 (EXPLAIN of advanced SQL) superseded by ADR-0039 - ADR-0031 — The SQL expression grammar — Accepted, the stratified SQL expression grammar fragment commissioned by ADR-0030 §3: a single precedence ladder (
OR/AND/NOT, the comparison/LIKE/IN/BETWEEN/IS NULLpredicate set, arithmetic incl.||, function calls,CASE) — the superset of ADR-0026's DSLWHEREgrammar, 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'sSubgrammarrecursion + depth cap unchanged; subquery expressions and qualified column refs deferred to ADR-0030 Phase 2; status note (2026-05-30) records that ADR-0022 Amendment 6 layers a curated known-function list on thesql_expr_identslot (§1) for completion + the typing-time typo hint (issues #15/#16) — the grammar itself is unchanged, and the no-validation-allowlist posture stands - ADR-0032 — The full SQL
SELECTgrammar — Accepted, the Phase-2 grammar commissioned by ADR-0030 §3: fullSELECTwithINNER/LEFT/RIGHT/FULL OUTER/CROSSjoins,GROUP BY/HAVING, all four set ops (UNION/UNION ALL/INTERSECT/EXCEPT),WITHandWITH RECURSIVECTEs,LIMIT … OFFSET,DISTINCT,t.*, and bare-alias projection (lifting Phase-1 §4.2); additive extensions to ADR-0031'ssql_exprfor scalar subqueries,IN (SELECT …),[NOT] EXISTS, and qualified column refs (redeeming ADR-0031 §7 OOS-1/OOS-2); grammar-recursion viaSubgrammar(&SQL_SELECT_COMPOUND)reuses ADR-0026'sMAX_SUBGRAMMAR_DEPTH = 64cap unchanged; softens ADR-0030 §8's "ambient assistance comes for free" claim: completion scope needs newWalkContextaccumulators (afrom_scope_stackofScopeFrames holdingfrom_scope/cte_bindings/projection_aliases), a new walker node variantNode::ScopedSubgrammar(&Node)as the push/pop trigger (existingNode::Subgrammarunchanged so DSLExprandsql_exprrecursion are unaffected), qualified-prefix completion narrowing, body-projection-derived CTE column resolution (soSELECT *and explicit-projection CTE bodies both yield real column completion pastcte_alias.|), and a post-walk fixup pass that re-resolves projection-list identifier highlighting/validity onceFROMis parsed (the projection-before-FROM problem); classifies every Phase-2 validation case against ADR-0027's ERROR/WARNING guideline (§11): five newdiagnostic.*keys for parse-time-detectable cases (unknown qualifier, ambiguous column, projection-alias misplaced, CTE/compound arity mismatch) plus eightengine.*translation keys; a MatchedPath-walking predicate-warnings variant that closes the Phase-1 gap where SQLWHEREexpressions emitted noLIKE-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.tomlgainscolumn_metadatato 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 ansrc/completion.rslook-ahead probe when the leading walk'sfrom_scopeis 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 reportdocs/handoff/20260523-phase-3-verification.md), the Phase-3 grammar commissioned by ADR-0030 §3: single- and multi-rowINSERT(incl.INSERT … SELECTrecursing through ADR-0032'sSQL_SELECT_COMPOUND),UPDATEwithSETassignment list,DELETE, all three optionally followed byRETURNING projection_list, plus fullON CONFLICT … DO NOTHING / DO UPDATEUPSERT on INSERT; fixes the DSL-vs-SQL dispatch architecture for shared entry words (insert/update/delete): SQL-first / DSL-fallback in Advanced mode via aChoice(SQL_shape, DSL_shape)per shape, gated by a new walker capabilityNode::Guard(fn)— a zero-byte-consumption gating node that fails the enclosing Seq with aValidationError; carriesCommand::SqlInsert/SqlUpdate/SqlDeletevariants anddo_sql_*worker handlers each of which knows the target table (for re-persistence) and thereturning: boolflag (for DataResult routing);shortidauto-fill mirrors the DSLdo_insertmechanism 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_mismatchERROR,auto_column_overriddenWARNING,not_null_missingWARNING) with positive + negative tests each; OOS list explicitly carves outDEFAULT VALUES(the project's planned seed feature), SQLite-specificOR REPLACE/OR IGNORE/OR ABORT/OR FAIL/OR ROLLBACKprefixes,UPDATE FROMmulti-table updates, and WITH-prefixed DML; theexcludedkeyword insideON CONFLICT DO UPDATEis 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-chosenNode::Guard(fn)+Choice(SQL_shape, DSL_shape)was found during 3a to be unworkable as framed (any guard-in-Choicemechanism forces awalk_choicechange —walk_choiceonly falls through onNoMatch, so Simple-mode valid-DSL would wrongly surface "this is SQL", andwalk_seqtreats aNoMatchpastidx 0as a hardFailed, breaking Advanced-mode DSL fall-through); replaced by category-grouped, mode-aware dispatch inwalker::walk(eachREGISTRYentry taggedCommandCategory::{Simple, Advanced}, generalising the existing whole-commandis_advanced_onlygate), shared entry words carrying a node in both groups, noNode::Guardand nowalk_choice/walk_seqchange, 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 byExpr-derived pre-count subqueries) and would have broken the §2 parity promise by reportingSET NULLthe DSL path doesn't; replaced by mirroringdo_delete's count-diff exactly (verbatim DELETE executes, child-count diff observes the cascade —ON DELETE CASCADErow removals only, SET NULL deferred for both paths to preserve parity), which shares the render-layer formatter for free viaCommandOutcome::Deleteand 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; noteupdate … --all-rowsdoes not fall back — the SQLSETexpression 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 anadvanced_mode.also_valid_sqlpointer ("… (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::Inserttyped-AST vsCommand::SqlInsertvalidated-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 callparse_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-wayMode: simple/advanced/advanced-one-shot threaded throughAction→worker, for mode-dependent output like echoing generated SQL) — today only the rendering side-channelOutputLine.mode_at_submissionexists, and the three-way distinction is not required for Phase 3 dispatch correctness; Amendment 4, 2026-05-27 (design agreed, pending impl): reverses Amendment 3'supdate … --all-rowscounter-example as a bug — surfaced by the ADR-0038 echo design. The walker has no--comment support (it lexes two minus operators) while the engine treats--as a comment, soupdate T set x=42 --all-rowswas silently parsed as the expression42 - -all - rowsover non-existent columnsall/rows(an ADR-0027 "flag-if-it-will-fail" case) and matchedSqlUpdate. Decision: the--all-rowssequence makes the SQLUPDATEshape fail, so dispatch falls back to the DSLUpdate { AllRows }— symmetry withdelete … --all-rows; no--comment feature introduced (trailing comments stay unsupported). Invertssql_dml_e2e.rs::e2e_update_all_rows_in_advanced_does_not_fall_back_to_dsl; mechanism settled test-first in the build; folded into the ADR-0038 effort (makesupdate … --all-rowsechoable); Amendment 5, 2026-05-28 (implemented + verified, user-confirmed):advanced_mode.also_valid_sql(the cross-mode pointer from Amendment 3) fires on validity, not just parse — "valid" meaninginput_verdict_in_mode(input, schema, Mode::Advanced) == Nonein the ADR-0027 sense (parse succeeds and no Warning/Error diagnostic from any pass). Surfaced by issue #1: a positionalINSERT INTO T VALUES (…)(no column list) with a value count that didn't match the target's column count parsed in advanced but failed at the engine, so the syntactic-only Amendment-3 gate promised a mode switch that wouldn't help. Closes the gap by (a) extendingdml_insert_arity_diagnostics(§8.1, previously Form A only — its own doc-comment deferred Form B) to also check the no-column-list form against the schema's column count, emitting a newdiagnostic.insert_arity_mismatch_form_bERROR per offending tuple, and (b) refactoringadvanced_alternative_noteto read the validity verdict instead of running its own bespoke check — any static diagnostic added to the pipeline in the future automatically participates in the pointer gate. Side benefit: the[ERR]validity indicator now lights up at typing time for the reported scenario, no longer needing a submit to learn the line is wrong. Tests pinned:insert_form_b_arity_mismatch_under_supply_fires/_over_supply_fires/_match_is_silent/_unknown_table_is_silent(walker);ambient_hint_omits_advanced_pointer_when_form_b_value_count_wouldnt_match(gate);simple_mode_submit_of_sql_construct_appends_advanced_pointer(pointer still fires for genuine SQL-only constructs against a known schema). Amendment 3's "would parse in advanced mode" should henceforth be read as a synonym for "valid in advanced mode" in this stricter sense; the user-confirmed behavioural change is exactly the issue #1 bug case (no other input flips its pointer state) - ADR-0034 —
history.logas a complete command journal; replay reads success-only — Accepted, resolves a three-way tension inhistory.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.lognever actually worked —run_replayparses 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 (thereplay_history_log_records_subcommands_onlytest only checks what replay writes, never replays the log as input). Decision:history.logbecomes a complete journal — every submission recorded, taggedok/errvia 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 readsokonly (and learns the journal format, while still accepting bare-command.commandsscripts; 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 journallederrbest-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 alwaysok") / §12 (hydration). Code deferred to two tracked test-first sub-tasks (journal-failures+filtering; replay-parses-journal-format); existing all-oklogs need no migration; implemented 2026-05-24 (plandocs/plans/20260524-adr-0034-history-journal.md); Amendment 1 (2026-05-24): replay filters out app-lifecycle commands — a workingreplay history.log(the §3 fix) exposed that the journal also recordssave 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 everyCommand::App+ nestedCommand::Replay; all skips continue (never abort — reversing the prior nested-replayrefusal, so a journal containing a once-runreplayneeds no hand-editing, and the infinite-loop footgun is closed by construction), with a[skip]warning onimportand nested-replayskips (their omission can leave replayed state incomplete) and silent skips for the rest;replay.error_nestedremoved,replay.skipped_import/replay.skipped_replayadded,ReplayCompletedcarrieswarnings - ADR-0035 — Advanced-mode SQL DDL — Accepted (design agreed 2026-05-24; validated end-to-end by sub-phases 4a/4a.2/4a.3/4b
CREATE TABLE(incl. foreign keys) + 4cDROP TABLE [IF EXISTS]+ 4dCREATE [UNIQUE] INDEX/DROP INDEX [IF EXISTS]+ 4eALTER TABLEadd/drop/rename column + 4fALTER TABLE … ALTER COLUMN TYPE+ 4gALTER TABLEadd/drop constraint + add FK + 4hALTER TABLE … RENAME TO+ 4i verification sweep (completion merge + simple/advanced completion colour + describe of table-level constraints + self-ref FK indicator + CREATE-TABLE help/usage), implemented 2026-05-25/26 — Phase 4 complete; Amendment 1, 2026-05-26: drop a composite UNIQUE via a derived, engine-neutralunique_<cols>name that reuses the existingDROP CONSTRAINT <name>grammar — no new syntax, no metadata, §4g anonymity intact;describeshows the name; dropping a UNIQUE-covered column now refuses with that name + the drop command), Phase 4 of the ADR-0030 roadmap (peer of 0031/0032/0033) and clarifies ADR-0030 §4. Advanced-modeCREATE/DROP/ALTER TABLE+CREATE/DROP INDEXget their own per-statement commands (SqlCreateTable/SqlAlterTable/SqlDropTable/SqlCreateIndex/SqlDropIndex), like DML'sSql*set — but unlike DML they execute structurally, not verbatim (raw execution would lose the playground's types, named relationships, andSTRICT; "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/dropreuse ADR-0033 Amendment 1's category-grouped mode-aware dispatch (SQL-first, simple fallback);alteris a new advanced-only entry word. Full surface (no pre-emptive cuts,Q4):CREATE TABLEwith column + table constraints, single/compoundPRIMARY KEY, inline + table-levelFOREIGN KEY→ named relationships (one statement = one command = one undo step, ADR-0006);ALTER TABLEadd/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 and table-CHECK metadata, closing the rename half ofC1);CREATE [UNIQUE] INDEX/DROP INDEX. Type slot accepts the ten playground keywords and standard-SQL aliases (integer→int,varchar→text,timestamp→datetime, …; length args accepted-and-ignored; no engine type names in/out — ADR-0030 §5).CHECK/DEFAULTreuse ADR-0031sql_expr. Pre-implementation/rundarefinements (2026-05-24, user-confirmed):CREATE TABLE/DROP TABLEadmitIF [NOT] EXISTS(no-op-that-succeeds-with-a-note — a near-universal cross-vendor idiom, reclassified into scope, not engine-specific);INTEGER PRIMARY KEYmaps to a plainintPK, not auto-increment (serialstays 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-conversionopts in) while advanced mode performs it with a loss note and relies onundoas 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 PostgresUSINGclause, and the DSL→SQL teaching echo (ADR-0030 Phase 5). Sub-phases 4a–4i, plus 4a.2 (per-columnCHECK/DEFAULTvia rawsql_exprtext —sql_expris validate-only, noExprAST — + compositeUNIQUE(a,b); no new internal table) and 4a.3 (table-level/multi-columnCHECK, landed via the new__rdbms_playground_table_checksmetadata table because SQLite has no PRAGMA for CHECK; the builder tells a table-level CHECK from a column-level one by element position) and 4b (foreign keys — inlineREFERENCES+ table-levelFOREIGN KEY→ ADR-0013 named relationships in the create transaction, one undo step; self-references + bareREFERENCES <parent>supported, user-confirmed) and 4c (DROP TABLE [IF EXISTS]→SqlDropTable, reusingdo_drop_table;IF EXISTSis a no-op-with-note viaDropOutcome::Skipped) and 4d (CREATE [UNIQUE] INDEX [IF NOT EXISTS] [<name>] ON <T> (cols)→SqlCreateIndexandDROP INDEX [IF EXISTS] <name>→SqlDropIndex, reusingdo_add_index/do_drop_index;CREATE UNIQUE INDEXadmitted — ADR-0025 Amendment 1 — via an additiveIndexSchema.uniqueflag that round-trips throughproject.yamland rebuild, with[unique]markers in the structure view + items panel, while simple-modeadd unique indexstays deferred;IF [NOT] EXISTSreuses the 4c skip path;create/dropeach gain a second advanced node, exercising the all-candidates dispatch) and 4e (ALTER TABLEadd/drop/rename column →SqlAlterTable;alteris a new advanced-only entry word, runtime-decomposed to the existingdo_add_column/do_drop_column/do_rename_column— no new worker layer;do_add_columnextended to consume rawdefault_sql/check_sqlso ADD COLUMN reaches CREATE-TABLE constraint parity; drop/rename refuse a column any CHECK references (table-level AND column-level, incl. a column's own self-check on rename) — the 4a.3 deferral, via a raw-CHECK-text tokenizer in the shared executors, so it guards both surfaces and fixes a latent rename-drift bug; SQL DROP COLUMN refuses an index-covered column with no--cascadespelling; the column executors +do_add_indexgained an internal-__rdbms_*-table guard — all user-confirmed) and 4f (ALTER TABLE … ALTER COLUMN TYPE→ a fourthAlterTableAction, runtime-decomposed to the existingchange_column_typewithChangeColumnMode::ForceConversion— which is the §7 advanced policy: lossy converts with a note (no force flag), incompatible + ADR-0017 static refusals (↔ blob, same-type,date ↔ datetime, non-int → serial) still refuse, whileint → serialis allowed (auto-fills nulls + UNIQUE, ADR-0018 §8 — the §7 "→serial refused" summary is looser than the code); the builder discriminates the fourth branch by thetypekeyword (unique — ADD COLUMN's type is an ident), the type slot reusesSQL_TYPE; the internal-__rdbms_*guard was folded intodo_change_column_type, closing the simplechange columnexposure too — user-confirmed) and 4g (ALTER TABLE … ADD [CONSTRAINT <name>] (CHECK | UNIQUE | FOREIGN KEY)+DROP CONSTRAINT <name>; ADD = CHECK + composite UNIQUE + FK, withADD PRIMARY KEYand a named UNIQUE refused — composite UNIQUE is anonymous in our model; each ADD reuses a low-level path (table-CHECK/UNIQUE rebuild with a dry-run guard; FK →add_relationship, bareREFERENCES <P>→ parent single-PK), DROP CONSTRAINT resolves the name to a table-CHECK then a child-side FK; named table-CHECKs round-trip via a nullablenamecolumn on__rdbms_playground_table_checks(rebuild-only arrival — pre-4g projects gain it onrebuild, a named add on an un-upgraded project is refused with a friendly "rebuild first" message) and aproject.yamlcheck_constraintsextension to an{expr, name}mapping (the bare-string form still reads); the internal-__rdbms_*guard was folded intodo_add_constraint/do_add_relationship, completing that guard class — all user-confirmed) and 4h (ALTER TABLE … RENAME TO— the one genuinely new low-level op,do_rename_table: a native engine rename plus one-transaction reconciliation of every metadata row naming the table (__rdbms_playground_columns, both ends of__rdbms_playground_relationships,__rdbms_playground_table_checks), the CSV file (the existing rewrite+delete path — no new persistence method), and CHECK text that qualifies a column with the old table name (T.age→U.age, a planning-/rundafinding — the engine rewrites the live CHECK but the stored text would drift and break a fresh rebuild;rewrite_check_table_qualifierkeeps them in step); grammar splits therenameverb into one branch with an inner Choice on a distinct second keyword (columnvsto), the new-name slot mirroring theCREATE TABLEname slot; refuses same-name / existing-target /__rdbms_*/ non-existent, with case-insensitive collision checks behind an engine-neutral pre-check (a finished-slice/rundafinding — the engine matches names case-insensitively); auto-named indexes and relationships keep their stale names (only table-name columns update — §6 scope); one undo step; advanced-only, closing the rename half ofC1— all user-confirmed) and 4i (the verification sweep that completes Phase 4: the shared-entry-word completion merge + the simple-vs-advanced completion colour-when-mixed with Both→Advanced→Simple block ordering;describeof table-level composite UNIQUE + table CHECK; the self-ref FK pre-submit indicator fix; and the CREATE-TABLE help/usage skeleton refresh). All of Phase 4 (4a–4i) is shipped. Each sub-phase has exit + DA gates; Amendment 2, 2026-05-27 (design agreed, pending impl): a standard-first dialect stance (refines ADR-0030's "standard SQL" posture — ISO spelling is canonical + echoed where one exists; a vendor shorthand may be accepted but isn't canonical; where ISO offers none, one documented vendor spelling is a deliberate extension) + anALTER COLUMNconstraint gap-fill surfaced by the ADR-0038 echo design: makes ISOALTER COLUMN … SET DATA TYPEthe canonical type-change verb withTYPEretained as a synonym (reverses §4f's "noSET DATA TYPE"), and addsSET/DROP DEFAULT(ISO) +SET/DROP NOT NULL(the one documented extension — ISO has no in-place NOT-NULL verb; PostgreSQL's chosen for being type-independent), all rebuild-backed via the existing ADR-0029do_add_constraint/do_drop_constraintexecutors (dry-run + internal-table guards free, no new worker layer), reaching simple↔advanced constraint-mod parity for NOT NULL + DEFAULT; the rebuild stays hidden (Category-1 engine detail, ADR-0038). Residual gap left open + flagged: dropping a column-level (anonymous) UNIQUE/CHECK (no portable name — same class as Am1's parallel gap), which ADR-0038's catalogue marks "no headline echo" - ADR-0036 — Value validation for advanced-mode DML — Accepted (design agreed +
/runda'd 2026-05-26; mechanism then deliberately narrowed from "bind literals via the DSL path" to surgical "validate-and-retain, execute verbatim" after the user resisted consolidating the modes and a concrete auto-fill difference confirmed even the single-row literal case isn't identical across modes; Phases 1–2 implemented 2026-05-26 —INSERT … VALUESandUPDATE … SETliteral validation + offending-value retention, capture-at-parse with no grammar change; Phase 3a implemented 2026-05-26 — live typed-slot hints + numeric-shape highlighting forUPDATE/UPSERTSET col = <literal>via a boundary-aware lookahead (Amendment 1 corrects this ADR's naive-Choicesketch); Phase 3b implemented 2026-05-27 — per-position typed slots forINSERT … VALUES(single/multi-row, Form A/B) via a new zero-widthNode::SetColumnprimitive + an arity-gating tuple lookahead that preserves the §8.1 arity diagnostic; fully implemented). Augments — does NOT supersede — ADR-0030 §4 / ADR-0033 §10: execution stays verbatim, ADR-0033 Amendment 3's two-command identity (InsertvsSqlInsert) stands. The problem (investigated 2026-05-26; characterization testsql_insert.rs::sql_dml_skips_app_level_value_validation_that_the_dsl_enforcesproves it): advanced-mode SQL DML gets none of the DSL's value feedback — a malformeddatelike2025/01/15is silently written, and the offending value is missing from constraint errors — because literal values are spliced into text and discarded (onlySTRICTstorage types check them). Fix (surgical): validate each literal value against its column type before the verbatim insert, and retain it for error reporting — sharing only the per-type validators (Value::bind_for_column/validate_date/shortid::validate), nothing else. No binding, no statement reconstruction, no auto-fill change, no command-identity collapse — because the two gaps are closed by validation + retention alone, and executing the user's own text is already safe. The literal set =NULL/boolean/string/signed-numeric; arithmetic/functions/subqueries/column-refs are expressions (skipped — the engine evaluates them).WHEREnot validated (it's an expression in general; motivation met byVALUES/SET);SELECT/INSERT … SELECT/RETURNING/ON CONFLICTneed no special handling since execution is untouched. Phased: Phase 1 capture-at-parse + validate + retain forINSERT … VALUES(no grammar change, no reparse — closes both proven gaps); Phase 2UPDATE … SETliterals; Phase 3 completion hinting/highlighting (the only part needing a grammar change — a typed-literal slot vssql_exprreusing the DSLTypedValueSlots atdata.rs:141/189/269, discriminated by a boundary-aware lookahead not a naiveChoiceper Amendment 1; split into 3aSET(done) and 3bVALUES(pending); supersedes only Phase 1/2's literal detection, not the validation/enrichment on top). Non-goals: binding/reconstruction, collapsing command identity (Am3 stands), changingserial/shortidauto-fill (requirements.mdX4, a separate possible-bug), a structuralSELECT, a full SQL-expression AST. Embodiesrequirements.mdX5 (share a mechanic, not a command); the neutral "that value" safety net (ADR-0035 Amendment 1) stays correct for genuinely-computed values; Amendment 2 (issue #17, 2026-05-29) brings the §8.1 arity diagnostic to simple mode at parity with advanced: atuple_value_list-style gate (dsl_insert_value_list, simple-mode-gated so advanced is byte-for-byte unchanged) routes a wrong-count DSL insert tuple to the type-blind fallback so it matches and the friendly arity diagnostic fires (instead of a bareexpected,/)``);dml_insert_arity_diagnosticsis now mode-aware (advanced Form B = all columns, simple Form B/C = user-fillable since serial/shortid auto-fill, ADR-0018 §3), counts the DSL Form A role (insert_first_item) and the keyword-less Form C tuple, with new keysinsert_arity_mismatch_form_b_simple/_all_auto; a wrong-count DSL insert now parsesOk+ carries the ERROR diagnostic (the[ERR]verdict), with a unified Ok-arm submit pre-flight (dsl_insert_count_mismatch_notes) blocking dispatch + teaching (the issue #1 Err-arm note retires). Arity-UX parity only — no consolidation of value-handling/execution/auto-fill; the deliberate mode-distinctness stands - ADR-0037 — Execution-time mode side-channel (the three-way submission mode) — Accepted (design agreed 2026-05-27; channel implemented + verified end-to-end by its motivating consumer — ADR-0038's fully-shipped DSL → SQL teaching echo — across handoff-46
04c8e42(channel + first echo slice), handoff-4790479cb(full Bucket A),275c726(Bucket B resolved-name + multi-statement renderers),e6ad1ae(the category-3--dont-convertcaveat — gated on this channel too), and2aab457(the §4 styled-runs rendering polish)), redeems the follow-up deferred by ADR-0033 Amendment 3 (which named this ADR and its motivating consumer). Establishes the channel that lets a command know, at execution time, the effective mode it ran under — so execution can adjust output without touching identity (the motivating case: a DSL-formcreate tableechoing the equivalent SQL when run in advanced mode, silent in simple — ADR-0030 §10, realised by ADR-0038). Introduces a new per-submission enumSubmissionMode{ Simple, Advanced, AdvancedOneShot } — refining Amendment 3's "widenMode" sketch: the persistent inputModestays two-way (mode.rskeeps the one-shot:out of persistent state), and the three-way distinction lives on the per-submission channel where the transient:belongs. Resolved at submit time (Simple+:→AdvancedOneShot; Advanced:is a no-op), threaded throughAction::ExecuteDsl→ worker, output-only (no executor branches its effect on it — Amendment 3 forbids behavioural mode dependence). The worker builds the teaching echo (+ category-3 expansion data — ADR-0038) for DSL-form commands in advanced/one-shot mode and returns it; the App renders it beneath[ok]. Co-located with execution because the echo's harder forms (resolved auto-names, generatedshortids, conversion counts) are worker-computed facts, and gating on mode means the work happens only when shown. Alternatives weighed + rejected: wideningMode(conflates transient/persistent state); App-side gating with the worker always emitting echo data (computes unconditionally, doesn't generalise, re-opens the render-side framing ruled against). Scope: channel + resolution rule only — the renderer/catalogue/Value → SQL-literalare ADR-0038, theALTER COLUMNgap-fill is the ADR-0035 amendment. Amendment 1 (2026-05-31, issue #10): the output tag is colour-coded by message status (itsOutputKind), not the mode — narrowing the side-channel to its stated purpose (the mode tint lives on the echo tag alone).[system]tag → green,[error]tag → red,Echotag → mode tint (the sole exception); bodies go neutral (theme.fg), the error body bold (rustc-style: severity-coloured label, readable bold message — a wall of red prose is harder to read). Yields a status traffic-light (green = ok, red = error) matching the ADR-0040 ✓/✗ markers; supersedes issue #10's three sketched options and closes the tag-colour gap ADR-0040 had flagged as orthogonal - ADR-0038 — The DSL → SQL teaching echo — Accepted (design agreed 2026-05-27; fully implemented + verified — every catalogue row in §7 Buckets A + B and the §6 category-3 prose round-trips per line through the advanced walker per §1, and the §4 de-emphasised styled-runs polish is wired: handoff-46
04c8e42shipped the channel + create-table slice, handoff-4790479cbthe full Bucket A expansion + a skeleton contract-gap fix (dropped per-columnDEFAULT/CHECK),275c726the Bucket B resolved-name + multi-statement renderers (auto- and user-namedadd index, positionaldrop index,add/drop relationshipin both selector forms,drop column --cascade,add relationship --create-fk),e6ad1aethe last category-3 line — thechange column --dont-convertcaveat (shortid + transform notes were already surfaced via pre-existingclient_side.*keys), and2aab457the §4 styled-runs polish: a newOutputKind::TeachingEchocustom rendering branch (dimmedExecuting SQL:prefix + the SQL re-lexed in advanced mode for token-class colouring, same as the input echo) plus a newOutputStyleClass::Hintfor every cat-3 prose line — caveat and the existing illuminating notes, user-confirmed broader scope), realises ADR-0030 §10 (the teaching bridge) — the Phase-5 echo ADR-0035 §12 forward-referenced — building on ADR-0037 (theSubmissionModegate) and ADR-0035 Amendment 2 (standard-first dialect +ALTER COLUMNgap-fill). When a DSL-form command runs in advanced/one-shot mode, the worker emits the equivalent SQL beneath[ok]as a de-emphasised styledOutputLine(ADR-0028); the App renders it. Defining invariant — the copy-paste contract: every echoed line is runnable advanced-mode SQL (round-trip-tested: parse the echo → same-effect command; a planned "copy the echo" affordance depends on it). Type vocabulary = the playground's own keywords (serial/shortid/…, accepted byfrom_sql_name, decision (a)); statement shape = the standard-first dialect (Am2). DML uses substituted literals, not?(per-typeValue → SQL-literal, round-trip-safe;blobmoot — no literal syntax exists; auto-gen columns omitted to matchdo_insert+ X4). Firing reality — a DDL +show datafeature: in advanced modeinsert/update/delete … whereare SQL-first (Sql*= already SQL = nothing to echo per §10); only DSL-only spellings echo (DDL +show data+ thedelete/update … --all-rowsfall-throughs — the latter via ADR-0033 Amendment 4, a bug-fix folded in here that reverses Amendment 3'supdate … --all-rowsmisparse). Three-category framework for "what happens beyond the literal SQL": (1) engine-implementation-hiding (the rebuild, rowid PK, non-PKserialMAX+1) — never surfaced; (2) decomposable into advanced SQL (drop column --cascade,--create-fkrelationship) — shown as the runnable multi-line sequence, one statement per line; (3) playground type-behaviour with no SQL-expressible form (shortidgeneration — noshortid(); type-conversion transforms — noUSING) — de-emphasised prose expansion from the worker'sclient_side.*notes. Carries the full catalogue (Buckets A single-statement / B resolved-name + multi-line / C no-echo) mapping every DSL-form command to its echo. OOS: reverse SQL→DSL echo (§13 OOS-5), app commands /show table/explain/replay, ablobliteral, the column-level UNIQUE/CHECK drop residual (Bucket C until Am2's gap closes), and surfacing any category-1 engine internal - ADR-0039 — EXPLAIN over advanced-mode SQL queries — Accepted (2026-05-27), implemented 2026-05-30 (issue #7), supersedes ADR-0030 §13 OOS-2. Lets
explainwrap the advanced SQL commands (Select/SqlInsert/SqlUpdate/SqlDelete, pluswith/CTE which builds aSelect) in addition to the DSLShowData/Update/Deleteit already covers (ADR-0028), runningEXPLAIN QUERY PLANover the validated SQL text through the existing ADR-0028 span-styled plan tree (advanced mode only; DSLexplainunchanged in both modes). Implemented via a secondAdvancedexplainCommandNode (EXPLAIN_SQL) registered under the sharedexplainentry word — reusing the establishedinsert/update/deleteshared-word dispatch (decide: SQL-first / DSL-fallback), soexplain show data …and DSL-only--all-rowsstill reach the DSL node; rejected aDynamicSubgrammarmode-gate (its resolution cache key omitsmode).build_explain_sqlslices the inner SQL off the source (excludesexplain) and reuses the existing SQL builders;do_explain_planruns the carried text verbatim, no params. Advancedexplain update/deletenow route through SQL (identical plan, full SQL syntax); DSL-explain tests pinned to simple mode. Reframed OOS-2 as a deferred exclusion (per ADR-0000's out-of-scope discipline), not a rejection. OOS (deferred): EXPLAIN of DDL (no query plan exists) - ADR-0040 — A per-command completion marker (✓/✗) replaces the
[ok]summary line — Accepted 2026-05-30 (issue #9), amends ADR-0014 / ADR-0028 / ADR-0019 output conventions, builds on ADR-0037's mode-tagged echo. An audit of the whole command surface found the[ok] <verb> <subject>summary line duplicates the echo line above it (verb+subject) everywhere; its only unique contribution is the success-vs-error signal (andexplain selecteven rendered[ok] explainwith an empty subject post-ADR-0039). Decision: drop the[ok]line and the symmetric"…" failed:prefix; the echo line gains a trailing inline ✓ (green, success) / ✗ (red, failure) —running:becomes a pending state that resolves to<input> ✓/✗on completion (status set via the existingrfind(Echo)lookup). Content (row counts, structure, data, plan tree, teaching echo) unchanged. Scoped to the DSL/data/SQL family that has the redundant echo+[ok]pair; app-command[ok]lines (rebuild/export/now editing) are payload-bearing, have no echo to mark, and stay as-is.ok.summaryretired;dsl.failedreduced to the rendered reason. Broad but mechanical snapshot churn. OOS: app-command[ok]lines, the[WRN]validity indicator, and the tag colours (issue #10)