04c8e4295f
Walking skeleton validating the whole echo architecture end to end; the Command→SQL renderer currently covers `create table`, with the rest of Bucket A / B / category-3 to follow (ADR-0038 §8). - Channel (ADR-0037): the three-way EffectiveMode (reusing the existing enum, not a new SubmissionMode — recorded in the ADR) rides on Action::ExecuteDsl to the runtime. `replay` bypasses the interactive spawn, so it never echoes (silent, for free). - Echo (ADR-0038): built at the runtime's ExecuteDsl dispatch — the worker gets decomposed calls, not the Command, so ADR §4's "worker builds it" was corrected to the dispatch layer. Gated by echo_for (advanced effective mode + DSL-form). Carried on DslSucceeded; rendered by note_ok_summary as `Executing SQL: …` immediately beneath `[ok]`. New src/echo.rs renderer; echo.executing_sql i18n key. - command_to_sql: `create table` → `CREATE TABLE T (id serial PRIMARY KEY)` (single inline / compound table-level PK), playground type vocabulary, round-trip-verified against the advanced walker (the §1 contract). Tests: echo.rs (render, round-trip contract, mode gate, Sql*-not-echoed); app.rs (submit carries the 3-way mode; echo renders beneath [ok]). Suite 1970/0/1; clippy clean.
304 lines
16 KiB
Markdown
304 lines
16 KiB
Markdown
# ADR-0038: The DSL → SQL teaching echo
|
|
|
|
## Status
|
|
|
|
Proposed (design agreed with the user 2026-05-27; pending `/runda` +
|
|
implementation). **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 `<col>`".
|
|
- **type-conversion transform** *(illuminating)* — `change column type`
|
|
when cells are actually transformed; the playground auto-converts
|
|
where standard SQL would need a cast clause it deliberately omits
|
|
(the Postgres `USING`, ADR-0035 §12): "converted `<col>` from
|
|
`<old>` to `<new>` [N values with loss]".
|
|
- **`change column … --dont-convert`** *(caveat)* — the headline
|
|
`ALTER TABLE T ALTER COLUMN c SET DATA TYPE <ty>` converts, but
|
|
`--dont-convert` left the stored values as-is, so the line is not
|
|
equivalent: "`--dont-convert` kept the stored values as-is; standard
|
|
SQL always converts, so running the line above would transform them
|
|
instead."
|
|
|
|
All are built from the worker's existing `client_side.*` result notes
|
|
(ADR-0017 §6 / ADR-0018 §9), not recomputed.
|
|
|
|
### 7. The catalogue
|
|
|
|
DSL-form commands that surface in advanced mode, with their echo. The
|
|
SQL uses the standard-first dialect (ADR-0035 Amendment 2) and playground
|
|
type keywords (§2). `<name>` denotes a worker-resolved name (auto-
|
|
generated or positional → resolved at execution, "B1").
|
|
|
|
**Bucket A — single runnable statement.**
|
|
|
|
| DSL command | Echoed SQL | Cat-3 expansion |
|
|
|---|---|---|
|
|
| `create table T with pk` | `CREATE TABLE T (id serial PRIMARY KEY)` | — |
|
|
| `create table T with pk a(int),b(int)` | `CREATE TABLE T (a int, b int, PRIMARY KEY (a, b))` | — |
|
|
| `add column to T: c (<ty>) [not null] [unique] [default v] [check e]` | `ALTER TABLE T ADD COLUMN c <ty> [NOT NULL] [UNIQUE] [DEFAULT v] [CHECK (e)]` | shortid (if `<ty>` = shortid) |
|
|
| `drop column from T: c` *(no covering index)* | `ALTER TABLE T DROP COLUMN c` | — |
|
|
| `rename column in T: old to new` | `ALTER TABLE T RENAME COLUMN old TO new` | — |
|
|
| `change column in T: c (<ty>)` | `ALTER TABLE T ALTER COLUMN c SET DATA TYPE <ty>` | conversion *(illum.)* if transformed (incl. → shortid); `--dont-convert` → *caveat* (§6) |
|
|
| `add constraint not null to T.c` | `ALTER TABLE T ALTER COLUMN c SET NOT NULL` | — |
|
|
| `add constraint default <v> to T.c` | `ALTER TABLE T ALTER COLUMN c SET DEFAULT <v>` | — |
|
|
| `add constraint unique to T.c` | `ALTER TABLE T ADD UNIQUE (c)` | — |
|
|
| `add constraint check <e> to T.c` | `ALTER TABLE T ADD CHECK (e)` | — |
|
|
| `drop constraint not null from T.c` | `ALTER TABLE T ALTER COLUMN c DROP NOT NULL` | — |
|
|
| `drop constraint default from T.c` | `ALTER TABLE T ALTER COLUMN c DROP DEFAULT` | — |
|
|
| `add index [as N] on T (cols)` | `CREATE INDEX <name> ON T (cols)` | — |
|
|
| `show data T [where …] [limit n]` | `SELECT * FROM T [WHERE …] [ORDER BY <pk> LIMIT n]` | — |
|
|
| `delete from T --all-rows` | `DELETE FROM T` | — |
|
|
| `update T set … --all-rows` | `UPDATE T SET …` | — |
|
|
|
|
**Bucket B — resolved-name and multi-line (one statement per line).**
|
|
|
|
| DSL command | Echoed SQL | Notes |
|
|
|---|---|---|
|
|
| `drop index on T(cols)` *(positional)* | `DROP INDEX <name>` | name resolved at execution |
|
|
| `add 1:n relationship [as N] from P.pc to C.cc [on delete X] [on update Y]` | `ALTER TABLE C ADD CONSTRAINT <name> FOREIGN KEY (cc) REFERENCES P (pc) [ON DELETE X] [ON UPDATE Y]` | name resolved |
|
|
| `drop relationship (from P.pc to C.cc \| N)` | `ALTER TABLE C DROP CONSTRAINT <name>` | name resolved |
|
|
| `add 1:n relationship … --create-fk` *(child col created)* | `ALTER TABLE C ADD COLUMN cc <ty>` ⏎ `ALTER TABLE C ADD CONSTRAINT <name> FOREIGN KEY (cc) REFERENCES P (pc) …` | category 2: two runnable lines (one if the column already existed) |
|
|
| `drop column T.c --cascade` *(drops covering indexes)* | `DROP INDEX <ix1>` ⏎ … ⏎ `ALTER TABLE T DROP COLUMN c` | category 2: index names resolved; plain `DROP COLUMN` would refuse an indexed column |
|
|
|
|
**Bucket C — no echo.**
|
|
|
|
| DSL command | Reason |
|
|
|---|---|
|
|
| `show table T` | structure display; no SQL spelling in the surface |
|
|
| `explain …` | `EXPLAIN` of advanced SQL is OOS-2 (ADR-0030 §13) |
|
|
| `replay <path>` | app-lifecycle; no SQL form |
|
|
| `drop constraint unique/check from T.c` *(column-level)* | residual gap (ADR-0035 Amendment 2): anonymous column constraint, no portable name |
|
|
| `Command::App(_)` (all app commands) | §10 — no SQL form |
|
|
| `Sql*` variants, `select`/`with` | already SQL-entered — not echoed by construction |
|
|
| `insert …`, `update …` / `update … where …`, `delete …` / `delete … where …` | route SQL-first in advanced mode (`Sql*`) — already SQL, nothing to echo (the two `--all-rows` forms fall back to DSL and *do* echo — Bucket A) |
|
|
|
|
### 8. Coverage and phasing
|
|
|
|
The renderer is one mechanism, so the natural unit is "Bucket A + B at
|
|
once." A reasonable phasing if a smaller first slice is wanted:
|
|
|
|
- **Phase 1** — Bucket A single-statement DDL + `show data` (the bulk of
|
|
the teaching value; no resolved-name or multi-line machinery).
|
|
- **Phase 2** — Bucket B (worker-resolved names; multi-line sequences).
|
|
- **Phase 3** — the category-3 prose expansion.
|
|
|
|
(Coverage/phasing is a sequencing choice for the user; the catalogue
|
|
itself is fixed.)
|
|
|
|
**Build-order dependencies (verified against the current grammar).** Two
|
|
groups of echoes emit SQL the advanced surface does **not yet parse**, so
|
|
their round-trip tests (§1) are gated on their prerequisites landing
|
|
first:
|
|
|
|
- The **constraint-modification** rows and the `change column` row —
|
|
`ALTER COLUMN … SET DATA TYPE / SET NOT NULL / DROP NOT NULL /
|
|
SET DEFAULT / DROP DEFAULT` — require **ADR-0035 Amendment 2**. Today
|
|
`alter table T alter column c …` accepts only `type <ty>` (probed:
|
|
`set data type` / `set not null` / `set default` all error with
|
|
"expected `type`"). So Amendment 2 must build **before** these rows'
|
|
round-trip tests. (`change column` could echo the `TYPE` synonym in
|
|
the interim, but the canonical/emitted form is `SET DATA TYPE` per
|
|
Amendment 2.)
|
|
- The **`update … --all-rows`** row requires **ADR-0033 Amendment 4**
|
|
(today it is `SqlUpdate`, not the DSL `Update` the echo needs).
|
|
|
|
All other Bucket A/B rows round-trip against the surface as it stands
|
|
today (probe-confirmed: `create table T (id serial primary key)`,
|
|
compound PK, `add unique (c)`, `add check (…)`, `create index … on …`,
|
|
`select * from …` all parse in advanced mode).
|
|
|
|
### 9. Out of scope
|
|
|
|
- **The reverse SQL → DSL echo** (ADR-0030 §13 OOS-5).
|
|
- **App commands**, `show table`, `explain`, `replay` (Bucket C).
|
|
- **A `blob` literal renderer** (§5 — moot; no blob literal exists).
|
|
- **Closing the column-level UNIQUE/CHECK drop gap** (ADR-0035
|
|
Amendment 2 residual) — those stay Bucket C until that gap is closed.
|
|
- **Surfacing category-1 engine internals** (the rebuild, rowid, MAX+1) —
|
|
never (§6).
|
|
|
|
## Consequences
|
|
|
|
- A new `Command → SQL` renderer plus the `Value → SQL-literal` renderer,
|
|
worker-side (§4), gated by `SubmissionMode` (ADR-0037). Each Bucket A/B
|
|
row is a round-trip test (§1); each category-3 row asserts the prose
|
|
fires from the worker note.
|
|
- The teaching surface is honest: a learner reads runnable, portable SQL
|
|
in the playground's own type vocabulary; engine quirks stay hidden;
|
|
playground-only conveniences are named, not mysterious.
|
|
- The echo's coverage is **DDL-centric by construction** (the firing
|
|
reality, Context). This is correct, not a shortfall — overlapping DML
|
|
is already SQL in advanced mode.
|
|
- Adding a new DSL-form DDL command later must add its catalogue row +
|
|
round-trip test, or consciously place it in Bucket C.
|
|
|
|
## See also
|
|
|
|
- ADR-0030 §10 — the teaching bridge this realises; §13 OOS-5 (no reverse
|
|
echo).
|
|
- ADR-0037 — the `SubmissionMode` side-channel that gates firing.
|
|
- ADR-0035 Amendment 2 — the standard-first dialect + `ALTER COLUMN`
|
|
gap-fill the echoes emit and rely on.
|
|
- ADR-0033 Amendment 3 — the SQL-first dispatch that makes this a
|
|
DDL + `show data` feature.
|
|
- ADR-0028 — the `OutputLine` styled-runs the echo renders through.
|
|
- ADR-0017 / ADR-0018 — the `client_side.*` notes feeding category-3.
|