发现于 #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
为什么值得记
与 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。
声明的批量意图被无声吞掉。 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 的一种:声明在,执行时被更早的一条规则盖掉,且没有任何诊断。
后果不是数据被覆盖,而是静默失灵 / 难读的驱动错误。 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 (同族:谓词写入的空组合子)。
发现于 #5480(把 update 的三分支派发抽成
engine-update-dispatch.ts时逐行核对生产者语义),PR 见该单。属 PD #10 的范围外发现;#5480 是行为保持的重构,已把这条语义原样抄进判定并在模块头 / 测试里写明,没有在那单里改。事实(origin/main @ 488b66c,已实测)
ObjectQL.update(object, data, options)取 id 的两步是不对称的: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不做任何测试,只要为真就直接当 id 用,并且先于where、也先于options.multi。于是
update(o, { id: { $in: ['a','b'] }, title: 'x' }, { multi: true })走的是按 id 分支:算子对象被原样交给driver.update(object, id, data, options)当主键,显式声明的multi: true被无声忽略。实测(记录型 driver 驱动真实引擎,
packages/objectql):为什么值得记
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。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 的一种:声明在,执行时被更早的一条规则盖掉,且没有任何诊断。multi被忽略了」。可达性:
data由调用方拼装,flow 的update_record把用户字段直接铺进载荷;AI 生成的元数据把id写进字段集合是完全可能的形状(PD #12 的老问题:宽松的消费者正是 AI 生成的元数据错误藏身的地方)。建议动作
按 contract-first 在生产者侧定:
data.id也过标量测试 —— 非标量的data.id不算 id,于是{ id: { $in: [...] } } + multi: true落到updateMany,不带multi则落到 reject 并给出现有的那条消息。与where.id侧一致,消除同一方法内的两套规则。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(同族:谓词写入的空组合子)。