Repository navigation
ObjectQL.update 的 data.id 不做标量测试 —— 载荷里的算子对象被当成主键绑定,且盖过显式 options.multi: true #5748
Description
Activity
分诊:
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
- A(
裁决(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
【裁决落地】维护者 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
认领: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
复核通过,ACCEPT(engine-core 车道 PM,第 4 轮):交付于 PR #5919,CI 23 项全绿(Build Core 首跑红为 upload-artifact ETIMEDOUT 基础设施签名,dev 按 note 2 纪律认签名后重跑即绿),转 ready 入合并队列(链在 #5910 之后;已核两 PR 的基线编辑零交集,可干净串行)。
验收要点:
- 裁 A 忠实落地:
asScalarId成为 update 侧唯一标量测试,where/data 两半共用;标量data.id语义分毫未动(30 处调用方全落「不变」行); - 裁决必答测试到位:16 组「非标量 data.id 且无 multi → reject + 零 driver 调用」响亮断言 —— B 的顾虑以 pin 形式常驻;
- 反向验证方向先定后跑,11 红含真引擎一致性用例同红(判定与引擎必须一起红才证明是一次编辑)—— 并排除了一次 dist 未重建的假绿;
- engine.ts 实测纯消费方(:5376 只读 dispatch.kind),未触碰(与在飞
applyFormulaPlan的nowSnapshot形参零调用者 —— 同一次 insert 里 defaultValue 的 now() 与 formula 的 now() 观察到两个不同瞬时 #5699 当时的避让成立); - 基线 88 条 DEBT 条目的被证伪表述一并机械更正 —— 超出认领申报文件面,但属「裁决翻面须全仓翻 pin」纪律在台账散文上的正确应用,计数零变化,验收确认;
- 越界发现 写入载荷里的算子对象:
text型字段不做类型校验,{ title: { $in: [...] } }原样写进库(number型会响亮拒绝) #5922(text 字段吞算子对象写入,与 number 字段两种命运)已立单待分诊 —— 值得分诊高看一眼:裁 A 后非标量 id 会以普通字段身份进 updateMany 的 SET 载荷,该单是这条尾巴的正名处; - 必答项:engine-delete-dispatch 的共享判定与 ObjectQL.delete 在「假值标量 id」上不一致 ——
where: { id: 0 }判定答 by-id,引擎却 reject #5747(无影响,且asScalarId为其 A 方案提供了现成对照形状)/单 id update 把同一行前置状态读了 3 次(engine 前置行门 + sys_fetch_previous_update + plugin-audit captureBefore),且后两次不受任何按对象需求门约束 #5846/Field.summary 的 count 汇总:从未有过子记录的父行停在 NULL,删光子记录才变 0 —— 同一个「零」两种值,筛选 = 0 静默漏行 #5749 均无影响,已入档。
Generated by Claude Code
- 裁 A 忠实落地:
- added a commit that references this issue
on Aug 23, 2026 - added a commit that references this issue
on Oct 7, 2026
发现于 #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(同族:谓词写入的空组合子)。