Skip to content

Commit 4ab2e7e

Browse files
docs(spec): a cleared optional multi-value field is stored as null, in FieldSchema's multiple and required docs (#29763)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBAoBepdw1XbHyLW2fvste1k)_ Fixes #29652 Clause-②: yes Metadata-protocol: semantic Skills-docs: none — `git grep` over `skills/**` for the empty-representation claim (`never null`, `reads back as []`, `emptied`) finds no page that teaches it; the hand-written `content/docs/**` pages that do are listed under the census below and move with the write-door PR that changes the behaviour ## 维护者速读 - 改了什么:`FieldSchema` 的 `multiple` / `required` 说明(docblock 与 `multiple` 的 describe)改为裁决后的规则:可选多值字段被清空时存 `null`(与从未赋值相同),写入 `[]` 或 `''` 也存 `null`;`required` 仍表示非空列表;`$empty` 表不变。重新生成了 3 个 references 页面。 - 为什么改:执行 #29343 裁决 A(记录 6591577985,「A:存 null,先修 #9447 spec 文本」),它推翻了 #9447「清空后读回 `[]`、永不 `null`」对可选列的那半句;按裁决先落 spec 文本,#29343 的写入口 PR 被本卡阻塞。 - 风险与回滚:只改文字,不动任何键、默认值或可接受集合;运行时行为由 #29343 的 PR 改,本 PR 落地后、那个 PR 落地前,文档先于行为。回滚即还原本 PR。 - 先看:`packages/spec/src/data/field.zod.ts`(`multiple` 的 docblock 与 describe,≈:1357-1380;`required` 的 docblock,≈:1314-1321)、`.changeset/29652-multi-value-empty-null-docs.md`。 - 两分钟验证:`git grep -n "never .null" -- packages/spec/src/data/field.zod.ts content/docs/references`,应只剩一行:field.zod.ts:1367,docblock 里被推翻的原句引文(修改前 field.zod.ts 3 处、3 个 references 页面共 5 处)。 - 需要你做的:无,席位落地。 ## What changes Text only, in `packages/spec/src/data/field.zod.ts` (measured on `origin/main` `1c1a5d40`; the line numbers are unchanged at the final merge base `0b2b309c`), plus one comment in `filter-empty-operator.ts` and the regenerated references. No key, default, accept set or export moves. **`required` doc block (≈:1314).** - Before: "The empty set is always representable (it reads back as `[]`, never `null` — see `multiple`), so the required check judges emptiness, not absence." - After: "An emptied OPTIONAL multi-value field is stored as `null`, the same as one never set (see `multiple`), so the required check judges emptiness: `null` and `[]` both fail it." The cite now reads "(#9447, maintainer ruling 2026-08-18; this half stands under ruling A on #29343, record 6591577985)". - The `required` describe is unchanged: its sentence ("`[]` does not satisfy it") is the half of #9447 the ruling keeps. **`multiple` doc block (≈:1357).** - Before: "Multi-value empty representation (#9447, maintainer ruling 2026-08-18): an emptied multi-value lookup reads back as `[]`, never `null`. This binds … EVERY writer … an array field always reads as an array, so readers (generated code, formula/filter predicates) never need a null branch." - After: "Multi-value empty representation (ruling A on #29343, record 6591577985, amending #9447, maintainer ruling 2026-08-18): a cleared OPTIONAL multi-value field — every type `isMultiValueField` answers true for … — is stored as `null` by the write door, the same as one never set, unique-covered or not: `[]` and `''` written to it store `null`. This binds … EVERY writer … It reverses #9447's sentence for optional columns: readers meet `null` for an empty set, and read `null` and `[]` alike as empty, since a row stored before the rule can still hold `[]`. The `$empty` table's `multi_value` row is unchanged … #9447's other half stands: `required` on a multi-value field means non-empty list." - "readers never need a null branch" is dropped. **`multiple` describe (≈:1380).** The rule only, no tracker id. - Before: "An emptied multi-value lookup reads back as `[]`, never `null` — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair." - After: "A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list." **`filter-empty-operator.ts` docblock (≈:91), the one other `packages/spec` hit.** - Before: "a field whose value is a list is emptied to `[]`." - After: "a field whose value is a list can hold `[]` (a cleared optional one stores `null`, but a row stored before that rule can still hold `[]`)." The `$empty` table and `expandEmptyOperator` are untouched. **Generated.** `pnpm --filter @objectstack/spec build`, then `check:generated` named one stale artifact (`content/docs/references/**`); `gen:docs` rewrote the `multiple` row in three pages, because `FieldSchema` is rendered in each: `references/data/field.mdx`, `references/data/object.mdx` (2 rows) and `references/system/migration.mdx` (2 rows). `check:generated` then reported all 14 artifacts current. **Changeset.** `.changeset/29652-multi-value-empty-null-docs.md`: `@objectstack/spec` minor, both declaration lines at line start. ## Why Ruling A on #29343 (record 6591577985), verbatim: 「A:存 null,先修 #9447 spec 文本」 — 「spec 席先改 field.zod.ts 的 multiple/required 说明,再落本卡 PR(代码已建好),另开一张清理已存 '[]' 的 migrate 卡。」 **The declarations, measured.** - `Metadata-protocol: semantic`. The `Check Changeset` "WHICH LEVEL" text: "`semantic` — the meaning or default of an authorable key changes → `minor`". `multiple` is an authorable key, and what it promises about the stored empty value changes; the published describe is that promise. The counter-reading is the `none` row's "description or error text", which is for wording that leaves the meaning alone. So `semantic`, and `@objectstack/spec` takes `minor`. - `Clause-②: yes`, no arm. A published semantic contract changes, and no accept set moves, so neither `(widening)` nor `(narrowing)` applies. - **ADR-0087.** `check-adr-0087-registration` asks for a disposition only on a changeset that declares a breaking change; this one does not. The gate exits 0. No `SemanticMigration` entry is written. Stored `'[]'` values are the ruling's step 3, a separate `os migrate` data card. ## Census: the "`[]`, never `null`" claim `git grep` on `1c1a5d40` for `never null`, `` never `null` ``, `reads back as []`, `emptied`, `9447`, and `multi-value` near `[]`: - **`packages/spec/**`, amended here:** `field.zod.ts:1316-1317` (`required` docblock), `:1357-1364` (`multiple` docblock), `:1371` (`multiple` describe); `filter-empty-operator.ts:91`. - **`packages/spec/**`, read and left:** `value-roundtrip-conformance.ts:216` (`m_empty`, "empty array must not degrade to null") is the driver round-trip table, below the write door, and the ruling does not change what a driver returns for a written `[]`. The `null`-or-`[]` rows in `filter-empty-operator.ts`, `filter.zod.ts:1802`/`:2948` and `filter-logic-conformance.ts:80` are the unchanged `$empty` table. `CHANGELOG.md:48935` is release-owned. The other `never null` hits (`organization.zod.ts`, `solution-blueprint.zod.ts`, tests) are other subjects. - **`skills/**`:** 0 hits. - **Hand-written `content/docs/**`, findings, not edited here:** each says an emptied `multiple: true` set is written or read back as `[]`, never `null`, and most point at the `multiple` doc block as their source: - `content/docs/api/data-api.mdx:279` - `content/docs/data-modeling/field-types.mdx:1025` - `content/docs/data-modeling/fields.mdx:295` - `content/docs/data-modeling/validation-rules.mdx:265` (and the #9447 cite at `:274`) - `content/docs/deployment/troubleshooting.mdx:254` - `content/docs/protocol/objectql/types.mdx:730` They describe what the runtime does today, which changes only with the write-door PR on #29343. Moving them there keeps each page true on every merged commit. ## Tests No test changes: no tested behaviour moves. `pnpm --filter @objectstack/spec test`: on the final head `3e61ed73`, under `os-verify-lock`: Test Files 803 passed (803), Tests 23795 passed, 1 todo. `pnpm --filter @objectstack/spec typecheck` ran in the same chain, lock `VERDICT command-exit 0`. The two earlier heads read the same: `f05af6cb` (803/803 files, 23786 passed) and `8b1ecbbc` after the first `origin/main` merge (803/803, 23792 passed). ## Acceptance notes - Docs before behaviour, by the ruling's order: until the PR on #29343 lands, the write door still stores `[]` as sent on these columns, so for a while the describe states a rule the runtime has not applied yet. - The `required` describe still says "On a multi-value lookup (`multiple: true`)". It is narrower than the rule (which covers every multi-value type) but not false. Widening it is outside the file surface this card claims. - `field.zod.ts` also moved on `main` while this was open: #29623 (`87956dcc`) landed and was merged in, a different region, and the regenerated references keep only this PR's `multiple` rows against `main`. PR #29553 (#29415) edits the same file near :1822, also a different region; the later lander merges `origin/main`. - Author of this body: `domain:spec#3 · os-support-ai · session_01WGufzeW5cprrZRccVNgPFr`. Lines 1 to 3 belong to the attribution block and the closing line, so the signature is here rather than first. ## Verification Every reading here is on the final head `3e61ed73`: the branch merged with `origin/main` at `0b2b309c`. The delta against `main` is exactly the 6 files above, 45 insertions and 20 deletions. - `pnpm exec turbo run build --concurrency=2`: 118/118 tasks, lock `VERDICT command-exit 0`. - `pnpm --filter @objectstack/spec check:generated`: "All 14 generated artifacts are up to date". - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 96 families from 6 changed paths. Each ran serially and was recorded as `CMD :: exit N`: **96 exit 0**. `dispatch-gates --ran` reported "96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN". - `GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event FILE`, with this body as the event: exit 0. Its verdict: "LEVEL AXIS: this PR declares clause-② `yes` and metadata-protocol `semantic`, and no package whose published source … it moves is graded `patch`". - `check-adr-0087-registration --base origin/main`: exit 0. No changeset here declares a breaking change, so the gate asks for no disposition. <details><summary>Earlier heads</summary> On `f05af6cb` (before any merge), the same 96 ran: 89 exit 0 and 7 exit 3 (PREREQUISITE NOT MET). The 7 lacked a built `dist` for `@objectstack/formula`, `@objectstack/lint`, `@objectstack/client-react` and `@objectstack/objectql`, or needed the `check-plugin-teardown-shape` fixture commit that the shallow clone was missing. After a full build and a fetch of that fixture commit, all 7 exited 0. On `8b1ecbbc` (after the first merge), all 96 exited 0. </details> Not run locally, left for CI: `pnpm lint`, the `test:repo` project, and the full dogfood suite (the title has no `!` and the PR declares no `(narrowing)` arm). --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzeW5cprrZRccVNgPFr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e82bf64 commit 4ab2e7e

6 files changed

Lines changed: 45 additions & 20 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
docs(spec): a cleared optional multi-value field is stored as `null`, the same as one never set; `FieldSchema`'s `multiple` and `required` docs now say so
6+
7+
Clause-②: yes
8+
Metadata-protocol: semantic
9+
10+
`FieldSchema.multiple` documented that an emptied multi-value lookup reads back as `[]`, never `null`, for every writer, and `required` leaned on that. The rule is now the other way round for optional columns.
11+
12+
- **Now.** A cleared optional multi-value field (`multiselect`, `checkboxes`, `tags`, and `select`, `lookup`, `user`, `file` or `image` with `multiple: true`) is stored as `null` at the engine's write door, the same as a field never set, whether or not a unique constraint covers it. `[]` and `''` written to such a column store `null`. The rule binds every writer: cascade repair, form clears and API writes.
13+
- **Reading.** Treat `null` and `[]` alike as empty. The `is_empty` filter (`$empty`) already does, and its table is unchanged. A row stored before this rule can still hold `[]`.
14+
- **Unchanged.** `required` on a multi-value field still means a non-empty list: `[]` is refused, as is `null`.
15+
- **This change is documentation.** The `multiple` and `required` descriptions (and the generated field reference) state the rule. The write-door change that applies it to `[]` and `''` ships separately, and stored `[]` values are cleaned by a separate `os migrate` data step.

‎content/docs/references/data/field.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const result = CurrencyConfigSchema.parse(data);
5858
| **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. |
5959
| **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. |
6060
| **searchable** | `boolean` | optional (default: `false`) | Search this field when its object declares no `searchableFields`. The set `search` scans, and the set a `$searchFields` override may name, is then the type-based default (the text-like and select columns) plus every field marked `searchable: true`, of any type except `secret`, `password` and `vector`, which stay excluded (a virtual `formula` field has no stored column, so it is never scanned either). `false`, the default, does not remove a field the type-based default already scans; to choose the set, declare `searchableFields`. When the object declares `searchableFields`, that list is the whole search set and this flag is not read (ADR-0061). |
61-
| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. An emptied multi-value lookup reads back as `[]`, never `null` — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair. |
61+
| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list. |
6262
| **unique** | `boolean \| 'global' \| 'organization'` | optional | Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization'. Omitted ⇒ false, EXCEPT on an `autonumber` field, where omitted ⇒ 'organization' (an auto-number is a business identifier, so the platform makes it unique per organization by default — the same tenant-composite shape an explicit `unique: true` produces). To opt an autonumber field out, write `unique: false` explicitly — legitimate only for a display-only sequence that is not used to identify the record; the platform's duplicate scan (`os migrate duplicates`) still treats every autonumber field as an identifier. |
6363
| **defaultValue** | `any` | optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes, discriminated in the engine's own order: a CEL Expression envelope `{ dialect: 'cel', source: 'today()' }` (accepted structurally; result type is a runtime concern); a runtime TOKEN — `NOW()` on `datetime`/`date`/`time` only, `current_user` on `user` or `lookup` with `reference: 'sys_user'` only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 `valueSchemaFor`). Anything else is refused at parse time with a prescriptive message. |
6464
| **maxLength** | `integer` | optional | Max character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. Checked on the WRITTEN value only (the `min`/`max` transition-gate class): a stored value longer than a bound declared later is never re-read and survives unrelated edits — only a write carrying an over-long value is refused. |

‎content/docs/references/data/object.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ const result = ApiMethod.parse(data);
227227
| **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. |
228228
| **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. |
229229
| **searchable** | `boolean` | optional (default: `false`) | Search this field when its object declares no `searchableFields`. The set `search` scans, and the set a `$searchFields` override may name, is then the type-based default (the text-like and select columns) plus every field marked `searchable: true`, of any type except `secret`, `password` and `vector`, which stay excluded (a virtual `formula` field has no stored column, so it is never scanned either). `false`, the default, does not remove a field the type-based default already scans; to choose the set, declare `searchableFields`. When the object declares `searchableFields`, that list is the whole search set and this flag is not read (ADR-0061). |
230-
| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. An emptied multi-value lookup reads back as `[]`, never `null` — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair. |
230+
| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list. |
231231
| **unique** | `boolean \| 'global' \| 'organization'` | optional | Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization'. Omitted ⇒ false, EXCEPT on an `autonumber` field, where omitted ⇒ 'organization' (an auto-number is a business identifier, so the platform makes it unique per organization by default — the same tenant-composite shape an explicit `unique: true` produces). To opt an autonumber field out, write `unique: false` explicitly — legitimate only for a display-only sequence that is not used to identify the record; the platform's duplicate scan (`os migrate duplicates`) still treats every autonumber field as an identifier. |
232232
| **defaultValue** | `any` | optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes, discriminated in the engine's own order: a CEL Expression envelope `{ dialect: 'cel', source: 'today()' }` (accepted structurally; result type is a runtime concern); a runtime TOKEN — `NOW()` on `datetime`/`date`/`time` only, `current_user` on `user` or `lookup` with `reference: 'sys_user'` only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 `valueSchemaFor`). Anything else is refused at parse time with a prescriptive message. |
233233
| **maxLength** | `integer` | optional | Max character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. Checked on the WRITTEN value only (the `min`/`max` transition-gate class): a stored value longer than a bound declared later is never re-read and survives unrelated edits — only a write carrying an over-long value is refused. |
@@ -598,7 +598,7 @@ const result = ApiMethod.parse(data);
598598
| **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. |
599599
| **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. |
600600
| **searchable** | `boolean` | optional (default: `false`) | Search this field when its object declares no `searchableFields`. The set `search` scans, and the set a `$searchFields` override may name, is then the type-based default (the text-like and select columns) plus every field marked `searchable: true`, of any type except `secret`, `password` and `vector`, which stay excluded (a virtual `formula` field has no stored column, so it is never scanned either). `false`, the default, does not remove a field the type-based default already scans; to choose the set, declare `searchableFields`. When the object declares `searchableFields`, that list is the whole search set and this flag is not read (ADR-0061). |
601-
| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. An emptied multi-value lookup reads back as `[]`, never `null` — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair. |
601+
| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list. |
602602
| **unique** | `boolean \| 'global' \| 'organization'` | optional | Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization'. Omitted ⇒ false, EXCEPT on an `autonumber` field, where omitted ⇒ 'organization' (an auto-number is a business identifier, so the platform makes it unique per organization by default — the same tenant-composite shape an explicit `unique: true` produces). To opt an autonumber field out, write `unique: false` explicitly — legitimate only for a display-only sequence that is not used to identify the record; the platform's duplicate scan (`os migrate duplicates`) still treats every autonumber field as an identifier. |
603603
| **defaultValue** | `any` | optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes, discriminated in the engine's own order: a CEL Expression envelope `{ dialect: 'cel', source: 'today()' }` (accepted structurally; result type is a runtime concern); a runtime TOKEN — `NOW()` on `datetime`/`date`/`time` only, `current_user` on `user` or `lookup` with `reference: 'sys_user'` only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 `valueSchemaFor`). Anything else is refused at parse time with a prescriptive message. |
604604
| **maxLength** | `integer` | optional | Max character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. Checked on the WRITTEN value only (the `min`/`max` transition-gate class): a stored value longer than a bound declared later is never re-read and survives unrelated edits — only a write carrying an over-long value is refused. |

0 commit comments

Comments
 (0)