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

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