From 493cebfa4fc24ecf9a46d3718bd0f96e5c704e0e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 01:22:47 +0000 Subject: [PATCH] docs(objectql): reword the #9625 cascade escalation justification after #9476 landed Two comments justified the required-FK set_null -> restrict escalation partly by the then-true clause that an empty array still satisfied `required` in the record validator. #9476's enforcement landed, so that clause is false: `[]` on a required multi-value field is rejected today. Prose only -- no executable line changes, the escalation stays exactly as ruled, and the test pin's assertions are untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM --- .../objectql/src/engine-cascade-delete.test.ts | 14 ++++++++++---- packages/objectql/src/engine.ts | 14 +++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/objectql/src/engine-cascade-delete.test.ts b/packages/objectql/src/engine-cascade-delete.test.ts index b8f50738c5..9843c32b8d 100644 --- a/packages/objectql/src/engine-cascade-delete.test.ts +++ b/packages/objectql/src/engine-cascade-delete.test.ts @@ -282,10 +282,16 @@ describe('cascadeDeleteRelations — required FK escalates set_null → restrict it('[#9625] refuses a required MULTI-VALUE lookup even when member removal would leave the set non-empty', async () => { // The escalation runs before the multi-value branch and keys on // `required` alone, so the other live member does not save the delete. - // Pinned as CURRENT behaviour, deliberately not changed here: `[]` - // still satisfies `required` in the record validator (#9476), so the - // blanket refusal is what stops an emptied required set landing - // silently. + // Pinned as CURRENT behaviour, deliberately not changed here: the + // refusal lands before the member-removal write runs, so what reaches + // the caller is `DELETE_RESTRICTED` about the `acct` it asked to + // delete, not a `required` error naming a field on `roster`. This + // comment used to add that `[]` still satisfied `required` in the + // record validator, making the refusal the only thing stopping an + // emptied required set from landing silently — #9476 has landed and + // `[]` is rejected there now, so that clause is gone. The assertions + // below never rested on it: they pin the 409 envelope and the + // untouched set. const a = await engine.insert('acct', { name: 'Acme' }); const b = await engine.insert('acct', { name: 'Beta' }); const r = await engine.insert('roster', { accounts: [a.id, b.id] }); diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index 25736849bf..821df9042e 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -10450,9 +10450,17 @@ export class ObjectQL implements IObjectQLEngine { // even when the child's set holds other members and member removal // would leave it non-empty — a state the #9447 ruling accepts. // Measured, pinned as current behaviour, and carded separately rather - // than changed here: today `[]` still satisfies `required` in the - // record validator (#9476), so this blanket refusal is what keeps an - // emptied required set from landing silently. + // than changed here. What justifies refusing is the paragraph above, + // not the validator's tolerance: the escalation refuses THIS relation + // before its own set_null write runs, so the caller is told + // `DELETE_RESTRICTED` about the record it asked to delete, instead of + // the child's own `required` 400 — which names a field that is not on + // that record's object at all. The predecessor of this comment rested + // it on `[]` still satisfying `required` in the record validator, + // which made this refusal the only thing between an emptied required + // set and a silent write; #9476 landed and `[]` is rejected there now + // too, so the refusal is no longer that last guard. It is the one that + // fires early, against the right record. if (behavior === 'set_null' && fdef.required === true) { behavior = 'restrict'; }