docs(adr): design the DSL→SQL teaching echo (ADR-0038) + dependencies
Realises ADR-0030 §10 (the DSL→SQL teaching bridge) as a /runda'd design
set, before implementation:
- ADR-0037 (new): execution-time mode side-channel — SubmissionMode
{Simple, Advanced, AdvancedOneShot} threaded Action→worker, output-only;
redeems ADR-0033 Amendment 3's deferred follow-up. Replay stays silent.
- ADR-0038 (new): the teaching echo + full catalogue (Buckets A/B/C),
the copy-paste round-trip contract, the three-category framework, and
the Value→SQL-literal renderer. DDL + show-data centric (overlapping
DML is SQL-first, so already SQL). Build-order deps recorded.
- ADR-0035 Amendment 2: standard-first dialect stance + ALTER COLUMN
SET/DROP NOT NULL, SET/DROP DEFAULT, ISO SET DATA TYPE gap-fill.
- ADR-0033 Amendment 4: reclassifies the `update … --all-rows`
non-fall-back as a bug; it now falls back to the DSL Update and echoes
(keyed on adjacent `--`; spaced arithmetic preserved).
- ADR-0039 (new): EXPLAIN over advanced SQL — decision recorded, build
deferred; supersedes ADR-0030 §13 OOS-2.
- ADR-0000: out-of-scope discipline (deferred vs rejected). README index
updated for all of the above.
Reconcile CLAUDE.md: simple-mode column ops are implemented, not pending
(requirements.md C2/B2 already [x]).
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
# 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 **worker builds** the echo (ADR-0037 §3) — it alone holds the facts
|
||||
several echoes need: auto-resolved index / relationship names, generated
|
||||
`shortid` values, and lossy-conversion counts. It returns the echo
|
||||
payload on the result event. 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
|
||||
itself 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.
|
||||
Reference in New Issue
Block a user