Skip to content

fix(metadata-protocol): deleteManyData has the same fake-atomic as batchData, updateManyData ignores atomic entirely #4620

Description

@os-zhuang

ADR-0118 D4 fixes batchData's atomic flag (it opened no transaction — it only broke the loop, stranding every prior write as committed). Its two siblings in the same file have the same class of defect and were out of that PR's confirmed scope.

1. deleteManyData — fake-atomic (packages/metadata-protocol/src/protocol.ts:5543)

Same break-without-rollback shape as pre-fix batchData; the comment there even says "Same stop semantics as batchData". Deletes performed before the failure stay deleted while the response is titled atomic — worse than the batchData case, because a partial delete has no natural undo.

2. updateManyData — ignores atomic entirely (protocol.ts:5405-5444)

The option is accepted and never read. A caller passing atomic: true gets best-effort with no signal.

Work

Reuse the two pieces ADR-0118 D4 introduces rather than re-deriving them: the extracted per-record loop helper, and the fail-closed capability gate (engine exposes transaction and, where the driver registry is inspectable, the default driver has beginTransaction; otherwise 501 NOT_IMPLEMENTED rather than a silent best-effort run). Apply the same response contract: a rolled-back batch reports zero successes, with rows marked ROLLED_BACK: / NOT_ATTEMPTED: so a client can tell which is which.

3. Second finding — per-row result shape diverges from its own schema

The implementation pushes { id, success, error: string, record } while BatchOperationResultSchema declares { errors: ApiError[], data }. ADR-0118 D4 deliberately kept the implementation shape (changing it there would have been a silent wire-compat break riding on a bug fix). Decide the direction on purpose: widen the schema to match reality, or migrate the implementation and version the response. Whichever way, the two should stop disagreeing.

Refs: ADR-0118 D4, #4612, ADR-0049.

Activity

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    认领:PM 循环第 3 轮
    会话:session_018iARDqtrhQgz6fVHDeDkbQ
    分支:claude/issue-4620-many-data-atomic
    Worktree:objectstack-issue-4620

    分诊:落地 objectstack(后端 packages/metadata-protocol),与 objectui / cloud 无关。

    批次独立性:与本轮在飞的 #4784(packages/objectql + skills/)文件面完全不相交,可并行,不串行等待。

    前提复核(issue 写于 08-02,行号已漂,逐条对着最新 main 重查)

    issue 引用的行号已经不准(deleteManyData 现在在 protocol.ts:5754,不是 5543;updateManyData 在 5661,不是 5405)。但两个缺陷都仍然成立,而且 issue 要求复用的两块料确实已经在 main 上:

    • deleteManyData 仍是假原子(protocol.ts:5799):if (options?.atomic) break; —— 只是跳出循环,没有任何回滚,失败前已删掉的行保持已删除,而响应仍以原子自居。issue 说这比 batchData 那次更糟,我同意:部分删除没有自然的撤销手段。
    • updateManyData 仍然完全不读 atomic(5661 起的整个方法体里 atomic 一次都没出现)。选项被接受、被忽略、无任何信号 —— 典型的 declared ≠ enforced。
    • 可复用的料在位:ADR-0119 D4 的原子臂(5338)、共享的逐记录循环(5408)、fail-closed 能力闸门(5382 抛 NOT_IMPLEMENTED)、以及 ROLLED_BACK: / NOT_ATTEMPTED: 的行标记(5563-5566)都已合入。派发时要求复用它们,不许再推导一套。

    派发范围:只含第 1、2 节,不含第 3 节

    第 1、2 节(两个 atomic 缺陷)直接派发,不问。理由:仓里已经把不变量写死了 —— ADR-0119 D4 刚给 batchData 定了原子语义的正确形状,ADR-0049 的 enforce-or-remove 也摆在那里。「同一个文件里的两个同胞有同一类缺陷」这个发现本身就带着它的结论,不需要再确认一次。

    第 3 节(逐行结果形状与 BatchOperationResultSchema 不一致)不派发:实现推的是 { id, success, error: string, record },schema 声明的是 { errors: ApiError[], data }。issue 自己写着「Decide the direction on purpose」—— 这是公开 wire 契约的形状决定,两个方向(放宽 schema 迁就现实 vs. 迁实现并给响应加版本)对已发布客户端的影响相反。ADR-0119 D4 当时刻意没动它,正是因为让它搭在一个 bug fix 上是静默的 wire 兼容性破坏。同样的理由,它也不该搭在这次修复上。已另开决策 issue 承接,见下条评论。


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    第 3 节(逐行结果形状与 BatchOperationResultSchema 不一致)已拆到 #4793,标 needs-user-decision 等维护者拍板。

    逐字核对后的分歧比 issue 描述的更具体:两个键名字对不上(error: string vs errors: ApiError[]、record vs data)、一个键实现从不发(index),而 droppedFields 反倒是对齐的。三个方案沿两轴分析完毕,建议 B(迁实现到 schema 形状)—— A(放宽 schema)我建议直接排除,它会让 schema 里同时躺着 error/errors 与 record/data,AI 照着声明写 result.errors[0].message 会静默拿到 undefined。真正需要维护者回答的是一个事实问题:仓外有没有已发布的消费方在读旧键 —— 有则 C(双发过渡),无则 B 直接切。

    本 issue 只跟第 1、2 节,dev agent 已派发。


    Generated by Claude Code

  3. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    PM 复核:返工已完成,通过(ACCEPT),等 CI 全绿后进合并队列

    PR:#4798(草稿,base main,body 首行 Fixes objectstack-ai/objectstack#4620,未触碰 content/docs/releases/)

    返工的那一处

    首轮 changeset 写的是 @objectstack/metadata-protocol: patch,与它自己援引的先例矛盾 —— .changeset/adr-0119-plugin-reachable-transactions.md(D4 给 batchData 做同一件事)是 minor。同一个包、同一类变更、同一种调用方可见后果(atomic: true 从「200 + 部分成功响应体」变成「抛 501」),不可能是 patch;而且包版本是 17.0.0-rc.1,patch 会发成 17.0.1,一个告诉消费方「什么都没变」的版本号。

    已修正并核对:changeset 现为 minor,返工提交 1f049c982 是一行 diff(1 file changed, 1 insertion(+), 1 deletion(-)),分支相对 main 的总 diffstat 仍是 4 文件 / 556 增 / 15 删,与首轮复核时逐字一致 —— 没有夹带任何其它改动。dev 也如实说明「changeset-only 编辑未重跑测试套件」,而不是把上一轮的输出当新结果报,这个诚实是对的。

    实现部分(首轮已核,记录在案)

    • 抽成一份而不是抄两遍:D4 的原子臂提成共享的 runAtomicBatch,batchData / updateManyData / deleteManyData 三个入口共用 —— 能力闸门、单个 engine.transaction()、abort 哨兵、零成功回滚响应都只存在一份。理由给得对:抄写正是下一个同胞漂移回谎言的路径。
    • 回滚是真回滚,钉的是状态不是响应体:假引擎是带快照/恢复语义的内存 store,测试在调用后读回存储断言 a、c 两行仍在。响应里的 succeeded: 0 是廉价的,行还在不在才是 atomic 一直在承诺的东西 —— 这一点本 issue 说得最重(部分删除没有自然撤销),测试钉在了正确的地方。
    • fail closed 而非静默降级:无 engine.transaction() 或默认 driver 无 beginTransaction → 501 NOT_IMPLEMENTED,且一次写都没发生。静默降级正是本 issue 的缺陷类别。
    • 克制得当:buildUpdateManyResponse 特意不复用 buildBatchDataResponse,避免顺手改掉非原子路径的 returnRecords 载荷;batchData 行为与其测试文件均未改动。
    • protocol.delete-many.test.ts 里那条 atomic aborts the remaining ids(断言 succeeded: 1)改成拒绝语义 —— 它钉的正是本次要修掉的缺陷,改它是对的,且注释写清了原委。

    第 3 节

    未触碰,已由 #4793 承接(等维护者回答一个事实问题:仓外有没有已发布消费方在读 result.error / result.record)。


    Generated by Claude Code

  4. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    结案:PR #4798 已合入 main(07:46Z),issue 由 Fixes 自动关闭

    落地的是第 1、2 节:deleteManyData 的假原子与 updateManyData 完全不读 atomic,现在都走真事务。

    • atomic: true → 整批跑在一个 engine.transaction() 里,首个失败回滚此前全部写入;
    • 回滚批次报告零成功,行标 ROLLED_BACK: / NOT_ATTEMPTED:,致因行保留原始错误;
    • 无法回滚的运行时 → 501 NOT_IMPLEMENTED,而不是静默降级成 best-effort —— 静默降级正是本 issue 的缺陷类别;
    • 三个入口(batchData / updateManyData / deleteManyData)共用抽出的 runAtomicBatch,而不是把 D4 的原子臂再抄两遍。抄写正是下一个同胞漂移回谎言的路径。

    关键验证是状态而非响应体:回滚测试在调用后读回存储,断言 a、c 两行仍可读 —— succeeded: 0 是廉价的,行还在不在才是 atomic 一直在承诺的东西。

    一轮返工:首版 changeset 标 patch,与它自己援引的先例(adr-0119-plugin-reachable-transactions.md,D4 给 batchData 做同一件事标的是 minor)矛盾。已改 minor —— 包版本 17.0.0-rc.1,patch 会发成 17.0.1,一个告诉消费方「什么都没变」的版本号。

    CI 插曲:Test Core (1/2) 曾红一次,死在 @objectstack/spec 的 tsup DTS build 且无任何输出。本 PR 不碰 packages/spec,判定为瞬态并重跑 —— 重跑后 20 项全绿,判定成立。(相关:#4796 记录了 spec 侧一条会随机把不相干 PR 踢出合并队列的 flaky,归 spec 车道。)

    第 3 节未触碰,已由 #4793 承接(方案 B 已由维护者拍板:迁实现到 schema 形状、硬切、changelog 诚实写明升级路径)。⚠️ 按维护者的 PM 车道裁定,#4793 属协议线,不由本会话派发;本 PR 合入后它的前置已清空。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions