Skip to content

test(driver-sql): give ttl onlyWhen $null live pg + mysql legs through the existing cell testkit - #10989

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-10836-ttl-onlywhen-live-legs
Aug 22, 2026
Merged

test(driver-sql): give ttl onlyWhen $null live pg + mysql legs through the existing cell testkit#10989
os-elon merged 2 commits into
mainfrom
claude/issue-10836-ttl-onlywhen-live-legs

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#10836

packages/drivers/driver-sql/src/sql-driver-ttl-onlywhen-null-dialects.test.ts measured the
Reaper-shaped where { expires_at: { $lt: cutoff }, revoked_at: { $null: true } }live on
sqlite but compile-only on pg and mysql2 — a hand-rolled
new SqlDriver({ client: 'pg', connection: {} }), .toSQL(), string assertions, connection never
opened. It was the only dialect suite in that directory hand-rolling connection: {}, and the only
$null dialect suite in the package.

Compile-only proves the SQL text. It cannot prove the server returns those rows, and it never
meets the column type each dialect actually creates — which is exactly where these two dialects part
company with sqlite.

This extends that file (no new file, so no second engine-double-contract pin) with live legs through
the PG_CELL / MYSQL_CELL testkit the sibling dialect suites already use. Convergence onto
machinery the repo already has
, not new machinery. The compile-only assertions stay — they run with
no server.

What the live legs assert, per dialect

legwhat it pins
temporal columncolumnInfo() reports a real temporal column, not the TEXT sqlite stores
candidate readthe ISO-Z cutoff selects sess_expired; the filter spares sess_tombstone (find + count)
the sweepdeleteMany deletes exactly 1 — survivors ["sess_live","sess_tombstone"]
UNFILTERED controldrop onlyWhen and the same sweep deletes 2 — survivors ["sess_live"]

Taxonomy, named: these are coverage-extension controls, not defect controls. Nothing was broken —
the filer measured this by hand and it held on both servers, and it still holds. There is no red
pre-fix tree to point at, so what earns the legs is the UNFILTERED control plus the ablation below.

The two ordinary rows sit only four hours either side of the cutoff on purpose. A zone leak on
these servers is ±8h and ±4/5h from a skewed process, so if the stored instant and the ISO-Z comparand
were ever folded through different clocks, sess_expired and sess_live would swap sides and the
fixture would go red. A day-wide margin would absorb exactly the defect the live legs exist to expose.

Measured, on real servers

Provisioned in the dev container — the filer's exact versions:

PostgreSQL 16.13 (Ubuntu 16.13-0ubuntu0.24.04.1), timezone = Asia/Shanghai
MariaDB 10.11.14-MariaDB-0ubuntu0.24.04.1, @@global.time_zone = +08:00,
sql_mode = STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
process TZ = America/New_York (all three clocks disagree: server / process / UTC)

columnInfo() types observed — they match the filed numbers exactly:

dialectexpires_atrevoked_at
pgtimestamp with time zonetimestamp with time zone
MariaDBdatetimedatetime
sqlite (existing leg)TEXTTEXT

Run at 419c627992, with OS_EXPECT_LIVE_DIALECT_MATRIX=1 — so a cell that had silently skipped would
have been a red, not a green:

✓ ttl onlyWhen {$null} on live pg (#10836) > stores the ttl field in a REAL temporal column …
✓ ttl onlyWhen {$null} on live pg (#10836) > the ISO-Z cutoff selects the expired row …
✓ ttl onlyWhen {$null} on live pg (#10836) > the sweep deletes exactly the expired row …
✓ ttl onlyWhen {$null} on live pg (#10836) > UNFILTERED control: drop onlyWhen …
✓ ttl onlyWhen {$null} on live mysql (#10836) > … (same four)
Test Files 1 passed (1) Tests 13 passed (13)

The skip is announced, not silent

With both URLs absent, declareDialectCell (the total form — a provisioned cell is measured, an
unprovisioned one is declared un-run, with no third outcome the caller can reach) emits a named
skip:

↓ sql-driver — ttl onlyWhen {$null} reap scope matrix (live postgres)
> is provisioned — set OS_TEST_POSTGRES_URL to run this cell of the D-A3 driver axis
↓ sql-driver — ttl onlyWhen {$null} reap scope matrix (live mysql)
> is provisioned — set OS_TEST_MYSQL_URL to run this cell of the D-A3 driver axis
Tests 5 passed | 2 skipped (7)

An un-run cell that reports nothing is the same defect class as the compile-only gap being closed here.

Ablation — the live legs are falsified on BOTH dialects

Predicted direction, stated before the run: red. Dropping onlyWhen from the three live-leg call
sites (the card's own UNFILTERED control, applied to the filtered assertions):

× live pg > the ISO-Z cutoff selects the expired row …
→ expected [ 'sess_expired', 'sess_tombstone' ] to deeply equal [ 'sess_expired' ]
× live pg > the sweep deletes exactly the expired row … → expected 2 to be 1
× live mysql > the ISO-Z cutoff selects the expired row …
→ expected [ 'sess_expired', 'sess_tombstone' ] to deeply equal [ 'sess_expired' ]
× live mysql > the sweep deletes exactly the expired row … → expected 2 to be 1
Tests 4 failed | 9 passed (13)

The tombstone gets reaped on both servers — the filter is what spares it. A live leg that stayed green
with the filter removed would not be testing the filter.

src/dist reasoning for this ablation: the mutation is confined to the .test.ts file itself, and
the subject enters through the relative '../src/index.js', which vitest resolves to source — no
dist/ participates in what was mutated, so no rebuild leg applies. (driver-sql's registered
dist-resolved deps are @objectstack/formula, @objectstack/observability, @objectstack/types;
none was touched.) Restoration proven on disk: git hash-object == git rev-parse HEAD:PATH
(7eb670e77020ddc751a4db114490fca9ce8f1b51), git diff --exit-code 0, porcelain empty.

⚠ Finding: the card's CI premise is FALSE — these legs DO run in CI

The card deliberately deferred this work with "CI provisions no live servers, so a committed live test
adds no CI protection today."
That is not true of this package..github/workflows/ci.yml
temporal-conformanceTemporal Conformance (live PG + MySQL) — provisions a live postgres:16
and a live mysql:8.0 (SET GLOBAL time_zone = '+08:00'), then runs

pnpm --filter @objectstack/driver-sql test

— the whole suite, not a filtered subset — at TZ: America/New_York with
OS_EXPECT_LIVE_DIALECT_MATRIX=1. So these legs execute in CI, and on a required check
(check:required-contexts pins that job name literally).

Two consequences worth stating plainly rather than discovering later:

  1. This card buys real CI coverage, not just a local verification path. The head note in the file
    says so; an earlier draft of it repeated the card's claim and was corrected in 419c627992 rather
    than shipped.
  2. CI will run these against real MySQL 8.0, not the MariaDB stand-in they were developed on. The
    only environment-dependent pin is the column type, and it is portable by construction: knex's mysql
    columnInfo() maps type from information_schema.columns.DATA_TYPE, which is datetime for a
    DATETIME(3) column on MySQL 8.0 and MariaDB alike (precision lives in COLUMN_TYPE). Everything
    else is fixed instants — no now(), no clock dependence, no wording assertions.

Per the card's fence, no MySQL error wording is pinned anywhere in these legs — MariaDB is a
protocol stand-in, fine for predicate compilation and row selection, not for error text.

Verification

  • pnpm --filter @objectstack/driver-sql typecheck — clean (tsc --noEmit, script name echoed)
  • pnpm --filter @objectstack/driver-sql test --maxWorkers=2 (no live URLs, the Test Core shape) —
    104 passed | 5 skipped (109) files, 1786 passed | 67 skipped (1853) tests
  • gate union via node scripts/pm/dispatch-gates.mjs (no path arguments), after the final commit,
    at 419c627992 — 6 path-matched + 5 convention-triggered + check:nul-bytes, all green:
    check:slot-lookup, check:test-source-alias, check:type-source-resolution,
    check-ci-filter-parity, check-plugin-teardown-shape, check-affected-docs,
    check:query-options-erasure, check:type-check-coverage, check:type-check-debt (with the
    workspace closure built), check:engine-double-contract, check:where-matcher, check:nul-bytes

No changeset, deliberately: the diff is one *.test.ts file. Nothing user-visible changes and
nothing ships — driver-sql publishes files: ["dist", "README.md", "CHANGELOG.md"], and test
sources are in none of them. Labelled skip-changeset accordingly.


Generated by Claude Code

The suite pinned the Reaper-shaped where
`{ expires_at: { $lt: cutoff }, revoked_at: { $null: true } }` live on
sqlite but COMPILE-ONLY on pg and mysql2 — hand-rolled
`new SqlDriver({ client: 'pg', connection: {} })`, `.toSQL()`, string
assertions. Compile-only proves the SQL text; it cannot prove the server
returns those rows, and it never meets the column type each dialect
actually creates (sqlite TEXT vs a real temporal column).
Extends the existing file through the PG_CELL / MYSQL_CELL testkit the
sibling dialect suites already use, so the compile-only assertions stay
and the live legs declare themselves un-run when the URLs are absent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
…ve legs
The card assumed CI provisions no live servers. It does: `Temporal
Conformance (live PG + MySQL)` runs the whole driver-sql suite against a
live postgres:16 and mysql:8.0 with OS_EXPECT_LIVE_DIALECT_MATRIX=1, so
these legs execute there — on a required check, against real MySQL 8.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

@os-elonos-elon added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 22, 2026 — with Claude
@os-elon
os-elon marked this pull request as ready for review August 22, 2026 03:15
@os-elon
os-elon enabled auto-merge August 22, 2026 03:15
@os-elon
os-elon added this pull request to the merge queueAug 22, 2026
Merged via the queue into main with commit 7ab286eAug 22, 2026
35 checks passed
@os-elon
os-elon deleted the claude/issue-10836-ttl-onlywhen-live-legs branch August 22, 2026 03:34
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32548714365 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 6.13s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 68 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] ttl.onlyWhen's {$null: true} is pinned COMPILE-ONLY on pg/mysql while a live-cell testkit exists — measured live by hand, holds on both

2 participants

@os-elon@claude