fix(metadata-core,objectql): update 的 data.id 同过标量测试,载荷里的算子对象不再被当成主键 (#5748) - #5919
Merged
Merged
Conversation
…atch, so a payload operator object is not bound as a primary key (#5748) `ObjectQL.update(object, data, options)` asked "does this call name one row?" of two places under two different rules: `options.where.id` went through a scalar test (an operator object / array / `null` is a multi-row predicate, not an id -- #4434 / #4550), while `data.id` was taken VERBATIM whenever truthy, ahead of both `where` and `options.multi`. So the same operator object was a predicate in `where` and a primary key in the payload: `update(o, { id: { $in: ['a','b'] }, title: 'x' }, { multi: true })` dispatched `by-id` with `{$in: [...]}` bound into `driver.update`'s primary-key position, and the caller's explicit `multi: true` was swallowed with no diagnostic -- declared != enforced, one layer under the `multi` intent key #5393 had just added to the flow `update_record` node. `data.id` now goes through the SAME scalar test as `where.id`, defined once as `asScalarId` and reached by both halves, so a non-scalar payload id names no row and stops shadowing the ladder below it: it falls through to `where.id`, then `multi`, then `reject`. A scalar `data.id` is untouched -- it still outranks `where` and `multi`, which is the common, legal `update(o, { id, ...fields })` spelling every in-repo caller uses. Ruled by the maintainer (2026-08-06) as option A over option B's "reject any non-scalar `data.id`". B's objection -- that an operator object in the payload is most likely a typo the author meant for `where`, and that A would promote it into a real bulk write -- is answered by the ladder rather than by a second error message: with no declared `multi`, a non-scalar `data.id` is the existing loud reject and nothing reaches the driver. That is pinned as its own test. `ENGINE_UPDATE_DISPATCH_CASES` gains an optional `expectId`, because the verdict alone cannot separate "picked an id" from "picked the RIGHT id": an operator `data.id` beside a scalar `where.id` is `by-id` before and after, and only the bound value says which source won. Reverse-verified: restoring the verbatim `data.id` read turns 11 of these red against the real engine. Fixes #5748 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…ate-data-id-scalar-test
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 6, 2026
baozhoutao
marked this pull request as ready for review
August 6, 2026 11:54
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Aug 6, 2026
baozhoutao
enabled auto-merge
August 6, 2026 12:21
…ate-data-id-scalar-test # Conflicts: # scripts/engine-double-contract.baseline.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5748
前提复核(先证再改)
issue 正文的两条事实在
origin/main上全部成立,但落点描述已过期 —— 正文写的是「必须两个文件一起改」(
packages/objectql/src/engine.ts+packages/objectql/src/engine-update-dispatch.ts),而 PR #5871(同日)已把共享判定沉到
packages/metadata-core/src/engine-update-dispatch.ts,objectql 的同名文件只剩 re-export shim。实测确认
engine.ts是纯消费方,没有残留取 id 逻辑:所以本 PR 没有动
engine.ts(在飞 #5699 同文件),也没有动 shim —— 判定改一处,engine.ts与全部 fake engine 自动跟随,这正是 #5480 抽取判定时要的效果。改了什么
ObjectQL.update(object, data, options)问「这次调用是否指名了一行」时看两个地方,而这两处此前用的是两套规则:
options.where.id走标量测试 —— 算子对象 / 数组 /null是多行谓词,不算 id(sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434 / 测试替身比真实实现宽松:四个缺陷因此带着绿灯发布——需要一条把替身钉在真实契约上的闸门 #4550);data.id不做任何测试,只要为真就原样当主键,且先于where、也先于options.multi。于是同一个算子对象,写在
where.id里被识别成谓词,写在data.id里却被绑进driver.update(object, id, …)的主键位置,而调用方显式写的multi: true被无声吞掉 —— declared ≠ enforced,而且就压在 #5393 刚给 flow
update_record补上的
multi批量意图键下面一层。现在
data.id与where.id共用同一个标量测试(asScalarId,模块内定义一次,两半都走它)。非标量
data.id不算 id,因此不再盖住任何东西:判定按where.id→multi→reject的原有阶梯继续往下走。行为差异逐条(FROM → TO)
标量
data.id的按 id 写法完全不受影响 —— 这是仓内每一个调用方用的写法(见下节扫描)。update(o, { id: 'rec_1', …f })'rec_1'update(o, { id: 'rec_1', …f }, { multi: true })'rec_1'data.id仍先于multi)update(o, { id: 'rec_1', …f }, { where: { id: 'rec_2' } })'rec_1'data.id仍先于where)update(o, { id: 0, …f }, { multi: true })0不标识行)update(o, { id: { $in: [...] }, …f }, { multi: true })update(o, { id: ['a','b'], …f }, { multi: true })update(o, { id: { $in: [...] }, …f })(无multi)update(o, { id: { $in: [...] }, …f }, { multi: false })update(o, { id: null, …f })null本就是假值)update(o, { id: { $in: [...] }, …f }, { where: { id: 'rec_1' } })'rec_1'最后一格是唯一「判定不变、绑定值变了」的一格。它也是这次改动里最容易被绿掉的一格:
前后都是
by-id,只看expect的用例两边都过。所以ENGINE_UPDATE_DISPATCH_CASES新增了可选的
expectId,把落进主键位的值本身钉住;真引擎侧的observeEngine也改成回报 recording driver 收到的那个 id,而不只是回报走了哪个分支。ENGINE_UPDATE_REJECT_MESSAGE(Update requires an ID or options.multi=true)、导出符号、类型签名均无变化。
裁决与必答项
维护者 2026-08-06 10:39Z 裁 A(
data.id同过标量测试,与where.id统一),取代 08:28Z 的裁 B 评论。
B 的顾虑 —— 「把算子对象写进载荷大概率是写错了位置,A 会把一次笔误静默变成一次真的
批量写」—— 不需要第二条错误消息来处置,阶梯本身就处置了:非标量
data.id落回阶梯后,和其他任何「没指名一行」的调用一样需要显式声明的
multi。没有multi就是现有的那条响亮 reject,且驱动一次都没被碰到。这条按裁决要求单独立为一条测试:
它对
{$in}/{$ne}/ 数组 /null×undefined/{}/{multi:false}/{where:{tenant}}共 16 组做笛卡尔断言(判定 +assertEngineUpdateDispatch抛错),再用真引擎断言
driver调用列表为[]。调用方扫描
grep -rn -E "\.update\([^)]*\{[^}]*\bid\s*:" packages/ examples/—— 30 处生产代码 +测试,每一处的
data.id都是标量(row.id/record.id/'rec_1'/userId…):plugin-auth/objectql-adapter.ts(4 处{ ...patch, id: record.id })、plugin-security/normalize-managed-by.ts、plugin-sharing/primary-bu-projection.ts、plugin-approvals、plugin-reports、plugin-email、metadata-protocol/seed-loader.ts、engine.ts自身的 roll-up / referential-integrity 回写等。全部落在上表「不变」行。可达性来自外部载荷:flow 的
update_record把作者字段直接铺进data(
crud-nodes.ts:409data.update(objectName, fields, { where: filter, multi })),REST 的 PATCH body 同理 —— AI 生成的元数据把
id写进字段集合是完全可能的形状(PD #12)。反向验证(方向先定,再跑)
预测:红。把
asScalarId(data.id)还原成data.id、重建metadata-core,新增/翻面的钉子应当全红 —— 而且不只是判定红,真引擎侧必须一起红,否则说明钉子
钉的是判定自己的复读而不是生产者。实测 11 条红,方向与预测一致:
恢复后 36/36 绿。
钉子的处置(翻面,不是删)
data.id outranks where and multi, and is NOT scalar-tested (the producer's rule, verbatim)→ 拆成两条,两条都继续承重:
a SCALAR data.id still outranks where and multi (…untouched by #5748)(旧语义里仍然为真的那一半,继续钉住)与
a NON-SCALAR data.id is not an id, so it no longer outranks a declared multi:true (#5748)(翻面的那一半)。ENGINE_UPDATE_DISPATCH_CASES的data.id wins over an explicit multi:true用的是标量'rec_1',新语义下判定不变 —— 所以它不是翻面项,而是「改名 + 加
expectId」:a SCALAR data.id still wins over an explicit multi:true,并钉住绑定值就是'rec_1'。真正翻面的是新增的非标量各例。
never binds a non-scalar into the primary-key position, on ANY case——对 CASES 全表跑真引擎,凡
by-id就断言落进主键位的值typeof∈ {string, number, bigint}。这条是把 issue 的伤害面(算子对象被绑成主键)直接钉成性质,而不是逐例列举。
顺带修正的过期表述
scripts/engine-double-contract.baseline.json的 88 条 update 切片 DEBT 条目里,每条都以同一句结尾:「…stricter on the one it invents (
data.id, which the producertakes verbatim when truthy, ahead of both
whereandmulti)」。这句描述的正是本 PR删掉的那条规则,留着就是 88 处对着读者说假话。统一替换成新的表述(一次机械替换,
逐字相同),门禁计数不变。
验证
metadata-protocol 的 13 个 fake engine 接线测试(
update()开头调assertEngineUpdateDispatch)全绿,无 fixture 命中非标量data.id形状,因此没有需要按新语义修正的期望值。
git merge origin/main(无 rebase)后重跑上述三个包 + 两个门禁,结果同上。🤖 Generated with Claude Code
https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
Generated by Claude Code