Skip to content

fix(objectql): engine.transaction hardening — silent degrade, default-driver-only, and no owned-vs-joined signal (ADR-0118 D1 caveats) #4619

Description

@os-zhuang

ADR-0118 D1 declares transaction on IObjectQLEngine and writes its caveats into the contract TSDoc as declared meaning rather than hidden behaviour. Declaring them is not fixing them. These are the three, recorded while scoping #4612.

All three are in ObjectQL.transaction(), packages/objectql/src/engine.ts:4934-4973.

1. Silent degrade when the driver has no beginTransaction (:4952-4954)

if (!drv?.beginTransaction) { return callback(baseContext); }

The callback runs with no transaction and no rollback, and the caller cannot tell. Every in-tree driver implements beginTransaction (it is required on IDataDriver), so this path exists for test doubles and foreign engines — but a caller asking for a transaction and silently not getting one is the failure mode that made batchData's atomic flag a lie for as long as it was (ADR-0118 D4). Proposal: warn-once, or an explicit opts.require: true that throws instead of degrading. ADR-0118 D4's batchData gate is the fail-closed pattern to generalize.

2. Default-driver-only (:4950)

const drv = this.drivers.get(this.defaultDriver) as any;

Objects mapped to another datasource via setDatasourceMapping are written outside the transaction with no diagnostic, so a multi-datasource "atomic" write partially commits. Refusing loudly when the operations inside a transaction span drivers would be a real improvement over today's silent partial commit; genuine cross-driver atomicity is a much larger design question (two-phase commit) and is explicitly not what this asks for.

3. No owned-vs-joined signal (:4946-4948)

Per ADR-0067 D2 a nested transaction() joins the ambient transaction and returns without owning commit/rollback. That is correct, but the caller cannot distinguish "I own this transaction" from "I joined someone else's". It matters for any caller whose response claims durability: batchData's rolled-back response (ADR-0118 D4) asserts prior ops were undone, which is only locally guaranteed when it owns the transaction. Today's callers are all top-level so the claim holds; nothing enforces that it keeps holding. Proposal: transaction(cb, base, opts?) surfacing an owned signal to the callback.

Refs: ADR-0118 D1, ADR-0034, ADR-0067 D2, #4612.

Activity

  1. self-assigned this
    on Aug 4, 2026
  2. removed their assignment
    on Aug 4, 2026
  3. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    退回队列(PM 循环第 1 轮,会话 session_01Pbu27iNUfQCHeuS551Rqo7):本单曾短暂认领,因维护者加急 #5038(批量写按行语义,同在 packages/objectql/src/engine.ts)而让位 —— 同文件严格串行,本单待 #5038 落地后下轮再派。

    给下一次派发的已知事实(本轮核查所得,避免重查):

    • transaction 的契约声明在 packages/spec/src/contracts/objectql-engine.ts:229,其 TSDoc 明写两条 caveat(default-driver-only、无 beginTransaction 时静默降级)是声明语义的一部分(ADR-0119 D1)。因此本单三点中,凡要收紧行为(opts.require 抛错、跨驱动拒绝、owned 信号进签名)都动 spec 契约,归 spec 车道;engine 车道可做的是不改契约的可发现性修复(降级 warn-once、事务内写路由到非默认驱动时的响亮诊断)。派发时按此拆定范围,或与 spec 车道协同。

    Generated by Claude Code

  4. self-assigned this
    on Aug 6, 2026
  5. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮追加批(engine-core 车道,维护者指令并发提至 5)
    会话:session_01V7WetGmnfoXNn8cLieKKmx
    分支:claude/issue-4619-transaction-observability
    Worktree:objectstack-issue-4619
    域:domain:engine-core
    范围:按 2026-08-04 退回评论的既知事实拆定 —— 仅 engine 侧不改契约的可发现性半边:①无 beginTransaction 降级时 warn-once;②事务内写被路由到非默认驱动时响亮诊断。⛔ 收紧行为的半边(opts.require 抛错、跨驱动拒绝、owned 信号进签名)动 packages/spec/src/contracts/objectql-engine.ts:229 的声明语义(ADR-0119 D1),归 spec 车道,本单不做 —— PR 用 "Part of #4619",落地后 PM 走跨座位转移把契约半边立到 spec 队列。
    文件面:packages/objectql/src/engine.ts(仅 transaction() 方法区)+ 测试、.changeset/*.md。⚠️ 同文件 #5504 在飞(insert/update 返回区),区域不相交,后落地者合 main 重验。


    Generated by Claude Code

  6. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    复核通过,ACCEPT(engine-core 车道 PM,第 2 轮追加批):engine 侧可发现性半边交付于 PR #5724("Part of",本 issue 待 spec 半边 #5696 完成后一并收口)。CI 绿后转 ready 入队。

    交付:行为一字未改,两条诊断按 AGENTS.md 降级日志判据逐条论证级别(降级 warn:什么都还没丢;跨数据源 error:声称原子的写已独立落盘);引擎 transaction() 与沙箱 ctx.api.transaction 共用同一对 helper,共享「只说一次」额度;TransactionScope 纯记录不参与路由。反向验证 10/14 红 + 似是而非实现探针证明 4 条反向对照不是幽灵检查 —— 这个双层验证形状值得写进范本。与 #5504 同文件但区域相距 250-400 行(方法开头 vs 返回路径),实测无需串行。

    重要连带发现(已按查重纪律评论至既有 #5351,未立重单):跨数据源写入实测拿到的是另一连接的事务句柄(buildDriverOptions 无条件穿 ambient trx),比契约 TSDoc 写的「在事务外写入」更糟;已用独立 in-memory 双数据源复现并在测试中如实钉住(标注「观测到,非认可」)。两条排序结论:(a) #5351 的方案 A 前置(txStore 记录属主 driver)已随本 PR 落地;(b) #5351 派发时应把 PR #5724 列为前序,其裁决落地后本 PR 的 error 语义需随之调整。


    Generated by Claude Code

  7. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    收口(执行席 PM):三点全部落地,本单关闭(completed)。

    契约与语义修订常驻 ADR-0119 / ADR-0067 的 2026-08-06 Amendment 段。两件独立跟进不属于本单三点、不拖住关闭:#6167(不可归属句柄的同源校验收口,需动 IDataDriver 契约)、#6168(ScopedContext.transaction 不 join ambient)。跨驱动原子性/两阶段提交是本单原文明确排除、被裁掉的方向 —— 不是被推迟,不欠。

    执行席 PM session_019Q7oc7ASjh8yxyS3Yz78We。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions