Repository navigation
fix(objectql,cli): os migrate stored-expressions fills a stored computed field on rows stored before it was declared, and the boot names it - #30227
Conversation
…y, boot advisory Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 68 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 49 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 c4468f88f1c869b193fe2f37dcfa1cf4b183bf83 && git checkout c4468f88f1c869b193fe2f37dcfa1cf4b183bf83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bb86ff96cc6e3854d00c39488a0a8985fb1312d1 a1ee9670a6325bdc0f6e1d226c74464a356f136c && git checkout -B drift-repro bb86ff96cc6e3854d00c39488a0a8985fb1312d1 && git merge --no-ff a1ee9670a6325bdc0f6e1d226c74464a356f136c
node scripts/docs-audit/affected-docs.mjs --json bb86ff96cc6e3854d00c39488a0a8985fb1312d1
|
The PR adds published @objectstack/objectql exports (the stored-expression backfill functions and types), one public method ObjectQL.computeStoredExpressionsOver and a new CLI command; nothing narrows. The dispatch order proposed `no`; corrected on the seat's review, as #30183 declared the same shape. Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
…rityResolversFacet engine.ts is a registered facade whose members hold delegations only (check-facade-delegation-only). The body moves beside serveFormulaField in engine-security-resolvers.ts, and ObjectQL.computeStoredExpressionsOver forwards to it in one call, as its neighbours do. No behaviour change. Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL Contract reviewServed-tier: ① Derived judgmentsFace = declaration only ( ② Semver levelAdditive only. ③ Boundary flags
Implemented-by: VERDICT: PASS |
…ck family rosters The command boots through bootSchemaStack, so the one-shot family table (schema-migrate.one-shot-family.integration.test.ts CALLERS) and the --json stdout purity family (json-stdout-purity.e2e.test.ts FAMILY) both discover it; each now drives it, as summary-nulls is driven. Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL Contract reviewServed-tier: ① Derived judgmentsRe-recorded on the new head: the delta from Face = declaration only ( ② Semver levelAdditive only. ③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL 维护者速读
席位核对 — ACCEPT · domain:server#2 · head
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 41965463000 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL Merge-queue kick, run 41965463000 (triage comment 6649684120): one re-queue, on the three facts.
Re-queued once ( |
Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6Cb2FL7H75wB1wA6PpZ
维护者速读
os migrate stored-expressions:给"在已有数据的对象上新加的存储型计算字段"补算存量行(默认只补 NULL,--changed再重算表达式改过后不一致的值),先报告、--apply才写;另在启动时抽查每个声明了存储型计算字段的对象的前 100 行,发现这种 NULL 就告警一次并点名命令。engine.update(保留updated_at),钩子、汇总、审计照常触发;表达式在某行算不出而该行已有值时整行跳过并列出,不会用 NULL 覆盖。启动抽查只读、有上限、出错不影响启动。回滚即撤 PR。packages/objectql/src/stored-expression-backfill.ts(分类与写入规则)、packages/objectql/src/engine.ts新方法computeStoredExpressionsOver、packages/objectql/src/plugin.ts的kernel:bootstrapped一行。pnpm --filter @objectstack/objectql exec vitest run src/stored-expression-backfill.test.ts,预期 29 个用例全过(含"加字段后旧行 NULL → --apply 补齐 → 再跑为 0")。Fixes #29961
Clause-②: yes (widening)
Metadata-protocol: none
Why
See the first two bullets of 维护者速读 and #29961: a stored computed field added to an object that already has rows stays NULL on every existing row until each row is next written, and an analytics view reading it treats the NULL as 0.
What changes
The producer. A stored computed field (a non-
formulafield declaringexpression) is computed bycomputeStoredExpressionson insert and update only. Schema sync adds its column NULL on every existing row, and nothing computes it until each row is next written. The fix leaves the write path alone and adds the missing producer for existing rows, in the shapeos migrate summary-nullsandos migrate recompute-home-amountsalready have: a report-first command, plus a bounded boot advisory.ObjectQL.computeStoredExpressionsOver(object, rows, context?, onFault?)(engine.ts, one method): the write's own plan (planStoredExpressions) and serving (declared option labels, the one hop, thecan()map) over rows already stored. Evaluates over copies, writes nothing.stored-expression-backfill.ts(new):backfillStoredExpressionswalks every row by keyset (id), computes each page through the method above, and classifies each (row, field):changed;A written row goes through
engine.updatewith the computed values and the row'supdated_atunderpreserveAudit, asrecompute-home-amountswrites. The write computes every stored field of the row again, so a stale value on a filled row is rewritten with it (counted underwritten). Refused before any row is read: an unknown object (OBJECT_NOT_FOUND), a named object orfielddeclaring no stored computed field, andfieldwithout exactly one object (INVALID_FIELD, 400). Idempotent; nosys_migrationflag (nothing gates on it).announceStoredExpressionHoles, called from the objectql plugin'skernel:bootstrappedhook besideannounceOpenMigrationGates): onefindof at most 100 rows per object that declares a stored computed field, nothing for objects declaring none. When the sample holds a hole, it warns once, naming eachobject.field (n of the first m rows)and the command. It writes nothing, and a read or compute failure is logged at debug and never fails the boot. A column added to an object with rows is NULL on every row, so the first page finds it.os migrate stored-expressions(packages/cli, new, discovered by oclif's pattern strategy, so no registration file):--object(repeatable),--field(needs--object),--changed,--apply,--yes,--json,--force,--max-records,--database-url; the occupancy gate, confirmation, read-only dry-run boot and SQLite journal-mode note of its siblings. Exits 1 while a row is skipped, a write failed, or--max-recordstruncated the walk.content/docs/deployment/cli.mdx's data-migration table.Tests
Measured at
c82c1c7f(branch merged withorigin/mainonce before this PR). Head72809e52adds only the changeset'sClause-②line on top of it.packages/objectql/src/stored-expression-backfill.test.ts(new, 29 tests): a realObjectQLover an in-file driver. Dry run reports and writes nothing;applyfills holes, keepsupdated_at, and a re-run finds 0; stale values are counted by default and written withchanged; a faulted row that holds a value is skipped and listed; a refused write is isolated to its row;fieldnarrows the walk; the three refusals; an unreadable object; the boot advisory (holes found, none found, a read failure that never throws); asameStoredValuetable.packages/cli/src/commands/migrate/stored-expressions.test.ts(new, 10 tests, integration project): flags reach the walk,--fieldneeds--object,--applywithout--yeswrites nothing and exits 1, an undeclared object is refused, exit 1 on skipped / failed / truncated, and the dry-run face closes with the report's line.migrate-confirmation-exit.pin.test.ts(the new command in the confirmation roster) andfederated-injected-column-readers.test.ts(the newupdated_atreader skips an unprovisioned federated column).if (!apply) continue;guard turns 5 backfill tests red; restore verified blob == HEAD.Acceptance notes
mainthe roll-up backfill (summary-backfill.ts) has no boot notice (the lint'sisAlwaysValuedSummarydocblock says "Nothing backfills a stored NULL automatically"). The in-tree boot-advisory shape isannounceOpenMigrationGatesatkernel:bootstrapped, which this follows.--changed. No boot can tell that an expression changed (the stored value is not a function of the declaration alone, and anow()-reading expression always differs), so the advisory does not report stale values; every run of the command counts them and its report says to re-run with--changed.yes (widening). The dispatch order proposedno. Measured: new published@objectstack/objectqlexports (the stored-expression backfill functions and types), one new public methodObjectQL.computeStoredExpressionsOver, and a new CLI commandos migrate stored-expressions; nothing narrows. Declared as feat(objectql,cli): recompute stored home-currency amounts at the rates in force now, report first, rows without a rate listed and skipped (multi-currency M2-3) #30183 declared the same shape; the changeset carries the same line from72809e52.Metadata-protocolstaysnone.idare not seen by the advisory; the command walks every row.Verification
Reproduction on SQLite and memory, before and after
An object with 4 rows, booted; stored computed fields then declared (a doubled number, a date offset, a boolean, a text, a scale-2 decimal) and booted again over the same file. Before: 4/4 rows NULL in each new column on both drivers.
os migrate stored-expressionsdry run reported the holes and wrote nothing;--applywrote the expected values (qty_double2/4/6/8,offset-5/-5/-5/null for a row with an empty input); a re-run found 0 holes and 0 stale. Changing an expression toqty*3then showed 4 stale in default mode,--changed --applyrewrote them, and a re-run found 0.Suites, typechecks, lint and gates
Human Minimum
packages/specholds no runtime logic. (packages/specuntouched.)pnpm lintpasses.main, one pull request.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.