Repository navigation
fix(objectql): a cleared optional multi-value field stores null, so a second record cleared on a unique one is no longer refused 409 - #29837
Conversation
An optional column on the $empty table's multi_value row (multiselect, checkboxes, tags, or a multi-capable type with multiple: true) written [] or '' now stores null. The core rule gains an 'emptyList' reach in blankAsNullColumns, and the door's row walk hands an empty list to it. Required columns, whitespace and non-empty lists are untouched. Claude-Session: https://claude.ai/code/session_01JQwxbZfihWgHXyWG4Mrd8N Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQwxbZfihWgHXyWG4Mrd8N Co-authored-by: Claude <noreply@anthropic.com>
…nal multi-value field stores null The set_null cascade repair that removes the last member of an optional multi-value reference now reads back null through the write door, so the objectql, runtime and service-automation pins move from [] to null, and the engine, required-emptiness and record-validator prose and six hand-written docs pages stop saying "[], never null". The required half (an empty list fails required) is unchanged. The changeset names the ruling, the widening and the read change. Claude-Session: https://claude.ai/code/session_01JQwxbWuRcoCQaZSTv51YHt Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 70 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin aa46f8ffe1b34c419f525a6d9b1857ea0db4b763 && git checkout aa46f8ffe1b34c419f525a6d9b1857ea0db4b763
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55dca466701606e691c1c96d14ba68dd32b6b06e b702c0f1cee285eed74ff9e93e23a53c3babe785 && git checkout -B drift-repro 55dca466701606e691c1c96d14ba68dd32b6b06e && git merge --no-ff b702c0f1cee285eed74ff9e93e23a53c3babe785
node scripts/docs-audit/affected-docs.mjs --json 55dca466701606e691c1c96d14ba68dd32b6b06e
|
|
domain:server#3 · os-warren · session_01JQwxbYyp6wSRcywGFK2L4A Contract reviewServed-tier: This review is owed for the ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentShould-fix (prose, outside the claimed surface): the docblock of
The seat amends the claim to add that docblock, and the dev fixes it in a patch round. There is no must-fix. Implemented-by: VERDICT: PASS |
…ulti-value columns are out of its run Prose only. The blankAsNullColumns map now carries the 'emptyList' arm for multi_value-row columns; this run leaves them out because it takes only reach 'empty', not by the map's own rule. Claude-Session: https://claude.ai/code/session_01JQwxbWuRcoCQaZSTv51YHt Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#3 · os-warren · session_01JQwxbYyp6wSRcywGFK2L4A Contract reviewServed-tier: This is a delta review by the same at-tier reviewer as record 6597445147, answering its one should-fix. The moved path is a code file under ① Derived judgments
② Semver levelUnchanged: ③ Boundary flagsNone new. The dev reran the cli unit and integration tests for the command (46/46, 6/6) and the cli typecheck (exit 0). The 101 derived gate families are all accounted for at ④ Design judgmentNo change from record 6597445147. There is no must-fix, and the one should-fix landed, so this review stops here. Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-warren · session_01JQwxbYyp6wSRcywGFK2L4A Contract reviewServed-tier: VERDICT: FAIL This record supersedes this reviewer's PASS records 6597445147 and 6597824563 on this PR. CI on this head ( ① Derived judgments
② Semver levelNot reached: the head fails. ③ Boundary flags
④ Design judgmentThis needs a ruling, not a dev fix, because each route changes either ruling A's reach or ADR-0139 E6. The routes the seat sees:
The seat puts this to the maintainer through a decision thread and holds the PR at this head. ⛔ No push until the ruling is recorded on #29343. Implemented-by: VERDICT: FAIL |
…amped [] is stored as written Ruling record 6598733139: a readonly multi-value column is platform-stamped and never cleared by a writer, so its [] is a stamped value, not a cleared field. sys_activity.changed_fields keeps [] (stamped, nothing changed) apart from null (unstamped, ADR-0139 E6). The 'blank' and 'empty' arms are unchanged. One unit pin and one changeset sentence. Claude-Session: https://claude.ai/code/session_01JQwxbWuRcoCQaZSTv51YHt Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#3 · os-warren · session_01JQwxbYyp6wSRcywGFK2L4A Contract reviewServed-tier: VERDICT: PASS This is a delta review by the same at-tier reviewer as FAIL record 6597870397, answering its M1 under ruling record 6598733139 (round 2, letter A, maintainer 08:17Z). It supersedes that FAIL for this PR. ① Derived judgments
② Semver levelUnchanged: ③ Boundary flags
④ Design judgmentThis head follows ruling A with the round-2 carve-out exactly, and there is no must-fix. The optional prose-only spec card (a readonly clause in the Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-warren · session_01JQwxbYyp6wSRcywGFK2L4A 维护者速读
席位核对ACCEPT ·
|
Requested by Warren · project thread
Fixes #29343
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
multiple: true的 lookup / user / select / file / image)写入[]或''时,写入口统一存成null;级联set_null删光最后一个成员时也读回null。[]被存成 JSON 文本'[]',唯一约束下第二条清空的记录被拒 409;按裁决 A(记录 6591577985)执行,spec 半边已由 docs(spec): a cleared optional multi-value field is stored as null, in FieldSchema's multiple and required docs #29763 落地。[]的可选多值字段改读null;$empty两者都算空,只按Array.isArray判断的读者要处理null。回滚即 revert 本 PR。packages/core/src/utils/blank-storage-form.ts(新 reach'emptyList')、packages/objectql/src/validation/record-validator.ts的normalizeBlankTypedRow、packages/objectql/src/engine-cascade-delete-multivalue-probe.test.ts(钉子从[]改为null)。pnpm --filter @objectstack/rest exec vitest run src/rest-data-multi-value-blank.test.ts,预期 15/15 通过(main 上 12 条红)。What changes
claude/project-thread-tfknlu,eb81c1f6→ce827aa2,55aa2656→f147106e):blankAsNullColumnsin@objectstack/coregains the reach'emptyList'for every optional column on the$emptytable'smulti_valuerow, andblankStorageFormstores[], or exactly'', there asnull. The objectql door's row walk (normalizeBlankTypedRow) now hands an empty list to the rule. Required columns, whitespace and non-empty lists are untouched; the$emptytable is unchanged.null(f0afc329), the cascadeset_nullrepair that empties an optional set now reads backnullthrough the door:packages/objectql/src/engine-cascade-delete-multivalue-probe.test.ts(docblock, the "LAST member" pin, the "every dependent row" pin);packages/objectql/src/engine-cascade-delete-user-field.test.ts(multi-valuedset_null);packages/runtime/src/cascade-delete-multivalue-lookup-real-driver.integration.test.ts(real SQLite driver, read back from the database);packages/services/service-automation/src/builtin/delete-record-predicate-walk-batched.integration.test.ts(batched predicate walk).[], nevernull" sentence is replaced for optional columns inpackages/core/src/utils/required-emptiness.ts, the twopackages/objectql/src/engine.tsblocks (set_nulldoc and the member-removal comment) and therecord-validator.test.tsdocblock. Therequiredhalf (an empty list failsrequired) is kept everywhere.null, that a row stored earlier can still hold[], and thatis_emptymatches both:content/docs/api/data-api.mdx,content/docs/data-modeling/field-types.mdx,content/docs/data-modeling/fields.mdx,content/docs/data-modeling/validation-rules.mdx,content/docs/deployment/troubleshooting.mdx,content/docs/protocol/objectql/types.mdx. No release notes, no CHANGELOG, nocontent/docs/references/.b702c0f1, round-2 ruling A, record 6598733139): the'emptyList'arm never reaches areadonly: truecolumn. A readonly column is platform-stamped and never cleared by a writer, sosys_activity.changed_fieldskeeps[]for "stamped, nothing changed" (ADR-0139 E6).BlankAsNullFieldDefgainsreadonly?: boolean. The'blank'and'empty'arms are unchanged. One unit pin covers it, with controls..changeset/29343-multi-value-blank-null.md(@objectstack/objectqland@objectstack/core, patch): names ruling A, the Clause-② widening and the read change ([]→nullfor optional multi-value, including the cascade repair).Why
Ruling A on this card (record 6591577985): an optional multi-value column written
[]or''storesnull, unique-covered or not;requiredstill means a non-empty list; the$emptytable is unchanged. The spec half (FieldSchema'smultipleandrequireddoc blocks) landed in #29763 for #29652. Stored'[]'/'""'rows are cleaned by #29653, not here.Tests
All readings below are at head
8e4ba024(the three commits plus a merge oforigin/mainb62463f1), except the red-first reading, which is atorigin/main97c2982c. Head60dc73ffadds one comment-only commit: theblankSingleValueColumnsdocblock inpackages/cli/src/commands/migrate/blank-single-values.ts(the contract review's should-fix, 6597445147). At60dc73ff, the cli unit test for the command passes 46/46, its integration test passes 6/6, and the cli typecheck exits 0. Headb702c0f1(after mergingorigin/main55dca466asb784ff6a) adds the readonly carve-out. Atb702c0f1the following pass:activity-changed-fields.integration4/4 andblank-single-values6/6;activity-changed-fields-backfill.integration5/5;With the carve-out removed, the new pin and exactly the CI reds go red (cli cases 2–4, plugin-audit case 2). They pass again once it is restored.
origin/main97c2982cwith its own build:rest-data-multi-value-blank.test.ts12 failed / 3 passed (15). Every per-type unique case (multiselect, multi-value lookup, tags, unique-index member) answered 409 on the second[]or''.rest-data-single-value-blankandrest-data-optional-unique-blank34/34; objectql targeted files 191/191; runtime real-driver file 7 passed, 1 skipped (the Postgres cell, needsOS_TEST_POSTGRES_URL); service-automation batched walk 4/4.@objectstack/objectqltest 545 files, 10767/10767 (before and after mergingmain);@objectstack/coretest 132 files, 2936/2936; metadata-protocolseed-loaderfiles 26, 257/257; cliblank-single-values.integration.test.ts6/6 (integration project). Typecheck exit 0 for core, objectql, runtime, service-automation.blankStorageForm's'emptyList'arm replaced to return the value unchanged (scripts/ablation-replace.mjs), core rebuilt,ablation-dist-preflight.mjsexit 0. Result: objectql 5 failed / 38 passed (both probe pins, the user-field pin, two door unit pins); REST 12 failed / 3 passed. Restored: blob equals HEAD,git diff HEADempty, rebuilt, preflight--absentexit 0, the same files 43/43 and 15/15. A first attempt carried its marker in a comment, which the build strips; its preflight answered exit 1, so that run is void and was repeated with a code marker.Acceptance notes
null, not[], including theset_nullcascade repair that removes the last member.$empty: truematches both. A reader that branches onArray.isArrayalone has to readnullas empty too.packages/cli/src/commands/migrate/blank-single-values.ts(docblock near line 109) still says theblankAsNullColumnsmap holds three arms; it now holds four. That file is outside this claim's surface; the behaviour is pinned (blank-single-values.integration.test.tskeeps its column set). Noted for the seat.test:repo(vitestrepoproject) was not run locally; it belongs to CI (Test Core).Verification
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsat8e4ba024: 101 families derived. All 101 run locally; every one exit 0. At60dc73ff, the derivation is the same 101 families. The 46 families on the changed path re-ran, all exit 0, and--ranreports 101 run with 0 NOT-MEASURED. Atb702c0f1: 101 derived, 101 run, all exit 0, and--ranexits 0.PREREQUISITE NOT MET, built output missing:check:skill-examples,check:dts-closure(exit 1, read while a sibling build rewrotedriver-sql/dist),check:dual-build-cjs-loads,check:i18n,check:i18n-coverage,check:i18n-walk-parity) or could not reach a pinned fixture commit on the shallow clone (check-plugin-teardown-shape.mjs --self-test). After building the client SDK and the packages closure, and fetching that commit, each re-ran exit 0.dispatch-gates --ran: 101 derived, 101 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.pnpm lint,test:repo, dogfood is not owed: no!in the title and no(narrowing)arm).Human Minimum
packages/specholds no runtime logic. (Nopackages/specchange.)'emptyList').pnpm lintbelongs to CI.main, one pull request.packages/specnot touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01JQwxbWuRcoCQaZSTv51YHt
Generated by Claude Code