Skip to content

Commit 61122e8

Browse files
baozhoutaoclaude
andauthored
docs(objectql): RUNTIME_OWNED_FIELD_TYPES 的 summary 排除理由改为与实现相符 (#6014) (#6119)
`RUNTIME_OWNED_FIELD_TYPES` 上方的 TSDoc 把 `formula` 和 `summary` 并列成 「computed on read from a plan, never stored from the write payload」。formula 那半句对(`applyFormulaPlan` 读时按 plan 求值);summary 那半句是错的:roll-up summary 是实打实的物理存储列 —— `recomputeSummaries()` 用 `update(parent, { [summaryField]: value })` 写入,#5749 / PR #6013 之后 `initializeSummaryFields` 在 insert 时也落初值,读路径直读该列(#5749 的 「筛选静默漏行」正因为它是库内列)。 危险方向具体:下一个作者按注释字面「修正代码以匹配注释」,把 summary 加进集合, 带汇总初值的历史导入/种子写入就会被静默 strip。而且 strip 站点(engine.insert :5276)跑在 seed 之后、且只认原始 caller payload,所以 caller 送了 `task_count: 42` 时,42 被 strip 掉、0 也不会补(seed 正因为 caller 供了值而跳过), 列落回 null —— 恰是 #6013 要消灭的状态,且写入仍报成功。 改注释,不改行为:formula / summary 拆成两段,写明 summary 是 persisted + runtime-maintained 但故意不 strip,真实判据是第三条「没有合法的 caller 供值」—— autonumber 满足(伪造业务标识且无人纠正),summary 不满足(派生缓存,下次子表写入 自愈,且显式写初值是 #6013 明确支持的路径)。成员判据补成 (a)/(b)/(c) 三条。 未触及 `RUNTIME_OWNED_FIELD_TYPES` 集合本身,未触及任何行为代码。 Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We Co-authored-by: Claude <noreply@anthropic.com>
1 parent ae490ef commit 61122e8

1 file changed

Lines changed: 38 additions & 4 deletions

File tree

packages/objectql/src/validation/rule-validator.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -565,10 +565,44 @@ export function stripReadonlyWhenFieldsMulti(
565565
* ownership here makes it enforced rather than merely asserted — the same
566566
* `declared ≠ enforced` correction as #4447 (`created_at`), one type over.
567567
*
568-
* Deliberately NOT `formula` / `summary`: those are computed on read from a
569-
* plan, never stored from the write payload, so there is no caller value to
570-
* strip. Keep this set to types whose value is (a) persisted and (b) issued by
571-
* the runtime.
568+
* Deliberately NOT `formula`: a formula field IS computed on read from a plan
569+
* (`applyFormulaPlan`) and never stored from the write payload, so there is no
570+
* caller value to strip in the first place.
571+
*
572+
* Deliberately NOT `summary` either — but for a COMPLETELY DIFFERENT reason,
573+
* and conflating the two is what this note exists to prevent (#6014). A roll-up
574+
* `summary` is NOT computed on read: it is a real stored column the runtime
575+
* maintains. `ObjectQL.recomputeSummaries` writes it with an ordinary
576+
* `update(parent, { [summaryField]: value })` after any child write, and since
577+
* #5749 / PR #6013 `initializeSummaryFields` also seeds it at parent INSERT.
578+
* Reads hit that stored column directly — which is exactly why
579+
* `["task_count","=",0]` is an in-database comparison, and why a never-seeded
580+
* `null` silently dropped rows from it (#5749). So `summary` satisfies BOTH
581+
* clauses a naive membership rule would use — persisted AND runtime-issued —
582+
* and is STILL excluded. Persistence and runtime ownership do not decide it.
583+
*
584+
* What decides it is the third clause: a runtime-owned type must have NO
585+
* legitimate caller-supplied value. `autonumber` qualifies — a client-chosen
586+
* record number bypasses the sequence and forges a business identifier nothing
587+
* later corrects. `summary` does not qualify: the value is a derived cache of
588+
* the child aggregate, self-healing on the next child write, and supplying an
589+
* initial value is a SUPPORTED authoring path — `initializeSummaryFields`
590+
* deliberately keeps a caller-supplied one ("author supplied a value"), so
591+
* historical imports and seed data may carry pre-computed totals.
592+
*
593+
* DO NOT "fix the code to match this comment" by adding `summary` here. The
594+
* insert-side strip ({@link stripRuntimeOwnedFields}) keys on the RAW caller
595+
* payload and runs in `engine.insert` AFTER the seed pass, so a plain
596+
* (non-`isSystem`, non-`preserveAudit`) import of a parent carrying
597+
* `task_count: 42` would lose the 42 to the strip and get no 0 from the seed
598+
* either — the seed already skipped that field precisely BECAUSE the caller
599+
* supplied it. The column lands `null`, the exact state #6013 was written to
600+
* eliminate, and the write still reports success. Only `isSystem: true` (whole
601+
* pass skipped) or `preserveAudit: true` (kept by {@link isPreservableUnderAudit},
602+
* since a summary field is not `system: true`) would survive it.
603+
*
604+
* Keep this set to types whose value is (a) persisted, (b) issued by the
605+
* runtime, and (c) never legitimately supplied by a caller.
572606
*/
573607
const RUNTIME_OWNED_FIELD_TYPES: ReadonlySet<string> = new Set(['autonumber']);
574608

0 commit comments

Comments
 (0)