Skip to content

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

Closed

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. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 6, 2026
  4. 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

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 6, 2026
  6. self-assigned this
    on Aug 6, 2026
  7. 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

  8. 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

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions