Files
rdbms-playground/docs/adr/0035-advanced-mode-sql-ddl.md
T
claude@clouddev1 5b76315d1e feat: ADR-0035 4f — ALTER TABLE … ALTER COLUMN TYPE
Fourth AlterTableAction (AlterColumnType), runtime-decomposed to the
existing change_column_type executor with ForceConversion — which IS the
§7 advanced policy: lossy converts with a note (no force flag),
incompatible + the ADR-0017 static refusals (↔blob, same-type,
date↔datetime, non-int→serial) still refuse, while int→serial is allowed
(auto-fills nulls + UNIQUE, ADR-0018 §8). No new mode/note/persistence;
undo is the advanced safety net.

Grammar adds a fourth action branch leading on `alter`, discriminated in
the builder by the `type` keyword (unique — ADD COLUMN's type is an
ident); the type slot reuses SQL_TYPE. The internal-__rdbms_* guard was
folded into do_change_column_type (user-confirmed), closing the simple
`change column` exposure.

Tests: 7 Tier-3 e2e via run_replay + 4 Tier-1 parse (incl. a column-named-
`type` discriminator probe) + the simple-surface guard. Help/usage
refreshed; ADR-0035 §13 4f + README + requirements.md in lockstep.
2026-05-25 21:16:37 +00:00

29 KiB
Raw Blame History

ADR-0035: Advanced-mode SQL DDL

Status

Accepted. Design agreed with the user (2026-05-24); the approach is validated end-to-end by sub-phases 4a / 4a.2 / 4a.3 / 4b / 4c / 4d / 4e / 4f (CREATE TABLE with column- and table-level constraints and foreign keys, DROP TABLE [IF EXISTS], CREATE [UNIQUE] INDEX / DROP INDEX [IF EXISTS], ALTER TABLE add/drop/rename column, and ALTER TABLE … ALTER COLUMN TYPE, implemented 2026-05-25 — plans docs/plans/20260524-adr-0035-sql-ddl-4a.md, …-4a2.md, …-4a3.md, docs/plans/20260525-adr-0035-sql-ddl-4b.md, …-4c.md, …-4d.md, …-4e.md, …-4f.md), so the decision is accepted while the remaining sub-phases (4g4i, §13) continue. This is Phase 4 of the ADR-0030 roadmap (the advanced-mode SQL surface), the peer of ADR-0031 (expression grammar), ADR-0032 (SELECT), and ADR-0033 (DML). It clarifies ADR-0030 §4 on how DDL is represented and executed.

Refinements (2026-05-24, pre-implementation /runda round, user-confirmed). Two open micro-calls were settled before 4a: (1) IF [NOT] EXISTS is admitted as a no-op-that-succeeds-with-a-note rather than refused — it is a near-universal cross-vendor idiom (PostgreSQL, MySQL/MariaDB, SQLite, Oracle 23ai), not an engine-specific spelling, so it belongs in the standard surface (§3/§4/§12/§13); (2) INTEGER PRIMARY KEY maps to a plain int primary key, not auto-increment — serial remains the sole auto-increment type (§3).

Context

ADR-0030 fixed the architecture of advanced mode — SQL authored as grammar in the unified tree (not a separate batch parser), with the playground's own type vocabulary and metadata model — and noted that each large grammar piece gets its own focused ADR. Phases 13 shipped: the SQL expression grammar (ADR-0031), full SELECT (ADR-0032), and DML — INSERT/UPDATE/DELETE (ADR-0033). Phase 4 is DDL: CREATE / DROP / ALTER TABLE and CREATE / DROP INDEX.

Two things from the earlier phases shape this one:

  1. The advanced surface gets its own commands. ADR-0033 established that a SQL statement produces a distinct command (SqlInsert / SqlUpdate / SqlDelete), separate from the simple-mode typed command for the same verb. Those DML commands execute as validated SQL run verbatim — possible only because DML changes no schema and touches no metadata.
  2. DDL cannot run verbatim. If CREATE TABLE Orders (id INTEGER) executed as-is, the engine would make the table, but the playground would lose what the user meant: that id is serial, that a REFERENCES clause is a named relationship, that STRICT applies, that the ten-type vocabulary governs. Recovering that needs the parsed statement either way.

ADR-0030 §4 said "DDL → a Command … run the typed executor." That remains right in spirit — DDL is structurally executed, not raw — but it predates the DML build and read as "reuse the simple-mode CreateTable variant." This ADR clarifies it: DDL gets its own advanced commands too, executed structurally (not verbatim). The "verbatim" execution of the DML commands is an implementation convenience available only because nothing about DML required otherwise — not an architectural rule.

Requirements touched: realizes Q4 for DDL; closes the advanced-mode side of table/column/index/constraint/relationship operations; lands the table-rename half of C1 (advanced mode only).

Decision

1. Own per-statement SQL DDL commands (clarifies ADR-0030 §4)

New Command variants, one per statement kind — granularity mirrors the DML phase:

  • SqlCreateTable
  • SqlAlterTable
  • SqlDropTable
  • SqlCreateIndex
  • SqlDropIndex

They are produced by the unified grammar's ast_builders in advanced mode. Unlike the DML Sql* commands they execute structurally: the handler reads the parsed structure and performs the schema change through the playground's metadata-maintaining machinery — writing __rdbms_playground_columns / __rdbms_playground_relationships, applying STRICT, using the ten-type vocabulary — so an advanced-mode-created object is a first-class playground object, identical to a simple-mode-created one (ADR-0030 §5).

Simple mode is untouched. The existing typed commands (CreateTable, AddColumn, AddRelationship, …) and their grammar are unchanged; advanced SQL DDL is purely additive.

Execution sharing (per the user's steer). The SQL DDL handlers reuse the low-level schema/metadata helpers — the table builder, the metadata writers, the rebuild-table primitive (ADR-0013) — where the underlying operation is genuinely the same, so the two surfaces cannot drift. Where the SQL path is genuinely different (e.g. a CREATE TABLE that declares several inline foreign keys, which has no simple-mode shape), it is implemented directly for clarity rather than bending the simple-mode command shapes to absorb it. Shared where it works; separate where it doesn't.

2. Dispatch — shared entry words, advanced-only alter

create and drop are already simple-mode entry words. They reuse the category-grouped, mode-aware dispatch from ADR-0033 Amendment 1: each appears in both the Simple and Advanced groups of the REGISTRY; in advanced mode the SQL node is tried first and falls back to the simple node when the SQL shape doesn't match. So in advanced mode CREATE TABLE T (id serial) parses as SQL while create table T with pk id(serial) still parses as the simple form — exactly as insert behaves today. alter is a new advanced-only entry word (CommandCategory::Advanced); simple mode keeps its add column / drop column / rename column / change column verbs and gains no alter.

3. Type vocabulary (restates ADR-0030 §5)

The type-name slot accepts the playground keywords directly (text, int, real, decimal, bool, date, datetime, blob, serial, shortid) and standard-SQL aliases mapped onto them: integer/smallint/bigintint; varchar/chartext; booleanbool; timestampdatetime; numericdecimal; float/double precisionreal; binary/varbinaryblob. A length/precision argument (varchar(255), numeric(10,2)) is accepted and ignored — the playground's types are unparameterised. Engine storage-type names are neither accepted as input nor shown (§9).

The map is purely lexical: INTEGER PRIMARY KEY becomes a plain int primary key — it is not treated as auto-increment, unlike the engine's rowid-alias idiom. Auto-increment is reached only through the explicit serial type (id serial primary key). This keeps the engine's storage behaviour from leaking into the standard surface and matches ADR-0005's single-auto-increment-type model.

4. The DDL surface (full; Q4, no pre-emptive cuts)

CREATE TABLE <name> ( <element>, … )

  • Column elements: <name> <type> [constraints…], where the column constraints are the ADR-0029 set spelled in SQL: NOT NULL, UNIQUE, PRIMARY KEY, DEFAULT <expr>, CHECK (<expr>), and an inline REFERENCES <T>(<col>) [ON DELETE …] [ON UPDATE …] (§5).
  • Table elements: PRIMARY KEY (<col>, …) (single and compound), UNIQUE (<col>, …), CHECK (<expr>), [CONSTRAINT <name>] FOREIGN KEY (<col>) REFERENCES <T>(<col>) [ON DELETE …] [ON UPDATE …] (§5).
  • CHECK and DEFAULT expressions reuse the ADR-0031 sql_expr grammar (the same fragment WHERE/HAVING/projections use).
  • CREATE TABLE IF NOT EXISTS <name> … is admitted: when the table already exists the statement is a no-op that succeeds with a note ("table already exists — skipped") instead of the plain-form "table already exists" error. IF NOT EXISTS is a near-universal cross-vendor idiom, not an engine-specific spelling, so it is part of the standard surface (refines §12).

DROP TABLE [IF EXISTS] <name>SqlDropTable. Cascade of inbound relationships follows the existing drop table semantics. IF EXISTS is admitted (universal across the major engines): dropping an absent table is then a no-op that succeeds with a note instead of the plain-form "no such table" error.

ALTER TABLE <name> <action>SqlAlterTable, where <action> covers, mapping to the existing low-level operations:

SQL action Underlying operation
ADD COLUMN <name> <type> [constraints] add-column (ADR-0013 rebuild where needed)
DROP COLUMN <name> drop-column
RENAME COLUMN <old> TO <new> rename-column
ALTER COLUMN <name> TYPE <type> change-column-type (§5 conversion)
ADD [CONSTRAINT <name>] <table-constraint> add-constraint / add-relationship (FK)
DROP CONSTRAINT <name> drop-constraint
RENAME TO <new> table rename (§6, new low-level op)

CREATE [UNIQUE] INDEX [<name>] ON <table> (<col>, …)SqlCreateIndex, mapped to the ADR-0025 index machinery; UNIQUE sets the index's uniqueness (a small extension to ADR-0025's index model if it does not already carry the flag, called out in §13).

DROP INDEX <name>SqlDropIndex.

5. Foreign keys → named relationships

A REFERENCES / FOREIGN KEY clause is the SQL spelling of an ADR-0013 relationship. Because SqlCreateTable is its own command carrying the whole parsed structure, a CREATE TABLE that declares FK columns creates the table and its relationship metadata together — one statement, one command, one transaction, one undo step (§10). No decomposition into separate commands is needed.

  • ON DELETE / ON UPDATE → the ADR-0013 referential actions.
  • A CONSTRAINT <name> FOREIGN KEY … names the relationship; an unnamed FK is auto-named by the existing ADR-0013 convention.
  • ALTER TABLE child ADD [CONSTRAINT <name>] FOREIGN KEY (<col>) REFERENCES <P>(<col>) … adds a relationship to an existing table (the clean 1:1 with add-relationship).
  • FK column type compatibility follows Type::fk_target_type (ADR-0011) unchanged.

6. Table rename — advanced mode only (C1)

ALTER TABLE <old> RENAME TO <new> is advanced-mode only; there is no simple-mode rename-table verb. It needs a genuinely new low-level operation (none exists today): within one transaction, rename the table in the database, rename its data/<table>.csv file, and update every metadata row that names it — the column-metadata rows, both ends of any relationship in __rdbms_playground_relationships that references the old name, and the table-level CHECK rows in __rdbms_playground_table_checks (added in 4a.3; keyed by table_name). Name validation and __rdbms_* rejection apply to the target. This closes the rename half of C1 for the advanced surface.

7. Column type conversion — one engine, mode-appropriate policy

The per-cell classification of ADR-0017 (clean / lossy / incompatible, plus static refusals for playground-type-specific targets such as → serial and ↔ blob) is a property of the type set, shared by both modes. The policy on the lossy tier differs by mode:

Tier Simple mode Advanced mode (ALTER COLUMN … TYPE)
clean auto-convert auto-convert
incompatible refuse (friendly) refuse (friendly) — real SQL errors too
static-refused (→serial, ↔blob, …) refuse refuse — our own types have no SQL meaning to mirror
lossy (3.143) refuse by default; --force-conversion opts in perform it (what SQL does), with a post-op "N values converted with loss" note; no force flag

Rationale: simple mode protects up front; advanced mode trusts the user like SQL does and lets undo catch regrets. A lossy advanced conversion is snapshotted (§10), so it is one undo away — there is no silent irreversible loss, and no need to drop to simple mode to "force". Conversions that exist only in the playground's vocabulary stay protected in both modes. The simple-mode --force-conversion / --dont-convert flags are unchanged and have no SQL spelling (advanced mode always performs the conversion); the Postgres USING <expr> clause is not adopted (§12).

8. Constraints

Column- and table-level constraints map to the ADR-0029 model: NOT NULL, UNIQUE, PRIMARY KEY (incl. compound, table-level), DEFAULT <expr>, CHECK (<expr>). A populated-column constraint addition reuses ADR-0029's pre-flight dry-run guard. CHECK / DEFAULT expressions are stored as the SQL the user could re-enter in advanced mode (ADR-0030 §11) — one syntax, not a third.

9. Engine neutrality (ADR-0030 §7)

No engine type names in or out (§3). STRICT is applied internally by the create path; it is not in the authored grammar, so typing it is an ordinary parse error, not a surfaced engine feature. Parse errors, out-of-subset refusals, and execution failures route through the friendly-error layer (ADR-0019) with engine-neutral wording.

10. Persistence, metadata, history, replay, undo

  • Structural execution keeps project.yaml, the metadata tables, and the CSV layer correct with the same guarantees as the simple-mode path (ADR-0015 §6 ordering preserved).
  • history.log records the literal submitted SQL line; replay re-runs it through the one walker with the advanced view active. create / drop / alter are schema-write entry words, not in ADR-0034 Amendment 1's app-lifecycle skip set, so SQL DDL replays as a write (re-applied) with no replay-filter change — unlike undo / redo, which had to be added to that skip set.
  • Undo (ADR-0006): each SQL DDL statement is a user mutation carrying a source, so it is snapshotted by the worker hook and is one undo step — including a CREATE TABLE with foreign keys, precisely because it is a single command (§5) rather than a decomposed sequence.

11. Ambient assistance comes for free (ADR-0030 §8)

Because the DDL is grammar in the unified tree, the walker mechanisms apply with no DDL-specific assistance code: syntax highlighting, the [ERR]/[WRN] validity indicator (ADR-0027), the per-command parse-error usage skeleton (ADR-0021), and the completion engine.

What each grammar node still authors (this is writing the grammar, not bolting assistance on afterwards): the correct IdentSource on every schema-name slot — so ALTER TABLE/DROP TABLE/DROP INDEX and REFERENCES T(col) / CREATE INDEX ON T (cols) complete from the SchemaCache; the per-node hint + usage catalog keys (as the app-command nodes carry help_id / usage_ids); and the DDL-specific walker diagnostics with their catalog keys — the DDL peers of the DML diagnostics ADR-0033 added (e.g. unknown type, column-already-exists, FK column-type mismatch, the §7 lossy-conversion note). The integration is structural, not free of authoring.

12. Out of scope

  • Per ADR-0030 §3: views, triggers, transaction control, PRAGMA, ATTACH/DETACH, VACUUM, virtual tables, multi-statement batches. One statement per submission; a trailing ; is tolerated.
  • The Postgres USING <expr> conversion clause (§7) — heavy (per-row expression evaluation), dialect-specific, and unable to express playground-type targets.
  • The simple-mode --dont-convert semantics have no SQL form (advanced ALTER COLUMN TYPE always converts).
  • The DSL → SQL teaching echo (ADR-0030 §10) is Phase 5, a separate ADR — not this one.
  • Engine-specific DDL spellings (AUTOINCREMENT, WITHOUT ROWID, collations) — the grammar admits the standard surface; extras are ordinary parse errors. (IF [NOT] EXISTS was reclassified into scope — see §4 — as a near-universal cross-vendor idiom rather than an engine-specific spelling.)

13. Phased implementation plan

Sub-phases, each opening with the smallest end-to-end slice and each with an explicit exit gate + a written Devil's-Advocate gate, mirroring ADR-0033's structure:

  • 4a — Dispatch + CREATE TABLE core. Advanced create dispatch; SqlCreateTable for columns + types (the §3 map, incl. the two-word double precision and discarded length args) + the clean-reuse column constraints onlyNOT NULL / UNIQUE / column-level PRIMARY KEY — + single/compound table-level PRIMARY KEY, plus IF NOT EXISTS (no-op-with-note, §4). Reuses do_create_table, whose inline-PK rule is aligned with the rebuild generator schema_to_ddl (inline only a first-column single PK) so a created table and its rebuilt form have identical DDL; serial autoincrement is independent of inline-vs-table-level PK (the insert path computes the next value), verified by round-trip tests. No FK (4b); no DEFAULT/CHECK/table-level UNIQUE (4a.2).
  • 4a.2 — Per-column CHECK/DEFAULT + composite UNIQUE(a,b). Split out (2026-05-24) and re-scoped (2026-05-25, user-confirmed) to the constraints that need no new internal table: (1) CHECK/DEFAULT via the full sql_expr surface stored as raw SQL textsql_expr is validate-only (no Expr AST for compile_check_sql/ColumnSpec), so a separate execution path captures the raw expression text; per-column CHECK reuses the existing __rdbms_playground_columns.check_expr column, DEFAULT round-trips via the engine's native PRAGMA table_info; (2) composite UNIQUE(a,b) — a new TableSchema.unique_constraints field, detected on read via the UNIQUE-constraint index (PRAGMA index_list origin u), round-tripped through YAML, with save/load/rebuild tests.
  • 4a.3 — Table-level / multi-column CHECK(…). (Implemented 2026-05-25 — plan docs/plans/20260525-adr-0035-sql-ddl-4a3.md.) Split from 4a.2 (2026-05-25, user-confirmed) because SQLite exposes no PRAGMA for CHECK constraints, so a table-level CHECK cannot be read back from the engine and needs a new __rdbms_* metadata table as its source of truth (the ADR-0012/0013 pattern) — a distinct architectural step. Landed as __rdbms_playground_table_checks (table_name, seq, check_expr); the builder distinguishes a table-level CHECK from a column-level one by element position (no column-def open). Composite UNIQUE deliberately stays PRAGMA-detected (engine-reportable, unlike CHECK). (The general rule: a DDL feature needs new model/execution work only when it introduces a structure simple mode could never produce, or an expression the structural helper cannot consume — cf. the UNIQUE-index flag in 4d and the rename op in 4h.)
  • 4b — Foreign keys in CREATE TABLE. (Implemented 2026-05-25 — plan docs/plans/20260525-adr-0035-sql-ddl-4b.md.) Inline REFERENCES <parent>[(<col>)] [ON DELETE/UPDATE …] + table-level [CONSTRAINT <name>] FOREIGN KEY (<col>) REFERENCES … → ADR-0013 relationship metadata, written in the create transaction (one undo step). Reuses the relationship name/uniqueness/metadata helpers shared with add relationship; do_create_table emits the FOREIGN KEY clause identically to schema_to_ddl. Self-references (parent = the table being created, validated against the in-statement columns/PK) and the bare REFERENCES <parent> form (resolves to the parent's single-column PK; composite → error) are both supported (user-confirmed). Inline FKs are auto-named; only the table-level form takes CONSTRAINT <name>. PK-target only (UNIQUE-target deferred with add relationship); Type::fk_target_type (ADR-0011) governs type compatibility.
  • 4c — DROP TABLE [IF EXISTS]SqlDropTable. (Implemented 2026-05-25 — plan docs/plans/20260525-adr-0035-sql-ddl-4c.md.) Reuses do_drop_table (cascade parity + the inbound-relationship refusal + metadata cleanup), so it matches the simple drop table; IF EXISTS on an absent table is a no-op-with-note (a new DropOutcome::Skipped mirroring CreateOutcome::Skipped; journalled, no snapshot, §4). drop is a shared entry word: drop table parses as SqlDropTable in advanced mode, drop column/relationship/ index/constraint fall back to the simple drop node. Advanced- mode drop completion now surfaces the SQL table (the shared-entry-word behaviour from create, ADR-0033 Amendment 3); the DSL drops still parse via fallback — 4i grows the surface as DROP INDEX lands in 4d.
  • 4d — CREATE [UNIQUE] INDEX / DROP INDEXSqlCreateIndex / SqlDropIndex. (Implemented 2026-05-25 — plan docs/plans/20260525-adr-0035-sql-ddl-4d.md.) Both reuse the ADR-0025 executors (do_add_index / do_drop_index), like 4c reused do_drop_table. CREATE [UNIQUE] INDEX [IF NOT EXISTS] [<name>] ON <T> (cols) (the unnamed form auto-named per ADR-0025; the leading [UNIQUE] is a concrete-keyword Choice, the optional name an on-led-first selector mirroring the drop index positional selector) and DROP INDEX [IF EXISTS] <name> (name-only — the positional drop index on T(…) stays the simple form via fallback). IF [NOT] EXISTS reuses the 4c no-op-with-note skip (journalled, not snapshotted). CREATE UNIQUE INDEX is admitted (user-confirmed 2026-05-25): ADR-0025 deferred UNIQUE indexes for the simple-mode DSL, but advanced mode "trusts the user like SQL does" (§7) — so the model gains an IndexSchema.unique flag (additive YAML, version 1; rebuild re-emits CREATE UNIQUE INDEX; the structure view + items panel mark [unique]), recorded as ADR-0025 Amendment 1. Simple-mode add unique index stays deferred. create/drop each gain a second advanced node, exercising the all-candidates dispatch (decide tries every advanced candidate).
  • 4e — ALTER TABLE add/drop/rename column. (Implemented 2026-05-25 — plan docs/plans/20260525-adr-0035-sql-ddl-4e.md.) alter is a new advanced-only entry word (like select/with); ALTER TABLE <T> ADD COLUMN <col> <type> [NOT NULL|UNIQUE|DEFAULT| CHECK] | DROP COLUMN <col> | RENAME COLUMN <old> TO <new>SqlAlterTable { AlterTableAction }, runtime-decomposed to the existing do_add_column / do_drop_column / do_rename_column (one undo step each) — no new worker layer. The COLUMN keyword is required (reserves bare RENAME TO for 4h, ADD CONSTRAINT for 4g); ADD COLUMN takes column constraints only (no PK / inline REFERENCES). do_add_column was extended to consume the SQL raw-text default_sql / check_sql (DEFAULT/CHECK; sql_expr is validate-only — the 4a.2 mechanism), so ADD COLUMN reaches parity with CREATE TABLE's column constraints. Drop/rename column now refuse a column a CHECK references — the 4a.3 deferral, extended (user-confirmed) to both table-level and column-level CHECKs — detected up-front by tokenizing the raw CHECK text (check_references_column, skipping string literals); for RENAME the column's own column-level CHECK counts (it drifts too), for DROP it does not (it drops with the column). This lives in the shared executors, so it guards both the simple drop/rename column and the SQL surface, fixing a latent rename-drift bug (a native rename rewrites the live CHECK but leaves the stored text — table-level in __rdbms_playground_table_checks or column-level in __rdbms_playground_columns — stale, breaking a later rebuild). SQL DROP COLUMN over an index-covered column is refused (no --cascade SQL spelling — matches SQLite + the simple default; user-confirmed). The shared column executors (and do_add_index) also gained an internal-__rdbms_*-table guard (refuse as "no such table"), closing a pre-existing exposure on both surfaces (user-confirmed). The friendly wording of the CHECK-guard refusal is H1. (The broader internal-table guard on do_change_column_type / do_add_constraint / do_add_relationship is a tracked follow-up.)
  • 4f — ALTER TABLE … ALTER COLUMN TYPE (the §7 conversion model + the lossy-with-note path). (Implemented 2026-05-25 — plan docs/plans/20260525-adr-0035-sql-ddl-4f.md.) A fourth AlterTableAction::AlterColumnType, runtime-decomposed to the existing change_column_type executor with ChangeColumnMode::ForceConversion — which is the §7 advanced policy: lossy cells are performed and counted (the engine-neutral client_side.transformed_lossy note fires), incompatible cells refuse, and the ADR-0017 static refusals (↔ blob, same-type, date ↔ datetime, non-int → serial) refuse in both modes. int → serial is allowed (auto-fills nulls, adds UNIQUE if non-PK — ADR-0018 §8; the §7 "static-refused →serial" summary is looser than the code). No force flag, no USING, no SET DATA TYPE synonym (§7/§12); undo is the advanced safety net. The grammar adds a fourth action branch leading on alter, discriminated in the builder by the type keyword (unique — ADD COLUMN's type is an ident); the type slot reuses SQL_TYPE. The internal-__rdbms_* guard was folded into do_change_column_type (user-confirmed 2026-05-25), closing the simple change column exposure too. (The remaining internal-table guard on do_add_constraint / do_add_relationship rides in 4g.)
  • 4g — ALTER TABLE add/drop constraint, add foreign key.
  • 4h — ALTER TABLE … RENAME TO (the §6 new low-level op).
  • 4i — Verification sweep. Typing-surface + matrix coverage, engine-neutral error pass, undo-parity check (one step per statement), help/usage for the new forms. Carried in from earlier slices: (a) refresh the CREATE TABLE help/usage skeleton for the 4a.2 DEFAULT/CHECK/composite-UNIQUE, 4a.3 table-CHECK, and 4b FK forms (deferred from each) — 4d's index forms already carry their own help/usage (ddl.sql_create_index / ddl.sql_drop_index + the parse.usage.* keys), since the nodes are new; (b) describe display of table-level constraints (composite UNIQUE + table CHECK) — note the unique-index marker shipped in 4d ([unique] in the structure view + items panel), so only the table-level constraint display remains here; (c) 4b self-ref FK indicator — a CREATE TABLE with a self-referencing FK (references <self>) parses + executes correctly, but the pre-submit schema-existence diagnostic falsely flags the not-yet-created self table as unknown (the FK parent slot is IdentSource::Tables). Make the diagnostic treat a FK parent equal to the CREATE TABLE target as valid, so the indicator stops lying for self-references. (d) 4c shared-entry-word completion merge — in advanced mode a shared entry word surfaces only the SQL node's continuations, so drop offers only table (not the DSL column/relationship/index/constraint) and a partial keyword like drop rel returns an empty list (a mid-word dead end), even though the DSL drops still parse + execute via fallback. Merge the expected sets of all candidate nodes for a shared entry word so advanced completion offers every valid continuation (drop → table + column + relationship + index + constraint; drop rel → relationship); verify create/insert/update/delete completion stays sensible. 4d widened this: create and drop now each have two advanced nodes (table + index), so a shared entry word's continuations now span two SQL shapes as well as the DSL ones — the merge matters more. (e) Discussion flag (user, 2026-05-25): before/with (d), discuss visually distinguishing simple- vs advanced-mode completions in the hint UI (likely by colour) so a learner can see which continuations are DSL and which are SQL — a UX design conversation, not just the mechanical merge.

Consequences

  • Advanced mode reaches DDL parity with simple mode and adds table-rename, so a learner can build and evolve a whole schema in standard SQL with the playground's types, metadata, and safety intact.
  • The command set grows by five Sql* DDL variants; the worker gains their handlers, which lean on shared low-level helpers where the operation matches the simple-mode path and stand alone where the SQL surface is genuinely richer (multi-FK CREATE TABLE).
  • One genuinely new capability — table rename — adds a low-level op that the simple mode does not have; it must keep the CSV file name and the relationship metadata in step with the table name.
  • ADR-0030 §4 is clarified (own Sql* DDL commands, structurally executed); no behaviour of the shipped DML/SELECT phases changes.
  • The conversion model unifies simple and advanced without a force flag in SQL, relying on undo (ADR-0006) as the advanced-mode safety net — a concrete payoff of having shipped undo first.

See also

  • ADR-0030 — the advanced-mode architecture; this is its Phase 4 and clarifies §4 (DDL representation) and restates §5 (types) / §7 (neutrality) / §8 (assistance) / §11 (persistence).
  • ADR-0033 — the DML phase; source of the category-grouped mode-aware dispatch (Amendment 1) reused for shared entry words.
  • ADR-0031sql_expr, reused for CHECK / DEFAULT.
  • ADR-0013 — relationships + the rebuild-table primitive that the ALTER/FK handlers build on.
  • ADR-0017 — the column type-change classification §7 shares.
  • ADR-0029 — column constraints; ADR-0025 — indexes; ADR-0011 — FK column-type compatibility; ADR-0005 — the ten-type vocabulary.
  • ADR-0006 — undo; each DDL statement is one undo step (§10).