Skip to content

test(driver-sql): D-A3 温度矩阵按 driver 轴参数化——server-timezone 轴与「Postgres at minimum」第一次真的执行 (#4245) - #4713

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4245-temporal-matrix-driver-axis
Aug 2, 2026
Merged

test(driver-sql): D-A3 温度矩阵按 driver 轴参数化——server-timezone 轴与「Postgres at minimum」第一次真的执行 (#4245)#4713
os-zhuang merged 1 commit into
mainfrom
claude/issue-4245-temporal-matrix-driver-axis

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4245

问题

ADR-0053 D-A3 把矩阵声明在 driver {SQLite, Postgres at minimum} 上,D-B3 又在它上面加了一条 server-timezone 轴——因为 D-B2 实测到过方言分歧的行结果:一个裸 YYYY-MM-DD comparand 在 PG 16 @ Asia/Shanghai 上意味着服务器时区的午夜,同一个 query、同一个 instant,落在了与 SQLite 不同的日历天上。

这两条轴对矩阵都没有执行过sql-driver-temporal-conformance.test.ts 的四个 describe 全部硬编码 client: 'better-sqlite3':66:114:157:197;本单派发时正文记的是三处,第四处是 #4205 之后新增的),也从不读 OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL;而它自己的头注写着「against real Postgres and MySQL too, with no workflow change needed」——declared ≠ enforced,与 #4191 记的 mongodb-temporal.ts 同species。真正连 live server 的三个套件断言的是各自的存储形态 / NOW() 默认值 / 往返精度,不是 TEMPORAL_CASES 的行 id 集合,所以「一个后端漂了就有一条具名用例红」这个信号对 PG / MySQL 根本不存在。

改了什么

  • live-dialect-matrix.testkit.ts(新):把 driver 轴写成数据(DIALECT_CELLS)——SQLite 恒有,PG / MySQL 在各自 URL 存在时加入——外加服务器时区探测与三方错开断言。一份需要显式 opt-out 的格子清单,而不是每个套件各自埋一个字面量:硬编码的 client 是看不见的,四个字面量能和一句写反的头注共存两个月就是证据。
  • 四个 sweep 全部按 cell 参数化(canonical datetime、relative token、Field.time、legacy storage),跑同一批 TEMPORAL_CASES / TEMPORAL_TIME_CASES,断言逐格相同expected 行 id 集合。
  • 非空转守卫:每个 live cell 断言「服务器 ≠ UTC、进程 ≠ UTC、服务器 ≠ 进程」三方错开;缺 URL 时是一条具名 skip(不是静默 pass),而 CI job 里新增的 OS_EXPECT_LIVE_DIALECT_MATRIX=1 把这条 skip 变成红——这正是该 job 已经用 node -e时区堵上的那个假绿洞的 URL 半边env: 块要是哪天丢了,矩阵会悄悄退回 SQLite-only 并保持全绿。
  • legacy-storage 两个 sweep 仍只在 SQLite 上跑,但理由从字面量变成了断言:pre-canonical 存储形态只能存在于 SQLite(PG/MySQL 存的是真温度类型,backfillCanonicalDatetimes 自己就写着「there is nothing on disk to rewrite」并提前 return)。每个 cell 现在拿 driver 自己的 needsLegacy*Repair 规则去核对这个声明,所以哪天某个方言长出了 repair path,缺的那一格会自己喊出来,而不是继续隐形。
  • 头注改写成代码实际做的事(本单验收第 3 条)。

验证(真实输出,不是推断)

本机起了 PG 16 @ Asia/ShanghaiMariaDB 10.11 @ +08:00,进程 TZ=America/New_York

TZ=America/New_York OS_TEST_POSTGRES_URL=... OS_TEST_MYSQL_URL=... OS_EXPECT_LIVE_DIALECT_MATRIX=1 \
  pnpm --filter @objectstack/driver-sql test
 Test Files  59 passed (59)
      Tests  752 passed (752)

矩阵文件单独跑:SQLite-only 63 passed | 2 skipped(两条具名 skip 就是未 provision 的 PG / MySQL 格),三方言 141 passedpnpm --filter @objectstack/driver-sql typecheck 干净,eslint 干净。

每条守卫都做了 sabotage 验证(守卫本身能被证伪才算守卫):

sabotage 结果
TZ=UTC the process runs at UTC (server=pg:Asia/Shanghai (480 min), process=UTC (0 min)) — re-run with a skewed zone…
PG 重启到 timezone=UTC the live postgres server runs at UTC … on UTC the D-B2 divergence is invisible and this cell proves nothing
TZ=Asia/Shanghai(与服务器同区) the live postgres server and the process share one UTC offset (480 min / 480 min)
OS_EXPECT_LIVE_DIALECT_MATRIX=1 但无 URL OS_TEST_POSTGRES_URL is unset while OS_EXPECT_LIVE_DIALECT_MATRIX=1 …
把 pg 的 hasLegacyStorageForm 谎报为 true ❌ PG 直接拒收:date/time field value out of range: "1785160800000" / invalid input syntax for type time: "0"

第一条 sabotage 当场测出守卫自己的一个洞:expect(x).not.toBe(0) 走的是 Object.is,而 Object.is(-0, 0)false,取负的 getTimezoneOffset() 在 UTC 下正好产出 -0,于是 TZ=UTC 一开始是绿的。已用 eastOfUtc-0 折叠成 +0 修掉,并把这次实测写进注释。

红了吗

**没有。**三个方言在 141 条用例上给出完全相同的行 id 集合——这是把格子建起来之后的实测结论,不是放宽断言换来的(上表证明断言会咬人)。本地 MySQL 腿用的是 MariaDB 10.11(#3942 当年的验证方言),CI 的 Temporal Conformance (live PG + MySQL) job 会在 MySQL 8.0 上再跑一遍。

范围

packages/plugins/driver-sql/** + .github/workflows/ci.yml 一行 env。packages/spec/** 零改动(共享用例表未动,本单只是让消费者按 ADR 声明的轴去消费它)。

顺带发现(已按 Prime Directive #10 单开 unassigned issue,不在本 PR 修):PAGINATION_CASESFILTER_LOGIC_CASES 两条共享矩阵的 driver-sql 消费者仍钉死 SQLite,是同一形状——而 pagination 那个文件的头注自己就写着 SQLite 上这半边「would pass the day someone deletes the feature」。

关联

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


Generated by Claude Code

…, not four SQLite drivers (#4245)

ADR-0053 D-A3 declares the conformance matrix over `driver {SQLite, Postgres at
minimum}`, and D-B3 added a server-timezone axis after D-B2 measured a
dialect-divergent ROW RESULT on PG 16 @ Asia/Shanghai: a bare `YYYY-MM-DD`
comparand 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.

Neither axis existed for the matrix. `sql-driver-temporal-conformance.test.ts`
hard-coded `client: 'better-sqlite3'` in all four of its sweeps and never read
`OS_TEST_POSTGRES_URL` / `OS_TEST_MYSQL_URL`, while its own head note claimed it
ran "against real Postgres and MySQL too, with no workflow change needed" — the
declared-not-enforced shape #4191 recorded for `mongodb-temporal.ts`. The three
live suites that DO connect assert their own storage forms / NOW() defaults /
round-trip precision, not the shared `TEMPORAL_CASES` row-id sets, so the
matrix's "one backend drifts, one named case goes red" signal did not exist for
PG or MySQL at all.

- `live-dialect-matrix.testkit.ts`: the driver axis as data (`DIALECT_CELLS`) —
  SQLite always, live PG and MySQL when their URL is provisioned — plus the
  server-timezone probe and the three-way skew assertion. A cell list you opt
  out of, rather than a literal per suite.
- All four sweeps run once per cell over the same `TEMPORAL_CASES` /
  `TEMPORAL_TIME_CASES`, asserting the same `expected` row-id sets cell for cell.
- Non-vacuity: each live cell asserts server ≠ UTC, process ≠ UTC and server ≠
  process, so identical answers cannot be answers no timezone could perturb; an
  unprovisioned cell is a NAMED skip, and `OS_EXPECT_LIVE_DIALECT_MATRIX=1` (set
  by the Temporal Conformance job) turns that skip into a red, closing the URL
  half of the vacuous-pass hole the job's `node -e` closes for the zone.
- The legacy-storage sweeps stay SQLite-only because a pre-canonical storage
  form can only exist there — and that is now ASSERTED per cell against the
  driver's own `needsLegacy*Repair` rule, so the day a dialect grows a repair
  path the missing grid says so instead of staying invisible.
- Head note rewritten to describe what the code does.

Verified against a live PG 16 (`Asia/Shanghai`) and MariaDB 10.11 (`+08:00`)
under `TZ=America/New_York`: 141 matrix tests green on all three dialects, whole
package 752/752. Each guard sabotage-checked: UTC process, UTC server,
process==server, missing URL under OS_EXPECT_LIVE_DIALECT_MATRIX=1, and a cell
falsely claiming a legacy storage form all go red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
@vercel

vercel Bot commented Aug 2, 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 2, 2026 8:18pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd tests tooling labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql.

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

  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-sql)

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

顺带发现已单开为 #4714(unassigned,不在本 PR 修):PAGINATION_CASES / FILTER_LOGIC_CASES 两条共享矩阵的 driver-sql 消费者仍钉死 better-sqlite3,与本单同形状。本 PR 留下的 live-dialect-matrix.testkit.ts 正好是那两格的现成范式。


Generated by Claude Code

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

Labels

ci/cd documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants