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]).
16 KiB
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 usesSELECT).- The
--all-rowsfall-throughs —delete … --all-rowsfalls back to the DSLDelete(the SQLDELETEhas no slot for the flag), andupdate … --all-rowslikewise falls back to the DSLUpdateper ADR-0033 Amendment 4 (which reclassifies Amendment 3's non-fall- back ofupdate … --all-rowsas a bug and reverses it). Plain /where-filteredinsert/update/deletestay 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
SubmissionModeto the worker, so a command knows at execution time whether it ran underAdvanced/AdvancedOneShot(echo) orSimple(silent). - ADR-0035 Amendment 2 adds the
ALTER COLUMN SET/DROP NOT NULL,SET/DROP DEFAULT, and the ISOSET DATA TYPEcanonical 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 replayed 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 OutputLines 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 TOon this engine; the rowid alias behind a PKserial; theMAX(col)+1behind a non-PKserial. 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-PKserialauto-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 --cascadeandadd relationship --create-fkbelow. -
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:
shortidgeneration (illuminating) — noshortid()function exists, so the generation cannot be written in SQL: "generated unique shortid(s) for<col>".- type-conversion transform (illuminating) —
change column typewhen cells are actually transformed; the playground auto-converts where standard SQL would need a cast clause it deliberately omits (the PostgresUSING, ADR-0035 §12): "converted<col>from<old>to<new>[N values with loss]". change column … --dont-convert(caveat) — the headlineALTER TABLE T ALTER COLUMN c SET DATA TYPE <ty>converts, but--dont-convertleft the stored values as-is, so the line is not equivalent: "--dont-convertkept 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 columnrow —ALTER COLUMN … SET DATA TYPE / SET NOT NULL / DROP NOT NULL / SET DEFAULT / DROP DEFAULT— require ADR-0035 Amendment 2. Todayalter table T alter column c …accepts onlytype <ty>(probed:set data type/set not null/set defaultall error with "expectedtype"). So Amendment 2 must build before these rows' round-trip tests. (change columncould echo theTYPEsynonym in the interim, but the canonical/emitted form isSET DATA TYPEper Amendment 2.) - The
update … --all-rowsrow requires ADR-0033 Amendment 4 (today it isSqlUpdate, not the DSLUpdatethe 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
blobliteral 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 → SQLrenderer plus theValue → SQL-literalrenderer, worker-side (§4), gated bySubmissionMode(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
SubmissionModeside-channel that gates firing. - ADR-0035 Amendment 2 — the standard-first dialect +
ALTER COLUMNgap-fill the echoes emit and rely on. - ADR-0033 Amendment 3 — the SQL-first dispatch that makes this a
DDL +
show datafeature. - ADR-0028 — the
OutputLinestyled-runs the echo renders through. - ADR-0017 / ADR-0018 — the
client_side.*notes feeding category-3.