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.tsresolveEngineUpdateDispatch#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(同族:谓词写入的空组合子)。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions