Uh oh!
There was an error while loading. Please reload this page.
test(driver-sql): measure what MySQL does with a conflict target it cannot honour - #8624
Conversation
…annot honour (#8592) The MySQL cell of the unbacked-conflict-target matrix was guarded one way only -- `if (!cell.available) declareUnprovisionedCell(...)` with no else. With OS_TEST_MYSQL_URL absent it announced itself un-run; with it PRESENT, on the one CI job that attaches a live MySQL 8.0, it declared nothing and measured nothing. The declaration disappeared exactly when the capability to measure appeared, and OS_EXPECT_LIVE_DIALECT_MATRIX could not catch it because a cell that emits no suite is not a skip. Structural fix: `declareDialectCell(cell, matrix, measure)` in the shared testkit is TOTAL -- `measure` is required, so the one-way form no longer typechecks. The other six consumers were checked and are already two-way. Measured on live MySQL 8.0.46 (system mysql-server, mysqld --daemonize), through the same knex + mysql2 path upsert takes. Table: `email` named in conflictKeys with no unique index, `tax_id` carrying the only unique one: - it does NOT refuse -- the identical call is VALIDATION_ERROR/400 on SQLite and Postgres; - it MERGES on `tax_id`, the key the caller never named, across two different `email` values -- a wrong write with no error; - it REPLACES the merged row's primary key while doing so (`id` sits in the merge set), which the card's inference did not contain; - it does NOT merge on `email`, the key it was given -- duplicates. The new pins are characterizations of a defect and say so; #8621 is the card that moves MySQL's accept set and will turn them red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoxQqG5FiUHZKCST7KDoZC
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
os-zhuang
commented
Aug 14, 2026
ACCEPT — reviewed against the tree at Verified independently of the description:
On the measurement: two of my own premises were wrong, and the dev's work is what corrected them. I reported this container has "no server binaries of any kind" for MySQL and treated that as not measurable here. The first half was true, the inference was not — The observation also beat the inference in substance. The card predicted a merge on the wrong key; the measurement additionally found the surviving row's primary key is replaced, and that fact is not MySQL-specific. Disposition of #8592 — closing it on mergeThe PR asks the PM to make this call, correctly. Steps 1 and 3 are the dispatched scope in full, and step 2 was cut from dispatch by triage before this card was claimed, not dropped by the dev — it is now #8621 with the measurement as its evidence. So #8592's scope is complete on merge and I will close it manually, since
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #8592 — steps 1 (measure) and 3 (make the cell structurally honest), which is the scope triage cut for dispatch. Step 2 (changing MySQL's accept set) is deliberately not here; it is filed as #8621 with its own argument. See "what this PR leaves" at the bottom.
The bug this found on the way in
The card's premise was that the MySQL half could not be measured because no runner has a server. That is wrong, and the correction is the substance of this PR. CI's
Temporal Conformance (live PG + MySQL)job attaches amysql:8.0service and runspnpm --filter @objectstack/driver-sql testwithOS_TEST_MYSQL_URLandOS_EXPECT_LIVE_DIALECT_MATRIX=1. The server was present and unused.Unused because the MySQL cell was guarded one way only, with no
else:OS_TEST_MYSQL_URLThe declaration disappeared exactly when the capability to measure appeared, and
OS_EXPECT_LIVE_DIALECT_MATRIX=1could not catch it because a cell emitting no suite at all is not a skip. This is not a criticism of #8591 — its declaration was correct for the container it was written in.The measurement
Raised a real server in the dev container rather than inferring: system MySQL 8.0.46 (Ubuntu noble
mysql-server,mysqld --daemonize,default_time_zone='+08:00'), driven through the same knex +mysql2pathupserttakes.emailis named inconflictKeysand has no unique index;tax_idcarries the only unique index.Four facts, now pinned:
VALIDATION_ERROR/ 400 on SQLite and Postgres.idsits in the merge set, soid = values(id)overwrites the stored row's identity with the nanoid minted for the insert that lost. The inference did not contain this, and it is the sharpest edge — every external reference to the old id dangles silently.email.So MySQL fails in both directions at once.⚠️ The new pins are characterizations of a defect and say so loudly in the code; when #8621 lands they go red and must be rewritten to the refusal, not relaxed.
Step 3 — the structural fix
declareDialectCell(cell, matrix, measure)inlive-dialect-matrix.testkit.tsis a total function:measureis required, so the half-pair form no longer typechecks. The other six consumers ofdeclareUnprovisionedCellwere checked and are already two-way (for … if (!available) … continue), so none of them needed changing.Reverse verification — direction predicted before each leg
measureargument:TS2554: Expected 3 arguments, but got 2.Matched.OS_EXPECT_LIVE_DIALECT_MATRIX=1: 19 passed → 14 passed, still green,tscexit 0. The five MySQL observation tests vanished with no signal anywhere. That is the defect reproduced exactly.Verification
Both live servers were raised locally, so this ran in CI's exact shape rather than a subset —
TZ=America/New_York, live PG 16.13 atAsia/Shanghai, live MySQL 8.0.46 at+08:00,OS_EXPECT_LIVE_DIALECT_MATRIX=1:Un-provisioned paths also checked both ways: with no URLs the cell reports a named skip; with
OS_EXPECT_LIVE_DIALECT_MATRIX=1and URLs dropped it is a named red.Gates re-derived against the actual changed paths (
scripts/pm/dispatch-gates.mjs) and run:check:test-source-alias,check:type-source-resolution,check:query-options-erasure,check:type-check-coverage,check:nul-bytes— all green.Changeset
skip-changeset. Tests plus a test-only helper —live-dialect-matrix.testkit.tsis not exported fromindex.ts(verified). No runtime path changed, nothing consumer-visible moved, so a changeset would announce a release that contains nothing.What this PR leaves
Part ofrather thanFixesbecause the card body's step 2 — decide, and possibly refuse pre-flight — is not done here: triage excluded it from dispatch as aminorneeding its own argument, and it is now #8621. Steps 1 and 3 are complete. If the re-scope is taken as authoritative, #8592 can be closed on merge; that call is the PM's, not this PR's.Findings filed on the way, both unassigned: #8621 (the accept-set decision, with the measurement as its evidence) and #8622 (the primary-key rewrite, which reproduces on SQLite too and is not MySQL-specific). Neither is addressed here.
Generated by Claude Code
Generated by Claude Code