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
Found while implementing #9362 (PR #9437). Filed unassigned; not fixed there, because the repair needs one decision this seat should not pick alone.
Reachable-after: #9362. Until that PR lands, this limb has never executed for a multi-value relationship in the history of the codebase — pre-#8895 the dependents probe swallowed its own failure and skipped the relation, post-#8895 it raised INVALID_FILTER / 400 and aborted the whole delete. Repairing the probe is what makes it run for the first time.
The defect
cascadeDeleteRelations (packages/objectql/src/engine.ts) applies deleteBehavior: 'set_null' — the DEFAULT for a plain lookup — by writing the field to null:
On a multiple: true field that slot holds an ARRAY of references. Nulling it discards every OTHER member, none of which has anything to do with the record being deleted.
Measured, on the real stack
Real ObjectQL over a real SqlDriver on better-sqlite3, with the #9362 probe fix applied (without it the call is a 400 and nothing runs):
zz_field_zoo#z1 holds refs: ["acc_a","acc_b"], where refs is { type: 'lookup', reference: 'zz_account', multiple: true } — deleteBehavior defaulted, i.e. set_null.
DELETE zz_account/acc_a returns 200.
zz_field_zoo#z1 re-reads as refs: null.
The live reference to acc_b is gone, silently. On the stock showcase the same shape is showcase_field_zoo.f_lookups at showcase_account.
The DIRECTION is not really in doubt — "set null" on a set-valued foreign key should drop the broken member, not the set. What is undecided is the residual shape when the array empties: [] or null. That difference is observable to clients on the read path and to a required multi-value validator, FieldSchema says nothing about it, and no sibling declaration or landed ruling pins it. Guessing it inside a P0 hotfix would be minting metadata semantics by accident.
Note there is a pinned sibling one branch over in the same block: a defaulted set_null on a REQUIRED lookup already escalates to restrict, on the reasoning that a FK which cannot be nulled must refuse rather than issue a contradictory write. An escalation on the same grounds is one of the options below, and is deliberately NOT taken unilaterally for the same reason.
Options
Remove the member.next = current.filter(v => String(v) !== String(id)), then write next. Needs the residual-shape answer.
Leave as is. Rejected: it is silent data loss on a published surface.
Recommendation: answer the residual shape and take option 1; take option 2 in the same round as #9437 if the answer is not immediate, so the lossy write is never live on main.
Reproduction
packages/runtime/src/cascade-delete-multivalue-lookup-real-driver.integration.test.ts on branch claude/issue-9362-cascade-probe-multiple-lookup carries the rig; add a zz_field_zoo fixture holding two ids and delete one of them.
Found while implementing #9362 (PR #9437). Filed unassigned; not fixed there, because the repair needs one decision this seat should not pick alone.
Reachable-after: #9362. Until that PR lands, this limb has never executed for a multi-value relationship in the history of the codebase — pre-#8895 the dependents probe swallowed its own failure and skipped the relation, post-#8895 it raised
INVALID_FILTER/ 400 and aborted the whole delete. Repairing the probe is what makes it run for the first time.The defect
cascadeDeleteRelations(packages/objectql/src/engine.ts) appliesdeleteBehavior: 'set_null'— the DEFAULT for a plainlookup— by writing the field tonull:On a
multiple: truefield that slot holds an ARRAY of references. Nulling it discards every OTHER member, none of which has anything to do with the record being deleted.Measured, on the real stack
Real
ObjectQLover a realSqlDriveron better-sqlite3, with the #9362 probe fix applied (without it the call is a 400 and nothing runs):zz_field_zoo#z1holdsrefs: ["acc_a","acc_b"], whererefsis{ type: 'lookup', reference: 'zz_account', multiple: true }— deleteBehavior defaulted, i.e.set_null.DELETE zz_account/acc_areturns 200.zz_field_zoo#z1re-reads asrefs: null.The live reference to
acc_bis gone, silently. On the stock showcase the same shape isshowcase_field_zoo.f_lookupsatshowcase_account.Why it was not repaired inside #9362
The DIRECTION is not really in doubt — "set null" on a set-valued foreign key should drop the broken member, not the set. What is undecided is the residual shape when the array empties:
[]ornull. That difference is observable to clients on the read path and to arequiredmulti-value validator,FieldSchemasays nothing about it, and no sibling declaration or landed ruling pins it. Guessing it inside a P0 hotfix would be minting metadata semantics by accident.Note there is a pinned sibling one branch over in the same block: a defaulted
set_nullon a REQUIRED lookup already escalates torestrict, on the reasoning that a FK which cannot be nulled must refuse rather than issue a contradictory write. An escalation on the same grounds is one of the options below, and is deliberately NOT taken unilaterally for the same reason.Options
next = current.filter(v => String(v) !== String(id)), then writenext. Needs the residual-shape answer.set_nulltorestrictwhen the field ismultiple: true, mirroring the required-FK escalation exactly. Refuses loudly instead of writing anything, decides no new semantics, and is a one-line revert once option 1's question is answered. Costs: a delete that "succeeded" before ObjectQL.cascadeDeleteRelations fails OPEN: a failed dependents probe skips therestrictguard entirely, so a delete that should be refused succeeds silently #8895 (by skipping the guard) now returns 409.Recommendation: answer the residual shape and take option 1; take option 2 in the same round as #9437 if the answer is not immediate, so the lossy write is never live on
main.Reproduction
packages/runtime/src/cascade-delete-multivalue-lookup-real-driver.integration.test.tson branchclaude/issue-9362-cascade-probe-multiple-lookupcarries the rig; add azz_field_zoofixture holding two ids and delete one of them.Generated by Claude Code