diff --git a/docs/adr/0035-advanced-mode-sql-ddl.md b/docs/adr/0035-advanced-mode-sql-ddl.md new file mode 100644 index 0000000..36495f6 --- /dev/null +++ b/docs/adr/0035-advanced-mode-sql-ddl.md @@ -0,0 +1,330 @@ +# ADR-0035: Advanced-mode SQL DDL + +## Status + +Proposed. Design agreed with the user (2026-05-24); implementation +is phased and pending (§13). This is **Phase 4** of the ADR-0030 +roadmap (the advanced-mode SQL surface), the peer of ADR-0031 +(expression grammar), ADR-0032 (`SELECT`), and ADR-0033 (DML). It +**clarifies ADR-0030 §4** on how DDL is represented and executed. + +## Context + +ADR-0030 fixed the *architecture* of advanced mode — SQL authored as +grammar in the unified tree (not a separate batch parser), with the +playground's own type vocabulary and metadata model — and noted that +each large grammar piece gets its own focused ADR. Phases 1–3 shipped: +the SQL expression grammar (ADR-0031), full `SELECT` (ADR-0032), and +DML — `INSERT`/`UPDATE`/`DELETE` (ADR-0033). Phase 4 is **DDL**: +`CREATE` / `DROP` / `ALTER TABLE` and `CREATE` / `DROP INDEX`. + +Two things from the earlier phases shape this one: + +1. **The advanced surface gets its *own* commands.** ADR-0033 + established that a SQL statement produces a distinct command + (`SqlInsert` / `SqlUpdate` / `SqlDelete`), separate from the + simple-mode typed command for the same verb. Those DML commands + execute as **validated SQL run verbatim** — possible only because + DML changes no schema and touches no metadata. +2. **DDL cannot run verbatim.** If `CREATE TABLE Orders (id INTEGER)` + executed as-is, the engine would make the table, but the + playground would lose what the user meant: that `id` is `serial`, + that a `REFERENCES` clause is a *named relationship*, that `STRICT` + applies, that the ten-type vocabulary governs. Recovering that + needs the parsed statement either way. + +ADR-0030 §4 said "DDL → a `Command` … run the typed executor." That +remains right in spirit — DDL is *structurally* executed, not raw — +but it predates the DML build and read as "reuse the simple-mode +`CreateTable` variant." This ADR clarifies it: **DDL gets its own +advanced commands too**, executed structurally (not verbatim). The +"verbatim" execution of the DML commands is an implementation +convenience available only because nothing about DML required +otherwise — not an architectural rule. + +Requirements touched: realizes `Q4` for DDL; closes the advanced-mode +side of table/column/index/constraint/relationship operations; lands +the table-rename half of `C1` (advanced mode only). + +## Decision + +### 1. Own per-statement SQL DDL commands (clarifies ADR-0030 §4) + +New `Command` variants, one per statement kind — granularity mirrors +the DML phase: + +- `SqlCreateTable` +- `SqlAlterTable` +- `SqlDropTable` +- `SqlCreateIndex` +- `SqlDropIndex` + +They are produced by the unified grammar's `ast_builder`s in advanced +mode. Unlike the DML `Sql*` commands they **execute structurally**: +the handler reads the parsed structure and performs the schema change +through the playground's metadata-maintaining machinery — writing +`__rdbms_playground_columns` / `__rdbms_playground_relationships`, +applying `STRICT`, using the ten-type vocabulary — so an +advanced-mode-created object is a first-class playground object, +identical to a simple-mode-created one (ADR-0030 §5). + +**Simple mode is untouched.** The existing typed commands +(`CreateTable`, `AddColumn`, `AddRelationship`, …) and their grammar +are unchanged; advanced SQL DDL is purely additive. + +**Execution sharing (per the user's steer).** The SQL DDL handlers +**reuse the low-level schema/metadata helpers** — the table builder, +the metadata writers, the rebuild-table primitive (ADR-0013) — where +the underlying operation is genuinely the same, so the two surfaces +cannot drift. Where the SQL path is genuinely different (e.g. a +`CREATE TABLE` that declares several inline foreign keys, which has no +simple-mode shape), it is implemented directly **for clarity rather +than bending the simple-mode command shapes to absorb it**. Shared +where it works; separate where it doesn't. + +### 2. Dispatch — shared entry words, advanced-only `alter` + +`create` and `drop` are already simple-mode entry words. They reuse +the **category-grouped, mode-aware dispatch** from ADR-0033 +Amendment 1: each appears in both the `Simple` and `Advanced` groups +of the `REGISTRY`; in advanced mode the SQL node is tried first and +falls back to the simple node when the SQL shape doesn't match. So in +advanced mode `CREATE TABLE T (id serial)` parses as SQL while +`create table T with pk id(serial)` still parses as the simple form — +exactly as `insert` behaves today. `alter` is a **new advanced-only +entry word** (`CommandCategory::Advanced`); simple mode keeps its +`add column` / `drop column` / `rename column` / `change column` +verbs and gains no `alter`. + +### 3. Type vocabulary (restates ADR-0030 §5) + +The type-name slot accepts the playground keywords directly (`text`, +`int`, `real`, `decimal`, `bool`, `date`, `datetime`, `blob`, +`serial`, `shortid`) **and** standard-SQL aliases mapped onto them: +`integer`/`smallint`/`bigint` → `int`; `varchar`/`char` → `text`; +`boolean` → `bool`; `timestamp` → `datetime`; `numeric` → `decimal`; +`float`/`double precision` → `real`; `binary`/`varbinary` → `blob`. A +length/precision argument (`varchar(255)`, `numeric(10,2)`) is +**accepted and ignored** — the playground's types are +unparameterised. Engine storage-type names are neither accepted as +input nor shown (§9). + +### 4. The DDL surface (full; `Q4`, no pre-emptive cuts) + +**`CREATE TABLE ( , … )`** + +- **Column elements**: ` [constraints…]`, where the + column constraints are the ADR-0029 set spelled in SQL: `NOT NULL`, + `UNIQUE`, `PRIMARY KEY`, `DEFAULT `, `CHECK ()`, and an + inline `REFERENCES () [ON DELETE …] [ON UPDATE …]` (§5). +- **Table elements**: `PRIMARY KEY (, …)` (single **and + compound**), `UNIQUE (, …)`, `CHECK ()`, + `[CONSTRAINT ] FOREIGN KEY () REFERENCES () + [ON DELETE …] [ON UPDATE …]` (§5). +- `CHECK` and `DEFAULT` expressions reuse the ADR-0031 `sql_expr` + grammar (the same fragment `WHERE`/`HAVING`/projections use). + +**`DROP TABLE `** → `SqlDropTable`. Cascade of inbound +relationships follows the existing `drop table` semantics. + +**`ALTER TABLE `** → `SqlAlterTable`, where `` +covers, mapping to the existing low-level operations: + +| SQL action | Underlying operation | +|---|---| +| `ADD COLUMN [constraints]` | add-column (ADR-0013 rebuild where needed) | +| `DROP COLUMN ` | drop-column | +| `RENAME COLUMN TO ` | rename-column | +| `ALTER COLUMN TYPE ` | change-column-type (§5 conversion) | +| `ADD [CONSTRAINT ] ` | add-constraint / add-relationship (FK) | +| `DROP CONSTRAINT ` | drop-constraint | +| `RENAME TO ` | **table rename (§6, new low-level op)** | + +**`CREATE [UNIQUE] INDEX [] ON (, …)`** → +`SqlCreateIndex`, mapped to the ADR-0025 index machinery; `UNIQUE` +sets the index's uniqueness (a small extension to ADR-0025's index +model if it does not already carry the flag, called out in §13). + +**`DROP INDEX `** → `SqlDropIndex`. + +### 5. Foreign keys → named relationships + +A `REFERENCES` / `FOREIGN KEY` clause is the SQL spelling of an +ADR-0013 relationship. Because `SqlCreateTable` is its own command +carrying the whole parsed structure, a `CREATE TABLE` that declares +FK columns **creates the table and its relationship metadata +together** — one statement, one command, one transaction, **one undo +step** (§10). No decomposition into separate commands is needed. + +- `ON DELETE` / `ON UPDATE` → the ADR-0013 referential actions. +- A `CONSTRAINT FOREIGN KEY …` names the relationship; an + unnamed FK is auto-named by the existing ADR-0013 convention. +- `ALTER TABLE child ADD [CONSTRAINT ] FOREIGN KEY () + REFERENCES

(

) …` adds a relationship to an existing table + (the clean 1:1 with add-relationship). +- FK column type compatibility follows `Type::fk_target_type` + (ADR-0011) unchanged. + +### 6. Table rename — advanced mode only (`C1`) + +`ALTER TABLE RENAME TO ` is **advanced-mode only**; there +is no simple-mode rename-table verb. It needs a genuinely new +low-level operation (none exists today): within one transaction, +rename the table in the database, rename its `data/
.csv` file, +and update every metadata row that names it — the column-metadata +rows, and **both ends of any relationship** in +`__rdbms_playground_relationships` that references the old name. Name +validation and `__rdbms_*` rejection apply to the target. This closes +the rename half of `C1` for the advanced surface. + +### 7. Column type conversion — one engine, mode-appropriate policy + +The per-cell classification of ADR-0017 (clean / lossy / incompatible, +plus static refusals for playground-type-specific targets such as +`→ serial` and `↔ blob`) is a property of the **type set**, shared by +both modes. The policy on the *lossy* tier differs by mode: + +| Tier | Simple mode | Advanced mode (`ALTER COLUMN … TYPE`) | +|---|---|---| +| **clean** | auto-convert | auto-convert | +| **incompatible** | refuse (friendly) | refuse (friendly) — real SQL errors too | +| **static-refused** (`→serial`, `↔blob`, …) | refuse | refuse — our own types have no SQL meaning to mirror | +| **lossy** (`3.14`→`3`) | **refuse by default**; `--force-conversion` opts in | **perform it** (what SQL does), with a post-op "N values converted with loss" note; **no force flag** | + +Rationale: **simple mode protects up front; advanced mode trusts the +user like SQL does and lets `undo` catch regrets.** A lossy advanced +conversion is snapshotted (§10), so it is one `undo` away — there is +no silent *irreversible* loss, and no need to drop to simple mode to +"force". Conversions that exist only in the playground's vocabulary +stay protected in both modes. The simple-mode `--force-conversion` / +`--dont-convert` flags are unchanged and have **no SQL spelling** +(advanced mode always performs the conversion); the Postgres `USING +` clause is **not** adopted (§12). + +### 8. Constraints + +Column- and table-level constraints map to the ADR-0029 model: +`NOT NULL`, `UNIQUE`, `PRIMARY KEY` (incl. compound, table-level), +`DEFAULT `, `CHECK ()`. A populated-column constraint +addition reuses ADR-0029's pre-flight dry-run guard. `CHECK` / +`DEFAULT` expressions are stored as the SQL the user could re-enter in +advanced mode (ADR-0030 §11) — one syntax, not a third. + +### 9. Engine neutrality (ADR-0030 §7) + +No engine type names in or out (§3). `STRICT` is applied internally by +the create path; it is not in the authored grammar, so typing it is an +ordinary parse error, not a surfaced engine feature. Parse errors, +out-of-subset refusals, and execution failures route through the +friendly-error layer (ADR-0019) with engine-neutral wording. + +### 10. Persistence, metadata, history, replay, undo + +- Structural execution keeps `project.yaml`, the metadata tables, and + the CSV layer correct with the same guarantees as the simple-mode + path (ADR-0015 §6 ordering preserved). +- `history.log` records the **literal submitted SQL line**; replay + re-runs it through the one walker with the advanced view active. + `create` / `drop` / `alter` are **schema-write entry words, not in + ADR-0034 Amendment 1's app-lifecycle skip set**, so SQL DDL + **replays as a write** (re-applied) with **no replay-filter change** + — unlike `undo` / `redo`, which had to be added to that skip set. +- **Undo (ADR-0006):** each SQL DDL statement is a user mutation + carrying a `source`, so it is snapshotted by the worker hook and is + **one undo step** — including a `CREATE TABLE` with foreign keys, + precisely because it is a single command (§5) rather than a + decomposed sequence. + +### 11. Ambient assistance comes for free (ADR-0030 §8) + +Because the DDL is grammar in the unified tree, the walker +**mechanisms** apply with no DDL-specific assistance code: syntax +highlighting, the `[ERR]`/`[WRN]` validity indicator (ADR-0027), the +per-command parse-error usage skeleton (ADR-0021), and the completion +engine. + +What each grammar node still **authors** (this is writing the grammar, +not bolting assistance on afterwards): the correct `IdentSource` on +every schema-name slot — so `ALTER TABLE`/`DROP TABLE`/`DROP INDEX` +and `REFERENCES T(col)` / `CREATE INDEX ON T (cols)` complete from the +`SchemaCache`; the per-node hint + usage catalog keys (as the +app-command nodes carry `help_id` / `usage_ids`); and the +DDL-specific walker diagnostics with their catalog keys — the DDL +peers of the DML diagnostics ADR-0033 added (e.g. unknown type, +column-already-exists, FK column-type mismatch, the §7 lossy-conversion +note). The integration is structural, not free of authoring. + +### 12. Out of scope + +- Per ADR-0030 §3: views, triggers, transaction control, `PRAGMA`, + `ATTACH`/`DETACH`, `VACUUM`, virtual tables, multi-statement + batches. One statement per submission; a trailing `;` is tolerated. +- The Postgres `USING ` conversion clause (§7) — heavy + (per-row expression evaluation), dialect-specific, and unable to + express playground-type targets. +- The simple-mode `--dont-convert` semantics have no SQL form + (advanced `ALTER COLUMN TYPE` always converts). +- The **DSL → SQL teaching echo** (ADR-0030 §10) is Phase 5, a + separate ADR — not this one. +- Engine-specific DDL spellings (`AUTOINCREMENT`, `WITHOUT ROWID`, + collations, `IF [NOT] EXISTS` if judged out-of-subset) — the + grammar admits the standard surface; extras are ordinary parse + errors. + +### 13. Phased implementation plan + +Sub-phases, each opening with the smallest end-to-end slice and each +with an explicit exit gate + a written Devil's-Advocate gate, mirroring +ADR-0033's structure: + +- **4a — Dispatch + `CREATE TABLE` core.** Advanced `create` + dispatch; `SqlCreateTable` for columns + types (the §3 map) + + column constraints + single/compound `PRIMARY KEY`. No FK yet. +- **4b — Foreign keys in `CREATE TABLE`.** Inline `REFERENCES` + + table-level `FOREIGN KEY` → relationship metadata, one undo step. +- **4c — `DROP TABLE`** → `SqlDropTable` (cascade parity). +- **4d — `CREATE [UNIQUE] INDEX` / `DROP INDEX`** → `SqlCreateIndex` + / `SqlDropIndex` (ADR-0025; the `UNIQUE` flag extension if needed). +- **4e — `ALTER TABLE` add/drop/rename column.** +- **4f — `ALTER TABLE … ALTER COLUMN TYPE`** (the §7 conversion + model + the lossy-with-note path). +- **4g — `ALTER TABLE` add/drop constraint, add foreign key.** +- **4h — `ALTER TABLE … RENAME TO`** (the §6 new low-level op). +- **4i — Verification sweep.** Typing-surface + matrix coverage, + engine-neutral error pass, undo-parity check (one step per + statement), `help`/usage for the new forms. + +## Consequences + +- Advanced mode reaches DDL parity with simple mode and adds + table-rename, so a learner can build and evolve a whole schema in + standard SQL with the playground's types, metadata, and safety + intact. +- The command set grows by five `Sql*` DDL variants; the worker gains + their handlers, which lean on shared low-level helpers where the + operation matches the simple-mode path and stand alone where the + SQL surface is genuinely richer (multi-FK `CREATE TABLE`). +- One genuinely new capability — table rename — adds a low-level op + that the simple mode does not have; it must keep the CSV file name + and the relationship metadata in step with the table name. +- ADR-0030 §4 is clarified (own `Sql*` DDL commands, structurally + executed); no behaviour of the shipped DML/`SELECT` phases changes. +- The conversion model unifies simple and advanced without a force + flag in SQL, relying on `undo` (ADR-0006) as the advanced-mode + safety net — a concrete payoff of having shipped undo first. + +## See also + +- **ADR-0030** — the advanced-mode architecture; this is its Phase 4 + and clarifies §4 (DDL representation) and restates §5 (types) / §7 + (neutrality) / §8 (assistance) / §11 (persistence). +- **ADR-0033** — the DML phase; source of the category-grouped + mode-aware dispatch (Amendment 1) reused for shared entry words. +- **ADR-0031** — `sql_expr`, reused for `CHECK` / `DEFAULT`. +- **ADR-0013** — relationships + the rebuild-table primitive that the + `ALTER`/FK handlers build on. +- **ADR-0017** — the column type-change classification §7 shares. +- **ADR-0029** — column constraints; **ADR-0025** — indexes; + **ADR-0011** — FK column-type compatibility; **ADR-0005** — the + ten-type vocabulary. +- **ADR-0006** — undo; each DDL statement is one undo step (§10). diff --git a/docs/adr/README.md b/docs/adr/README.md index c808b4f..95105b1 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -40,3 +40,4 @@ This directory contains the project's ADRs, recorded per - [ADR-0032 — The full SQL `SELECT` grammar](0032-sql-select-grammar.md) — **Accepted**, 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 `ScopeFrame`s 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`)](0033-sql-dml-grammar.md) — **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-only](0034-history-journal-and-replay-filter.md) — **Accepted**, 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 `||` 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 DDL](0035-advanced-mode-sql-ddl.md) — **Proposed** (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 KEY` → **named 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 (`integer`→`int`, `varchar`→`text`, `timestamp`→`datetime`, …; length args accepted-and-ignored; no engine type names in/out — ADR-0030 §5). `CHECK`/`DEFAULT` reuse ADR-0031 `sql_expr`. **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 (4a–4i), each with exit + DA gates