fix(objectql): by-id 更新不再把「已判定不是主键」的载荷 id 写进 SET 子句 (#6435) - #6475
Conversation
…e payload (#6435) The by-id half of #6262 / PR #6433. When `data.id` is a non-scalar (operator object, array, `null`) or a falsy scalar and `options.where.id` is a truthy scalar, `resolveEngineUpdateDispatch` correctly rules the payload value is not a primary key and binds `where.id` instead (#5748 / PR #5919). The dispatch was right; the PAYLOAD was never cleaned, so `driver.update(object, 'rec_1', data)` carried the ruled-not-an-id value into the SET clause and driver-sql wrote `UPDATE task SET id = '{"$in":["a","b"]}' WHERE id = 'rec_1'` — the row's identity overwritten irreversibly. Route A only: strip that payload `id`, on a copy, leaving a truthy scalar `data.id` exactly as it was (there the payload key IS the bound id — a same-value no-op). Zero dispatch verdicts change; membership is asked by calling the producer's own `resolveEngineUpdateDispatch`, never by re-deriving the unexported scalar test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…id-payload-id-strip
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
范围外发现,已按 PD #10 单独立单,未在本 PR 内修改:#6479 读 REST/协议 ingress 核实本 PR 的 与本 PR 的边界:本 PR 只剥「派发已判定不是主键」的那份载荷 CI:22 项检查全部完成,0 失败( Generated by Claude Code |
Fixes #6435
update的 by-id 臂把「派发已判定不是主键」的那份载荷id原样交给驱动写进 SET 子句。本 PR 把 #6262 / PR #6433 已经给 multi 臂的剥离语义,同构地带到 by-id 臂。缺陷
update(o, { id: { $in: ['a','b'] }, title: 'x' }, { where: { id: 'rec_1' } })的派发是对的,自 #5748 裁 A / PR #5919 起就是:算子对象不是主键,判定顺阶梯落到where.id,绑定rec_1——ENGINE_UPDATE_DISPATCH_CASES里就写着这一行(expect: 'by-id'/expectId: 'rec_1')。没做的是载荷那一半。origin/main 实测(记录型 driver 驱动真实引擎):driver-sql的update()用整个data出formatted(applyWriteColumnMap(formatInput(object, data)),id不在任何跳过名单里),于是 SQL 形如UPDATE task SET id = '{"$in":["a","b"]}', title = 'x' WHERE id = 'rec_1'——rec_1 的行标识被一个序列化的算子对象不可逆覆盖。前提复核(动手前逐条实测)
packages/objectql/src/engine.ts内容定位driver.update(object, hookContext.input.id, hookContext.input.data, …)(#6467 落地后行号漂到 6115)。探针实测六种形状,见下表engine-update-multi-payload-id.test.ts的#6262 — the by-id path is untoucheddescribe,基线 47 tests 全绿else if (options?.multi …)分支一行;对照 pin 见下P1 探针实测(改动前,
options: { where: { id: 'rec_1' } }):data.iddriver.update的 data{ $in: ['a','b'] }rec_1{"id":{"$in":["a","b"]},"title":"x"}← 缺陷['a','b']rec_1{"id":["a","b"],"title":"x"}← 缺陷nullrec_1{"id":null,"title":"x"}← 缺陷0rec_1{"id":0,"title":"x"}← 缺陷''rec_1{"id":"","title":"x"}← 缺陷'rec_9'rec_9{"id":"rec_9","title":"x"}← 路线 A 刻意不动与 PR #6433 的同构说明
同族同构,不发明第二套:
encryptSecretFields/normalize/validateRecord之前const { id, ...rest }),不改写调用方对象id不是真值标量的那一份;真值标量data.id不动resolveEngineUpdateDispatch(data, undefined).kind !== 'by-id'logger.warn,刻意不走onFieldsDropped成员判定这一条是本 PR 唯一的形状差异,且是刻意的:by-id 臂里载荷
id有合法的一种(真值标量 = 被绑定的主键),所以需要一个谓词。这个谓词不在这里重新推导——asScalarId是故意不导出的(engine-update-dispatch.ts:"给同一个问题添第三种公开写法,正是一条规则长出第二条的方式"),手抄一份正是 #4434 / #4550 这一族存在的理由。于是改为调用生产者自己的裁决:"这份载荷单独拿出来,能不能标识一行?"范围(⛔ 未越)
data.id现状不动:那里载荷的id就是被绑定的主键(标量data.id压过where与multi),写出来是SET id = 'rec_1' WHERE id = 'rec_1',同值空写,冗余而非破坏,且是长期行为。要不要一并剥是另一个决定,已按现状钉死为对照 pin。expect: 'by-id',属对 ObjectQL.update 的data.id不做标量测试 —— 载荷里的算子对象被当成主键绑定,且盖过显式options.multi: true#5748 裁 A 的部分回退,维护者专属。实施中未出现非 B 不可的证据。{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 / sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434 那一族。packages/metadata-core:ENGINE_UPDATE_DISPATCH_CASES一行未动;派发不变量在测试里用assertEngineUpdateDispatch(data, options)逐条核对。data: { id: null }回写入口 —— 分诊列为"未验证形状",本 PR 给出的结论这条落在剥离集内(派发阶梯:
asScalarId(null)为undefined⇒ 落到where.id⇒ 绑定where.id,载荷的null与算子对象同类),已同批剥离并有测试覆盖。REST 层是否先剥
id:否——静态读源码得出(file:line),非端到端 HTTP 复现:packages/rest/src/rest-server.ts的PATCH /data/:object/:id(约 :5268)只从请求体里剥expectedVersion,id不动;packages/spec/src/api/protocol.zod.ts:519的UpdateDataRequestSchema把data声明为z.record(z.string(), z.unknown()),null通过校验;packages/metadata-protocol/src/protocol.ts:5636的updateData把请求体原样交给engine.update(object, request.data, { where: { id } })。即客户端 GET 一条记录、改两个字段、整体 PUT 回来,而序列化把
id写成null,就落在这条臂上。端到端 HTTP 复现未跑,如实标注。pin 翻转清单
packages/objectql/src/engine-update-multi-payload-id.test.ts的#6262 — the by-id path is untoucheddescribe(PR #6433 写下时说"钉住,好让将来扩大剥离是一个刻意的动作"——本 PR 就是那个刻意的动作),三条中翻转一条:a scalar data.id outranks multi:true …AS SENT{ id: 'rec_1', title: 'x' }a scalar where.id …AS SENT{ title: 'x' }operator data.id BESIDE a scalar where.id{ id: { $in: ['a','b'] }, title: 'x' }{ title: 'x' }(并加断call.id === 'rec_1')[#6435]前缀,describe 重命名为…as this file left it and as #6435 changed it未删除任何旧断言而不留对应新断言。
反向验证(方向先写死,再实测)
摘除方式:把剥离条件里的
resolveEngineUpdateDispatch(...).kind !== 'by-id'换成false,其余不动。data.id+ 标量where.iddata.idnulldata.id0''data.id两条对照 pinengine-update-dispatch.test.ts全部⑤ 的方向是"绿",不是模板预设的"红" —— 如实记录。 摘掉剥离后调用方对象同样没有被变异:本实现的剥离是拷贝式的,而 by-id 臂里
encryptSecretFields/normalizeMultiValueFields对该 fixture 无改动,两个 readonly strip 在无掉落时返回同一引用。所以这条钉的是实现形状(将来若有人改用delete data.id就地改写会转红,#5591 / #6343 的教训),不是剥离的存在性。按"预测方向先写死"的纪律,这里预测的就是绿,实测也是绿。命令输出
TEST_DEBT 台账未抬(355 ⇒ 实测 353,只缩不抬;按 gate 说明未强行改写台账)。
文件面
packages/objectql/src/engine.ts—— 仅 by-id update 臂,+75 行(其中绝大部分是理由注释)packages/objectql/src/engine-update-by-id-payload-id.test.ts—— 新增,11 条用例packages/objectql/src/engine-update-multi-payload-id.test.ts—— 一条 pin 改写 + 两条补注.changeset/engine-update-by-id-payload-id-strip.md—— patch@objectstack/objectql关联:#6262 / PR #6433(multi 臂那一半)、#5748 / PR #5919(
data.id的标量判定)、#5480 / #5619(派发谓词的提取与下沉)、#6437(DroppedFieldsEvent.reason词表)、#5240 / #4434(为什么共享谓词而不是第二个答案)。Generated by Claude Code