Files
rdbms-playground/docs/adr/0038-dsl-to-sql-teaching-echo.md
T

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 uses SELECT).
  • The --all-rows fall-throughsdelete … --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 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 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-convertcaveat (§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.