Skip to content

Commit 161b5ce

Browse files
claude[bot]os-steveclaude
authored
docs: state the multi-value set_null semantics on the six pages that document it (#9521) (#9627)
`deleteBehavior: 'set_null'` on a `multiple: true` reference removes the deleted MEMBER and keeps the rest, and a set emptied that way is written as `[]`, never `null` — the representation `FieldSchema` pins (the `multiple` doc block in packages/spec/src/data/field.zod.ts, #9447 maintainer ruling 2026-08-18). Each page is judged on its own job rather than given one pasted block: - protocol/objectql/types.mdx — corrects "Set field to null", which was the only page stating the single-value behaviour as universal; adds the residual-shape guarantee at protocol precision. - data-modeling/fields.mdx — the `null`/`""`/`[]` sentence now says which one is written when, instead of leaving all three equivalent. - data-modeling/field-types.mdx — the two table cells an author reads when choosing `deleteBehavior` and `multiple`. - deployment/troubleshooting.mdx — in place, for the mid-incident reader: a cleanup query looking for `null` will not match an emptied set. - api/data-api.mdx — the read-back shape an integrator's client branches on. - data-modeling/validation-rules.mdx — the live representation guarantee, plus the ruled `required`-means-non-empty half marked DECLARED BUT NOT YET ENFORCED (#9476), so the page promises nothing the validator does not do today. Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja Co-authored-by: Claude <steve@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 890b38f commit 161b5ce

6 files changed

Lines changed: 55 additions & 7 deletions

File tree

‎content/docs/api/data-api.mdx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,14 @@ Delete a record.
222222

223223
**Response**: `{ object: "account", id: "1", success: true }`
224224

225+
Every relation pointing at the deleted record honours its own `deleteBehavior`
226+
(`cascade` / `set_null` / `restrict`). On a `multiple: true` reference, `set_null`
227+
removes just the deleted id from the array and keeps the rest, and a reference set
228+
emptied that way reads back as `[]` — never `null`, so a client that branches on
229+
`null` for "no link" misses the emptied case (the `multiple` doc block in
230+
`packages/spec/src/data/field.zod.ts`, rendered in the
231+
[Field reference](/docs/references/data/field)).
232+
225233
---
226234

227235
## Batch Operations

‎content/docs/data-modeling/field-types.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ Reference to a record in another object (foreign key).
315315
|:---|:---|:---|:---|
316316
| `reference` | `string` | **required** | Target object name (snake_case) |
317317
| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property (unknown keys are stripped by the schema). Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) |
318-
| `deleteBehavior` | `'restrict' \| 'cascade' \| 'set_null'` | `'set_null'` | Behavior when referenced record is deleted (a *required* lookup left at the default `set_null` is escalated to `restrict`, since a NOT NULL foreign key cannot be cleared) |
318+
| `deleteBehavior` | `'restrict' \| 'cascade' \| 'set_null'` | `'set_null'` | Behavior when referenced record is deleted (a *required* lookup left at the default `set_null` is escalated to `restrict`, since a NOT NULL foreign key cannot be cleared). On a `multiple: true` lookup `set_null` removes only the deleted **member** — the other members are kept, and a set emptied that way is stored as `[]`, never `null` |
319319

320320
```typescript
321321
{ name: 'company', label: 'Company', type: 'lookup', reference: 'account' }
@@ -655,7 +655,7 @@ These properties are available on **all** field types:
655655
| `type` | `FieldType` | **required** | One of the supported field types |
656656
| `required` | `boolean` | `false` | Whether the field is required |
657657
| `unique` | `boolean` | `false` | Enforce uniqueness |
658-
| `multiple` | `boolean` | `false` | Allow array of values |
658+
| `multiple` | `boolean` | `false` | Allow array of values (multi-record lookup, multi-select, multi-file). An emptied multi-value lookup reads back as `[]`, never `null` — the `multiple` doc block in [`FieldSchema`](/docs/references/data/field) |
659659
| `searchable` | `boolean` | `false` | Include in search index |
660660
| `sortable` | `boolean` | `true` | Allow sorting by this field |
661661
| `hidden` | `boolean` | `false` | Hide from default views |

‎content/docs/data-modeling/fields.mdx‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,13 @@ rejected with `400 VALIDATION_FAILED` and a `fields[]` entry whose `code` is
189189
`reference_not_found` (see the [error catalog](/docs/api/error-catalog)). The
190190
same check runs on bulk updates.
191191

192-
Clearing a relationship is not a dangling reference: `null`, `""` and `[]` mean
193-
"no link" — exactly what `deleteBehavior: 'set_null'` writes when the parent
194-
goes away.
192+
Clearing a relationship is not a dangling reference: `null`, `""` and `[]` all mean
193+
"no link". Which of them `deleteBehavior: 'set_null'` writes when the referenced
194+
record goes away depends on the field: a single-value lookup is set to `null`, while
195+
a `multiple: true` lookup keeps its other members and loses only the deleted one —
196+
and a set emptied that way is written as `[]`, never `null` (the `multiple` doc block
197+
in `packages/spec/src/data/field.zod.ts`, rendered in the
198+
[Field reference](/docs/references/data/field)).
195199

196200
<Callout type="info">
197201
`deleteBehavior` governs what happens to *this* record when the **referenced**

‎content/docs/data-modeling/validation-rules.mdx‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,26 @@ These properties apply to **all** field types and are validated by the base `Fie
244244
| `reference` | `string` | — | **Required.** Target object name |
245245
| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer part of the `Field` schema; use `lookupFilters` + `dependsOn` instead |
246246
| `deleteBehavior` | `enum` | `set_null` | `set_null`, `cascade`, or `restrict` |
247-
| `multiple` | `boolean` | `false` | Allow multiple references |
247+
| `multiple` | `boolean` | `false` | Allow multiple references; the value is validated as an array of ids |
248248

249249
**Default constraints:** Validates that referenced record exists. Foreign key integrity enforced.
250250

251+
**Multi-value lookups (`multiple: true`).** The empty set is representable: an emptied
252+
multi-value lookup is stored and read back as `[]`, never `null` — including when
253+
`deleteBehavior: 'set_null'` removes the last remaining member. The declared contract
254+
lives in the `multiple` and `required` doc blocks of
255+
`packages/spec/src/data/field.zod.ts` (rendered in the
256+
[Field reference](/docs/references/data/field)), and it makes `required` on a
257+
multi-value lookup mean **non-empty array**.
258+
259+
<Callout type="warn">
260+
The `required`-means-non-empty half is **declared but not yet enforced** by the record
261+
validator: today `[]` passes the required check on both insert and update, because the
262+
check treats only `undefined`, `null` and blank strings as missing. Validate emptiness
263+
in application code until that lands (tracked in #9476). The representation guarantee
264+
above (`[]`, never `null`) is live.
265+
</Callout>
266+
251267
### `master_detail`
252268

253269
| Property | Type | Default | Validation Behavior |

‎content/docs/deployment/troubleshooting.mdx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,15 @@ client.data.find('project_task', { /* query */ });
218218
{ name: 'project', type: 'lookup', reference: 'project', deleteBehavior: 'set_null' }
219219
```
220220

221+
<Callout type="info">
222+
On a `multiple: true` lookup, `set_null` clears only the **member** that was deleted —
223+
the child's other references survive — and a set emptied that way is stored as `[]`,
224+
never `null` (the `multiple` doc block in `packages/spec/src/data/field.zod.ts`,
225+
rendered in the [Field reference](/docs/references/data/field)). A cleanup query or
226+
client branch that looks for `null` to find cleared references will not match those
227+
rows; test for an empty array instead.
228+
</Callout>
229+
221230
---
222231

223232
### "defineStack() validation errors"

‎content/docs/protocol/objectql/types.mdx‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,10 +621,21 @@ const opportunities = await engine.find('opportunity', {
621621
```
622622

623623
**On Delete Options:**
624-
- `set_null`: Set field to null when referenced record is deleted
624+
- `set_null`: Clear the reference when the referenced record is deleted. On a
625+
single-value lookup the stored id becomes `null`; on a `multiple: true` lookup the
626+
reference is a set, so only the deleted **member** is removed and the remaining
627+
members are kept
625628
- `restrict`: Prevent deletion if references exist
626629
- `cascade`: Delete this record when referenced record is deleted
627630

631+
> **Residual shape of a multi-value reference.** A `multiple: true` lookup emptied by
632+
> member removal is written as `[]`, **never `null`**. That is not a cascade
633+
> convention: the representation binds every writer — cascade repair, form clears and
634+
> API writes alike — so a reader of an array field never needs a null branch. The
635+
> guarantee is pinned by `FieldSchema`, in the `multiple` doc block of
636+
> `packages/spec/src/data/field.zod.ts` (rendered in the
637+
> [Field reference](/docs/references/data/field)).
638+
628639
> **Required foreign keys.** A `required: true` lookup cannot be nulled, so the
629640
> *default* `set_null` automatically escalates to `restrict` on such a field —
630641
> deleting the parent is refused with `409 DELETE_RESTRICTED` (the response

0 commit comments

Comments
 (0)