Skip to content

ObjectQL.update 的 data.id 不做标量测试 —— 载荷里的算子对象被当成主键绑定,且盖过显式 options.multi: true #5748

Description

@os-zhuang

发现于 #5480(把 update 的三分支派发抽成 engine-update-dispatch.ts 时逐行核对生产者语义),PR 见该单。属 PD #10 的范围外发现;#5480 是行为保持的重构,已把这条语义原样抄进判定并在模块头 / 测试里写明,没有在那单里改。

事实(origin/main @ 488b66c,已实测)

ObjectQL.update(object, data, options) 取 id 的两步是不对称的:

于是 update(o, { id: { $in: ['a','b'] }, title: 'x' }, { multi: true }) 走的是按 id 分支:算子对象被原样交给 driver.update(object, id, data, options) 当主键,显式声明的 multi: true 被无声忽略。

实测(记录型 driver 驱动真实引擎,packages/objectql):

✓ FINDING B: an operator object in data.id is bound as a primary key, outranking multi:true
   expect(calls).toEqual(['update'])   // 实际就是 ['update'],不是 ['updateMany']
Tests  1 passed

为什么值得记

  1. 与 where.id 侧的判定自相矛盾。 同一个引擎方法,同一个算子对象,写在 where.id 里被正确识别为谓词(不加 multi 直接 reject),写在 data.id 里却被当成主键。这正是 sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434 / 测试替身比真实实现宽松:四个缺陷因此带着绿灯发布——需要一条把替身钉在真实契约上的闸门 #4550 记录的「看着像 id 其实是谓词」那一半 —— 只是发生在载荷侧,而所有既有防线(scalarDeleteId / scalarUpdateId / 门禁)都只看 where。
  2. 声明的批量意图被无声吞掉。 flow 的 delete_record / update_record 无法表达批量意图 —— 节点 schema 无键、执行器不传 options.multi,谓词批量写对所有 flow 平台级不可达,而节点描述符宣称支持 #5393 刚给 flow 的 update_record 补了 multi 批量意图键,flow-multi-write-unfiltered 不判空组合子:filter: { $and: [] } + multi: true 是整表删除,却零告警 —— 身份归约在 producer 侧有三份,lint 侧不该再抄第四份 #5659 也在追同族的「谓词写入无告警」。调用方明确写了 multi: true 却拿到一次按 id 写,属于 declared ≠ enforced 的一种:声明在,执行时被更早的一条规则盖掉,且没有任何诊断。
  3. 后果不是数据被覆盖,而是静默失灵 / 难读的驱动错误。 SQLite 侧把对象绑进主键位置会直接报参数绑定错误;别的驱动可能只是匹配零行。两种都不会告诉调用方「你的 multi 被忽略了」。

可达性:data 由调用方拼装,flow 的 update_record 把用户字段直接铺进载荷;AI 生成的元数据把 id 写进字段集合是完全可能的形状(PD #12 的老问题:宽松的消费者正是 AI 生成的元数据错误藏身的地方)。

建议动作

按 contract-first 在生产者侧定:

  • A(推荐):data.id 也过标量测试 —— 非标量的 data.id 不算 id,于是 { id: { $in: [...] } } + multi: true 落到 updateMany,不带 multi 则落到 reject 并给出现有的那条消息。与 where.id 侧一致,消除同一方法内的两套规则。
  • B:非标量 data.id 响亮拒绝(专门的错误消息,而不是复用 Update requires an ID or options.multi=true),因为把算子对象写进 data.id 大概率是作者写错了位置,静默改道去 updateMany 会把一次「写错地方」变成一次真的批量写。

两者都需要先扫一遍现有调用方(update(o, { id, ...fields }) 的按 id 写法非常常见且完全合法 —— 那里的 id 是标量,A/B 都不影响),再定是否要 ENGINE_UPDATE_DISPATCH_CASES 的用例翻面。

⚠️ 一旦修,必须两个文件一起改:packages/objectql/src/engine.ts 的 update 取 id 处,和 packages/objectql/src/engine-update-dispatch.ts 的 resolveEngineUpdateDispatch。#5480 之后这已经是一次编辑而不是两次 —— 判定就是生产者自己用的那一份,engine-update-dispatch.test.ts 会用真实引擎逐例对照,任何一侧单独改都会在那里红。现有的两条钉子写明了当前语义,修的时候连同它们一起翻面:

  • data.id outranks where and multi, and is NOT scalar-tested (the producer's rule, verbatim)
  • ENGINE_UPDATE_DISPATCH_CASES 里的 data.id wins over an explicit multi:true

关联:#5480(发现来源)、#4434 / #4550(「看着像 id 其实是谓词」家族)、#5393(update_record 的 multi 批量意图键)、#5659(同族:谓词写入的空组合子)。

Activity

  1. claude commented on Aug 6, 2026

    @claude
    Contributor

    分诊:needs-user-decision(产品语义拍板),域 domain:engine-core(落点 packages/objectql)。

    落点与前提复核(origin/main 889ae47):packages/objectql/src/engine.ts 的 update 取 id 处(hookContext.input.id 一族,5518/5563 行)与 engine-update-dispatch.ts 的 resolveEngineUpdateDispatch 为同一份判定(#5480 之后是一次编辑,不是两次)。where.id 走标量测试、data.id 不做任何测试且先于 where 与 options.multi —— 前提成立。

    为什么进决策箱而不是直接入队:A/B 两条路都同意现状是错的(算子对象被当主键绑定、显式 multi: true 被无声吞掉),分叉在替代方案,而两者的数据安全后果方向相反:

    • A(data.id 也过标量测试):非标量 data.id 不算 id ⇒ { id: { $in: [...] } } + multi: true 落到 updateMany,与 where.id 侧规则统一;
    • B(响亮拒绝):非标量 data.id 给专门的错误消息 —— 理由是把算子对象写进 data.id 大概率是写错了位置,而 A 会把一次「写错地方」静默变成一次真的批量写。

    这是「统一规则」与「不把作者的笔误升级成批量写」之间的取舍,属产品裁定,不由 dev 自选。两条路都要求先扫一遍现有调用方(update(o, {id, ...fields}) 的按 id 写法常见且合法 —— 那里的 id 是标量,A/B 均不影响),并决定 ENGINE_UPDATE_DISPATCH_CASES 的用例是否翻面。

    严重性(为什么值得占一个决策位):可达性真实 —— data 由调用方拼装,flow 的 update_record 把用户字段直接铺进载荷,AI 生成的元数据把 id 写进字段集合是完全可能的形状。后果不是覆盖数据而是静默失灵:SQLite 侧报难读的参数绑定错误,别的驱动可能只匹配零行,两种都不会告诉调用方「你的 multi 被忽略了」。#5393 刚给 flow 的 update_record 补了 multi 批量意图键,声明却会被更早的一条规则盖掉 —— declared ≠ enforced 的一种。

    查重(三仓 open issue + open PR:data.id / scalarUpdateId / resolveEngineUpdateDispatch):无同题单;命中均指向发现来源 #5480 / PR #5754。相邻:#4434 / #4550(「看着像 id 其实是谓词」的 where 侧半边)、#5659(谓词写入无告警)。

    ⚠️ 裁决后晋级时须一并处理的依赖:实施要同时改 engine.ts 的 update 取 id 处与 engine-update-dispatch.ts 的 resolveEngineUpdateDispatch,而 #5480 的 PR #5754 尚未合并且正动这两个文件 —— 晋级 pm:queue 时请在正文补一行 Blocked-by: #5480,不要在 #5754 在飞期间派发。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 6, 2026

    @claude
    Contributor

    裁决(2026-08-06):方案 B——非标量 data.id 响亮拒绝。

    ObjectQL.update 的 data.id 为非标量(算子对象等)时拒绝并给专门错误消息,不做标量测试改道;标量 data.id 按主键路由的现状不动。理由:载荷里的算子对象没有任何合法业务形状,响亮报错优于把「写错位置」静默升级成真批量写(方案 A 的隐患);与「错误响亮优于静默改道」既有口径一致,三轴同向。

    落点:packages/objectql/src/engine-update-dispatch.ts(前置 PR #5754 已合并,阻塞解除)+ 翻两个钉子测试(engine-update-dispatch.test.ts 与 ENGINE_UPDATE_DISPATCH_CASES)。本裁决与 #5574 的按行契约裁决同写一份 bulk-write ADR 附录。量级 S。

    经办:PM 会话 session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。


    Generated by Claude Code

  3. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:

    裁 A——data.id 同过标量测试,与 where.id 统一:{$in} 等算子对象 + multi:true 落 updateMany,规则一处定义两处复用。

    实现必答项:

    • B 的顾虑转化为显式测试——「非标量 data.id 且无 multi:true」的行为(笔误场景)必须有响亮断言,不许静默升级成批量写;
    • 开工先扫仓内调用方 + 翻 ENGINE_UPDATE_DISPATCH_CASES 钉子,行为差异逐条记录。

    前提更新:分诊要求的 Blocked-by #5480 已解除(08-06 05:26Z closed),裁决即达即可派。

    流转:摘 needs-user-decision → pm:queue,归 engine-core 车道。

    评估与落地会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

  4. self-assigned this
    on Aug 6, 2026
  5. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    认领:PM 循环第 4 轮(engine-core 车道;维护者 2026-08-06 已裁 A,10:39Z 裁决为准)
    会话:session_019Q7oc7ASjh8yxyS3Yz78We
    分支:claude/issue-5748-update-data-id-scalar-test
    Worktree:objectstack-issue-5748
    域:domain:engine-core
    文件面:⚠️ 落点已随 PR #5871 迁移 —— packages/metadata-core/src/engine-update-dispatch.ts(共享判定 + ENGINE_UPDATE_DISPATCH_CASES)+ packages/objectql/src/engine-update-dispatch.test.ts(真引擎钉子,留在 objectql)+ .changeset/*.md。⛔ 不碰 packages/objectql/src/engine.ts(在飞 #5699)与 metadata-protocol 面。

    裁决依据:10:39Z 维护者批复裁 A(data.id 同过标量测试,与 where.id 统一;{$in} + multi:true 落 updateMany),B 的顾虑转必答测试 ——「非标量 data.id 且无 multi:true」的行为必须有响亮断言,不许静默升级成批量写。08:28Z 的裁 B 评论已被该批复取代。


    Generated by Claude Code

  6. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    复核通过,ACCEPT(engine-core 车道 PM,第 4 轮):交付于 PR #5919,CI 23 项全绿(Build Core 首跑红为 upload-artifact ETIMEDOUT 基础设施签名,dev 按 note 2 纪律认签名后重跑即绿),转 ready 入合并队列(链在 #5910 之后;已核两 PR 的基线编辑零交集,可干净串行)。

    验收要点:


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions