Skip to content

test(driver-sql): isolate the live-dialect matrix per test file - #10381

Merged
os-elon merged 3 commits into
mainfrom
claude/issue-9350-live-matrix-schema-isolation
Aug 20, 2026
Merged

test(driver-sql): isolate the live-dialect matrix per test file#10381
os-elon merged 3 commits into
mainfrom
claude/issue-9350-live-matrix-schema-isolation

Conversation

@os-elon

@os-elonos-elon commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes#9350

Per-file schema isolation for the live-dialect matrix — option C, the maintainer's ruling of 2026-08-20 (「C 拆掉争用(推荐)」).

Revision history. The first push (21b756e38) isolated MySQL with use and turned Temporal Conformance red on three files. The mechanism, the measurement that found it and the control are in What went wrong the first time below.

What was shared

Every live-matrix file resolved its connection from one env var per dialect, so all of them ran against one conformance database: one public schema on Postgres, one conformance database on MySQL. That included _objectstack_sequences — the driver's internal auto-number counter table, which each file's autonumber path lazily creates through a hasTablecreateTable check-then-act pair, then writes rows into, and which the driver will also rebuild in place (_objectstack_sequences__rebuild, a drop+rename). Vitest runs those files in parallel workers.

Measured on a real Postgres 16 before the change: a run of the live-PG files left exactly one schema behind — public, holding that one shared table.

What changed

  • cell.config() derives a per-file schema (Postgres) / database (MySQL) from vitest's own testPath. currentLiveSchema() takes no argument — a parameter is the one thing a caller could get wrong, and two files handed the same literal are back to sharing a database with nothing at the call site looking wrong.
  • MySQL names that database in the connection URL, so knex's client.database() and the session cannot disagree. Postgres carries searchPath.
  • A vitest globalSetup creates the schemas before any pool opens, and drops them afterwards. It has to be globalSetup: cell.config() is reached from inside beforeEach in most of the eleven consumers, and the testkit module is cached per worker rather than per file, so a hook registered at its module scope would attach to one file and no other.
  • Postgres introspection now follows the session rather than assuming public — see the driver change.
  • Four files that read process.env.OS_TEST_*_URL directly now go through the cell; a source scan in the new suite keeps the cell the only route to a live server. The 20 existing cell.config() call sites needed no change.

⛔ Not in this change, deliberately: no test is skipped, quarantined, retry-wrapped, or given a larger budget; the 5000ms default is untouched; every assertion is byte-identical; nothing in .github/ moves.

What went wrong the first time

The first push moved the MySQL session with use while leaving the connection pointed at conformance. knex binds client.database() — the connection's database — into columnInfo (mysql-querycompiler.js: table_schema = ?). So DDL executed in the per-file database while the column read answered from conformance: the driver saw an empty column set for a fully populated table and emitted alter table … add label, which the server rejected with Duplicate column name. The unique indexes it thought were missing were never created either, which is where the #8807 identity assertions went red.

Measured, rather than reasoned about:

client.database()session database()
plain connectionconformanceconformance
first attempt (use)conformanceos_lv_… ← the split

Naming the database in the URL makes those the same value by construction; there is no second place for them to disagree.

The driver change

introspectIndexes pinned n.nspname = 'public' and introspectSchema pinned table_schema = 'public'. Once the suites moved off public, both returned empty — measured on live Postgres 16, [] for a table carrying a primary key and a declared unique index, and no tables at all from introspectSchema.

Empty does not read as "I could not see" downstream; it reads as "there are no indexes", which assertConflictTargetHonoured turns into a refusal. A fail-open on an identity check, invisible to every existing test because the suites that exercise that path are MySQL-gated. introspectIndexes now resolves the table with to_regclass (the same first-match-along-search_path resolution every other statement performs, and unambiguous where a schema list would not be); introspectSchema lists ANY (current_schemas(false)). Identical results for a default deployment — there current_schemas(false) is exactly {public}. Changeset included.

Verification — on live servers, both dialects

Postgres 16 at Asia/Shanghai and MariaDB 10.11 at +08:00, process TZ=America/New_York, at 80589de9a:

  • full driver-sql suite, both servers, OS_EXPECT_LIVE_DIALECT_MATRIX=1: 106 passed | 1 failed (107 files). The one failure is ER_BAD_FIELD_ERROR (1054) — canary lands on 'absent', which is red at the merge base too on this server: MariaDB says Unknown column '…' in 'INSERT INTO' where MySQL 8.0 says in 'field list'. An artefact of the stand-in server, not of this change — CI's real MySQL 8.0 never flagged that file.
  • the three files CI failed on — sql-driver-datetime-mysql-storage, sql-driver-time-live-dialects, sql-driver-upsert-conflict-target-dialects — go 18 red → 97 passed with the isolation suite alongside.
  • server-free: pnpm --filter @objectstack/driver-sql test102 passed | 5 skipped; typecheck clean.

Isolation, measured on the servers rather than asserted: 107 per-file databases on MySQL and 107 per-file schemas on Postgres, with 0 tables left in the shared conformance database and 0 in public — where before the change public held _objectstack_sequences. The two files that use that counter table now each hold their own copy.

The controls, each predicted before it was run

The flake ran 4 times in 3 days and zero times in the 3 days since, so a green run proves nothing — it was already going green. Every claim here therefore comes with the negative case:

  1. Isolation. Reverting the resolution to a shared constant turns the isolation suite red on exactly the predicted cases, collapses the live server's schemas to 1, and leaves the driver-sql suite green at 549 passed — which is precisely why the acceptance criterion had to be structural.
  2. Caused, not pre-existing. The three CI-failing files at 21b756e38 on live MariaDB: 18 failed. Same three files at the merge base: 80 passed, 0 failed.
  3. The driver fix. Reverting the two introspection hunks turns exactly the two new live-PG cases red — introspectIndexes returning [] against two indexes the server really holds — and only those two.
  4. The scan. It asserts its own pattern still matches a string that does read the env var, so a needle that quietly stopped matching cannot report a clean scan forever.

Restore legs were re-run and re-measured in every case. No build step is involved — the suites import by relative path, so vite loads the source; a stale-artifact read would have left each ablation green.

Gates

Re-derived at the final commit with node scripts/pm/dispatch-gates.mjs (not a hand-written list) and run there: check:nul-bytes, check:changeset-gate-self-tests, check:objectui-changeset, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-cross-package-test-inputs, scripts/docs-audit/check-affected-docs.mjs — all green on their own verdict lines.

⚠️Limits. MariaDB 10.11 is a stand-in for MySQL 8.0 — the same server #3942 was originally hand-verified on, and the same wire protocol, but not the same server; the canary above is exactly where they differ. check:type-check-debt --re-measure was not run: its population is the DEBT/TEST_DEBT ledger entries, @objectstack/driver-sql carries none, and both changed source files are inside the package's tsc program, which is green.

Every live-matrix file resolved its connection from one env var per dialect, so
all 14 of them shared one conformance database — one `public` schema on
Postgres, one `conformance` database on MySQL — including the driver's internal
`_objectstack_sequences` counter table, which each file's autonumber path
lazily creates and writes.
`cell.config()` now derives a per-FILE schema (Postgres) / database (MySQL)
from vitest's own `testPath`, created by a `pool.afterCreate` hook so every
pooled connection lands in it. No assertion changed; no test is skipped,
quarantined, retried, or given a larger budget.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@os-elonos-elon added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 20, 2026 — with Claude
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

17 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/drivers/driver-sql/vitest.config.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bc400aff75d5891f4b147bd1286e637a83d9896dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 1f5eb7fec7f7d29c5e0be15a351cec9613193eb5 — the merge of head 80589de9a77947f48a18940836eba5884d61f0f2 into base bc400aff75d5891f4b147bd1286e637a83d9896d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1f5eb7fec7f7d29c5e0be15a351cec9613193eb5 && git checkout 1f5eb7fec7f7d29c5e0be15a351cec9613193eb5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bc400aff75d5891f4b147bd1286e637a83d9896d 80589de9a77947f48a18940836eba5884d61f0f2 && git checkout -B drift-repro bc400aff75d5891f4b147bd1286e637a83d9896d && git merge --no-ff 80589de9a77947f48a18940836eba5884d61f0f2
node scripts/docs-audit/affected-docs.mjs --json bc400aff75d5891f4b147bd1286e637a83d9896d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

os-elonand others added 2 commits August 20, 2026 20:07
…t PG introspection follow the session (#9350)
The first attempt moved the session with `use` (MySQL) while leaving the
connection pointed at `conformance`. knex binds `client.database()` — the
CONNECTION's database — into `columnInfo`, so DDL ran in the per-file database
while the column read answered from `conformance`: the driver saw an empty
column set for a fully populated table and emitted `alter table ... add
<column>`, which the server rejected with *Duplicate column name*. Measured on a
live MariaDB 10.11: 18 red, matching CI's three failing files exactly.
The database is now named in the connection URL, so the two halves cannot
disagree, and a vitest globalSetup creates the schemas before any pool opens.
Postgres' index read pinned `n.nspname = 'public'`, which returned [] for a
table that measurably had a primary key and a declared unique index once the
suites moved off `public` — and [] reads as "no indexes", which
assertConflictTargetHonoured turns into a refusal. It now resolves the table
with to_regclass, the same way every other statement in the session resolves it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@os-elonos-elon removed tests skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Aug 20, 2026 — with Claude
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 20, 2026
@os-elonClaude

Copy link
Copy Markdown
CollaboratorAuthor

The PR body was rewritten for the second push and lost its signature footer in the edit; recording it here rather than rewriting the body a third time.

Authored by the domain:engine dev seat, session session_019yDEhPBC3tcGkW9bkce1HM, on issue #9350.


Generated by Claude Code


Generated by Claude Code

@os-elon
os-elon marked this pull request as ready for review August 20, 2026 20:43
@os-elon
os-elon added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit 46cfa5bAug 20, 2026
36 of 37 checks passed
@os-elon
os-elon deleted the claude/issue-9350-live-matrix-schema-isolation branch August 20, 2026 20:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@os-elon