13 KiB
Session handoff — 2026-05-25 (40)
Fortieth handover. This session shipped ADR-0035 Phase-4 sub-phases
4f and 4g (each test-first, with planning escalation and a combined
/runda finished-slice critique at the end) plus a /runda-found
rebuild fix. The next session implements 4h —
ALTER TABLE … RENAME TO (the §6 new low-level op), then 4i — the
verification sweep. After 4h+4i, Phase 4 (ADR-0035) is complete.
§1. State at handoff
Branch: main. HEAD 50a889e. Tests: 1885 passing, 0 failing,
0 skipped, 1 ignored (the unchanged friendly/mod.rs ```ignore
doctest). Clippy: clean (cargo clippy --all-targets -- -D warnings).
This session's commits (oldest → newest, local):
5b76315 feat: ADR-0035 4f — ALTER TABLE … ALTER COLUMN TYPE
6ff97f6 feat: ADR-0035 4g — ALTER TABLE add/drop constraint + add FK
50a889e fix: ADR-0035 4g — reconstruct table-CHECK metadata on rebuild
Git state — no divergence. origin/main is at 5b76315 (4f),
so 6ff97f6 (4g) + 50a889e (the fix) are unpushed — a normal
working state. (The earlier a2fc3c9 docs commit + 5b76315 4f were
pushed at some point this session.) Push is the user's step; nothing
blocks. Working tree clean.
§2. What shipped this session
- 4f —
ALTER TABLE <T> ALTER COLUMN <col> TYPE <type>(5b76315). A fourthAlterTableAction::AlterColumnType, runtime-decomposed tochange_column_type(…, ChangeColumnMode::ForceConversion)— which is the ADR-0035 §7 advanced policy (lossy converts with a note, no force flag; incompatible + the ADR-0017 static refusals still refuse;int → serialis allowed per ADR-0018 §8 — the §7 "→serial refused" summary is looser than the code). Builder discriminates the branch by thetypekeyword (unique — ADD COLUMN's type is an ident; checked first becausealter column …also containscolumn). Internal-__rdbms_*guard folded intodo_change_column_type. - 4g —
ALTER TABLEadd/drop constraint + add FK (6ff97f6).ADD [CONSTRAINT <name>] (CHECK | composite UNIQUE | FOREIGN KEY)+DROP CONSTRAINT <name>. User-confirmed scope: ADD = CHECK + FK + composite UNIQUE;ADD PRIMARY KEYand a named UNIQUE are refused (composite UNIQUE is anonymous in our model). Each ADD reuses a low-level path with a dry-run guard (table-CHECK/UNIQUE rebuild; FK →do_add_relationship, bareREFERENCES <P>→ parent single PK,create_fk = false).DROP CONSTRAINTresolves the name to a named table-CHECK then a child-side named FK, else refuses. Named-CHECK round-trip: a nullablenamecolumn on__rdbms_playground_table_checks(rebuild-only arrival) and aproject.yamlcheck_constraintsextension to a{expr, name}mapping (the bare-string form still reads — back-compatible). The internal-__rdbms_*guard was folded intodo_add_constraint/do_add_relationship, completing the 4d/4e/4f guard class. ADD CHECK also refuses a name colliding with an existing CHECK or relationship (keeps DROP unambiguous). Grammar restructured: the walker'sChoicedoes not backtrack between two same-leading- keyword branches, so the actionChoicekeeps one branch per verb (add/drop/rename/alter) andadd/dropfan out to an innerChoicewhose branches each lead on a distinct second keyword (column / constraint / check / unique / foreign / primary). - Rebuild fix (
50a889e,/runda-found).do_rebuild_from_textre-emitted table-CHECKs into the recreated DDL (so enforced) but never repopulatedCHECK_TABLE— so a fresh rebuild (missing.db, reconstructed fromproject.yaml) lost the CHECK metadata (incl. the name):DROP CONSTRAINT/describe/ a later save would drop it. In-place rebuilds only worked because the wipe never touched the table. (Latent since 4a.3 for unnamed checks.) Rebuild now wipes + repopulatesCHECK_TABLEfrom yaml (name + seq + expr), like META/REL, and adds thenamecolumn if a pre-4g table predates it (the rebuild-only migration). Regression test:e2e_named_check_metadata_survives_a_fresh_rebuild.
ADR-0035 stays Accepted; README + requirements.md Q1 + ADR §13
(4f, 4g) updated each slice.
§3. The NEXT job — 4h (ALTER TABLE … RENAME TO)
No plan written yet — 4h needs its own plan (Phase 1→3 + a planning
/runda/written-DA gate) before building, because it is the one
genuinely new low-level op in Phase 4 (ADR-0035 §6), not a reuse.
Spec (ADR-0035 §6): ALTER TABLE <old> RENAME TO <new> is
advanced-mode only (no simple-mode rename-table verb). Within one
transaction:
- Rename the table in the database.
- Rename its
data/<old>.csv→data/<new>.csv(persistence layer). - Update every metadata row that names it:
__rdbms_playground_columnsrows (table_name),- both ends of any
__rdbms_playground_relationshipsrow (parent_tableandchild_table), __rdbms_playground_table_checksrows (table_name, added in 4a.3, keyed by table name).
- Name validation +
__rdbms_*rejection apply to the target.
Closes the rename half of C1 for the advanced surface.
Grammar: a new AlterTableAction::RenameTo { new: String } (or
similar) — a fifth action branch in AT_ACTION_CHOICES. It leads on
rename — but rename column already leads on rename (the walker
Choice does not backtrack same-led branches; see §7.4). So rename
must follow the §2 pattern: one rename branch with an inner Choice
on the distinct second token (column → rename-column; to → rename-
table). Mirror the add/drop restructure 4g did. The builder keys on
contains_word("column") (already the §4f/§4g discriminator order) vs a
bare rename … to.
Executor: a genuinely new do_rename_table (no existing reuse) — the
CSV rename is a persistence-layer concern (cf. how do_drop_table cleans
CSV + metadata; ADR-0015 §6 ordering, commit-db-last). One undo step
(snapshot is whole-project). Internal-__rdbms_* guard on the target
(and refuse renaming an internal table as source — reject_internal_ table_name on both).
Tests: Tier-1 parse (rename-table vs rename-column dispatch); Tier-3
e2e via run_replay — rename a table that has rows (CSV follows), a
relationship (both ends update), and a table-level CHECK (the
table_checks rows follow); survives rebuild (the renamed table +
its metadata round-trip through yaml); one undo step; refuse rename to an
existing name / to an __rdbms_* name.
§4. Then 4i — verification sweep (the running deferral tail)
Canonical list: ADR-0035 §13 4i. Unchanged from handoff-39 except 4f/4g now carry their own help/usage (so the CREATE TABLE skeleton refresh is the only help/usage debt left). The tail:
- (a) Refresh the
CREATE TABLEhelp/usage skeleton for the 4a.2DEFAULT/CHECK/composite-UNIQUE, 4a.3 table-CHECK, 4b FK forms (4d/4e/4f/4g forms already carry their own help/usage). - (b)
describedisplay of table-level constraints (compositeUNIQUE+ tableCHECK, now incl. named CHECKs from 4g) — the unique-index[unique]marker shipped in 4d, so only the table- level constraint display remains. - (c) 4b self-ref FK pre-submit indicator (the diagnostic
false-flags a
references <self>parent as unknown). - (d) shared-entry-word completion merge —
create/drop(and now constraint forms) should offer every valid continuation across the SQL + DSL nodes; 4d/4g widened this. - (e) user discussion flag (2026-05-25): visually distinguish simple- vs advanced-mode completions in the hint UI (likely colour) — a UX design conversation, before/with (d).
- Plus the §4i staples: typing-surface + matrix coverage, engine-neutral error pass, undo-parity (one step per statement).
§5. Tracked follow-ups (not lost)
- H1 wording —
--create-fkleak (4g).ALTER … ADD FOREIGN KEYon a missing child column reusesdo_add_relationship's error, which mentions the DSL flag--create-fk— off-key in an SQL context. Fix the wording for the SQL path (or branch the message). H1. - Coverage gap — pre-4g 3-column
CHECK_TABLEmigration (4g/fix). The rebuild-timeALTER … ADD COLUMN namemigration and the named- CHECK-on-old-project refusal (check_table_has_name_column) are logically sound + guarded, but the genuinely-old 3-column table in-place path is untested — a 3-colCHECK_TABLEcan't be fabricated through the public API (no raw-SQL exec). The common case (old project whose.dbis deleted → fresh rebuild → 4-col table) IS tested. A test helper (raw-SQL exec, or a fixture.db) would close it; flagged to the user, not yet decided. - H1 friendly wording for the 4e CHECK-guard refusals and the 4f type-conversion diagnostics (engine-neutral but terse) — carried.
- The §8 items from handoff-39 (app-lifecycle-command runtime-failure
journalling; M4 execution-time mode side-channel;
blobvalue literal; CI/TT5; DSL→SQL teaching echo) remain open.
§6. Patterns the implementer must not forget
- Walker
Choicedoes not backtrack between same-leading-keyword branches. This bit 4g (twoaddbranches "expected column"). Keep one branch per leading verb, fan out on a distinct second token via an innerChoice. 4h'srenamemust joinrename columnunder onerenamebranch the same way. - Reuse low-level executors; the runtime decomposes
SqlAlterTable. 4f wrappedchange_column_type; 4g wrappeddo_add_relationship+ addeddo_alter_add_table_check/do_alter_add_unique/do_drop_constraint_by_name. 4h is the exception — a genuinely newdo_rename_table(§6). - Two DDL generators stay in sync (
do_create_table/schema_to_ddl). 4g added theCONSTRAINT <name> CHECKemission toschema_to_ddl;do_create_table's table-CHECK insert writesname = NULL(CREATE-TABLE CHECKs stay unnamed — an ALTER-only naming, a noted consistency item). do_rebuild_from_textreconstructs ALL metadata from yaml. It now wipes + repopulatesMETA/REL/CHECK_TABLEand recreates indexes. A new metadata table or column MUST be added to both the wipe and the repopulation or a fresh rebuild silently drops it (the exact bug50a889efixed). 4h touches metadata values (renames), so verify a renamed table round-trips through a fresh rebuild.- Exhaustive matches: a new
AlterTableActionvariant forces arms inruntime.rs(innermatch action) andapp.rs(build_translate_context's inner match). The compiler finds them. - Catalog lockstep + vocab audit — refresh
sql_alter_tablehelp/usage for the rename form; keep wording engine-neutral (engine_vocabulary_auditenforces it). - Probe, don't reason — the combined
/rundacaught the rebuild bug precisely because a test was written for the fresh-rebuild path the original e2e skipped. For 4h, test the CSV-rename + both-relationship- ends + table_checks-follow paths explicitly, and a fresh rebuild.
§7. Process pins (unchanged, still binding)
- Confirm every commit. Propose the message; wait for the go-ahead. No AI attribution.
- Push is the user's step. Never push/force-push. Unpushed commits are normal (see §1).
- Escalate ambiguity / new cost. 4g's three scope forks (ADD set,
CHECK migration, DROP scope) and the discovered
project.yaml-format implication were all surfaced + user-decided before building. Do the same for 4h's open calls (e.g. rename-to-existing-name behaviour, any describe/echo wording). - DA hat each slice;
/rundaat the planning exit AND on the finished slice. The user opted to combine the finished-slice/rundaacross 4f+4g this session — it found a real bug (the rebuild metadata loss). Budget for at least one finished-slice/rundacovering 4h (+4i); it pays off. - Keep docs lockstep — ADR status/§13 + README +
requirements.mdin the same edit.
§8. How to take over
- Read, in order: this file →
docs/adr/0035-advanced-mode-sql-ddl.md(§6 rename, §13 4h/4i) →CLAUDE.md→docs/requirements.md(Q1,C1). The 4f/4g plans (docs/plans/20260525-adr-0035-sql-ddl-4f.md,…-4g.md) are good models for the 4h plan you must write. - Baseline:
cargo test # expect 1885 passing / 0 failing / 0 skipped / 1 ignored cargo clippy --all-targets -- -D warnings # clean - Plan 4h (Phase 1→3 + planning
/runda), surfacing any open calls to the user; then implement test-first per §3 (grammar restructure of therenamebranch →do_rename_tableexecutor → runtime/app arms → Tier-1/Tier-3 tests incl. a fresh rebuild → catalog + docs lockstep → finished-slice/runda→ commit proposal). - Then 4i (§4) — the verification sweep closes Phase 4.