Three Phase-4 sub-phases shipped this session (all local-only on main, 1805 tests passing, clippy clean): - 4a.3 table-level/multi-column CHECK (new __rdbms_playground_table_checks) - 4b foreign keys in CREATE TABLE (ADR-0013 named relationships) - 4c DROP TABLE [IF EXISTS] (DropOutcome::Skipped no-op-with-note) Handoff records the growing 4i deferral list (a–e, canonical in ADR-0035 §13 4i) and flags the 4d escalation (IndexSchema.unique model extension for CREATE UNIQUE INDEX).
15 KiB
Session handoff — 2026-05-25 (38)
Thirty-eighth handover. This session shipped three ADR-0035 Phase-4
sub-phases — 4a.3, 4b, 4c — each test-first with a /runda (4a.3, 4b)
or written-DA (4c) gate, all green. The next session implements 4d —
CREATE [UNIQUE] INDEX / DROP INDEX (§4), which carries one
escalation: the IndexSchema.unique model extension (ADR-0025 deferred
unique indexes). A growing 4i list (a–e) is now the running tail of
deferred polish — see §6, tracked canonically in ADR-0035 §13 4i.
§1. State at handoff
Branch: main. Tests: 1805 passing, 0 failing, 0 skipped,
1 ignored (the unchanged friendly/mod.rs ```ignore doctest).
Clippy: clean (cargo clippy --all-targets -- -D warnings).
HEAD (local-only): e52e90c (4c). origin/main is at 1991fb4
(handoff-37); the 3 commits since are local-only. Unpushed commits
are a normal working state; pushing is the user's step — do not prompt.
This session's commits (oldest → newest):
60111f6 feat: ADR-0035 4a.3 — table-level / multi-column CHECK
76d6059 feat: ADR-0035 4b — foreign keys in CREATE TABLE
e52e90c feat: ADR-0035 4c — DROP TABLE [IF EXISTS]
(Plus this handoff commit.)
§2. What shipped this session
Advanced-mode SQL CREATE TABLE is now feature-complete for constraints
- FKs, and
DROP TABLEis live.
- 4a.3 — table-level / multi-column
CHECK(60111f6).CREATE TABLE t (a int, b int, CHECK (a < b)). SQLite exposes no PRAGMA for CHECK, so a table-level CHECK is stored in a new internal table__rdbms_playground_table_checks (table_name, seq, check_expr)as its source of truth (read byread_schema/read_schema_snapshot, cleared bydo_drop_table, round-tripped throughproject.yamlviaTableSchema.check_constraints). The builder distinguishes a table-level CHECK from a column-level one by element position (no column-def open), using a depth-aware tracker so a length-arg comma (numeric(10,2)) or a table-PRIMARY KEY (a,b)comma is not mistaken for an element separator. CompositeUNIQUEstays PRAGMA-detected (user-confirmed)./rundaadded the rebuild-survival + drop-clean + drop-column-referenced cross-cutting guards. - 4b — foreign keys in
CREATE TABLE(76d6059). Inline<col> … REFERENCES <parent>[(<col>)] [ON DELETE/UPDATE …]and table-level[CONSTRAINT <name>] FOREIGN KEY (<col>) REFERENCES …, each an ADR-0013 named relationship created in the create transaction (one undo step). Validation/naming/metadata-insert/ type-compat factored into shared helpers thatdo_add_relationshipwas refactored to use (this pre-pays 4g).do_create_tableemits theFOREIGN KEYclause identically toschema_to_ddl. Self-refs (validated against the in-statement columns/PK) and bareREFERENCES <parent>(resolves to the parent's single-column PK; composite → error) supported (user-confirmed). FKs round-trip via the existing relationship plumbing — no new persistence structures./rundaadded 5 cross-cutting probes (FK survives the rebuild primitive + a laterrebuild_from_text; actions survive rebuild; drop semantics; bare self-ref). - 4c —
DROP TABLE [IF EXISTS](e52e90c).SqlDropTablereusesdo_drop_table(cascade / inbound-relationship refusal / metadata cleanup) — full parity with simpledrop table.IF EXISTSon an absent table is a no-op-with-note via a newDropOutcome { Dropped, Skipped }mirroringCreateOutcome(journalled, no snapshot), with a newddl.drop_skipped_absentnote +DslDropSkippedevent.dropis now a shared entry word (SQL-first;drop column/relationship/index/constraintfall back to simple and still execute).
ADR-0035 stays Accepted; README + requirements.md Q1 updated each
slice; plan docs docs/plans/20260525-adr-0035-sql-ddl-4a3.md,
…-4b.md, …-4c.md.
§3. Design decisions settled this session (do not re-litigate)
All user-confirmed unless marked implementer-call:
- 4a.3 metadata table
__rdbms_playground_table_checks— focused, not a generictable_constraints(expand for named CHECK in 4g). - Composite
UNIQUEstays PRAGMA-detected — engine-reportable, unlike CHECK; the PRAGMA/metadata split is principled and stable. - 4b self-referencing FK supported — validated against the in-statement columns/PK when the parent is the table being created.
- 4b bare
REFERENCES <parent>supported — resolves to the parent's single-column PK; composite-PK parent → "name the column" error. (The user leaned standard-SQL here.) - Inline FK auto-named; only the table-level form takes
CONSTRAINT <name>(ADR §4/§5). PK-target only; UNIQUE-target stays deferred. - 4c
dropdispatch:drop table T→SqlDropTablein advanced (equivalent execution to the simpleDropTable); otherdropforms fall back to simple.IF EXISTSskip is journalled, not snapshotted. - Drop-column-referenced-by-table-CHECK (4a.3 finding): fails cleanly (rollback, table intact). Up-front detection is 4e; friendly wording is H1 (user-confirmed split).
- (implementer) The grammar leading-
Optionaltrap: aChoicebranch / element must start on a concrete keyword, never anOptional—walk_seqadvancesidxpast a skipped Optional, so a later mismatch becomes a hardFailedthat aborts the enclosingChoice(caught whenTABLE_FKandSQL_DROP_TABLEshapes broke allCREATE TABLEparsing). HenceTABLE_FK/TABLE_FK_NAMEDare split onforeign/constraint.
§4. The NEXT job — 4d (CREATE [UNIQUE] INDEX / DROP INDEX)
Goal: CREATE [UNIQUE] INDEX [<name>] ON <table> (<col>, …) →
SqlCreateIndex, and DROP INDEX <name> → SqlDropIndex, mapped to the
ADR-0025 index machinery. Plan it like the others (short plan doc,
test-first, escalate the one open call below).
The escalation (surface FIRST, ADR-0035 §13 4d / ADR-0025):
CREATE UNIQUE INDEX needs an IndexSchema.unique flag — ADR-0025
deferred unique indexes, so the index model has no uniqueness slot.
This is a model extension of the same class as 4a.2/4a.3 (a structure
the model doesn't yet carry). Escalate the index-model extension to
the user before implementing — it touches IndexSchema
(src/persistence/mod.rs), the YAML round-trip, and how read_…
detects a unique index (PRAGMA index_list reports unique/origin c).
Reuse / patterns that carry in:
- Shared entry words gain a second advanced node.
dropalready hasSQL_DROP_TABLE; 4d addsSQL_DROP_INDEX(also entrydrop), andcreategainsSQL_CREATE_INDEX(entrycreate) alongsideSQL_CREATE_TABLE. Verify the dispatch tries all advanced candidates for an entry word (commands_for_entry_wordreturns them; confirm the walker/completion_probeactually walks each). This is new — until now each shared entry word had exactly one advanced node. Add a parse test:drop index ix→SqlDropIndex,drop table T→SqlDropTable, both in advanced mode. DROP INDEX IF EXISTSreuses the 4c skip path verbatim — theDropOutcome::Skipped+ journalled-no-snapshot pattern + theddl.drop_skipped_absentnote (or an index-specific note key).- Existing index ops:
add index/drop index(ADR-0025) already exist as DSL commands (Command::AddIndex/DropIndex,do_add_index/do_drop_indexindb.rs). 4d's SQL forms should reuse those low-level executors, like 4c reuseddo_drop_table. - Index name auto-generation already exists (the
<T>_<cols>_idxconvention, ADR-0025) — mirror it for an unnamedCREATE INDEX.
§5. Everything else remaining in Phase 4 (ADR-0035 §13)
In order after 4d:
- 4e —
ALTER TABLEadd/drop/rename column (the ADR-0013 rebuild primitive). Includes the drop-column-referenced-by-table-CHECK up-front detection (the 4a.3 deferral; friendly wording is H1). - 4f —
ALTER TABLE … ALTER COLUMN TYPE— the §7 conversion model; advanced mode performs lossy conversions with a note + relies on undo (no force flag). - 4g —
ALTER TABLEadd/drop constraint, add FK. The add-FK path reuses the 4b shared relationship helpers; named CHECK adds anamecolumn to__rdbms_playground_table_checks. - 4h —
ALTER TABLE … RENAME TO— the §6 new low-level op (rename table +data/<t>.csv+ relationship metadata + thetable_checksrows — the §6 amendment this session added). Advanced-only (C1). - 4i — Verification sweep (the running deferral tail — §6 below).
§6. The 4i list (the running deferral tail — READ THIS)
Canonical location: ADR-0035 §13 4i bullet, items (a)–(e). Every
other mention (the two newest plan docs, the NOTE (4i) code comments
in src/dsl/grammar/sql_create_table.rs and src/completion.rs, the
README) points back to it — there is no independent list. Reproduced
here so the next session sees it without digging:
- (a) Refresh the
CREATE TABLEhelp/usage skeleton for the 4a.2DEFAULT/CHECK/composite-UNIQUE, 4a.3 table-CHECK, and 4b FK forms (each deferred its skeleton update; 4c's were added since the node is new). Add 4d's index forms too when they land. - (b)
describedisplay of table-level constraints (compositeUNIQUE+ tableCHECK) —TableDescriptionsurfaces neither today (symmetric gap, not a regression). - (c) 4b self-ref FK pre-submit indicator —
references <self>parses + executes fine, but the schema-existence diagnostic falsely flags the not-yet-created self table (FK parent slot isIdentSource::Tables). Teach the diagnostic about theCREATE TABLEtarget. - (d) Shared-entry-word completion merge — advanced-mode
dropoffers onlytable; a partial DSL keyword (drop rel) returns an empty list (mid-word dead end). The DSL drops still parse + execute. Merge all candidate nodes' continuations for a shared entry word (drop→ table + column + relationship + index + constraint;drop rel→ relationship); verifycreate/insert/update/deletecompletion stays sensible. (4d will widen this —drop/creategain a second advanced node, so the merge matters more.) - (e) Discussion flag (user): before/with (d), discuss visually distinguishing simple- vs advanced-mode completions in the hint UI (likely by colour) — a UX design conversation, not just the mechanical merge.
Watch item: that 4i bullet is getting long. If 4d–4h keep adding
deferrals, consider promoting it to a dedicated short "4i scope" doc or
a requirements.md checklist (flagged to the user, not yet needed).
§7. Patterns the implementer must not forget
- Two DDL generators stay in sync.
do_create_tableandschema_to_ddlboth emit a table's DDL; any new clause must be emitted by BOTH, identically (the 4aseriallesson). 4b's FK clause and 4a.3's CHECK clause follow this; round-trip/rebuild tests are the net. - Reuse low-level executors. SQL DDL commands wrap the existing
helpers (
do_create_table,do_drop_table,do_add_index, the rebuild primitive, the relationship helpers) — they do not fork. 4c reuseddo_drop_table; 4d should reusedo_add_index/do_drop_index. - Metadata survives the rebuild primitive because it uses a raw
DROP(notdo_drop_table, which clears__rdbms_*rows) +schema_to_ddlre-emit. Proven for CHECK (4a.3) and FK (4b). Any new metadata keyed by table name inherits this — but 4h RENAME must update every such table (the §6 list now includestable_checks). - Undo: every mutating
Sql*worker variant wraps insnapshot_then(one undo step). TheIF [NOT] EXISTSskip arms journal without a snapshot (no-op = nothing to undo). - Grammar leading-
Optionaltrap (§3) — element/Choicebranches start on a concrete keyword. - Catalog lockstep: new
help_id/usage_id/note keys need akeys.rsentry and anen-US.yamlbody (thekeys_validate_against_catalogtest enforces both ways); engine-neutral wording (the vocab audit enforces it). Help keys carry ahelp.prefix inkeys.rs. - Exhaustive matches: a new
Command/Requestvariant forces arms incommand.rs(verb/target_table),app.rs(failure-translate),runtime.rs(dispatch + outcome→event), andtests/typing_surface/mod.rs(the matrix label). The compiler finds them.
§8. Other tracked deferred items (nothing lost)
- (A) App-lifecycle-command runtime-failure journalling (ADR-0034 follow-up).
- M4 — execution-time mode side-channel (ADR-0033 Amendment 3).
blobvalue literal —Valuehas no blob variant (pre-existing).- Undo residual edge (ADR-0006 note): an entirely-unwritable
.snapshots/can leave a stale redo — accepted. - CI / TT5, DSL→SQL teaching echo (ADR-0030 Phase 5, after DDL), then the §6 polish phase.
- Friendlier FK/CHECK error messages (e.g. "create the parent first", "can't drop a column a CHECK references") — H1 (the friendly-error layer is partial; current messages are engine-neutral but terse).
§9. Process pins (unchanged, still binding)
- Confirm every commit. Propose the message; wait for the go-ahead.
- Push is the user's step. Never push; never prompt about it.
- No AI attribution in commits (global rule overrides any default).
- Probe, don't reason. This session's real findings — the grammar
leading-
Optionaltrap, the FK-survives-rebuild invariant, the drop-column-referenced clean-failure, the unevendropcompletion — all came from running probes, not reasoning. Reproduce before concluding; delete throwaway probes (or promote them) before committing. - Escalate ambiguity / new cost. 4b's self-ref + bare-ref and 4c's
completion deferral were all surfaced to the user, not decided
silently. 4d's
IndexSchema.uniqueextension is the next escalation. - Keep docs lockstep. ADR status/§13 + README +
requirements.mdupdate in the same edit; the 4i list lives in ADR §13 4i (§6). - DA hat each slice;
/rundaat phase-4 end. The user ran/rundaon 4a.3 and 4b this session (both PASS, both added cross-cutting tests); 4c got a written DA pass.
§10. How to take over
- Read, in order: this file →
docs/adr/0035-advanced-mode-sql-ddl.md(§4/§5 FK, §13 sub-phase list incl. the 4i (a)–(e) tail; 4d is next) → the three plan docs (…-4a3.md,…-4b.md,…-4c.md) →CLAUDE.md→docs/requirements.md(Q1/Q4/C1). - Baseline:
cargo test # expect 1805 passing / 0 failing / 0 skipped / 1 ignored cargo clippy --all-targets -- -D warnings # clean - Start 4d per §4: escalate the
IndexSchema.uniquemodel extension first, then short plan doc, then implement test-first (grammar → command/builder → worker reusingdo_add_index/do_drop_index→ round-trip). Verify the shared-entry-word dispatch handles a second advanced node per entry word. - Mirror the established slices:
tests/sql_drop_table.rs(Tier-3), the in-cratesql_drop_table_tests(parse), and thebuilder_tests/Tier-3 split used byCREATE TABLE.