Files
rdbms-playground/docs/handoff/20260610-handoff-62.md

9.6 KiB

Session handoff — 2026-06-10 (62)

Sixty-second handover. Continues from handoff-61 (X1 logging full sweep

  • T3 residuals). This session was a list-trimming + one-feature run: it closed C4 (the create m:n relationship convenience command, ADR-0045) and, in passing, resolved Gitea issue #19 (drop-PK guard). Handoff-61 itself was written mid-session, so the X1 / T3 work it describes is also part of this session's commit range.

§1. State at handoff

Branch: main. HEAD 8bd43cc. Push is the user's step.

Tests: 2237 passing / 0 failing / 1 ignored (the 1 ignored is the long-standing doc-test). Clippy clean (nursery, all targets). +30 over the handoff-60 baseline of 2207.

This session's commits (8, on top of session-60's 5):

8bd43cc feat: create m:n relationship convenience command (C4, ADR-0045)
e598008 docs: ADR-0045 m:n convenience command (C4); accepted
e44d298 test+docs: lock drop-PK-refused on advanced surface; document no-PK advanced mode (#19)
b803468 docs: session handoff 61 — X1 logging full sweep + T3 residuals closed
5a33f2a fix(fk): compound-FK violation message names every column pair
6985a43 fix(fk): inline FK referencing a compound PK points at the table-level form
0a7612e feat: comprehensive logging across parser, app, persistence, runtime (X1)
a8ad0c6 feat(db): comprehensive logging across worker + executors (X1)

Requirements closed this session: X1 [x] (logging), T3 residuals (both ADR-0043 messaging items), C4 [x] (m:n). Gitea #19 closed.

§2. X1 — comprehensive logging (closed) — see handoff-61 §2

Full detail in handoff-61. In brief: ~75 → 137 tracing sites under a documented level discipline (read the src/logging.rs module doc before adding logs). Logs go to a file (--log-file > RDBMS_PLAYGROUND_LOG_FILE > ~/.rdbms-playground/playground.log); level via the separate RDBMS_PLAYGROUND_LOG env (default info). debug = per-command detail (off by default), trace = hot paths (per-keystroke parse).

§3. T3 residuals (both closed) — see handoff-61 §3

6985a43 inline-FK arity wording (points at the table-level form; added inline: bool to SqlForeignKey). 5a33f2a compound-FK violation names every column pair (comma-joined in the single-column facts slots; enrich_fk_violation). ADR-0043 now has no residuals.

§4. Issue #19 — drop-PK guard (closed, e44d298)

A parallel check the user requested. Finding: dropping a PK column is already refused in both modes via the shared do_drop_column guard ("cannot drop primary-key column …") — simple drop column and advanced ALTER … DROP COLUMN both route through it. Added end-to-end coverage (tests/it/sql_alter_table.rs: single + compound PK, refusal for the right reason). Corrected a long-standing misconception: the issue's premise ("we don't support creating a table with no PK") is true only in simple mode — advanced SQL create table t (a int) makes a real PK-less table (SQLite's implicit rowid keys it; only WITHOUT ROWID lacks one, which this app never creates). The simple-mode with pk requirement is pedagogical (ADR-0029), not an engine constraint. Documented in docs/simple-mode-limitations.md.

§5. C4 — create m:n relationship (the feature, ADR-0045)

create m:n relationship from <T1> to <T2> [as <name>] generates a junction table: one FK column per parent PK column ({table}_{pkcol}, typed via fk_target_type — ADR-0011), a compound PK over all of them, and two CASCADE 1:n relationships — all in one do_create_table call = one undo step (no batch needed; do_create_table already takes foreign_keys + writes per-FK relationship metadata). Auto-named {T1}_{T2} (optional as), available in both modes, compound-parent PKs supported (ADR-0043).

Forks (all user-confirmed): compound-over-FKs PK (vs surrogate / none); CASCADE actions; auto-name + optional as; both modes; FK columns {table}_{pkcol}. Refused: self-referential m:n (from T to T — full stop, OOS); PK-less parent; internal __rdbms_* junction name; name collision.

Where the code lives:

  • Grammar: a separate CREATE_M2N CommandNode in dsl/grammar/ddl.rs (entry create, opener Node::Literal("m") — not a keyword, so it never shadows an identifier), registered Simple in grammar/mod.rs REGISTRY. build_create_m2nCommand::CreateM2nRelationship { t1, t2, name }.
  • Worker: Request::CreateM2nRelationship, Database::create_m2n_relationship, executor do_create_m2n_relationship (reads each PK, guards self-ref / PK-less, builds columns + compound PK + 2 SqlForeignKeys, calls do_create_table).
  • Runtime: execute_command_typed arm. Echo: echo::render_create_m2n (advanced-mode DSL→SQL teaching echo, ADR- 0038 — the generated CREATE TABLE … FOREIGN KEY …, round-trips as valid SQL), wired in build_schema_echo.
  • Surfaces: completion ("m","m:n") composite; help.ddl.create_m2n + parse.usage.create_m2n catalog (+ keys.rs declarations); highlighting is grammar-driven (automatic).

Tests: 14 integration (tests/it/m2n.rs), 7 typing-surface matrix (tests/typing_surface/create_m2n.rs — completion/hint/highlight/parse), plus echo / highlight / usage-disambiguator / internal-name units.

§6. Framework fixes the C4 build + two /runda passes surfaced

C4's "separate node" design rested on an ADR premise that proved only half true: "the walker already dispatches multiple nodes per entry word" held in advanced mode but not simple. Three latent simple-mode assumptions ("≤1 DSL form per entry word") were generalized, all behaviour-preserving for existing single-form commands:

  1. Dispatch (walker/mod.rs decide) committed simple.first() unconditionally → now tries simple candidates (so create table no longer shadows create m:n). Reduces to the old single-candidate commit when there is one.
  2. Completion continuation-merge (walker/mod.rs) was gated if mode == Advanced → now runs in simple mode too, gated on simple_count > 1 so single-form entry words are untouched.
  3. Usage disambiguator (grammar/mod.rs usage_key_for_input) knew the 1:n opener but not m:n → added an explicit branch.

Plus a root-cause bug fix (user-chosen scope): do_create_table now rejects internal __rdbms_* names. This closed both the C4 as __rdbms_* hole and a pre-existing hole — simple-mode DSL create table __rdbms_* was accepted at parse (the TABLE_NAME_NEW slot had no guard; only the advanced-SQL path rejected internal names). The shared executor is the single choke point; the SQL path still rejects earlier at parse.

Process note: the two /runda passes were worth it. The first (pre-build) corrected the inverted "no PK-less tables" assumption and confirmed the do_create_table reuse against code. The second (pre-commit) closed five test-coverage gaps — two of which (highlighting, persistence round-trip) had been wrongly claimed verified (the typing-surface Assessment has no highlight field; "transitively covered" was a hand-wave) — and found the two bugs above. Lesson re-confirmed: verify a claimed-tested surface actually has an assertion; "transitively covered" is a DA red flag.

§7. Remaining open landscape

Closed since handoff-60: X1, both T3 residuals, C4, #19. ADR-0043 and ADR-0045 fully landed.

Still open (by readiness, unchanged otherwise):

  1. TT5 CI — test infra solid (2237 green); no pipeline. Gitea Actions / Woodpecker (a fresh decision tied to the migration + ADR-0001's reopened distribution question). Friction: the requirement is Linux/macOS/Windows on stable — self-hosted Gitea can do Linux easily, but mac/Windows runners need machines that may not exist; likely needs a Linux-first scope decision.
  2. SD1 seed then H2 hint — the two unblockers for A1 app-commands; both net-new, own ADR (SD2 is the seed-generator design ADR). SD1 should now seed m:n junctions too (valid FK refs from parent rows) — C4 makes that concrete.
  3. V2/S3 multi-result tabs or V4 journal — larger output-model redesign, design-first, own ADR. V4 also unlocks diagram live-reflow.
  4. C3a modify relationship — small follow-up (drop+add covers it today; ADR pending).

ADR-0045 OOS for later: self-referential m:n (deliberate non-goal); per-relationship action overrides; extra junction payload columns; m:n-as-diagram echo. Pre-existing, now-fixed: the internal-name hole (§6) — no separate issue needed, it's closed.

§8. How to take over

  1. Read handoffs 60 → 61 → 62, then CLAUDE.md, docs/requirements.md (X1/C4 now [x]), docs/adr/README.md.
  2. Before adding logging: the level discipline in the src/logging.rs module doc.
  3. For grammar/command work: an entry word can now carry multiple DSL forms in simple mode (C4 generalized the dispatch + completion + usage paths). create is the first such entry word (table + m:n).
  4. For relationship/FK work: ADR-0013/0043/0044/0045 are all landed; SqlForeignKey carries inline; do_create_table now guards internal names.
  5. Codebase on main at 8bd43cc, clean. Commits user-confirmed, append-only, no AI attribution. Process pins that paid off: two /runda passes per feature (design + pre-commit) — both found real bugs and gaps every time; verify a claimed-tested surface has an actual assertion; escalate genuine forks (every C4 design choice
    • the internal-name fix scope was the user's).