Build-order plan (sub-phases 3a–3k) with per-sub-phase scope, exit gates, and written DA gates, modelled on docs/plans/20260520-adr-0032-phase-2.md. Centrepiece is the cross-cut verification matrix scaffold (~75 rows) grouped by ADR-0033 section (statement shapes, dispatch, RETURNING, shortid, cascade, diagnostics, UPSERT, inherited Phase-2 diagnostics, ambient assistance, engine-neutrality, persistence, OOS rejections), to be filled in during 3k. Carries the handoff-29 §4 process pins into the relevant sections: DA critiques listed before verdict, no silent out-of-scope classification, and matrix attribution requiring SQL-input tests for SQL claims. Records four open questions to escalate before code starts (shortid SELECT row-source path, R1 mechanism fallback, cascade pre-count construction, UPSERT catalog wording).
67 KiB
Implementation plan — ADR-0033 (ADR-0030 Phase 3)
Plan author date: 2026-05-20 Driving ADR: ADR-0033 — The full SQL DML grammar (INSERT / UPDATE / DELETE) Parent ADR: ADR-0030 — Advanced mode: the standard-SQL surface
Purpose
ADR-0033 is the decision: what Phase 3 covers, how dispatch works on shared entry words, what diagnostics land, what the execute-path Command variants look like. This plan is the mechanics: the build order (3a → 3k), the per-sub-phase exit gates, the cross-cut verifications that prevent the kind of silent gap Phase 1 shipped (ADR-0032 §11.6) and the kind of matrix-attribution slippage Phase 2's first DA pass let through (handoff-28 §3.4).
The plan does not re-litigate ADR-0033's decisions. Any finding during implementation that suggests a decision should change is escalated to the user and resolved by ADR amendment (or a new ADR) before code proceeds — never by silent drift.
Why this plan exists
Phase 2 closed clean but the closing DA review needed a second
pass to surface four production gaps. Phase 3 introduces a
structural risk Phase 2 didn't have: a new walker capability
(Node::Guard) sits at the foundation, and every later
sub-phase depends on it working. If 3a ships broken-but-believed-
green, 3b–3j build on broken scaffolding. The plan exists to
make 3a's verification rigorous and to keep every "X comes for
free" claim from ADR-0033 explicit and tested.
The three failure modes the plan is built to prevent:
- The Guard mechanism shipping without proof that it fires BEFORE the Seq commits. R1 in ADR-0033's Open Implementation Risks; 3a's DA gate makes this the make-or-break test.
- A "free for free" claim shipping without a test that proves the claim. Every "X works for SQL DML automatically" in ADR-0030/0031/0032/0033 needs an explicit cross-cut test.
- Matrix attribution pointing at a DSL test for a SQL claim. Handoff-28 §3.4: rows pointing at a SQL surface must point at a test whose INPUT is SQL syntax. Cross-checked at attribution time, not at DA-review time.
Working-mode reminder
Per CLAUDE.md's "Working mode — solo with sub-agents by
default": this work proceeds in solo mode unless the user
explicitly invokes /team. The phased structure below maps to
solo-mode's Phase 1 → 5 discipline within each sub-phase:
- Requirements extraction (the sub-phase's scope below) is Phase 1.
- Divergent exploration is rarely needed for individual sub-phases here (ADR-0033 already selected the approach), but if implementation reveals an ambiguity, it lands as a Phase-2 exploration before code proceeds.
- The DA hat is worn at every sub-phase gate. The DA critique is written with specific findings BEFORE the verdict; a clean PASS without listed findings is a process failure even if the work is sound (handoff-28 §4.1; handoff-29 §4.1).
- Escalation rather than guessing applies throughout. "Out of scope" / "non-blocking" require user confirmation (handoff-29 §4.2).
Standing authorizations and continuation policy
The user granted these authorizations for Phase 2; absent a
fresh statement to the contrary, they extend to Phase 3
because the work pattern is identical. They are recorded here
verbatim from docs/plans/20260520-adr-0032-phase-2.md so any
implementer (the original session, a future session, or a
fresh agent) operates on the same terms. If the user has
narrowed these for Phase 3, the narrowest interpretation
wins.
1. Walk the plan without interruption when no interruption is needed
The plan's sub-phases (3a–3k) have explicit, written exit gates. Within a sub-phase, the implementer proceeds through the build steps and verifies the gate without asking for permission at each step. Between sub-phases, the implementer proceeds to the next one without asking, provided the previous gate is green.
The implementer does NOT interrupt to:
- Confirm "should I continue to the next sub-phase?"
- Confirm minor implementation choices that match surrounding
code (
CLAUDE.md's code-style discipline applies; pick the consistent option and move on). - Confirm test naming, file organisation, commit granularity within a sub-phase.
- Report progress mid-step. A status update arrives at gate boundaries — not every five minutes.
2. When the implementer MUST escalate
The escalation rule from CLAUDE.md ("Escalate ambiguity — do
not decide for the user") is not relaxed. Standing
authorization to walk uninterrupted is not authorization to
guess on design questions. The implementer escalates if:
- An exit gate cannot be made green by work in the current sub-phase's scope — a test fails for a reason that suggests the ADR is wrong, or a behaviour the gate requires turns out to need machinery not in ADR-0033.
- An ambiguity arises that the ADR and this plan do not settle. See "Open questions to escalate before code starts" at the end of this plan — those are pre-identified. Anything similar that surfaces is also escalation territory.
- An autonomous design decision is required (a choice outside
ADR-0033 §§1–13's settled list and the plan's per-sub-phase
scope). Per
CLAUDE.md's "Out of scope and non-blocking require user confirmation": the default assumption is that the work is in scope and is blocking unless the user has said otherwise (handoff-29 §4.2 pin). - A "real blocker" is encountered — environment, dependency,
or test-infrastructure failure that prevents progress and
cannot be resolved within the sub-phase. A flaky test is
not a real blocker — investigate per
CLAUDE.md's "no excuses" rule on test infrastructure.
3. Commits are pre-authorized at standard granularity
The user has explicitly pre-authorized commits within this
plan's scope, overriding CLAUDE.md's "All commits MUST be
confirmed by the user first" for this plan. The implementer:
- Commits at natural break points within each sub-phase and always at sub-phase gate boundaries. Granular is better than monolithic.
- Writes detailed commit messages in the project's existing
style —
area: short subjectfirst line, then a body explaining what changed and why. Body should reference the ADR section being implemented (e.g., "ADR-0033 §2 — Guard mechanism, option (a) chosen") so a reader can trace each commit back to a decision. - Includes NO AI attribution of any form (
Co-Authored-By, generated-by lines, AI footers, …). This is a global rule (CLAUDE.md); the standing authorization here is for committing, not for relaxing attribution discipline. - Names the mechanism choice in 3a's commit message — option (a) / (b) / (c) per ADR-0033 §2. If 3a fell back to (b) or (c), update ADR-0033 §2's "Default choice" sentence in the same commit (per handoff-29 §3 step 6).
4. Pushes remain a user step
CLAUDE.md's "Push is a user step — agents cannot and shall
never do it" is not overridden. The implementer commits
freely; the user pushes when they choose. Unpushed commits are
a normal working state, and the implementer never describes
work as blocked on a push.
5. End-of-plan handoff
At the end of 3k, after the verification report is produced and the DA's final PASS verdict is recorded (with specific critiques listed first, not a rubber-stamp), the implementer stops and surfaces a summary to the user — exact test counts, the filled cross-cut matrix, the requirements-to-test mapping, any autonomous decisions made and their justification. The user confirms acceptance before the plan is considered done. This is the one explicit hand-back point.
If the user wants follow-up work — adjustments, polish, additional tests — the implementer addresses it and re-runs the gate before re-surfacing. The plan does not declare itself complete; the user does.
Sub-phase overview
| Sub-phase | Title | Test-suite gate |
|---|---|---|
| 3a | Node::Guard + mode-gated Choice mechanism |
Smoke-test grammar: 4 mode × shape combinations green; R1 invariant proven |
| 3b | INSERT grammar + minimal execution | Single + multi-row VALUES end-to-end; CSV re-persisted; __rdbms_* rejected |
| 3c | INSERT … SELECT | Compound row source + WITH-prefixed SELECT row source end-to-end |
| 3d | shortid auto-fill (worker) |
Auto-fill on VALUES (single/multi) + INSERT…SELECT; override warning path |
| 3e | UPDATE grammar + execution | Multi-column SET, with/without WHERE; sql_expr in SET works |
| 3f | DELETE grammar + execution + cascade summary | Cascade parity with DSL; WHERE-with-subquery case (R2) |
| 3g | RETURNING | All three statement kinds; result-column type recovery via Phase-2 path |
| 3h | UPSERT (ON CONFLICT … DO NOTHING / DO UPDATE) |
Both branches; excluded.col resolves to target table's columns |
| 3i | Diagnostics | Three new keys: positive + negative each; per-row arity emit verified |
| 3j | Dispatch wiring on shared entry words | Existing DSL tests still green; SQL inputs route via shared CommandNode |
| 3k | Verification sweep | Cross-cut matrix every row green; phase-exit verification report |
Each sub-phase ships independently: green tests, clippy clean, a written DA gate review with specific critiques listed first, and a commit (pre-authorized per §3 above).
Sub-phase 3a — Node::Guard + mode-gated Choice mechanism
This sub-phase is the foundation. If the Guard mechanism doesn't work cleanly, ADR-0033 §2's mitigation paths (mechanism options (b) or (c)) get evaluated before any DML grammar lands on top.
Scope (in)
- Add a new walker node variant
Node::Guard(fn(&WalkContext) -> Result<(), ValidationError>)tosrc/dsl/grammar/mod.rs::Node— zero-byte consumption, fails the enclosing Seq withValidationFailedwhen the validator returns Err. - Implement
walk_guardinsrc/dsl/walker/driver.rs. Match arm inwalk_node_inner. - Add the
reject_sql_in_simple_modeguard function. ReturnsValidationError { message_key: "advanced_mode.sql_in_simple", args: … }in Simple mode,Ok(())in Advanced. - Add the catalog key
advanced_mode.sql_in_simplewith engine-neutral wording matching the existingselect/withgating pattern (ADR-0030 §2). - Build the smoke-test grammar: a single experimental
CommandNodewhoseshapeisChoice([Seq[Guard(reject_sql_in_simple_mode), SQL_marker_token], DSL_marker_token])using two distinguishable, non-DML marker tokens (e.g., synthetic__sql_marker/__dsl_marker) so the smoke test exercises the mechanism without depending on any DML grammar. - Verify the four mode × input cases listed in Exit gate below.
Scope (out — explicit)
- Any real DML grammar — that is 3b onwards.
- Wiring
data::INSERT/UPDATE/DELETEshapes — that is 3j. - Any other guard-based gating beyond
reject_sql_in_simple_mode— the mechanism is reusable but only this guard is required by this sub-phase.
Build steps
- Add
Node::Guardvariant. Cover the match-arm in every exhaustivematch Nodein the walker / grammar / hint helpers — clippy will surface incomplete arms. - Implement
walk_guardin the driver. Push zero bytes; returnWalkOutcome::ValidationFailedfrom the Err path. - Author
reject_sql_in_simple_modeinsrc/dsl/grammar/mod.rs(or a sibling module if it fits better there). - Author the catalog key + an engine-neutral i18n message.
- Build the smoke-test grammar in a temporary file
(
src/dsl/grammar/_guard_smoke.rsor similar; remove before 3a's commit, OR keep it as a unit-test fixture in the walker driver tests — implementer's choice as long as it doesn't leak into the runtime CommandNode registry). - Author the four exit-gate tests.
- Author the R1 invariant test (DA gate below).
cargo test,cargo clippy --all-targets -- -D warnings.
Exit gate
Required green tests:
- Smoke-test grammar — Simple mode + DSL-shape input: the DSL branch matches. (Baseline: Choice fall-through works.)
- Smoke-test grammar — Advanced mode + SQL-shape input: the SQL branch matches.
- Smoke-test grammar — Simple mode + SQL-shape input:
WalkOutcome::ValidationFailedwithmessage_key: "advanced_mode.sql_in_simple". (The Guard fires; no other branch is attempted because the SQL marker consumed bytes after the Guard pushed empty.) - Smoke-test grammar — Advanced mode + DSL-shape input: the DSL branch matches. (Choice fall-through works even when the SQL branch is admissible — the Guard returned Ok but the marker token didn't match the input, so the SQL Seq fails NoMatch and Choice falls through to DSL.)
R1 invariant test (this is the make-or-break case):
-
Advanced mode + an input whose first tokens match the SQL branch's Guard-then-marker pattern but the SQL branch's later tokens fail, while the DSL branch's tokens would match the full input. The expected outcome is:
- If the Choice falls through to DSL → R1 mechanism (a) works as designed; 3a is green.
- If the Choice commits to SQL and reports Failed → R1 mechanism (a) is broken in the same way ADR-0033 §2's open risk anticipated; sub-phase REOPENS with mechanism option (b) or (c).
Note: in 3a's smoke-test grammar the SQL and DSL markers are different, so the simplest exercise of this invariant uses a 2-token SQL branch (
SQL_marker_A,SQL_tail_X) and an inputSQL_marker_A SQL_tail_YwhereSQL_tail_Yisn't admissible to the SQL branch but is admissible to the DSL branch. The Choice MUST fall through.
Other gates:
cargo testtotal: 1446 (baseline) + the new smoke-test + R1 invariant tests; expect ≥ 1450 (rough order).- Zero failures, one ignored (the unchanged doctest), zero skipped.
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
The DA hat is worn explicitly here. Specific critiques listed first, verdict last (handoff-29 §4.1 pin):
- Does the Guard actually fire BEFORE the Seq commits?
Read
walk_seq/walk_choiceinsrc/dsl/walker/driver.rs. Trace the path: Choice picks branch 0 → walk_seq enters → walk_guard runs → if Err, Seq's outcome is Failed → does walk_choice see Failed and fall through to branch 1, or does it commit on branch 0's "tried but failed"? The R1 invariant test above is the empirical answer; the DA reads the code and confirms the test's assertion matches the code path. - Is the Guard mechanism reusable beyond DML? Phase 4
(DDL) is the likely future consumer. If yes, document the
pattern in
src/dsl/grammar/mod.rsdoc comments so the next phase doesn't reinvent it. - Does
reject_sql_in_simple_mode's catalog key match the existingselect/withentry-word gate's wording posture? Inconsistent UX between entry-word gates and branch gates is a real gap; the DA readssrc/dsl/grammar/mod.rs::ADVANCED_ONLY_ENTRIESand confirms the catalog key + i18n string land at the same user-facing message shape. - Did 3a introduce any structure ADR-0033 §2 doesn't sanction? A new node variant added; a new validator signature shape; a new catalog key. If anything beyond these landed, that's an autonomous decision to surface.
- If the R1 invariant test fails, was the fallback path taken cleanly? The fallback path means: (a) ADR-0033 §2 amended with the chosen mechanism, (b) the smoke-test grammar rewritten on (b) or (c), (c) the gate's tests pass on the new mechanism, (d) the commit message names the mechanism that landed.
Sub-phase 3b — INSERT grammar + minimal execution
Scope (in)
- Author
src/dsl/grammar/sql_insert.rsexportingpub static SQL_INSERT_SHAPE: Node(the per ADR-0033 §1 shape minus row-source SELECT, RETURNING, UPSERT — those land in 3c/3g/3h). - The shape covers:
INSERT INTO table_name [ '(' column_name_list ')' ] VALUES value_tuple ( ',' value_tuple )* [ ';' ]. - The
__rdbms_*rejection on the table-name slot (per ADR-0030 §6 / ADR-0032 §1'sreject_internal_tablevalidator). Command::SqlInsert { sql, source, target_table, listed_columns, returning: false }—returningalways false in 3b.Request::RunSqlInsert+do_sql_insertworker handler (no shortid auto-fill yet — explicit values required for every column).- Persistence write-through on the target table after execution (ADR-0030 §11).
- History-log of the literal submitted line (ADR-0030 §11).
- A standalone development CommandNode (e.g.,
sql_insert) in the registry so 3b–3i can exercise the SQL INSERT path without depending on 3j's dispatch wiring on shared entry words. Removed in 3j.
Scope (out — explicit)
INSERT … SELECT— that is 3c.RETURNING— that is 3g.UPSERT— that is 3h.shortidauto-fill — that is 3d.- Walker diagnostics specific to INSERT (
insert_arity_mismatch,auto_column_overridden,not_null_missing) — that is 3i. - DSL-vs-SQL dispatch wiring on
data::INSERT— that is 3j.
Build steps
- Author
sql_insert.rswith the value-list shape, plus reject-internal-table on the target slot. - Add
Command::SqlInsertwith the field list above. - Add
Request::RunSqlInsertanddo_sql_insert. - Wire history-log write-before-execute (mirror
do_run_select's pattern). - Wire persistence write-through after execute.
- Build the development CommandNode (
sql_insert) and register it. - Unit + integration tests per the exit gate.
Exit gate
Required green tests:
- Single-row INSERT runs end-to-end —
INSERT INTO orders (customer_id, total) VALUES (1, 99.50)succeeds; affected-row count surfaces; CSV is re-persisted with the new row. - Multi-row INSERT runs end-to-end —
INSERT INTO orders VALUES (1, 'a'), (2, 'b')succeeds; both rows land; CSV reflects both. - Column-list variant —
INSERT INTO t (a, b) VALUES (1, 'x')succeeds; the unmentioned columns get their defaults / NULLs. - No column-list variant —
INSERT INTO t VALUES (…)accepts the table's full column arity in the tuple. __rdbms_*rejection at the INSERT target-table slot (per ADR-0030 §6) — parse-rejects.- History line is the literal submitted SQL.
- Failed INSERT (engine error) does NOT re-persist the CSV — invariant: persistence happens only on engine success.
- All Phase-2 tests (1446 baseline) still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does an INSERT failing mid-multi-row leave persistence in a consistent state? SQLite's default is per-statement transactional — verify the worker doesn't re-persist when the engine returns an error.
- Does the
target_tableextraction handle case-folding the same way the rest of the codebase does? ADR-0009: identifiers are case-preserving but ADR-0012's metadata uses canonical casing for lookups. Verify the path. - Are the existing DSL INSERT tests still green? Any DSL-side regression means 3b broke something it shouldn't have. Run the DSL INSERT test module explicitly.
- Does the history-log line capture pre-engine state? ADR-0030 §11: the literal submitted line is logged before execution. Verify the order (log → execute) and that a failing INSERT still leaves a log entry (for replay).
- Did 3b introduce any structure ADR-0033 §1 doesn't sanction?
Sub-phase 3c — INSERT … SELECT
Scope (in)
- Extend
SQL_INSERT_SHAPE's row-source slot to be a Choice betweenvalues_clauseandSubgrammar(&sql_select::SQL_SELECT_COMPOUND). - The Phase-2 SELECT machinery applies for free — CTEs
(Amendment 2 admits leading
WITH), JOINs, set ops, subqueries, scope-frame harvest. do_sql_inserthandles the SELECT-driven path; the engine handles the insert-from-query flow (no special composite preparation needed).
Scope (out — explicit)
- Arity diagnostic (
insert_arity_mismatch) — that is 3i; 3c's only verification of arity is what the engine reports via the friendly layer. - Schema-existence on the SELECT's projection — comes for free from Phase-2 machinery (ADR-0032 §11); 3c's job is to cross-cut-verify it fires on a DML SELECT slot, not to re-implement.
Build steps
- Wrap the existing values_clause in a Choice with the subgrammar reference.
- Verify the development CommandNode admits both row sources.
- Tests per the exit gate, including the WITH-prefixed case (R4 invariant).
Exit gate
Required green tests:
- Plain INSERT … SELECT —
INSERT INTO archive SELECT * FROM orders WHERE created < '2025-01-01'runs; the rows land in archive; archive's CSV reflects them. - Column-list + projection —
INSERT INTO target (a, b) SELECT x, y FROM sourceruns. - WITH-prefixed SELECT row source (R4) —
INSERT INTO archive WITH t AS (SELECT * FROM orders) SELECT * FROM truns. (This proves the Amendment-2 carry-through.) - Schema-existence diagnostic on the SELECT's projection
(cross-cut) —
INSERT INTO archive SELECT nonexistent_col FROM ordersfiresdiagnostic.unknown_column(or the equivalent Phase-2 key) on the projection — the Phase-2 pass applies here for free; the test pins the claim. - All 3a–3b tests still green; baseline 1446 still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does the WITH-prefixed SELECT row source actually parse
through
SQL_SELECT_COMPOUND? Or does the INSERT prefix break the Phase-2 recursion? The R4 test above pins this; the DA readssql_select::SQL_SELECT_COMPOUNDto confirm the entry shape admitsWITH. - Does the cascade-summary path (3f DELETE) accidentally fire for INSERTs whose SELECT touches the target table? It must not — INSERTs have no cascade output. 3c's tests shouldn't see a cascade summary even when the source is the same as the target.
- Does a SELECT-driven INSERT respect the
__rdbms_*rejection on the SELECT'sFROMslot? Phase-2 already gates this; 3c verifies it doesn't silently regress on the DML path.
Sub-phase 3d — shortid auto-fill (worker)
Scope (in)
- Worker-side post-fill in
do_sql_insert(per ADR-0033 §6). - Detect omitted
shortidcolumns by intersecting the user'slisted_columnsagainst the target table's auto-gen column list from__rdbms_playground_columns. - Synthesise fresh shortids per row (the existing
shortid::generatepath). - Rewrite the executed SQL to bind the synthesised values
positionally (or use parameter binding — implementer's
choice, but the original submitted SQL stays unchanged in
history.log). - Cover both row sources: VALUES (single + multi-row) and SELECT (each yielded row gets a fresh shortid for the omitted auto-gen columns).
Scope (out — explicit)
- The
auto_column_overriddenwalker WARNING — that is 3i. serialauto-fill — handled by SQLite rowid; the worker has no work to do forserialcolumns.
Build steps
- Locate the existing DSL
do_insert's shortid auto-fill logic. Refactor as needed so the SQL path can call the same generator (the generator itself, not the surrounding typedCommand::Insertplumbing). - In
do_sql_insert, after parsinglisted_columnsvs table schema, identify omitted auto-gen columns. - For VALUES, synthesise per-row shortids and bind them.
- For SELECT, the strategy needs an explicit design call:
- Option A: Rewrite SQL to wrap the SELECT, projecting a synthesised shortid alongside (heavier; touches the SQL text).
- Option B: Run the SELECT first into a result set, synthesise per-row shortids in the worker, then INSERT the augmented rows row-by-row (simpler; loses the engine's set-based optimisation).
- ESCALATE this choice to the user. Both options are valid; ADR-0033 §6 doesn't pick. Plan default if the user is unavailable: Option B (simpler, matches the pedagogical posture; performance is not a goal).
- Unit + integration tests per the exit gate.
Exit gate
Required green tests:
- VALUES single-row auto-fill —
INSERT INTO t (name) VALUES ('x')wherethasid shortid pkauto-fillsidwith a fresh shortid;SELECT * FROM tshows the synthesised id. - VALUES multi-row distinct shortids —
INSERT INTO t (name) VALUES ('a'), ('b'), ('c')yields three rows with three DISTINCT shortid values. - Explicit value respected (override) —
INSERT INTO t (id, name) VALUES ('hardcoded', 'x')preserves'hardcoded'; the warning fires in 3i but the value is honoured here. - INSERT … SELECT auto-fills shortids —
INSERT INTO target (name) SELECT name FROM sourcewheretarget.idis shortid produces fresh shortids for every yielded row (verified distinct). - Combined
serial+shortidcolumns — on a table with both,serialis engine-filled,shortidis worker-filled, no collision. - All 3a–3c tests still green; baseline 1446 still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Are the synthesised shortids actually unique across a multi-row INSERT? The generator must be called per-row, not once and reused. The multi-row distinct test pins this.
- Does the auto-fill respect the column's case-preserved name? ADR-0009. Verify with a mixed-case shortid column name.
- What happens if the table has both
serialandshortidPK columns? Spec: serial via SQLite rowid; shortid via worker post-fill. Verify no double-fill collision and no double-insertion. - Did the SELECT row-source path choose option A or B, and was it escalated to the user before landing? If the implementer chose without escalating, that's an autonomous decision and 3d's DA verdict is FAIL until the user confirms.
Sub-phase 3e — UPDATE grammar + execution
Scope (in)
- Author
src/dsl/grammar/sql_update.rsexportingpub static SQL_UPDATE_SHAPE: Node:UPDATE table_name SET assignment_list [ where_clause ] [ ';' ]. - The
__rdbms_*rejection on the target slot. assignment_listreusessql_expr::SQL_OR_EXPRfor the RHS of each assignment.Command::SqlUpdate,Request::RunSqlUpdate,do_sql_update.- Persistence write-through on the target table.
- A development CommandNode
sql_updateuntil 3j. - No
--all-rowsrail (ADR-0030 §12: SQL UPDATE without WHERE executes as written).
Scope (out — explicit)
RETURNING— that is 3g.UPDATE … FROM other_table— OOS-3 in ADR-0033 §13.- Walker WARNING on UPDATE-touching-auto-gen-column — that
warning lands in 3i if the implementer extends
auto_column_overridden's scope; the spec leaves it as an INSERT-only diagnostic per ADR-0033 §8.2. If the implementer wants to extend, escalate.
Build steps
- Author the shape.
- Add Command/Request/handler.
- Wire history-log + persistence.
- Register the dev CommandNode.
- Tests per the exit gate.
Exit gate
Required green tests:
- Single-column UPDATE with WHERE —
UPDATE t SET v = 'x' WHERE id = 1runs; affected-row count surfaces; CSV reflects the change. - Multi-column UPDATE —
UPDATE t SET a = 1, b = 2 WHERE id = 1runs. - UPDATE without WHERE —
UPDATE t SET active = falseruns across all rows (per ADR-0030 §12, no--all-rowsgate); CSV reflects all rows changed. - UPDATE with sql_expr in SET —
UPDATE t SET total = price * quantity WHERE …runs; the engine evaluates the expression. - Schema-existence diagnostic fires on unknown columns in SET (cross-cut from Phase-2 machinery).
- Predicate warning fires on a bad WHERE
(cross-cut from Phase-2 machinery —
= NULLin the WHERE.) - All 3a–3d tests still green; baseline 1446 still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does the SET assignment expression accept the full
sql_expr(function calls, subqueries, CASE)? Verify withSET v = (SELECT max(other) FROM other_table)andSET v = CASE WHEN x > 0 THEN x ELSE 0 END. - Does an UPDATE without WHERE actually run as written with no rail? Per ADR-0030 §12. The test pins the claim.
- Does the persistence write-through fire even when affected-row count is 0? Edge case: an UPDATE that matches no rows. Worker should still treat the operation as successful (engine returned OK); CSV doesn't need to rewrite if nothing changed, but the path mustn't crash.
- Did 3e regress any DSL UPDATE behaviour?
Sub-phase 3f — DELETE grammar + execution + cascade summary
Scope (in)
- Author
src/dsl/grammar/sql_delete.rsexportingpub static SQL_DELETE_SHAPE: Node:DELETE FROM table_name [ where_clause ] [ ';' ]. - The
__rdbms_*rejection on the target slot. Command::SqlDelete,Request::RunSqlDelete,do_sql_delete.- Cascade-summary pre-count per ADR-0033 §7: extract WHERE
bytes from the matched path, inject into pre-count
subqueries for each child table FK with
ON DELETE CASCADE/SET NULL. - Cascade-summary formatter shared with
do_delete(DSL path) — one formatter, two callers (ADR-0033 §7's shared-formatter promise). - Persistence write-through on target + every cascade- affected child table (multi-table persistence is new for SQL DML; the DSL path already does this).
- Development CommandNode
sql_deleteuntil 3j.
Scope (out — explicit)
RETURNING— that is 3g; 3f leaves the cascade summary + affected-count as the only output for now.DELETE … FROM other_table— not a thing in standard SQL; OOS by silence.
Build steps
- Author the shape.
- Extract the WHERE-byte-range capture in the walker's
ast-builder for
SQL_DELETE_SHAPE— the matched path spans the WHERE node; the ast-builder pulls the source bytes covered. - Refactor
format_cascade_summaryfromdo_deleteso it accepts both the DSL typed-Expr path and a string-WHERE path. Implementer's call whether to extract via two pre-count strategies or one unified pre-count constructor that accepts either input form. Plan default: extract the pre-count constructor, keep formatting unified. - Wire
do_sql_delete: pre-count → execute → format summary → re-persist target + every child whose row count changed. - Tests per the exit gate, including R2's WHERE-with- subquery case.
Exit gate
Required green tests:
- Plain DELETE with WHERE —
DELETE FROM orders WHERE id = 1runs; affected-row count + cascade summary surface; target CSV re-persisted. - DELETE without WHERE —
DELETE FROM ordersruns across all rows; cascade summary covers all child rows; target + every cascade- affected child CSV re-persisted. - Cascade parity with DSL — on the same schema and
data, running
DELETE FROM customers WHERE id = 1via the SQL path produces the same per-relationship summary as running the DSLdelete customers where id = 1. - R2 invariant: WHERE-with-subquery —
DELETE FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE country = 'DE')runs; the cascade pre- count uses the same WHERE bytes (the subquery runs N+1 times — once for the count, once for the DELETE; the test pins correctness, not performance). - Cascade pre-count runs BEFORE execute — verify order by exercising a case where the pre-count would yield 0 if it ran after the DELETE (i.e., the rows being deleted are exactly the ones whose children are pre-counted).
- Cascade-affected children CSVs re-persisted — after
a cascading DELETE, every affected child table's CSV
reflects the post-state (rows gone for
ON DELETE CASCADE, NULL forSET NULL). __rdbms_*rejection at the DELETE target slot.- All 3a–3e tests still green; baseline 1446 still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- R2: does the predicate-extraction byte-range injection
work on a WHERE containing a subquery? Test
DELETE FROM a WHERE id IN (SELECT id FROM b WHERE …). The pre-count SQL becomesSELECT count(*) FROM child WHERE fk_col IN (SELECT pk_col FROM a WHERE id IN (SELECT id FROM b WHERE …)). The subquery runs twice (R2's noted performance concern). The DA readsdo_sql_delete's pre-count construction to confirm the byte range covers the entire WHERE (including the inner subquery) and not just the top-level predicate. - Does the cascade pre-count run in a way that doesn't see the post-DELETE state? Order matters: pre-count BEFORE execute. Verified by the order test above; DA reads the code to confirm.
- Is the cascade-summary formatter actually shared? No duplicate copy of the formatter — one function, two callers (DSL + SQL).
- Are cascade-affected child CSVs identified by querying
the metadata tables for
ON DELETE CASCADE/SET NULLrelationships, or by walking the result of the cascade? Implementer's choice; DA confirms whichever path is taken is sound and matches DSLdo_delete.
Sub-phase 3g — RETURNING
Scope (in)
- Add
RETURNING projection_listas an optional tail on all three SQL DML shapes (SQL_INSERT_SHAPE, SQL_UPDATE_SHAPE, SQL_DELETE_SHAPE). projection_listis the Phase-2 projection list, imported unchanged.- Walker's
ast_builderfor each shape setsreturning: booltrue when the RETURNING tail matched. - Worker handlers branch on
returning:true→ prepare + step + collect rows into aDataResult. Reuse the SELECT result-column type recovery (resolve_select_column_typesindb.rs, ADR-0032 §12 + Amendment 1).false→ execute, collect affected-row count (current 3b/3e/3f behaviour).
- DELETE … RETURNING preserves the cascade summary alongside the result set — both surface to the user.
Scope (out — explicit)
- The Command variant shape changes —
returning: boolis already in the spec (ADR-0033 §10); 3b set it false, 3g flips it.
Build steps
- Add the RETURNING tail to each shape.
- Update each ast_builder to set
returning. - Update each worker handler to branch.
- Reuse
resolve_select_column_typesfor the prepared statement's column-origin metadata. - For DELETE + RETURNING: ensure cascade pre-count still runs and its summary surfaces alongside the rows.
- Tests per the exit gate.
Exit gate
Required green tests:
- INSERT … RETURNING * —
INSERT INTO t (a) VALUES (1) RETURNING *returns the inserted row as a DataResult; affected-row count is implicit (rows.len() = 1). - INSERT multi-row … RETURNING id —
INSERT INTO t (a) VALUES (1), (2), (3) RETURNING idreturns three rows; ids distinct. - UPDATE … RETURNING id, new_value —
UPDATE t SET v = 'x' WHERE id = 1 RETURNING id, vreturns the modified columns of the affected row. - DELETE … RETURNING * —
DELETE FROM t WHERE id = 1 RETURNING *returns the pre-DELETE row (BEFORE it's gone). - DELETE … RETURNING + cascade summary — on a parent DELETE with cascade children, the user sees both the RETURNING result set AND the cascade per-relationship summary.
- Result-column type recovery on RETURNING — for each of the ten playground types, a bare-column RETURNING reference recovers the playground type via the Phase-2 column-origin path. (At least one test per type; bundle into a single parametric test for compactness.)
- Computed expression in RETURNING stays typeless —
RETURNING a + 1yieldscolumn_types[0] = None. - R3: RETURNING on multi-row INSERT — column-origin is the same for all yielded rows (per ADR-0032 Amendment 1); one test pins it.
- All 3a–3f tests still green; baseline 1446 still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does the RETURNING result-set rendering use the same DataResult path SELECT uses? Or did a DML-specific copy sneak in? Read the renderer; confirm one path, not two.
- Does
RETURNING expr AS aliasadmit aliases the same way the projection list does? The projection list is Phase-2 machinery; 3g imports unchanged. Verify via a test. - For DELETE + RETURNING, does the cascade pre-count fire correctly? The cascade summary is per-relationship child rows affected — independent of whether the user asked for RETURNING. Both must surface.
- Result-column type recovery test count: ten types? All ten playground types covered (text, int, real, decimal, bool, date, datetime, blob, serial, shortid).
Sub-phase 3h — UPSERT (ON CONFLICT … DO NOTHING / DO UPDATE)
Scope (in)
- Add
on_conflict_clausetoSQL_INSERT_SHAPE:- Optional
(column_name_list)conflict target. DO NOTHINGbranch.DO UPDATE SET assignment_list [ where_clause ]branch.
- Optional
excludedadmitted as a table alias inside DO UPDATE expressions only — its columns are the target table's columns (per ADR-0033 §9). Scoped to the DO UPDATE branch.- Walker captures the UPSERT shape; worker executes the validated SQL as-is (no special pre-processing).
Scope (out — explicit)
INSERT OR REPLACE / IGNORE / ABORT / FAIL / ROLLBACK— OOS-2 in ADR-0033 §13.
Build steps
- Author the on_conflict_clause node tree.
- Wire the conflict target column-name list to use the
same
Columnsident slot as the column-name list at the top of INSERT. - Author the
excludedpseudo-alias machinery: a specialTableBindingpushed onto the current frame's from_scope ONLY while walking the DO UPDATE expressions. The binding's columns are sourced from the target table's schema cache entry. - Tests per the exit gate.
Exit gate
Required green tests:
- DO NOTHING on conflict —
INSERT INTO t (id, name) VALUES (1, 'x') ON CONFLICT (id) DO NOTHINGon a pre-existingid = 1runs; affected-row count is 0; CSV unchanged. - DO UPDATE with excluded —
INSERT INTO t (id, name) VALUES (1, 'new') ON CONFLICT (id) DO UPDATE SET name = excluded.nameon a pre-existingid = 1runs; the row's name is updated to'new'; CSV reflects the change. - DO NOTHING without target spec —
INSERT INTO t … ON CONFLICT DO NOTHINGhandles any conflict. excluded.colresolves to the target table's columns (cross-cut completion / highlight test: Tab at… DO UPDATE SET name = excluded.|lists the target table's columns).excludeddoes NOT leak outside DO UPDATE — parsingINSERT INTO t VALUES (excluded.name, …)(withexcludedused in the value list, not inside DO UPDATE) firesdiagnostic.unknown_qualifieror equivalent. The alias is strictly scoped to the DO UPDATE branch.- Persistence write-through on DO UPDATE — yes; the target row is modified.
- All 3a–3g tests still green; baseline 1446 still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does ON CONFLICT … DO UPDATE trigger persistence write-through? Yes; the target row is modified. The worker doesn't accidentally bypass re-persistence on the upsert path.
- Is the
excludedalias scoped to ONLY the DO UPDATE branch, not leaking into the outer INSERT? The leak test above pins this. - Is
excludedengine-neutral? No — it's SQLite + PostgreSQL spelling (standard SQL usesMERGE … WHEN MATCHED). ADR-0033 §9 carves this out explicitly; the DA confirms the carve-out is justified (it is) and that the catalog wording referencing UPSERT doesn't leak product names. - Does the conflict target column list reject
__rdbms_*columns? The columns are owned by the target table; if the target is rejected at the outer table slot, the inner column list is by construction fine. Sanity-verify.
Sub-phase 3i — Diagnostics
Scope (in)
diagnostic.insert_arity_mismatch(ERROR, ADR-0033 §8.1): walker pre-flight when(column_name_list)'s arity disagrees with a value_tuple's arity (per-row for multi-row VALUES), or with the SELECT's projection arity for INSERT…SELECT.diagnostic.auto_column_overridden(WARNING, ADR-0033 §8.2): walker emit when INSERT explicitly assigns a value to aserial/shortidcolumn.diagnostic.not_null_missing(WARNING, ADR-0033 §8.3): walker emit when INSERT's(column_name_list)omits a NOT-NULL-no-default column.- Per-key positive + negative tests (the ADR-0032 §2d pattern reused).
- Catalog entries with engine-neutral wording.
Scope (out — explicit)
- Engine-side translations (FK / UNIQUE / NOT-NULL on execute) — ADR-0033 §11 reuses existing keys; no new ones needed.
- Predicate warnings on DML's sql_expr slots — inherited from Phase-2 machinery; 3i cross-cuts-verifies they fire on DML slots but doesn't re-implement.
Build steps
- Author the three walker diagnostic passes (or extend existing passes if cleaner — implementer's call).
- Author the catalog keys + i18n strings.
- Author the positive + negative tests per key.
- Verify per-row emit for multi-row arity mismatch.
- Cross-cut-verify the inherited Phase-2 passes fire on DML sql_expr slots.
Exit gate
Required green tests:
insert_arity_mismatchpositive (single-row) —INSERT INTO t (a, b) VALUES (1, 2, 3)fires.insert_arity_mismatchnegative (matched arity) —INSERT INTO t (a, b) VALUES (1, 2)doesn't fire.insert_arity_mismatchper-row emit (multi-row) —INSERT INTO t (a, b) VALUES (1, 2), (3, 4, 5), (6), (7, 8)fires on rows 2 and 3, NOT on rows 1 and 4 (two diagnostics, two right rows silent).insert_arity_mismatchon INSERT…SELECT —INSERT INTO t (a) SELECT a, b FROM sourcefires; anchored on the SELECT's first projection_item span.auto_column_overriddenpositive —INSERT INTO t (id, name) VALUES (5, 'x')(id isserial) fires WARNING; the statement still runs.auto_column_overriddennegative — omittingiddoesn't fire.auto_column_overriddenon shortid — same as serial butidisshortid.not_null_missingpositive —INSERT INTO t (a) VALUES (1)(b is NOT NULL no default) fires WARNING; the statement still parses (the engine rejects on execute).not_null_missingnegative — includingbdoesn't fire.not_null_missingdoes NOT fire for columns with a default value — even if NOT NULL.- Cross-cut: schema-existence diagnostic fires on
INSERT VALUES —
INSERT INTO t (nonexistent) VALUES (1)fires the Phase-2 key on the column name. - Cross-cut: schema-existence fires on UPDATE SET —
UPDATE t SET nonexistent = 1 WHERE id = 1fires. - Cross-cut: schema-existence fires on UPDATE / DELETE WHERE — already verified in 3e/3f exit gates; re-reference here.
- Cross-cut: predicate warnings fire on DML WHERE / SET
/ VALUES —
UPDATE t SET v = 1 WHERE x = NULLfireseq_null. - All 3a–3h tests still green; baseline 1446 still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does each new key have BOTH a positive and a negative test? Walk the keys; verify both rows above.
- Is
auto_column_overriddena Warning, not an Error? Read the diagnostic's severity field; confirm. - Is
not_null_missinga Warning, not an Error? Same. - Are the catalog wordings engine-neutral? No "SQLite", "PRAGMA", "STRICT", or "rusqlite" leak. Verify by reading the i18n strings.
- Does multi-row arity-mismatch actually emit per-row? The test pins it; the DA reads the code to confirm the emit happens inside the per-row visit, not once for the values_clause as a whole.
Sub-phase 3j — Dispatch wiring on shared entry words
Scope (in)
- Convert
data::INSERT,data::UPDATE,data::DELETECommandNode shapes from their current DSL-only form toChoice([SQL_<X>_SHAPE, DSL_<X>_SHAPE])per ADR-0033 §2. - Wire
Node::Guard(reject_sql_in_simple_mode)at the head of each SQL__SHAPE's Seq (the 3a mechanism). - Remove the development CommandNodes (
sql_insert/sql_update/sql_delete) from the registry. - Verify the public test surface: existing DSL tests use unchanged inputs and see unchanged outputs; new SQL tests reach the SQL branch via the shared entry words.
Scope (out — explicit)
- Any behaviour change. This is a wiring refactor: observable behaviour must be identical before and after 3j.
Build steps
- Convert each shape. The Choice declares SQL first, DSL second (per ADR-0033 §2).
- Insert the Guard at the head of each SQL branch's Seq.
- Update every test that referenced
sql_insert/sql_update/sql_deleteto useinsert/update/deleteinstead. - Remove the development CommandNodes.
- Run the full suite.
Exit gate
Required green tests:
- All existing DSL INSERT/UPDATE/DELETE tests still green — unmodified inputs, unmodified outputs.
- All Phase-3 SQL DML tests still green via the new
dispatch path — the SQL branch is reached through
data::INSERTetc., not through the development CommandNodes. Verifiable: grep forsql_insert/sql_update/sql_deleteand confirm zero remaining references in src/dsl/grammar/. - Structurally-ambiguous input routes SQL-first in
Advanced —
delete from t where id = 1in Advanced mode goes through the SQL DELETE branch (verifiable: a SQL-only side effect fires, e.g., the cascade-summary code path shared by both — pin by checking a unique-to-SQL trace point, OR by verifying that a SQL-syntax-only construct on the same input matches). - DSL-specific input falls through to DSL in Advanced —
delete from t --all-rowsin Advanced matches the DSL branch (the--all-rowsflag is DSL-only, so the SQL Seq's NoMatch lets Choice fall through). - Simple mode + SQL input fires the Guard —
delete from t where id = 1 returning *(DML+RETURNING, clearly SQL) in Simple mode producesValidationFailedwithmessage_key: "advanced_mode.sql_in_simple". - R1 invariant holds in real grammar — the smoke-test
from 3a was minimal; 3j is the first time the Guard
mechanism sees real DML branches with overlapping
prefixes. A specific test: in Advanced mode, an INSERT
that the SQL branch parses partway and then rejects
(e.g., a malformed
RETURNINGtail that's also not a valid DSL fragment) — the Choice's behaviour is what ADR-0033 §2 dictates: SQL > DSL ordering means the SQL branch's Failed propagates as the Choice's outcome (no fallback because the DSL branch wouldn't match either). The test pins the user-facing error wording. - All 3a–3i tests still green; baseline 1446 still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Verify that no test was changed to make 3j pass. Only
the CommandNode shape's structure changes; the
observable behaviour at the public test surface is
identical for inputs that worked before. Grep the diff
for tests whose
INPUTstrings changed in 3j — there should be none for existing DSL tests; the Phase-3 SQL tests' inputs are the same (only their dispatch path differs). - Is the test count higher than before 3j? It should be the same or higher (no tests removed, possibly a handful added for the structurally-ambiguous + fall- through cases above).
- R1 hold-up in real grammar? The 3a smoke-test pinned the invariant on synthetic tokens; the DA confirms it holds on real DML.
Sub-phase 3k — Verification sweep
Scope (in)
The catch-all sub-phase. Verifies the full end-to-end Phase 3 surface and every cross-cut "X comes for free" claim. Produces the final phase-exit verification report (next section).
Build steps
- Author the end-to-end integration tests (Tier 3) for real-world-shape DML queries (the exit-gate list below).
- Author or extend the cross-cut verification matrix (next section) for every "X comes for free" claim from ADR-0030/0031/0032/0033.
- Run the full test suite. Confirm 1446 baseline + all sub-phase 3a–3j additions + the 3k integration tests.
- Run
cargo clippy --all-targets -- -D warnings. - Produce the final verification report.
Exit gate
Required end-to-end integration tests:
- INSERT … SELECT cross-table —
INSERT INTO archive SELECT * FROM orders WHERE created < '2025-01-01'— runs, archive has the rows, both CSVs reflect state, renderer clean. - Multi-row INSERT with all ten playground types — every type lands in a row via a multi-row INSERT; values round-trip correctly.
- UPDATE with subquery in SET —
UPDATE customers SET last_order_date = (SELECT max(date) FROM orders WHERE customer_id = customers.id)runs; values updated; CSV reflects. - DELETE with cascade — end-to-end including per-relationship summary, multi-table CSV re-persistence.
- UPSERT round-trip — DO UPDATE updates an existing row; DO NOTHING on a separate conflict no-ops; both surface their expected outcomes.
- RETURNING on each of INSERT / UPDATE / DELETE — each produces a DataResult; result-column types recovered.
history.logreplay: every Phase-3 statement form written above replays from the log faithfully (ADR-0030 §11 unchanged for DML).
Cross-cut verification matrix (next section): every claim must have a passing test, with the test's INPUT being SQL syntax for SQL claims (handoff-29 §4.4 pin).
Other gates:
- Total test count: ≥ 1446 + (additions from 3a–3k). The exact count depends on per-step authorship; the gate is "every required-test row above and in the cross-cut matrix has at least one green test", not a magic number.
- Zero failures, zero skipped (excluding the unchanged doctest).
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
The final DA review, per CLAUDE.md Phase-5 verification and handoff-29 §4.1 pin (specific critiques first, verdict last; rubber-stamp PASS is a process failure):
- Walk the cross-cut matrix row-by-row. Every row's test must (a) exist, (b) be green, (c) have INPUT that matches the row's claim source (SQL claims → SQL inputs).
- Are there any "free for free" claims from ADR-0030/0031/0032/0033 that have no explicit test? Phase-1 gap pattern, Phase-2's four DA findings. Each must have one.
- Did any sub-phase silently downgrade a requirement to "later"? Phase-2's "out of scope" defer-trap reflex. Any "not in scope" call by the implementer that wasn't user-confirmed is a verdict FAIL until escalation closes (handoff-29 §4.2 pin).
- Did any sub-phase make an autonomous decision that's not in ADR-0033? Specific items to check: the 3d Option A/B choice (must be user-confirmed if Option A was chosen unilaterally); any new catalog keys beyond the three in §8.1–§8.3; any new Command variants beyond the three in §10.
- Are ALL tests green AND zero-skipped? If not, the verdict is FAIL and 3k loops back to whichever sub-phase introduced the gap.
Cross-cut verification matrix
Each row is a claim from ADR-0030, ADR-0031, ADR-0032, or ADR-0033 that needs an explicit test, with the test's location. The matrix is filled in during 3k; the gate is "every row green AND every SQL claim's test takes SQL input" (handoff-29 §4.4 pin).
| Claim source | Claim | Test location | Status |
|---|---|---|---|
| Statement shapes (§1) | |||
| ADR-0033 §1 | Single-row VALUES INSERT runs end-to-end | TBD (3b) | ⏳ |
| ADR-0033 §1 | Multi-row VALUES INSERT runs end-to-end | TBD (3b) | ⏳ |
| ADR-0033 §1 | INSERT with (column_name_list) runs |
TBD (3b) | ⏳ |
| ADR-0033 §1 | INSERT without column list (full table arity) runs | TBD (3b) | ⏳ |
| ADR-0033 §4 | INSERT … SELECT runs |
TBD (3c) | ⏳ |
| ADR-0033 §4 / R4 | INSERT … WITH … SELECT row source runs (Amendment-2 carry-through) |
TBD (3c) | ⏳ |
| ADR-0033 §1 | UPDATE with WHERE runs | TBD (3e) | ⏳ |
| ADR-0033 §1 / §12 | UPDATE without WHERE runs (no --all-rows rail) |
TBD (3e) | ⏳ |
| ADR-0033 §1 | UPDATE with multi-column SET | TBD (3e) | ⏳ |
| ADR-0033 §1 | UPDATE with sql_expr in SET (function call, subquery, CASE) | TBD (3e) | ⏳ |
| ADR-0033 §1 | DELETE with WHERE runs | TBD (3f) | ⏳ |
| ADR-0033 §1 / §12 | DELETE without WHERE runs (no --all-rows rail) |
TBD (3f) | ⏳ |
| Dispatch (§2) | |||
| ADR-0033 §2 / 3a | Guard fires BEFORE Seq commits (R1 invariant on synthetic markers) | TBD (3a) | ⏳ |
| ADR-0033 §2 | Simple mode + DSL INSERT → DSL branch | TBD (3j) | ⏳ |
| ADR-0033 §2 | Simple mode + SQL INSERT (RETURNING) → ValidationFailed advanced_mode.sql_in_simple |
TBD (3j) | ⏳ |
| ADR-0033 §2 | Advanced + structurally-ambiguous (delete from t where id = 1) → SQL branch |
TBD (3j) | ⏳ |
| ADR-0033 §2 | Advanced + DSL-only (--all-rows) → DSL branch (Choice falls through) |
TBD (3j) | ⏳ |
| ADR-0033 §2 | Same for UPDATE shared entry word | TBD (3j) | ⏳ |
| ADR-0033 §2 | Same for INSERT shared entry word | TBD (3j) | ⏳ |
| RETURNING (§5 / §12) | |||
| ADR-0033 §5 | INSERT … RETURNING * surfaces DataResult | TBD (3g) | ⏳ |
| ADR-0033 §5 | UPDATE … RETURNING cols surfaces DataResult | TBD (3g) | ⏳ |
| ADR-0033 §5 | DELETE … RETURNING * surfaces DataResult (pre-DELETE row) | TBD (3g) | ⏳ |
| ADR-0033 §5 | DELETE … RETURNING preserves cascade summary alongside result set | TBD (3g) | ⏳ |
| ADR-0033 §12 | Result-column type recovery for each of ten playground types via RETURNING | TBD (3g) | ⏳ |
| ADR-0033 §12 | Computed expression in RETURNING stays typeless | TBD (3g) | ⏳ |
| ADR-0033 §5 / R3 | Multi-row INSERT … RETURNING: column-origin same for all rows | TBD (3g) | ⏳ |
| shortid auto-fill (§6) | |||
| ADR-0033 §6 | Single-row VALUES auto-fills omitted shortid PK | TBD (3d) | ⏳ |
| ADR-0033 §6 | Multi-row VALUES yields DISTINCT shortids per row | TBD (3d) | ⏳ |
| ADR-0033 §6 | INSERT … SELECT auto-fills shortids per yielded row | TBD (3d) | ⏳ |
| ADR-0033 §6 | Explicit value for shortid column respected (override; warning in §8.2) | TBD (3d) | ⏳ |
| ADR-0033 §6 | serial + shortid combo: no double-fill collision | TBD (3d) | ⏳ |
| Cascade summary (§7) | |||
| ADR-0033 §7 | DSL parity: same schema/data, SQL DELETE produces same per-relationship summary | TBD (3f) | ⏳ |
| ADR-0033 §7 / R2 | WHERE-with-subquery: cascade pre-count uses correct byte range | TBD (3f) | ⏳ |
| ADR-0033 §7 | Pre-count runs BEFORE execute | TBD (3f) | ⏳ |
| ADR-0033 §7 | Cascade-affected child tables re-persisted | TBD (3f) | ⏳ |
| ADR-0033 §7 | Cascade-summary formatter shared (one function, two callers) | TBD (3f) | ⏳ |
| Diagnostics (§8) | |||
| ADR-0033 §8.1 | insert_arity_mismatch positive (single-row) |
TBD (3i) | ⏳ |
| ADR-0033 §8.1 | insert_arity_mismatch negative (matched arity) |
TBD (3i) | ⏳ |
| ADR-0033 §8.1 | insert_arity_mismatch per-row (multi-row VALUES) |
TBD (3i) | ⏳ |
| ADR-0033 §8.1 | insert_arity_mismatch on INSERT…SELECT (projection arity) |
TBD (3i) | ⏳ |
| ADR-0033 §8.2 | auto_column_overridden positive (serial) |
TBD (3i) | ⏳ |
| ADR-0033 §8.2 | auto_column_overridden positive (shortid) |
TBD (3i) | ⏳ |
| ADR-0033 §8.2 | auto_column_overridden negative (omitted) |
TBD (3i) | ⏳ |
| ADR-0033 §8.3 | not_null_missing positive |
TBD (3i) | ⏳ |
| ADR-0033 §8.3 | not_null_missing negative |
TBD (3i) | ⏳ |
| ADR-0033 §8.3 | not_null_missing does NOT fire for NOT-NULL-with-default |
TBD (3i) | ⏳ |
| UPSERT (§9) | |||
| ADR-0033 §9 | ON CONFLICT (col) DO NOTHING runs; no-ops on conflict | TBD (3h) | ⏳ |
| ADR-0033 §9 | ON CONFLICT (col) DO UPDATE SET … = excluded.… runs; row updated | TBD (3h) | ⏳ |
| ADR-0033 §9 | ON CONFLICT DO NOTHING (no target spec) accepts any conflict | TBD (3h) | ⏳ |
| ADR-0033 §9 | excluded.col completes to target table's columns inside DO UPDATE |
TBD (3h) | ⏳ |
| ADR-0033 §9 | excluded rejected outside DO UPDATE (scoping) |
TBD (3h) | ⏳ |
| Inherited Phase-2 diagnostics on DML slots (§8.4) | |||
| ADR-0032 §11 | schema_existence fires on INSERT VALUES column |
TBD (3i) | ⏳ |
| ADR-0032 §11 | schema_existence fires on UPDATE SET |
TBD (3i) | ⏳ |
| ADR-0032 §11 | schema_existence fires on UPDATE / DELETE WHERE |
TBD (3i) | ⏳ |
| ADR-0032 §11 | schema_existence fires on RETURNING projection |
TBD (3i / 3g) | ⏳ |
| ADR-0032 §11.6 | Predicate warning eq_null fires on UPDATE WHERE |
TBD (3i) | ⏳ |
| ADR-0032 §11.6 | Predicate warning like_numeric fires on DELETE WHERE |
TBD (3i) | ⏳ |
| ADR-0032 §11.6 | Predicate warning fires inside INSERT VALUES sql_expr (e.g., = with NULL) |
TBD (3i) | ⏳ |
| Ambient assistance (§§ from ADR-0030 §8) | |||
| ADR-0030 §8 | Syntax highlighting works for SQL DML keywords (INSERT, UPDATE, DELETE, INTO, VALUES, SET, RETURNING, ON, CONFLICT) | TBD (3k) | ⏳ |
| ADR-0030 §8 | Tab completion works for SQL DML entry keywords in Advanced mode | TBD (3k) | ⏳ |
| ADR-0030 §8 | Tab completion works for target table after INSERT INTO |
TBD (3k) | ⏳ |
| ADR-0030 §8 | Tab completion works for column names inside (column_list) |
TBD (3k) | ⏳ |
| ADR-0030 §8 | Tab completion works for column names in UPDATE SET / WHERE | TBD (3k) | ⏳ |
| ADR-0030 §8 | Hint-panel prose appears at representative DML slots | TBD (3k) | ⏳ |
| ADR-0030 §8 | [ERR]/[WRN] validity indicator fires for SQL DML diagnostics |
TBD (3k) | ⏳ |
| ADR-0030 §8 | Per-command parse-error usage fires for SQL DML | TBD (3k) | ⏳ |
| Engine-neutrality + safety | |||
| ADR-0030 §6 / ADR-0033 §1 | __rdbms_* rejection at INSERT target slot |
TBD (3b) | ⏳ |
| ADR-0030 §6 / ADR-0033 §1 | __rdbms_* rejection at UPDATE target slot |
TBD (3e) | ⏳ |
| ADR-0030 §6 / ADR-0033 §1 | __rdbms_* rejection at DELETE target slot |
TBD (3f) | ⏳ |
| ADR-0030 §6 / ADR-0033 §1 | __rdbms_* rejection in INSERT…SELECT row source's FROM |
TBD (3c) | ⏳ |
| ADR-0030 §7 / ADR-0033 §11 | Engine UNIQUE constraint failure surfaces via friendly layer (engine-neutral) | TBD (3k) | ⏳ |
| ADR-0030 §7 / ADR-0033 §11 | Engine NOT NULL constraint failure surfaces via friendly layer | TBD (3k) | ⏳ |
| ADR-0030 §7 / ADR-0033 §11 | Engine FK constraint failure on DELETE-without-cascade surfaces | TBD (3k) | ⏳ |
| Persistence + history (§10–§11) | |||
| ADR-0030 §11 | INSERT writes history.log + re-persists target CSV | TBD (3b) | ⏳ |
| ADR-0030 §11 | UPDATE writes history.log + re-persists target CSV | TBD (3e) | ⏳ |
| ADR-0030 §11 | DELETE writes history.log + re-persists target + cascade-affected CSVs | TBD (3f) | ⏳ |
| ADR-0030 §11 | Every Phase-3 statement form replays from history.log faithfully | TBD (3k) | ⏳ |
| ADR-0033 §10 | INSERT failure does NOT re-persist CSV | TBD (3b) | ⏳ |
| ADR-0006 / ADR-0033 §10 | Auto-snapshot fires for SQL DML the same way it does for DSL DML | TBD (3k) | ⏳ |
| OOS rejections (§13) | |||
| ADR-0033 §13 OOS-1 | INSERT INTO t DEFAULT VALUES parse-rejects |
TBD (3b) | ⏳ |
| ADR-0033 §13 OOS-2 | INSERT OR REPLACE parse-rejects |
TBD (3b) | ⏳ |
| ADR-0033 §13 OOS-3 | UPDATE … FROM other_table parse-rejects |
TBD (3e) | ⏳ |
| ADR-0033 §13 OOS-4 | WITH x AS (…) UPDATE … parse-rejects |
TBD (3e) | ⏳ |
| ADR-0033 §13 OOS-4 | WITH x AS (…) DELETE … parse-rejects |
TBD (3f) | ⏳ |
The implementer fills in Test location and Status
(✅ / ❌) as 3k proceeds. A row marked red blocks the
phase exit.
Attribution rule (handoff-29 §4.4 pin): for any row
whose Claim source is a SQL claim, the Test location must
point at a test whose INPUT is SQL syntax. Cross-checked
at attribution time, not at DA-review time.
Final phase-exit verification report
Produced at the end of 3k. A markdown document at
docs/handoff/<date>-phase-3-verification.md (matching the
Phase-2 naming pattern). The report contains:
- Test suite totals — passed / failed / skipped /
ignored, exact numbers from
cargo testoutput. Compared against the Phase-2 baseline (1446 / 0 / 0 / 1). - The full cross-cut verification matrix with every row green.
- A requirements-to-test mapping — every numbered decision in ADR-0033 §§1–13 with the test(s) that prove it.
- A list of any autonomous decisions made during implementation, with rationale and explicit user- confirmation pointer for each. (Per CLAUDE.md: an autonomous decision without explicit user confirmation is a verdict FAIL.)
- The DA's written final review — with specific critiques listed first and the PASS / FAIL verdict last. No "conditional" verdicts (handoff-28 §4.1 pin).
Only after the report is produced and signed off does the user-facing "Phase 3 complete" message issue.
Open questions to escalate before code starts
These are matters the plan deliberately leaves to the implementer to escalate when reached, not to decide silently:
- shortid auto-fill on INSERT … SELECT (3d). Option A (SQL rewrite) vs Option B (worker materialisation). ADR-0033 §6 doesn't pick. Plan default if the user is unavailable: Option B (simpler, matches pedagogical posture). Implementer escalates BEFORE 3d code lands; if user unavailable, choice is recorded in the verification report under "autonomous decisions" for retroactive confirmation.
- R1 mechanism fallback (3a). If option (a) (Guard validator) doesn't work, options (b) and (c) per ADR-0033 §2's enumeration. The plan does NOT pre-pick; if (a) fails, the implementer evaluates (b) and (c) and brings a recommendation to the user.
- Cascade-summary pre-count construction (3f). Two pre-count strategies (typed-Expr from DSL, byte-range string from SQL) vs one unified constructor. Plan default: unified constructor that accepts either input form, with the formatter alone shared. Implementer's call; not an escalation unless a third option surfaces.
- UPSERT engine-neutral catalog wording (3h). The
excludedkeyword is engine-neutral-carved-out per ADR-0033 §9; the surrounding catalog wording (e.g., the help-text for an UPSERT statement, the error message ifexcludedis used outside DO UPDATE) must not leak SQLite product names. If the implementer isn't sure, escalate before writing the i18n string.
What this plan does NOT contain
- Time estimates. Phase 3 is medium-sized but introduces R1's structural risk; calibration is poor. Milestones, not hours.
- Sub-phase commit granularity beyond "ship green tests at each gate". The implementer commits at natural break points within a sub-phase; the gate is what gets verified.
- Re-statements of ADR-0033's decisions. The plan refers to ADR-0033 by section; the implementer reads both.
- Process pins from the prior session that don't change anything mechanical. They land in the implementer's context via handoff-29 §4, not via duplication here.