Skip to content

fix(objectql): by-id 更新不再把「已判定不是主键」的载荷 id 写进 SET 子句 (#6435) - #6475

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-6435-by-id-payload-id-strip
Aug 8, 2026
Merged

fix(objectql): by-id 更新不再把「已判定不是主键」的载荷 id 写进 SET 子句 (#6435)#6475
baozhoutao merged 2 commits into
mainfrom
claude/issue-6435-by-id-payload-id-strip

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #6435

updateby-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.update('task', 'rec_1', { "id": { "$in": ["a","b"] }, "title": "x" })
                                 ^^^^^^^^^^^^^^^^^^^^^^^^ 这是 SET 子句

driver-sqlupdate()整个 dataformatted(applyWriteColumnMap(formatInput(object, data)),id 不在任何跳过名单里),于是 SQL 形如 UPDATE task SET id = '{"$in":["a","b"]}', title = 'x' WHERE id = 'rec_1'——rec_1 的行标识被一个序列化的算子对象不可逆覆盖。

前提复核(动手前逐条实测)

前提 结论 证据
P1 by-id 臂仍把未清理载荷交驱动 ✅ 成立 packages/objectql/src/engine.ts 内容定位 driver.update(object, hookContext.input.id, hookContext.input.data, …)(#6467 落地后行号漂到 6115)。探针实测六种形状,见下表
P2 PR #6433 的 by-id 现状 pin 仍在钉着 ✅ 成立 engine-update-multi-payload-id.test.ts#6262 — the by-id path is untouched describe,基线 47 tests 全绿
P3 multi 臂剥离在 main 上原样 ✅ 成立 本 PR 未触碰 else if (options?.multi …) 分支一行;对照 pin 见下

P1 探针实测(改动前,options: { where: { id: 'rec_1' } }):

data.id 派发 绑定 id 交给 driver.update 的 data
{ $in: ['a','b'] } by-id rec_1 {"id":{"$in":["a","b"]},"title":"x"} ← 缺陷
['a','b'] by-id rec_1 {"id":["a","b"],"title":"x"} ← 缺陷
null by-id rec_1 {"id":null,"title":"x"} ← 缺陷
0 by-id rec_1 {"id":0,"title":"x"} ← 缺陷
'' by-id rec_1 {"id":"","title":"x"} ← 缺陷
'rec_9' by-id rec_9 {"id":"rec_9","title":"x"} ← 路线 A 刻意不动

与 PR #6433 的同构说明

同族同构,不发明第二套:

#6262 / PR #6433(multi 臂) 本 PR(by-id 臂)
落点 分支首条语句,在 encryptSecretFields / normalize / validateRecord 之前
动作 拷贝式剥离(const { id, ...rest }),不改写调用方对象
剥离集 到达该分支即全部(multi 意味着两处都没有真值标量 id) 载荷 id 不是真值标量的那一份;真值标量 data.id 不动
成员判定 分支本身即判定 调用派发去问:resolveEngineUpdateDispatch(data, undefined).kind !== 'by-id'
verdict 变更
观测通道 logger.warn,刻意不走 onFieldsDropped 同(理由同 #6437)

成员判定这一条是本 PR 唯一的形状差异,且是刻意的:by-id 臂里载荷 id 有合法的一种(真值标量 = 被绑定的主键),所以需要一个谓词。这个谓词不在这里重新推导——asScalarId 是故意不导出的(engine-update-dispatch.ts:"给同一个问题添第三种公开写法,正是一条规则长出第二条的方式"),手抄一份正是 #4434 / #4550 这一族存在的理由。于是改为调用生产者自己的裁决:"这份载荷单独拿出来,能不能标识一行?"

范围(⛔ 未越)

data: { id: null } 回写入口 —— 分诊列为"未验证形状",本 PR 给出的结论

这条落在剥离集内(派发阶梯:asScalarId(null)undefined ⇒ 落到 where.id ⇒ 绑定 where.id,载荷的 null 与算子对象同类),已同批剥离并有测试覆盖。

REST 层是否先剥 id:否——静态读源码得出(file:line),端到端 HTTP 复现:

  1. packages/rest/src/rest-server.tsPATCH /data/:object/:id(约 :5268)只从请求体里剥 expectedVersion,id 不动;
  2. packages/spec/src/api/protocol.zod.ts:519UpdateDataRequestSchemadata 声明为 z.record(z.string(), z.unknown()),null 通过校验;
  3. packages/metadata-protocol/src/protocol.ts:5636updateData 把请求体原样交给 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 untouched describe(PR #6433 写下时说"钉住,好让将来扩大剥离是一个刻意的动作"——本 PR 就是那个刻意的动作),三条中翻转一条:

用例 改动前断言 改动后断言 处置
a scalar data.id outranks multi:true …AS SENT { id: 'rec_1', title: 'x' } 不变 保留为对照 pin,补注为何刻意不动
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,其余不动。

用例 预测 实测
① PROBE:算子对象 data.id + 标量 where.id ✅ 红
② 数组 data.id ✅ 红
null data.id ✅ 红
② 假值标量 0 ✅ 红
② 假值标量 '' ✅ 红
P2 改写后的 pin(multi 文件) ✅ 红
⑤ 调用方 data 未被变异 绿(见下) ✅ 绿
③ 标量 data.id 两条对照 pin 绿 ✅ 绿
④ multi 臂两条对照 pin 绿 ✅ 绿
engine-update-dispatch.test.ts 全部 绿 ✅ 绿
合计 6 红 / 52 绿 6 failed / 52 passed

⑤ 的方向是"绿",不是模板预设的"红" —— 如实记录。 摘掉剥离后调用方对象同样没有被变异:本实现的剥离是拷贝式的,而 by-id 臂里 encryptSecretFields / normalizeMultiValueFields 对该 fixture 无改动,两个 readonly strip 在无掉落时返回同一引用。所以这条钉的是实现形状(将来若有人改用 delete data.id 就地改写会转红,#5591 / #6343 的教训),不是剥离的存在性。按"预测方向先写死"的纪律,这里预测的就是绿,实测也是绿。

命令输出

$ pnpm --filter @objectstack/objectql test
 Test Files  145 passed (145)
      Tests  2438 passed (2438)

$ pnpm --filter @objectstack/objectql typecheck
> tsc --noEmit           (无输出 = 通过)

$ pnpm --filter @objectstack/metadata-protocol test     # by-id 调用点(消费半径)
 Test Files  53 passed (53)
      Tests  552 passed (552)

$ pnpm --filter @objectstack/rest test                  # REST 入口(消费半径)
 Test Files  64 passed (64)
      Tests  881 passed (881)

$ pnpm check:engine-double-contract
check-engine-double-contract: OK — 80 pinned, 133 in the DEBT ledger, 4 exempt.

$ pnpm check:query-options-erasure
✓ query-options-erasure ratchet holds: 83 unswept non-test site(s) in 19 file(s), none new.
  test surface: 263 site(s) in 49 file(s) — at the ceiling.

$ pnpm check:type-check-debt     (先跑全量 build:70 tasks successful,§9 陷阱)
check-type-check-coverage: OK — 62/77 workspace packages type-checked …
  ℹ @objectstack/objectql: TEST_DEBT records 355, tsc now reports 353 (-2) -- the entry can be lowered.
check-type-check-coverage --re-measure: OK — 34 ledger entr(ies) re-measured, none above its recorded number.

$ node scripts/check-nul-bytes.mjs
check-nul-bytes: OK (scanned 6093 tracked text file(s); no raw ASCII control bytes).

$ npx eslint --no-inline-config (三个改动文件)    (无输出 = 通过)

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

claude added 2 commits August 8, 2026 01:12
…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
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 1:16am

Request Review

@github-actions github-actions Bot added the size/m label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Copy link
Copy Markdown
Contributor Author

范围外发现,已按 PD #10 单独立单,未在本 PR 内修改:#6479

读 REST/协议 ingress 核实本 PR 的 data: { id: null } 可达性时顺带看到的另一条:PATCH /data/:object/:id 里,请求体的真值标量 id 会压过路径 :id(#5748 裁 A 的载荷优先规则),于是存在性探测与 OCC 判在路径那一行、写却落到请求体那一行、响应的 idrecord 互相矛盾。

与本 PR 的边界:本 PR 只剥「派发已判定不是主键」的那份载荷 id,真值标量 data.id 按路线 A 刻意保持原状并钉了对照 pin——#6479 的落点在 REST/协议 ingress,不在引擎的 by-id 臂,严重度留给分诊裁。

CI:22 项检查全部完成,0 失败(Check Changeset / ESLint / TypeScript Type Check / Test Core 1-3 / Dogfood Regression Gate 1-3 / Build Core / Temporal Conformance 均绿)。


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review August 8, 2026 01:28
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit b7d3be4 Aug 8, 2026
25 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-6435-by-id-payload-id-strip branch August 8, 2026 01:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

update 的 **by-id** 路径同样把非标量 data.id 交给驱动写主键列(#6262 的孪生形状,where.id 胜出时)

2 participants