You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding(objectql): the delete-cascade path's two registry-read swallows are the #8895 shape one layer up — catch → return disables every referential guard at once, silently #9002
Found while implementing #8895 (the dependents-probe fail-open). Filed unassigned, not a claim. The #8895 PR deliberately does not touch these — its declared file surface was the dependents-probe catch only.
Measured on origin/main @ fdf0fbb, located by symbol (the neighbouring line numbers move constantly).
The two seams
Both live in packages/objectql/src/engine.ts, both read the registry, both exit silently and valuelessly:
cascadeDeleteRelations(), first statement — the one that decides whether the cascade runs at all:
A return here skips every child relation: no restrict refusal, no set_null, no cascade — the same fail-open consequence #8895 fixed for one relation, applied to all of them at once, and reached before the per-relation probe ever runs.
Why this is filed as an observation, not as a defect card
The class matches #8895 — a failed read answered with an invented "nothing to do", disabling an integrity guard, with no log — but the read does not:
SchemaRegistry.getAllObjects() is a walk over in-memory Maps calling resolveObject(), which returns undefined on every failure branch it models (missing contributors, orphan overlay — the latter console.warns and returns). No I/O, no driver, and no throw statement on the path measured.
So unlike the dependents probe — which crosses a driver and fails on any connection drop, timeout, permission denial or query error — there is no measured way to make these two catches fire today. The looseness is dormant.
Dormant is not harmless, on the #8895 reasoning: seam 1's blast radius is strictly larger than the one just fixed, and the day getAllObjects() grows a throwing path (a lazily-resolved contributor, a validating merge) it fails open across every relation with nothing in the log.
Suggested disposition (triage's call, not mine)
Cheapest correct shape is the one #8895 landed — discriminate or propagate. Here there is no benign class to discriminate at all: an unreadable registry is never truthfully "no relations", so seam 1's catch can simply be removed (let it propagate) or made loud. Seam 2's 'none' is a genuine judgement call and its docblock already argues a fail-toward direction, so it should be decided with seam 1 rather than separately.
Related: #8895 (the dependents-probe fix), #7413 (cascade atomicity — the card that introduced planCascadeAtomicity), #8845 (the gate blind spot that keeps this shape invisible to check:durability-log-level).
Found while implementing #8895 (the dependents-probe fail-open). Filed unassigned, not a claim. The #8895 PR deliberately does not touch these — its declared file surface was the dependents-probe
catchonly.Measured on
origin/main@fdf0fbb, located by symbol (the neighbouring line numbers move constantly).The two seams
Both live in
packages/objectql/src/engine.ts, both read the registry, both exit silently and valuelessly:cascadeDeleteRelations(), first statement — the one that decides whether the cascade runs at all:A
returnhere skips every child relation: norestrictrefusal, noset_null, nocascade— the same fail-open consequence #8895 fixed for one relation, applied to all of them at once, and reached before the per-relation probe ever runs.planCascadeAtomicity()— same call, verdict'none', and its comment says so explicitly: "Same swallow ascascadeDeleteRelations— an unreadable registry cascades nothing, so there is nothing to make atomic." Consequence: the cascade is not wrapped in a transaction (ObjectQL.delete's single-id cascade is not transactional — a refusal mid-cascade leaves earlier children deleted while the response says the delete failed #7413's atomicity), which matters only if seam 1 does not fire on the same call — i.e. a read that fails once and succeeds the next time.Why this is filed as an observation, not as a defect card
The class matches #8895 — a failed read answered with an invented "nothing to do", disabling an integrity guard, with no log — but the read does not:
SchemaRegistry.getAllObjects()is a walk over in-memoryMaps callingresolveObject(), which returnsundefinedon every failure branch it models (missing contributors, orphan overlay — the latterconsole.warns and returns). No I/O, no driver, and nothrowstatement on the path measured.Dormant is not harmless, on the #8895 reasoning: seam 1's blast radius is strictly larger than the one just fixed, and the day
getAllObjects()grows a throwing path (a lazily-resolved contributor, a validating merge) it fails open across every relation with nothing in the log.Suggested disposition (triage's call, not mine)
Cheapest correct shape is the one #8895 landed — discriminate or propagate. Here there is no benign class to discriminate at all: an unreadable registry is never truthfully "no relations", so seam 1's
catchcan simply be removed (let it propagate) or made loud. Seam 2's'none'is a genuine judgement call and its docblock already argues a fail-toward direction, so it should be decided with seam 1 rather than separately.Related: #8895 (the dependents-probe fix), #7413 (cascade atomicity — the card that introduced
planCascadeAtomicity), #8845 (the gate blind spot that keeps this shape invisible tocheck:durability-log-level).