test(driver-sql): D-A3 温度矩阵按 driver 轴参数化——server-timezone 轴与「Postgres at minimum」第一次真的执行 (#4245) - #4713
Merged
Conversation
…, 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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Contributor
Author
|
顺带发现已单开为 #4714(unassigned,不在本 PR 修): Generated by Claude Code |
os-zhuang
marked this pull request as ready for review
August 2, 2026 20:47
This was referenced Aug 2, 2026
Merged
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 3, 2026
… the driver axis (objectstack-ai#4714) (objectstack-ai#4735) objectstack-ai#4245 (PR objectstack-ai#4713) put the D-A3 driver axis under the temporal matrix and left a reusable `live-dialect-matrix.testkit.ts` behind. Two other consumers of the shared `@objectstack/spec/data` matrices in this package were still pinned to one engine: - `sql-driver-pagination-conformance.test.ts` — `client: 'better-sqlite3'` in both describes, over `PAGINATION_CASES` / `PAGINATION_UNORDERED_CASES`; - `sql-driver-or-filter.test.ts` — same client, over `FILTER_LOGIC_CASES`, with the describe named `(SQLite)`. The pagination one is the costly pin. Its own head note says the property half proves nothing on SQLite: twelve rows come back in rowid order every time, so the partition check passes with or without the tie-breaker. That is a local fact about one engine, stated as a permanent excuse — on a real server the property is the half with teeth, which is what objectui#3106 was reported as. Both files now sweep once per cell of `DIALECT_CELLS` over the same cases, asserting the same row-id sets cell for cell. Measured on a live PG 16 @ Asia/Shanghai and MariaDB 10.11 @ +08:00 under TZ=America/New_York: 46 tests per cell, all three cells identical. With `paginationTieBreaker` sabotaged to return null, the live-postgres cell reports 11 distinct ids over a 12-row walk — one row served twice, another never — while every sorted case on SQLite stays green. The cell exists and it bites. - Table names carry the issue prefix (`os4714_*`); the bare `t` / `task` this suite used while SQLite-only would collide with a parallel suite on a shared live database and read as a conformance failure. - `declareUnprovisionedCell` moves into the testkit and the temporal file now calls it too, so one definition of the non-vacuity guard serves all three matrices: missing URL is a named skip, and a red under `OS_EXPECT_LIVE_DIALECT_MATRIX=1` (objectstack-ai#4646). - No server-timezone axis here: nothing in these matrices compares an instant, so requiring a non-UTC server would only manufacture reds that say nothing about pagination or `$or`. - The unpaged-unordered read keeps its exact-sequence pin on SQLite only. objectstack-ai#4363 promises the driver adds nothing to that shape, not that the server returns a particular order — MySQL hands back InnoDB primary-key order and Postgres its heap order, and pinning either would assert the server's plan as our contract. The contract half (no ORDER BY emitted) is asserted per dialect. - No new CI job: `Temporal Conformance (live PG + MySQL)` already runs the whole package against both servers. `@objectstack/spec/data` is consumed, not edited: no case was softened and no tie-breaker was added to the fixture to keep a dialect green. Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4245
问题
ADR-0053 D-A3 把矩阵声明在
driver {SQLite, Postgres at minimum}上,D-B3 又在它上面加了一条 server-timezone 轴——因为 D-B2 实测到过方言分歧的行结果:一个裸YYYY-MM-DDcomparand 在 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 是看不见的,四个字面量能和一句写反的头注共存两个月就是证据。Field.time、legacy storage),跑同一批TEMPORAL_CASES/TEMPORAL_TIME_CASES,断言逐格相同的expected行 id 集合。OS_EXPECT_LIVE_DIALECT_MATRIX=1把这条 skip 变成红——这正是该 job 已经用node -e为时区堵上的那个假绿洞的 URL 半边:env:块要是哪天丢了,矩阵会悄悄退回 SQLite-only 并保持全绿。backfillCanonicalDatetimes自己就写着「there is nothing on disk to rewrite」并提前 return)。每个 cell 现在拿 driver 自己的needsLegacy*Repair规则去核对这个声明,所以哪天某个方言长出了 repair path,缺的那一格会自己喊出来,而不是继续隐形。验证(真实输出,不是推断)
本机起了 PG 16 @
Asia/Shanghai与 MariaDB 10.11 @+08:00,进程TZ=America/New_York:矩阵文件单独跑:SQLite-only
63 passed | 2 skipped(两条具名 skip 就是未 provision 的 PG / MySQL 格),三方言141 passed。pnpm --filter @objectstack/driver-sql typecheck干净,eslint 干净。每条守卫都做了 sabotage 验证(守卫本身能被证伪才算守卫):
TZ=UTCthe process runs at UTC (server=pg:Asia/Shanghai (480 min), process=UTC (0 min)) — re-run with a skewed zone…timezone=UTCthe live postgres server runs at UTC … on UTC the D-B2 divergence is invisible and this cell proves nothingTZ=Asia/Shanghai(与服务器同区)the live postgres server and the process share one UTC offset (480 min / 480 min)OS_EXPECT_LIVE_DIALECT_MATRIX=1但无 URLOS_TEST_POSTGRES_URL is unset while OS_EXPECT_LIVE_DIALECT_MATRIX=1 …hasLegacyStorageForm谎报为truedate/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_CASES与FILTER_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