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

… 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

vercelBot commented Aug 6, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 6, 2026 12:56am

Request Review

@github-actions

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-zhuang@claude