grammar+db: 3f — SQL DELETE + cascade summary (ADR-0033 §1/§7)
New src/dsl/grammar/sql_delete.rs (FROM <table> [WHERE] [;]), Command::SqlDelete, Request::RunSqlDelete, do_sql_delete worker. do_sql_delete mirrors the DSL do_delete: detect FK cascade by before/after child row-count diffing, re-persist target + every cascade-affected child, history-on-success inside the tx. Reuses CommandOutcome::Delete -> handle_dsl_delete_success, so the per-relationship cascade summary formatter is shared, not duplicated. ADR-0033 Amendment 2: supersedes §7's WHERE-injected pre-count. Its premise (DSL handler builds pre-counts from the typed Expr) was wrong — do_delete uses count-diff. The pre-count would also have broken the §2 parity promise by reporting SET NULL the DSL path doesn't. Count- diff gives exact parity, no WHERE-byte extraction, and withdraws R2. SET NULL reporting deferred for both paths (user-confirmed). Tests: +6 grammar unit, +12 integration (cascade parity with DSL, both R2 subquery cases, before-execute order, no-WHERE, FK-rejection rollback, childless-parent, two-child cascade). 1542 pass / 0 fail / 1 ignored. Clippy clean. Dev sql_delete entry word removed in 3j.
This commit is contained in:
@@ -1176,6 +1176,100 @@ Concretely:
|
||||
shared-entry problem (`create`, …): tag the SQL DDL nodes
|
||||
`Advanced`; the dispatcher handles the rest.
|
||||
|
||||
## Amendment 2 — Cascade summary on SQL DELETE: count-diff parity supersedes WHERE-injected pre-count (2026-05-22)
|
||||
|
||||
This amendment **supersedes §7's "Predicate extraction" subsection
|
||||
and the pre-count mechanism in §7 steps 1–3**, and **withdraws Open
|
||||
Implementation Risk R2**. It was written during sub-phase 3f, after
|
||||
tracing §7's prescribed mechanism against the actual DSL `do_delete`
|
||||
handler, and is recorded with explicit user approval before any 3f
|
||||
code landed.
|
||||
|
||||
### The finding — §7's premise about the DSL handler is incorrect
|
||||
|
||||
§7 prescribes a per-child **pre-execution pre-count** of the form
|
||||
`SELECT count(*) FROM <child> WHERE <fk_col> IN (SELECT <pk_col> FROM
|
||||
<target_table> WHERE <user_where_predicate>)`, with the
|
||||
`<user_where_predicate>` re-injected from the SQL DELETE's
|
||||
WHERE-clause source bytes. Its stated justification:
|
||||
|
||||
> "The DSL handler reuses the typed `Expr` AST to construct the
|
||||
> pre-count subqueries."
|
||||
|
||||
Tracing `do_delete` in `src/db.rs` shows this premise is factually
|
||||
wrong. The DSL handler does **not** construct pre-count subqueries
|
||||
from the typed `Expr`. It detects cascade effects by **before/after
|
||||
row-count diffing**: read the inbound relationships, count each child
|
||||
table's rows *before* the DELETE, execute the DELETE inside a
|
||||
transaction, count each child *after*, and report the positive
|
||||
difference as a `CascadeEffect`. No pre-count query and no
|
||||
`Expr`-derived subquery exist anywhere in the path.
|
||||
|
||||
### Two consequences of the correct mechanism
|
||||
|
||||
1. **Count-diff reports `ON DELETE CASCADE` row removals only.** `ON
|
||||
DELETE SET NULL` does not change a child's row count, so the DSL
|
||||
path does not report it (its own code comment notes this).
|
||||
Reporting SET NULL would require value-level diffing.
|
||||
|
||||
2. **§7's pre-count is in tension with the parity promise.** §2
|
||||
promises "a DSL-style … DELETE in Advanced mode produces identical
|
||||
observable behaviour whether it routes through the SQL or DSL
|
||||
path", and the plan's 3f exit gate requires the SQL cascade summary
|
||||
to *match* the DSL `do_delete` output. A pre-count that counted
|
||||
`ON DELETE CASCADE` *and* `SET NULL` (as §7 step 1 says) would
|
||||
report **more** than the DSL path — breaking that parity. To match
|
||||
the DSL path it would have to drop SET NULL anyway, leaving
|
||||
WHERE-byte extraction and the R2 N+1-subquery pathology as pure
|
||||
cost with no benefit over count-diff.
|
||||
|
||||
### The replacement — count-diff parity
|
||||
|
||||
`do_sql_delete` mirrors `do_delete` exactly: read inbound
|
||||
relationships, snapshot child row counts, run the **verbatim**
|
||||
validated DELETE SQL inside a transaction via
|
||||
`execute_with_fk_enrichment`, diff the child counts, build the same
|
||||
`Vec<CascadeEffect>`, and re-persist the target plus every
|
||||
cascade-affected child before commit. Because both handlers return
|
||||
the same `DeleteResult` and both route through
|
||||
`CommandOutcome::Delete` → `handle_dsl_delete_success`, the
|
||||
per-relationship summary formatter is **already shared at the render
|
||||
layer** — no formatter refactor and no duplicate path.
|
||||
|
||||
This dominates the §7 mechanism on every axis the 3f exit gate
|
||||
measures:
|
||||
|
||||
- **Exact parity** with the DSL path — identical detection mechanism,
|
||||
not merely a matching formatter.
|
||||
- **No WHERE-clause byte extraction.** The verbatim DELETE (including
|
||||
any subquery in its WHERE) executes once; the engine cascades; the
|
||||
diff observes the result. The R2 invariant case (`DELETE FROM a
|
||||
WHERE id IN (SELECT … )`) is correct by construction and carries no
|
||||
N+1 cost. **R2 is withdrawn.**
|
||||
- **Shared render** with no duplicate formatter, satisfying §7's
|
||||
shared-formatter promise structurally.
|
||||
|
||||
### SET NULL reporting — deferred for both paths (user-confirmed)
|
||||
|
||||
3f keeps strict DSL parity: `ON DELETE CASCADE` row removals are
|
||||
reported; `SET NULL` is not, on either path. Adding SET NULL
|
||||
reporting (value-level diffing) is a tracked future enhancement to be
|
||||
applied to **both** `do_delete` and `do_sql_delete` together so
|
||||
parity is preserved. The user confirmed this deferral.
|
||||
|
||||
### Consequences of the amendment
|
||||
|
||||
- §7's "Predicate extraction" subsection and pre-count steps 1–3 are
|
||||
superseded by count-diff; the per-relationship summary and
|
||||
shared-formatter outcomes are unchanged.
|
||||
- **R2 (cascade-summary predicate extraction) is withdrawn** — the
|
||||
mechanism it budgeted for is no longer used.
|
||||
- `Command::SqlDelete` carries `{ sql, target_table }` only — no
|
||||
WHERE-clause field is needed (the worker never inspects the
|
||||
predicate).
|
||||
- The handoff-31 §4 "WHERE-byte-extraction is tractable for DELETE"
|
||||
heads-up is moot — no extraction happens.
|
||||
|
||||
## See also
|
||||
|
||||
- ADR-0005 — the ten-type vocabulary INSERT works with.
|
||||
|
||||
Reference in New Issue
Block a user