Skip to content

ADR-0053 D-A3:共享矩阵的 driver-sql 消费者被钉死在 SQLite —— server-timezone 轴与 D-A3 自己写的「Postgres at minimum」都从未执行过 #4245

Closed

Description

@os-zhuang

从 #4191 收尾时核实 server-timezone 轴现状发现。ADR 里那句话点了两条轴,storage-form 已由 #4205 / #4223 / cloud#938 / cloud#942 落地,server-timezone 这条的现状比「只是隐含在 CI 配置里」更差。

D-A3's conformance matrix should gain a storage-form axis (canonical, legacy-epoch, legacy-naive) and a server-timezone axis, since both are now known to have produced dialect-divergent row results.
—— docs/adr/0053-date-and-datetime-semantics.md,D-B3 的 Consequences

现状(已逐条核对,不是推测)

packages/plugins/driver-sql/src/sql-driver-temporal-conformance.test.ts 是共享矩阵的 driver-sql 消费者。它的三个 describe 全部硬编码 client: 'better-sqlite3'(:63、:112、:155),也从不读 OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL。真正连 live server 的只有三个专用套件:

  • sql-driver-datetime-postgres-timezone.test.ts
  • sql-driver-time-live-dialects.test.ts
  • sql-driver-date-now-default-live.test.ts

它们断言的是各自的存储形态 / NOW() 默认值 / 往返精度,不是 TEMPORAL_CASES / TEMPORAL_TIME_CASES 的行 id 集合。

于是逐轴现状是:

轴 矩阵是否执行
进程时区偏斜(job 的 TZ=America/New_York) ✅ 覆盖——整包在该 TZ 下跑,矩阵也在内
服务器时区(PG Asia/Shanghai、MySQL +08:00,job 已provision) ❌ 矩阵根本没连服务器
driver 轴(D-A3 原文 driver {SQLite, Postgres at minimum}) ❌ 对矩阵不成立

附带:一处 declared ≠ enforced

该文件头注写着:

Runs against a real SQLite, and — through the Temporal Conformance (live PG + MySQL) CI job, which runs this package's whole suite under TZ=America/New_York against servers on Asia/Shanghai — against real Postgres and MySQL too, with no workflow change needed.

前半句成立(整包确实在该 job 下跑、进程时区确实偏斜),后半句对本文件不成立:矩阵里的 driver 永远是 SQLite。这与 #4191 记的 mongodb-temporal.ts canon 表同species——文档写对了、代码没实现,直到有人去执行它才可见。

为什么这格值得补,而不只是记账

服务器时区已知造成过方言分歧的行结果,这正是 ADR 点名它的原因:

A bare YYYY-MM-DD comparand (what a {30_days_ago} token expands to) meant midnight in the SERVER's timezone, so the identical query over the identical instant put a row on a different calendar day than it did on SQLite.
—— D-B2,对 PG 16 @ Asia/Shanghai 的实测

D-B2 的修复(comparand 规则方言无关、两侧都写明 Z)有回归锁,但那是它自己的夹具。矩阵的意义恰恰是「一个后端漂了就有一条具名用例红」——而 PG/MySQL 在矩阵里没有格子,所以这个信号对它们不存在。这与 #4081 follow-up 的教训逐字相同:NativeSQLStrategy 曾在矩阵的后端表里被列名却没有消费者。

建议形状

  1. 把矩阵消费者按 driver 参数化,而不是新写一份。三个 live 套件已有现成范式:OS_TEST_*_URL 存在则跑、缺失则 skip,并断言服务器非 UTC 以防空转。让 sql-driver-temporal-conformance.test.ts 对 [sqlite, postgres?, mysql?] 各跑一遍同一批 TEMPORAL_CASES / TEMPORAL_TIME_CASES,行 id 集合逐格相同。
  2. server-timezone 成为显式轴:断言「服务器时区 ≠ 进程时区 ≠ UTC」三方错开确实生效(job 已经 provision 了 Asia/Shanghai / +08:00 与 America/New_York,缺的是矩阵去消费它并在非空转守卫下断言)。
  3. 修正头注,让它描述代码实际做的事——或者在第 1 步之后它自然成立。
  4. 顺带值得考虑:driver-turso(cloud)local 模式同样只跑 SQLite,但它是 SQLite-only 驱动,不适用;driver-sqlite-wasm 同理。这一格只对 driver-sql 有意义。

验收

同一批用例在 SQLite / Postgres / MySQL 上返回完全相同的行 id 集合,且三者的服务器时区与进程时区互不相同、非空转守卫生效。任何一格不同即该方言的 comparand 或存储路径与共识有偏差。

关联

#4191(storage-form 轴,已落地)、#4081(D-A3 本体)、#4205、#4223、#4220、cloud#938、cloud#942;ADR-0053 D-A3 / D-A3.1 / D-B2 / D-B3 / D-B4 / D-C3。

Activity

  1. self-assigned this
    on Aug 2, 2026
  2. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    🔒 认领

    • 会话:session_015Br2xsJsczFsTR9bvbh2Ny(PM 循环,2026-08-02 19:50Z 轮)
    • 分支:claude/issue-4245-temporal-matrix-driver-axis
    • Worktree:objectstack-issue-4245

    ⚠️ 所有 agent 共用一个 GitHub 身份,assignee 无法区分认领人。若你读到本条且你的 session ID 不是上面那个,本单已被占,请另选。

    同时段协作提示:另一个 PM 会话在推进 #4535(packages/spec 双源清账)。本单不要碰 packages/spec/** —— 本轮分工上它整包归对方。本单的宇宙是 packages/plugins/driver-sql/** 与 CI workflow。


    PM 复核:前提成立,但范围比正文大一格

    对 origin/main @ bbb1192 实测:

    sql-driver-temporal-conformance.test.ts:66   client: 'better-sqlite3'
    sql-driver-temporal-conformance.test.ts:115  client: 'better-sqlite3'
    sql-driver-temporal-conformance.test.ts:158  client: 'better-sqlite3'
    sql-driver-temporal-conformance.test.ts:198  client: 'better-sqlite3'   ← 正文未提
    

    正文写的是三个 describe(:63/:112/:155),现在是四个(行号也整体下移)。第四个是本单提出后新增的,同样硬编码 —— 参数化必须覆盖全部四处,漏一处就是矩阵里留一个永远只跑 SQLite 的格子,而那正是本单要消灭的形状。开工请自行再核一遍,别照抄这里的行号。

    OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL 在该文件仍是零命中,live 三套件的 skip-guard 范式照旧可抄。

    边界

    • 本单是 测试与 CI 的一致性执行,不是改 temporal 语义。若参数化后 PG / MySQL 真的红了,那是发现,不是要在本单里修:按 Prime Directive chore: version packages #10 单开 issue 记录方言分歧,本单的交付是「格子存在且会红」,不是「格子是绿的」。这条很重要 —— 为了让矩阵全绿而放宽断言,等于把本单做成反面。
    • 非空转守卫是硬要求:缺 OS_TEST_*_URL 时 skip 必须可见,且连上后要断言服务器时区 ≠ 进程时区 ≠ UTC 三方错开确实生效。没有这条守卫,矩阵可以在「连上了但时区没错开」的情况下假绿。
    • 头注那句「against real Postgres and MySQL too, with no workflow change needed」在第 1 步落地后自然成真;若最终没能让它成真,就得改掉它,不能留着。

    Generated by Claude Code

  3. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    ✅ PM 验收通过 —— PR #4713,CI 全绿(20/20),入合并队列

    复核是对着 PR 本身做的,不是对着报告:

    范围(5 文件,全部在界内):新增 live-dialect-matrix.testkit.ts、参数化后的 sql-driver-temporal-conformance.test.ts、legacy-datetime-storage.testkit.ts 上 19 行的规则暴露、ci.yml 一行 env、一份 changeset。packages/spec/** 零改动(本轮硬约束,与另一 PM 的 spec 串行车道不冲突);⛔ content/docs/releases/ 未碰。changeset 是空 frontmatter + Releases nothing,test-only 的正确形态,Check Changeset 已绿。

    验收逐条:四处硬编码 client: 'better-sqlite3' 全部消灭(实测确为四处,不是正文写的三处);四个 sweep 按 DIALECT_CELLS 跑同一批 TEMPORAL_CASES / TEMPORAL_TIME_CASES 并断言逐格相同的行 id 集合;server-timezone 成了真正的 it()(assertThreeWayZoneSkew:服务器 ≠ UTC、进程 ≠ UTC、服务器 ≠ 进程);未 provision 的格是具名 skip,OS_EXPECT_LIVE_DIALECT_MATRIX=1 把它变红;头注改写成代码实际做的事。

    这次交付里最值钱的一处,不是参数化本身:legacy sweep 仍只在 SQLite 跑,但理由从注释升级成断言 —— 每个 cell 拿 driver 自己的 needsLegacy*Repair 规则核对 hasLegacyStorageForm。哪天某方言长出 repair path,缺的那一格会自己红着喊出来。这正是本单要消灭的「声明 ≠ 执行」在下一层的预防。

    守卫做了 sabotage 五连,其中第一条当场测出守卫自身的洞:expect(x).not.toBe(0) 走 Object.is,而 Object.is(-0, 0) 为 false —— 取负的 getTimezoneOffset() 在 UTC 下产出 -0,于是 TZ=UTC 原本是绿的。已用 eastOfUtc 折叠并复测转红。守卫自己能被证伪才算守卫,这一条做到了。

    结论:三方言在 141 条用例上行 id 集合完全一致 —— 没有方言分歧要另开单。 且这不是放宽断言换来的:上面五条 sabotage 证明断言会咬人。CI 的 Temporal Conformance (live PG + MySQL) 在 MySQL 8.0 上复跑亦绿(本地那条腿是 MariaDB 10.11)。

    顺带发现已按 Prime Directive #10 单开 #4714(未认领):PAGINATION_CASES / FILTER_LOGIC_CASES 两条共享矩阵的消费者仍钉死 SQLite,同一形状。那一单打开后 PG/MySQL 很可能真的红(无 tiebreaker 的分页在真服务器上会重排)—— 按其验收,交付的是「格子存在且会红」。已进队列候选,不搭本单顺风车。


    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