Skip to content

fix(objectql): 写路径回包水合 formula 虚拟列,与 GET 等价 (#5504) - #5698

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5504-write-response-formula-hydration
Aug 6, 2026
Merged

fix(objectql): 写路径回包水合 formula 虚拟列,与 GET 等价 (#5504)#5698
os-zhuang merged 2 commits into
mainfrom
claude/issue-5504-write-response-formula-hydration

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5504

问题

applyFormulaPlan 只有两个调用点 —— find 结果与 findOne 结果。写路径回包原样返回入库文档,而 formula 是虚拟列,任何 driver 都不会为它返回一列,所以 POST /data/:objectPATCH /data/:object/:idrecord 里这些键整个缺失(不是 null),紧接着的 GET 却每个都有值。

失效方向恰好是最难察觉的那一种:回包自称 record,消费方会直接拿来渲染 —— HotCRM 里 account / product / case / campaign / quote / knowledge_article / forecast / lead / contact 的 nameField 全部指向 formula,于是「创建后显示标题」一律是空,必须再多一次 GET 才能拿到派生值。而且缺失是静默的:键不存在,容易被当成「字段没配」误排查。

改动

engine.insertengine.update 通过一个共享的水合点,把 formula 虚拟列求值到各自的回包上。刻意复用读路径的同一个 plan 构建器同一套求值(planFormulaProjection(schema, undefined) 就是 find 的无投影分支),不新造写路径方言;执行上下文按 find 现在的方式穿透,os.user / os.org 两侧解析一致(扩展 context 承载的内容是 #1979 的工作,本单不碰)。

求值对象是 driver 已经返回的整行(createRETURNING *,update 有回读),因此不需要额外往返,也不存在对半行求值的问题。

覆盖面由落点决定,不靠逐个调用点枚举:单条 insert、批量 insert、insertMany / createManyData / insertManyData、以及单条 update,都经过每个动词唯一的那个水合点。谓词(multi)update 维持原状 —— driver.updateMany 解析出的是受影响行计数、不指名任何行,没有 record 可以物化;这一点用测试钉住,免得后来的读者把「没接」误读成漏掉的调用点。

落点的两侧顺序(都有测试钉住)

packages/rest 无生产码改动

POST/PATCH handler 原样透传协议层结果,拿到的就是水合后的记录。但「这一层不用改」是一个关于的断言,唯一诚实的证明方式是驱动它:rest-write-response-formula.test.ts 用真实 ObjectQL + 真实 ObjectStackProtocolImplementation + 已注册路由走一遍 issue 的原始复现形状,这样将来若有人在回包上加投影/白名单/序列化而丢掉虚拟列,失败点在那里,而不是悄悄把本 issue 重新打开。

反向验证(方向先判,后测)

这是新增水合,不属于「倒置」或「诊断变多」两个家族,预判就是普通的 RED:把两处水合删掉后,断言这些键存在的 pin 必须转红。

实测 objectql 侧 15 红 / 3 绿,3 个绿全部是刻意留的对照组 —— 谓词 update 的计数契约、strict 拒绝、以及无 formula 对象不求值 —— 三者断言的都是本改动不触碰的行为。REST 侧 3 个全红。

首轮反向验证曾出现 5 绿,多出来的 3 个是假绿:测试用的内存 driver 从 find/findOne 直接返回了 store 里的引用,而读路径的水合是就地改写 driver 返回的行 —— 于是第一次 GET 就把 display_title 写进了自己的 store,后续写回包跟着「有值」,删掉写路径水合也照样通过。这正是 driver-memory 用整段注释写明的契约(「Return shallow copies, never live references into the backing table」)。改成返回副本后假绿消失,反向验证才可信。

顺带修正的两处过期注释

本改动把「写结果不带 formula 字段」这个事实反转了,有两处别的包的注释还在陈述旧事实(纯注释,无逻辑改动):

验证


Generated by Claude Code

claude added 2 commits August 6, 2026 00:33
… on reads (#5504)

`applyFormulaPlan` had exactly two call sites — the `find` result and the
`findOne` result — so `POST /data/:object` and `PATCH /data/:object/:id`
answered with the stored document, in which a `formula` field is not `null`
but ABSENT (formulas are virtual; no driver returns a column for one). The
next `GET` of the same row carried every one of them: read-your-write broken
in the direction hardest to notice, since the response calls itself `record`
and consumers render it directly. Every object whose `nameField` points at a
formula rendered blank until a second round-trip.

`engine.insert` and `engine.update` now hydrate through one shared helper that
reuses the read path's plan builder and evaluation — same formula semantic on
both verbs, no write-path dialect. It evaluates over the row the driver already
returned (`create` uses `RETURNING *`, `update` re-reads), so there is no extra
round-trip and no formula sees a partial record.

Coverage falls out of the placement rather than being enumerated per call site:
single insert, batch insert, insertMany / createManyData / insertManyData and
single-id update all pass through one hydration point per verb. A predicate
(`multi`) update is unchanged — `driver.updateMany` resolves to an affected-row
count and names no row.

Ordering is pinned on both sides: after the same-day write-path strips and
refusals (#5503 runtime-owned autonumber, #2948 readonly, #5126
strictReadonlyWrites), and before the afterInsert/afterUpdate dispatch, which
mirrors the read path's applyFormulaPlan → afterFind order.

`packages/rest` needed no production change; its end-to-end test proves the
handlers pass the hydrated record through, so a future reshaping of the write
response fails there instead of silently reopening this.

Also corrects two comments in other packages that asserted the now-inverted
fact ("the write result does not carry formula fields"): plugin-audit's
computed-field exclusion is keyed on the field TYPE and is unaffected, and
trigger-record-change's re-read still earns its keep for summary/rollup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 12:56am

Request Review

@github-actions github-actions Bot added the size/l label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/plugin-audit.

15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-audit)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-audit)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/plugin-audit)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/plugin-audit)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants