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
Filing unassigned — recording, not claiming. Measured while landing #9625; that PR pins the current behaviour and states it in the docs. This card is the remaining judgement: publish-time rejection versus delete-time coercion.
restrict is the only value that deviates. Every other value a master_detail can declare — including set_null — resolves to cascade.
Measured with a real engine + stub driver: a master_detail field declaring deleteBehavior: 'set_null', parent deleted → the child row is deleted, not kept with a nulled parent. No warning, no log line, no parse-time complaint.
FieldSchema accepts the combination: deleteBehavior is z.enum(['set_null', 'cascade', 'restrict']) on the shared field schema with no per-type narrowing, so packages/spec says the value is authorable on a master_detail and the engine drops it.
Why it matters more than the enum suggests
This is the ADR-0049 declared-but-unenforced shape, on a delete path. An author who writes deleteBehavior: 'set_null' on a master-detail reference is asking for their child rows to be KEPT. What they get is the child rows deleted — the opposite outcome, silently, at the moment the parent goes away. The failure is not a no-op; it is data loss relative to the declared intent.
The neighbouring lookup case (#9625) is the same defect class — a resolution that collapses "the author wrote it" and "we defaulted it" — but its consequence is a refused delete, which is loud. This one is quiet.
Not prescribing the fix
At least three shapes, not equivalent:
Reject at publish time. Narrow the schema so deleteBehavior: 'set_null' on a master_detail is a named parse-time rejection. Matches the house preference for declared = enforced and for catching AI-authored metadata errors at authoring time rather than at delete time. Cost: it is a tightening on an authorable surface, so any existing app declaring it starts failing validation — needs the usual retirement ceremony rather than a one-line schema edit.
Recommendation, weakly held and not acted on: option 1. It is the only one where a wrong declaration is answered at the time it is written. But it changes an authorable surface, which is a maintainer call, not a mechanical edit.
Current behaviour is pinned
packages/objectql/src/engine-cascade-delete.test.ts (via #9625): [#9625] a master_detail declaring an explicit deleteBehavior:set_null still cascades.
Refs: #9625 (where this was measured), #9164 (the closed docs card about master-detail's default), #8772 / #9138 (orphan detail rows), ADR-0049.
Filing unassigned — recording, not claiming. Measured while landing #9625; that PR pins the current behaviour and states it in the docs. This card is the remaining judgement: publish-time rejection versus delete-time coercion.
Measured
cascadeDeleteRelations,packages/objectql/src/engine.ts:restrictis the only value that deviates. Every other value amaster_detailcan declare — includingset_null— resolves tocascade.Measured with a real engine + stub driver: a
master_detailfield declaringdeleteBehavior: 'set_null', parent deleted → the child row is deleted, not kept with a nulledparent. No warning, no log line, no parse-time complaint.FieldSchemaaccepts the combination:deleteBehaviorisz.enum(['set_null', 'cascade', 'restrict'])on the shared field schema with no per-type narrowing, sopackages/specsays the value is authorable on amaster_detailand the engine drops it.Why it matters more than the enum suggests
This is the ADR-0049 declared-but-unenforced shape, on a delete path. An author who writes
deleteBehavior: 'set_null'on a master-detail reference is asking for their child rows to be KEPT. What they get is the child rows deleted — the opposite outcome, silently, at the moment the parent goes away. The failure is not a no-op; it is data loss relative to the declared intent.The neighbouring
lookupcase (#9625) is the same defect class — a resolution that collapses "the author wrote it" and "we defaulted it" — but its consequence is a refused delete, which is loud. This one is quiet.Not prescribing the fix
At least three shapes, not equivalent:
deleteBehavior: 'set_null'on amaster_detailis a named parse-time rejection. Matches the house preference for declared = enforced and for catching AI-authored metadata errors at authoring time rather than at delete time. Cost: it is a tightening on an authorable surface, so any existing app declaring it starts failing validation — needs the usual retirement ceremony rather than a one-line schema edit.master_detailtakeset_null. Cheapest to write, and the worst of the three: a master-detail child whose master reference is nulled becomes an unreachable orphan, which is precisely what Acontrolled_by_parentobject may declare its master reference withoutrequired, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772 / spec builder: forcerequired: trueon amaster_detailreference undercontrolled_by_parent(ruled Direction 2 of #8772) #9138 spent their effort preventing.deleteBehavior: 'set_null'written EXPLICITLY on a required lookup — the escalation torestrictcannot see the difference #9625 states it inprotocol/objectql/types.mdxand themaster_detailrow ofdata-modeling/field-types.mdx, and pins it inengine-cascade-delete.test.ts. A doc sentence does not stop the AI-authored app from writing the key.Recommendation, weakly held and not acted on: option 1. It is the only one where a wrong declaration is answered at the time it is written. But it changes an authorable surface, which is a maintainer call, not a mechanical edit.
Current behaviour is pinned
packages/objectql/src/engine-cascade-delete.test.ts(via #9625):[#9625] a master_detail declaring an explicit deleteBehavior:set_null still cascades.Refs: #9625 (where this was measured), #9164 (the closed docs card about master-detail's default), #8772 / #9138 (orphan detail rows), ADR-0049.
Generated by Claude Code