# ADR-0038: The DSL → SQL teaching echo ## Status Accepted. Design agreed with the user (2026-05-27); **fully implemented and verified** — every catalogue row (§7 Buckets A, B, plus the §6 category-3 prose) round-trips per line through the advanced-mode walker (the §1 copy-paste contract; §6 category 2 holds it per line), and the §4 de-emphasised styled-runs rendering polish (ADR-0028) is wired. Shipped across four feature commits: **Phase 1 — Bucket A single-statement** (handoff-46's `04c8e42` delivered the channel + create-table slice; handoff-47's `90479cb` expanded to the full Bucket A — `Value → SQL-literal` + `Expr → SQL`, `add`/`drop`/`rename`/`change column`, the four `add constraint` + two `drop constraint` forms, `show data [where] [limit]` with PK-sourced `ORDER BY`, and the `delete`/`update … --all-rows` fall-throughs; also fixed a skeleton contract gap that silently dropped per-column `DEFAULT` / `CHECK` on the create-table echo). **Phase 2 — Bucket B resolved-name and multi-statement** (`275c726`): `add index` (auto- and user-named, resolved from the post-execution description), positional `drop index`, `add`/`drop relationship` in both `Endpoints` and `Named` selector forms (the named drop resolved by a small list-tables scan, acceptable for teaching- playground schemas), `drop column --cascade` (multi-line via `DropColumnResult::dropped_indexes`), and `add relationship --create-fk` (multi-line iff the child column was newly created; parent PK type + pre-state captured pre-execution via the runtime's new `collect_echo_lookups`). **Phase 3 — category-3 prose** (`e6ad1ae`): `shortid` generation and type-conversion transforms already surfaced via the pre-existing `client_side.auto_fill_*` / `client_side.transformed*` notes — Phase 3 only added the missing `change column --dont-convert` **caveat** (the only Bucket A caveat; every other category-3 line is illuminating), gated on an advanced effective mode because it references "the line above". **Phase 4 styled-runs polish** (`2aab457`): the echoed SQL renders as code — a new `OutputKind::TeachingEcho` line with a dimmed `Executing SQL:` prefix followed by the SQL re-lexed in advanced mode via `input_render::lex_to_runs_in_mode` for token-class colouring (the same treatment the input echo gets); every category-3 prose line (the new caveat plus the pre-existing illuminating notes) renders dimmed via a new `OutputStyleClass::Hint` styled-runs class resolving to `theme.muted`. Scope went slightly beyond §4's "echo + caveat" to cover the illuminating cat-3 notes too — user-confirmed, in service of §6's "de-emphasised prose line" wording for every cat-3 line. **Realises ADR-0030 §10** ("The DSL → SQL teaching bridge") — the Phase-5 echo that **ADR-0035 §12 forward-referenced** as "a separate ADR." Builds on **ADR-0037** (the execution-time mode side-channel that gates it) and **ADR-0035 Amendment 2** (the standard-first dialect + `ALTER COLUMN` gap-fill that makes its DDL echoes runnable). ## Context ADR-0030 §10 specified a teaching bridge: when a **DSL-form** command runs **in advanced mode**, its output includes the equivalent SQL, so a learner who knows the simple-mode form reads off how to spell it in SQL. The §10 contract: fires only for DSL-entered commands in advanced mode (a command already typed as SQL is not echoed; simple mode stays uncluttered); renders as a de-emphasised `OutputLine` beneath the `[ok]` summary (styled-runs, ADR-0028); app-level commands have no SQL form and are not echoed; the reverse SQL → DSL echo is out of scope (§13 OOS-5). ### The firing reality — this is a DDL + `show data` feature A consequence of ADR-0033 Amendment 3 sharpens the scope and must be stated plainly, because it is easy to over-estimate coverage: **in advanced mode the overlapping data commands route SQL-first.** A user who types `insert into T values (…)` / `update T set …` / `delete from T where …` in advanced mode produces `Command::SqlInsert` / `SqlUpdate` / `SqlDelete` — they **already typed SQL**, so §10 explicitly does not echo them. The echo is therefore meaningful only where the **DSL spelling differs from the SQL spelling** — i.e. the DSL-*only* surface: - **DDL DSL spellings** with no winning SQL competitor — `create table … with pk`, `add`/`drop`/`rename`/`change column`, `add`/`drop constraint`, `add`/`drop index`, `add`/`drop relationship`. - **`show data`** (DSL-only; SQL uses `SELECT`). - The **`--all-rows` fall-throughs** — `delete … --all-rows` falls back to the DSL `Delete` (the SQL `DELETE` has no slot for the flag), and `update … --all-rows` likewise falls back to the DSL `Update` per **ADR-0033 Amendment 4** (which reclassifies Amendment 3's non-fall- back of `update … --all-rows` as a bug and reverses it). Plain / `where`-filtered `insert`/`update`/`delete` stay SQL-first (`Sql*`). Everything a learner types that is *already SQL* needs no echo by construction. So the catalogue below is dominated by DDL. ### Dependencies - **ADR-0037** delivers the `SubmissionMode` to the worker, so a command knows at execution time whether it ran under `Advanced` / `AdvancedOneShot` (echo) or `Simple` (silent). - **ADR-0035 Amendment 2** adds the `ALTER COLUMN SET/DROP NOT NULL`, `SET/DROP DEFAULT`, and the ISO `SET DATA TYPE` canonical form, so the constraint-modification echoes are runnable advanced-mode SQL, and fixes the dialect stance the echoes emit in. ## Decision ### 1. The copy-paste contract — every echo is runnable advanced-mode SQL The defining invariant, stronger than "looks like SQL": **each echoed line is exactly what the user could paste into advanced mode and run — to the same effect, except where a Category-3 *caveat* line (§6) flags a divergence the SQL surface cannot express** (today the sole such case is `change column … --dont-convert`). This is testable as a **round-trip**: parse the echo through the advanced-mode walker and assert it yields a command with the same effect as the one that produced it (a caveated line still parses and runs — it just runs differently, which the caveat states). A planned one-shot "copy the echo" UX affordance depends on this contract, so it is a hard requirement, not a nicety. Multi-statement echoes (§6 category 2) hold the contract per line. ### 2. Type vocabulary — the playground's own keywords The echo emits the playground's `Type::keyword()` spelling (`serial`, `shortid`, `decimal`, `bool`, `date`, `datetime`, …), **not** engine storage types. This is sound because the advanced-mode type slot (`Type::from_sql_name`) accepts those ten keywords, so the echo round- trips (§1), and because the curated vocabulary is what the learner knows from the DSL and what carries the teaching information (`serial`'s auto- increment intent, `shortid`'s identity flavour) that `INTEGER`/`TEXT` would erase. The **statement shape** follows the standard-first dialect (ADR-0035 Amendment 2); the **type slots** carry playground keywords. ### 3. Firing rule The echo fires **iff** the executed command is a **DSL-form** command — *not* a `Sql*` variant and *not* a `Command::App(_)` — **and** its `SubmissionMode` (ADR-0037) is `Advanced` or `AdvancedOneShot` **and** the command is an interactive submission (**not** a `replay`ed line). Silent in `Simple`; silent for SQL-entered commands (they are `Sql*`); silent for app commands (no SQL form, §10); silent during `replay` (per-line echoes would bury the replay summary — ADR-0037 §3). ### 4. Where it is built and rendered The echo is built at the **runtime's `ExecuteDsl` handler** (build correction, ADR-0037 §3 + Implementation notes): the db.rs worker receives *decomposed* calls, not the `Command`, so it cannot render `Command → SQL`. The runtime is the one place where the `Command`, the threaded `EffectiveMode`, and the worker's **result** (resolved auto- names, generated `shortid`s, conversion counts) all converge — so it builds the echo there, still **execution-time aware** (it consumes the results). The **App renders** it as one or more **de-emphasised** `OutputLine`s beneath the `[ok]` summary, using the ADR-0028 styled-runs mechanism (a dimmed `Executing SQL:` prefix; the SQL in a code-ish run). One statement per line (§6 category 2). ### 5. `Value → SQL-literal` rendering DML echoes substitute the **actual literal values** (not `?` placeholders — decision B1), so the line is runnable. A per-type renderer produces SQL literals that **round-trip** (§1): | Type | Literal form | |---|---| | `text` / `shortid` | single-quoted, `'` doubled (`'O''Hara'`) | | `int` / `serial` | bare integer | | `real` / `decimal` | bare numeric (decimal as authored) | | `bool` | `true` / `false` | | `date` / `datetime` | quoted ISO-8601 (`'2026-05-27'`) | | `NULL` | `NULL` | **`blob` has no literal:** neither the DSL nor advanced SQL has a blob- literal syntax (the playground deliberately does not provide one), so a DSL command **cannot carry a blob value** for the echo to render — this is moot, not a gap. **Auto-generated columns are omitted** from an `INSERT` echo: `do_insert` filters out PK `serial` (rowid alias), non-PK `serial` (MAX+1), and `shortid` columns the user did not list, and advanced-mode SQL auto-fills the same omissions (`requirements.md` X4). So the echo omits them too — matching both the executed statement and the advanced-mode form a learner would type. ### 6. The three-category framework Everything that happens "beyond the literal SQL line" sorts into exactly three categories; naming them makes the rule testable: - **Category 1 — engine-implementation-hiding. Never surfaced.** The table rebuild that backs `ALTER COLUMN …` / `ADD CONSTRAINT` / `RENAME TO` on this engine; the rowid alias behind a PK `serial`; the `MAX(col)+1` behind a non-PK `serial`. These are how *this* engine honours a standard operation — not the lesson (ADR-0030 §7, ADR-0035 §9). The headline echo shows the clean statement; the means stays invisible. (Non-PK `serial` auto-fill is **here, not category 3**: auto-increment is an ordinary database feature; only the engine's *means* of faking it on a non-PK column is hidden.) - **Category 2 — decomposable into advanced-mode SQL. Shown as the runnable multi-line sequence.** One statement per line; the lines *are* the explanation, no prose. See `drop column --cascade` and `add relationship --create-fk` below. - **Category 3 — a playground type-system behaviour the SQL line cannot express (no function, no clause exists to write it). Surfaced as a de-emphasised prose line.** The note is one of two kinds: *illuminating* — the headline echo *is* equivalent and the note merely reveals a value-add the SQL does not show; or *caveat* — the headline is the **nearest** SQL but **not** equivalent because a playground flag's semantics have no SQL form (these caveats are the §1 contract's only exceptions). Members: - **`shortid` generation** *(illuminating)* — no `shortid()` function exists, so the generation cannot be written in SQL: "generated unique shortid(s) for `