5cb105b74b
Surfaces from a Devil's-Advocate audit of the DSL → SQL teaching echo (ADR-0038) after Phases 1-3 landed: three doc-drift bugs introduced by the earlier handoff-47 / ADR-promotion commits — requirements.md M4 and both ADR-0038 README index entry + Status block still said "Phase 2 / Phase 3 remain," but `275c726` and `e6ad1ae` shipped them. Updated to reflect actual state: Buckets A + B complete plus the category-3 prose; only the §4 styled-runs polish remains. ADR-0037's README entry also touched to note all four shipping commits of its consumer. Plus a missing test slice the DA flagged: explicit no-echo coverage for the Bucket C cases that flow through command_to_sql's catch-all (show table, explain, replay, every Command::App variant). The contract — ADR-0030 §10 / ADR-0038 §7 Bucket C — forbids echoes for these; a future renderer arm added at the wrong place could silently leak one. The new bucket_c_no_echo_commands_all_return_none pins that. Tests: 2015 passed / 0 failed / 1 ignored (pre-existing); clippy clean. Nothing to escalate.
333 lines
18 KiB
Markdown
333 lines
18 KiB
Markdown
# ADR-0038: The DSL → SQL teaching echo
|
||
|
||
## Status
|
||
|
||
Accepted. Design agreed with the user (2026-05-27); **Phases 1–3
|
||
implemented and verified** by round-tripping every catalogue row
|
||
(§7 Buckets A, B, plus the §6 category-3 prose) through the
|
||
advanced-mode walker (the §1 copy-paste contract; §6 category 2
|
||
holds it per line). Shipped across three 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".
|
||
**The §4 de-emphasised styled-runs rendering polish remains** — the
|
||
echo + caveat currently surface as plain `[system]` lines.
|
||
**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.
|