Skip to content

Docs and engine disagree on deleteBehavior: 'set_null' written EXPLICITLY on a required lookup — the escalation to restrict cannot see the difference #9625

Description

@os-steve

Filing unassigned — recording, not claiming. Found while writing the multi-value set_null sentences for #9521 (the page carrying the wrong claim is one of that card's six), and deliberately not fixed there: different defect class, and the correct form is a decision rather than a mechanical edit.

Measured

Read on main at ba0a84685. cascadeDeleteRelations, packages/objectql/src/engine.ts:10265-10281:

let behavior: string =
  fdef.type === 'master_detail'
    ? (fdef.deleteBehavior === 'restrict' ? 'restrict' : 'cascade')
    : (fdef.deleteBehavior || 'set_null');
// ...
if (behavior === 'set_null' && fdef.required === true) {
  behavior = 'restrict';
}

The escalation keys on the resolved behavior, so by construction it cannot distinguish a defaulted set_null from one the author wrote out. A field declaring deleteBehavior: 'set_null' explicitly on a required: true lookup is escalated to restrict exactly like the default.

Two places state the opposite:

  • content/docs/protocol/objectql/types.mdx, the callout under On Delete Options: "An explicit set_null or cascade is always honored as written."
  • the engine's own comment, engine.ts:10276-10278: "This only escalates the defaulted set_null; an explicit cascade/restrict is untouched." — true of cascade/restrict, not of an explicit set_null.

Nothing pins either reading. packages/objectql/src/engine-cascade-delete.test.ts covers a defaulted set_null on a required FK (escalates; DELETE_RESTRICTED, 409) and an explicit cascade on a required FK (honored, no escalation). No fixture declares deleteBehavior: 'set_null' explicitly, so this is an unpinned divergence rather than a ruling-versus-pin conflict.

Same branch, second observation: the multi-value case

Since the #9447 maintainer ruling (2026-08-18), required on a multiple: true lookup means non-empty array — the contract now landed verbatim in the required and multiple doc blocks of packages/spec/src/data/field.zod.ts. The escalation above runs before the multi-value branch and keys on required alone, so deleting one referenced record is refused with DELETE_RESTRICTED even when the child's set holds other members and member removal would leave it non-empty — a state the ruled contract accepts. Whether that is deliberate conservatism or an over-restriction the ruling now supersedes is not mine to decide.

Not prescribing the fix

At least three shapes are available and they are not equivalent: correct the doc sentence (and the engine comment) to say that any set_null on a required lookup escalates; or honor an explicit set_null and settle what the child's required-field validator then does with the cleared FK; or make the multi-value case judge emptiness rather than presence. The lane that owns packages/objectql should pick.

Refs: #9521 (the docs card this fell out of), #9447 (the ruling), #9438 / #9520 (multi-value member removal), #9476 (the required-means-non-empty enforcement gap).


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions