Uh oh!
There was an error while loading. Please reload this page.
fix(objectql): a failed cascade-delete dependents probe surfaces instead of silently skipping the referential guard (#8895) - #9006
Conversation
…ad of skipping the guard (#8895)
…cade-delete-fail-open
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8895
ObjectQL.cascadeDeleteRelations()probes each child relation to decide what the parent's delete must do. That probe is the referential-integrity guard, and it sat behind a barecatch { continue; }, so any failure of it was indistinguishable from "this child has no rows": adeleteBehavior: 'restrict'relation never refused,set_null/cascadenever ran, nothing was logged, and the caller was told the delete succeeded. Fail-OPEN on an integrity guard.The change — one
catch, discriminatedpackages/objectql/src/engine.ts, incascadeDeleteRelations(located by symbol; the card's line numbers had rotted):The
catchis not deleted. It asks by error type, through the sharedisMissingTableErrorpredicate (@objectstack/metadata/errors) already imported and used at 4 sites in this file — the same callseedAutonumberandresolveFileReferencesmake, never a hand-rolled code test:No new error code and no new response field, per the maintainer's 2026-08-15 ruling for this family (propagate via existing discrimination; unprovisioned is truthful emptiness, everything else must surface).
The premise held. Verified on
origin/mainbefore implementing: the seam, the fail-open reading and the already-present import are all as described.Is unprovisioned-table the only benign case? Measured, yes for this seam. The probe is
this.find(childName, …)wherechildNamecomes from the registry walk, so "object not registered" cannot occur here; every remaining failure class means the dependents may exist and simply were not seen. A permission denial on the child is deliberately in the "must surface" set: a guard the caller was not allowed to evaluate has not been evaluated. No legitimate caller regressed — the full@objectstack/objectqlsuite (212 files / 3728 tests) is green with the fix in.Pin —
packages/objectql/src/engine-cascade-delete-probe-failure.test.tsReal
ObjectQLengine plus a stub driver with a per-object read-failure injector. Every expectation is written against literals (the injected error object itself, its literal message, the literalDELETE_RESTRICTED/409envelope, literal row counts read straight out of the stub store), never against a value re-derived from the code under test. 7 tests:code === 'DELETE_RESTRICTED',status === 409, both rows surviveECONNRESET: the delete rejects with that exact error object, parent and child both still presentcolumn "amount" of relation "opp" does not exist(42703) — a legal missing-table phrase as a substring — and still surfacescascaderelation: surfaces; no parent deleted over unread childrenno such table: opp(sqlite phrasing): the delete proceedsrelation "opp" does not exist(42P01): the delete proceedsThe two controls are what make a vacuous pass impossible on the refusal assertions, and each benign test additionally asserts the injected throw actually fired (
readCallscontains the child) — otherwise "the delete succeeded" would also be consistent with a harness that stopped probing.Reverse verification (fix committed first, ablated with
git restore --source=HEAD~1, direction predicted before running: red on the three surfacing tests, green on controls and benign). Observed exactly that —Tests 3 failed | 4 passed, each failure readingexpected true to be Error: …, i.e. pre-fix the delete resolved withtrue: the silent success this card is about. Fix restored viagit restore --source=HEAD --staged --worktreeand re-verified green.Verification — all at final head
cc9ad86a(post-merge oforigin/main)Gate union re-derived with
node scripts/pm/dispatch-gates.mjsagainst the actual changed paths and run at the final head — all PASS:check:durability-log-level·check:stack-collection-maps·scripts/check-engine-split-ratio.mjs·check:nul-bytes· added beyond the dispatch list (convention-triggered by the new test file):check:query-options-erasure·check:engine-double-contract·check:where-matcher·check:type-check-coverage.Two of those were worth the run rather than the assumption:
check:where-matcherdiscovers the pin's hand-written WHERE matcher (243 matchers without the file, 244 with it, all judged conformant), and the pin's fixtures are typedServiceObjectand registered with theirpackageIdso the file contributes 0 raw errors to@objectstack/objectql's TEST_DEBT ledger — measured directly, sincecheck:type-check-debt --re-measureneeds the whole built workspace closure and is CI's to run.Note on the gate that cannot see this fix.
check:durability-log-level's read-seam census reports byte-identical numbers before and after (67 read seam(s) … 7 answer on a type-discriminated benign branch): the benign branch still exits valuelessly (continue), so neither invention criterion sees it. That is the documented #8845 blind spot behaving as its header says, and the reason this seam had to be fixed on its own terms.Out of scope, filed instead of widened
catch → returndisables every referential guard at once, silently #9002 — the same shape one layer up:cascadeDeleteRelations's owngetAllObjects()catch { return; }and its twin inplanCascadeAtomicitydisable every referential guard at once. Same class, different read (an in-memory registry walk with no measured throw path), so it is filed as an observation rather than fixed here — and it is outside this PR's declared file surface.cascadeDeleteRelationsas a live fail-open instance — #8895 fixed it, so the header now teaches a repaired example as current #9004 — the read-seam rule's check-durability-degradation-log-level: the read-seam invention rule keys onreturn, so acatchthat degrades by FALLING THROUGH into an empty accumulator is structurally invisible #8845 measurement block citescascadeDeleteRelationsas a live fail-open exhibit; this PR repairs it, so the checker header now teaches a fixed example. Documentation-accuracy only, andscripts/is outside this PR's declared file surface.Generated by Claude Code