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
fix(engine): set_null on a multiple:true reference removes the deleted member, emptied set stored as [] per the ruled FieldSchema contract; drop the #9437 interim 409 hold (#9520)
The set_null limb on a set-valued foreign key now filters the deleted id
out of the stored array and writes what remains; an emptied set is written
as [], never null — consuming the representation FieldSchema pins
(packages/spec/src/data/field.zod.ts, multiple/required doc blocks, #9447
maintainer ruling 2026-08-18). The temporary restrict escalation and its
TEMPORARY developerMessage limb shipped with #9437 are removed in the same
stroke, as ruled.
Closes#9438.
Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
Co-authored-by: Claude <noreply@anthropic.com>
fix(engine): `deleteBehavior: 'set_null'` on a `multiple: true` reference field removes the deleted MEMBER from the stored array instead of nulling the whole slot, and the temporary 409 hold shipped with #9437 is removed (#9438)
6
+
7
+
On a set-valued foreign key, "set null" now means what it has to mean: the
8
+
reference to the deleted record is filtered out of the stored array and the
9
+
remaining members are written back untouched. Before the interim hold, this
10
+
limb wrote `null` over the WHOLE array — a row holding `["acc_a","acc_b"]`
11
+
re-read as `null` after `acc_a` was deleted, silently dropping the live
12
+
reference to `acc_b`.
13
+
14
+
**The residual shape is the ruled one, consumed rather than decided here.**
15
+
When the last member is removed, the field is written as **`[]`, never
16
+
`null`** — the representation `FieldSchema` pins as verbatim contract
17
+
(`packages/spec/src/data/field.zod.ts`, the `multiple` and `required` doc
18
+
blocks; #9447, maintainer ruling 2026-08-18, binding for every writer). The
19
+
open question that kept this limb held back was exactly that shape; it is
20
+
now answered at the spec, and this write consumes the answer.
21
+
22
+
**The temporary holding position is removed in the same stroke — it was
23
+
built to be removed.**#9437 shipped an explicit interim: any delete that
24
+
would take the `set_null` limb on a multi-value reference was refused
25
+
`DELETE_RESTRICTED` / 409, with a `developerMessage` naming the refusal
26
+
TEMPORARY and citing the tracking issue literally. Those deletes now
27
+
succeed and remove the member. The refusal envelope for a genuinely
28
+
configured `restrict` is unchanged, and the interim's extra sentence is gone
29
+
with the interim.
30
+
31
+
Removal compares whole members (`String(v) !== String(id)`), the same
32
+
reading the dependents narrowing already applies — the probe's `$contains`
33
+
pushdown is a substring superset, so an id that is a prefix of another
34
+
neither loses its own member nor takes its neighbor's. Every other
35
+
disposition is untouched: `cascade` still deletes dependents, a
36
+
single-valued `set_null` still clears its foreign key to `null`, an
37
+
explicit `restrict` still refuses with its own sentence, and the required-FK
38
+
escalation stays exactly as it was.
39
+
40
+
Pinned against the driver double that models the JSON TEXT column and
41
+
against a real `SqlDriver` on better-sqlite3 through the real data-plane
42
+
delete: member removal with a surviving sibling that still resolves, the
43
+
emptying case asserted literally as `[]` and not `null` on a re-read of the
44
+
database, and the controls beside them.
45
+
46
+
Note: `required` on a multi-value lookup now *documents* non-empty-array
47
+
semantics (same ruling), but the validator does not yet enforce it — that
48
+
enforcement gap is tracked separately in #9476 and is not changed here.
0 commit comments