From 496b3de13cffb2e24f31ed200e62d258865c3dca Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 13:35:17 +0000 Subject: [PATCH] docs: state the multi-value `set_null` semantics on the six pages that document it (#9521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja --- content/docs/api/data-api.mdx | 8 ++++++++ content/docs/data-modeling/field-types.mdx | 4 ++-- content/docs/data-modeling/fields.mdx | 10 +++++++--- .../docs/data-modeling/validation-rules.mdx | 18 +++++++++++++++++- content/docs/deployment/troubleshooting.mdx | 9 +++++++++ content/docs/protocol/objectql/types.mdx | 13 ++++++++++++- 6 files changed, 55 insertions(+), 7 deletions(-) diff --git a/content/docs/api/data-api.mdx b/content/docs/api/data-api.mdx index 988057c1a0..5e129d0757 100644 --- a/content/docs/api/data-api.mdx +++ b/content/docs/api/data-api.mdx @@ -222,6 +222,14 @@ Delete a record. **Response**: `{ object: "account", id: "1", success: true }` +Every relation pointing at the deleted record honours its own `deleteBehavior` +(`cascade` / `set_null` / `restrict`). On a `multiple: true` reference, `set_null` +removes just the deleted id from the array and keeps the rest, and a reference set +emptied that way reads back as `[]` — never `null`, so a client that branches on +`null` for "no link" misses the emptied case (the `multiple` doc block in +`packages/spec/src/data/field.zod.ts`, rendered in the +[Field reference](/docs/references/data/field)). + --- ## Batch Operations diff --git a/content/docs/data-modeling/field-types.mdx b/content/docs/data-modeling/field-types.mdx index d5eb9f9182..5fcd2fc5df 100644 --- a/content/docs/data-modeling/field-types.mdx +++ b/content/docs/data-modeling/field-types.mdx @@ -315,7 +315,7 @@ Reference to a record in another object (foreign key). |:---|:---|:---|:---| | `reference` | `string` | **required** | Target object name (snake_case) | | `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) | -| `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) | +| `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` | ```typescript { name: 'company', label: 'Company', type: 'lookup', reference: 'account' } @@ -655,7 +655,7 @@ These properties are available on **all** field types: | `type` | `FieldType` | **required** | One of the supported field types | | `required` | `boolean` | `false` | Whether the field is required | | `unique` | `boolean` | `false` | Enforce uniqueness | -| `multiple` | `boolean` | `false` | Allow array of values | +| `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) | | `searchable` | `boolean` | `false` | Include in search index | | `sortable` | `boolean` | `true` | Allow sorting by this field | | `hidden` | `boolean` | `false` | Hide from default views | diff --git a/content/docs/data-modeling/fields.mdx b/content/docs/data-modeling/fields.mdx index 91bb02b00a..3889faa298 100644 --- a/content/docs/data-modeling/fields.mdx +++ b/content/docs/data-modeling/fields.mdx @@ -189,9 +189,13 @@ rejected with `400 VALIDATION_FAILED` and a `fields[]` entry whose `code` is `reference_not_found` (see the [error catalog](/docs/api/error-catalog)). The same check runs on bulk updates. -Clearing a relationship is not a dangling reference: `null`, `""` and `[]` mean -"no link" — exactly what `deleteBehavior: 'set_null'` writes when the parent -goes away. +Clearing a relationship is not a dangling reference: `null`, `""` and `[]` all mean +"no link". Which of them `deleteBehavior: 'set_null'` writes when the referenced +record goes away depends on the field: a single-value lookup is set to `null`, while +a `multiple: true` lookup keeps its other members and loses only the deleted one — +and a set emptied that way is written as `[]`, never `null` (the `multiple` doc block +in `packages/spec/src/data/field.zod.ts`, rendered in the +[Field reference](/docs/references/data/field)). `deleteBehavior` governs what happens to *this* record when the **referenced** diff --git a/content/docs/data-modeling/validation-rules.mdx b/content/docs/data-modeling/validation-rules.mdx index d1ef9e5b69..198b24a155 100644 --- a/content/docs/data-modeling/validation-rules.mdx +++ b/content/docs/data-modeling/validation-rules.mdx @@ -244,10 +244,26 @@ These properties apply to **all** field types and are validated by the base `Fie | `reference` | `string` | — | **Required.** Target object name | | `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer part of the `Field` schema; use `lookupFilters` + `dependsOn` instead | | `deleteBehavior` | `enum` | `set_null` | `set_null`, `cascade`, or `restrict` | -| `multiple` | `boolean` | `false` | Allow multiple references | +| `multiple` | `boolean` | `false` | Allow multiple references; the value is validated as an array of ids | **Default constraints:** Validates that referenced record exists. Foreign key integrity enforced. +**Multi-value lookups (`multiple: true`).** The empty set is representable: an emptied +multi-value lookup is stored and read back as `[]`, never `null` — including when +`deleteBehavior: 'set_null'` removes the last remaining member. The declared contract +lives in the `multiple` and `required` doc blocks of +`packages/spec/src/data/field.zod.ts` (rendered in the +[Field reference](/docs/references/data/field)), and it makes `required` on a +multi-value lookup mean **non-empty array**. + + +The `required`-means-non-empty half is **declared but not yet enforced** by the record +validator: today `[]` passes the required check on both insert and update, because the +check treats only `undefined`, `null` and blank strings as missing. Validate emptiness +in application code until that lands (tracked in #9476). The representation guarantee +above (`[]`, never `null`) is live. + + ### `master_detail` | Property | Type | Default | Validation Behavior | diff --git a/content/docs/deployment/troubleshooting.mdx b/content/docs/deployment/troubleshooting.mdx index e47c57a0eb..b49b42ff41 100644 --- a/content/docs/deployment/troubleshooting.mdx +++ b/content/docs/deployment/troubleshooting.mdx @@ -218,6 +218,15 @@ client.data.find('project_task', { /* query */ }); { name: 'project', type: 'lookup', reference: 'project', deleteBehavior: 'set_null' } ``` + +On a `multiple: true` lookup, `set_null` clears only the **member** that was deleted — +the child's other references survive — and a set emptied that way is stored as `[]`, +never `null` (the `multiple` doc block in `packages/spec/src/data/field.zod.ts`, +rendered in the [Field reference](/docs/references/data/field)). A cleanup query or +client branch that looks for `null` to find cleared references will not match those +rows; test for an empty array instead. + + --- ### "defineStack() validation errors" diff --git a/content/docs/protocol/objectql/types.mdx b/content/docs/protocol/objectql/types.mdx index 5af6347961..1649e47f63 100644 --- a/content/docs/protocol/objectql/types.mdx +++ b/content/docs/protocol/objectql/types.mdx @@ -621,10 +621,21 @@ const opportunities = await engine.find('opportunity', { ``` **On Delete Options:** -- `set_null`: Set field to null when referenced record is deleted +- `set_null`: Clear the reference when the referenced record is deleted. On a + single-value lookup the stored id becomes `null`; on a `multiple: true` lookup the + reference is a set, so only the deleted **member** is removed and the remaining + members are kept - `restrict`: Prevent deletion if references exist - `cascade`: Delete this record when referenced record is deleted +> **Residual shape of a multi-value reference.** A `multiple: true` lookup emptied by +> member removal is written as `[]`, **never `null`**. That is not a cascade +> convention: the representation binds every writer — cascade repair, form clears and +> API writes alike — so a reader of an array field never needs a null branch. The +> guarantee is pinned by `FieldSchema`, in the `multiple` doc block of +> `packages/spec/src/data/field.zod.ts` (rendered in the +> [Field reference](/docs/references/data/field)). + > **Required foreign keys.** A `required: true` lookup cannot be nulled, so the > *default* `set_null` automatically escalates to `restrict` on such a field — > deleting the parent is refused with `409 DELETE_RESTRICTED` (the response