19 KiB
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 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 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 shortids, 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
OutputLines 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 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.