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'; }