Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): compile the seed-tenancy migration statements for the connected dialect, so they run on MySQL (#9381) - #9440
Conversation
… for the connected dialect (#9381) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
…live MySQL (#9381) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop):
|
os-zhuang
commented
Aug 18, 2026
PM review — accepted. The premise step earned its place, and it found that the card's own fix direction was insufficient.The headline: fixing what the card asked for would not have workedThe card named one defect — the quote character. You measured three, on a live MySQL 8.0.46 running the exact session the driver opens:
Had this been implemented from the card's description alone, the statements would still not have parsed, and the fix would have shipped looking complete — into the same by-design warning-swallow that hid the original defect. That is the entire argument for premise-first dispatch, demonstrated rather than asserted, and it is why The The verification is unusually well-constructedThe non-vacuity assertion is the part I want to single out:
You built the positive control into the test, permanently. A green run on a server that happened to have Ablating each half separately is what makes the three-defect claim credible rather than a story:
And you correctly identify that the one green in both ablations is the The CI wiring converts this from a measurement into a ratchet — running the live file in the job that already provisions MySQL, with The structural half is the right instinctMaking it impossible to obtain the exec without the dialect beside it fixes the class, not the instance. A bare Step 2 — both results are results, and one produced a finding
#9434 is a good catch and correctly scoped out: the duplicate-probe statement it prints for the operator names the column Accepted, with the two flagged items answered
NextCI is mid-flight. Once green I'll flip and enqueue with The #9380 is unblocked by this PR existing — it was held only for serial landing in the same module family, and its surface (migration gating in Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9381
Step 1 — the premise, confirmed on a live MySQL
The card's central claim was explicitly not measured, and the dispatch made confirming it the binding first step. It is confirmed: no layer sets
ANSI_QUOTES, and every statement this module builds failed to parse on a real server.Live MySQL 8.0.46, the session the driver itself would open (mysql2 + the one session
SETSqlDriver.withUtcSessionperforms,SET time_zone = '+00:00'):Running the seven statements the module builds, verbatim from its own builders, against that server before the fix — all seven
ER_PARSE_ERROR:premise_still_valid: true.What was actually wrong — three MySQL-only defects, not one
Fixing the quote character alone would still not have made these statements run. All three measured on the same server:
ANSI_QUOTES, so ANSI"x"is a string literal, not an identifier.last_valueis RESERVED on MySQL 8.0 (theLAST_VALUE()window function). An unqualifiedlast_valueisER_PARSE_ERROReven with the table spelled correctly, so the COLUMNS are quoted now, not only the tables. (A qualifiedg.last_valueis legal — measured — which is why the split probe was never the whole story and why Report the business identifiers already minted twice by the tenancy split — an operator-facing inventory, on installs the #8686 backfill cannot repair #8928's own probe is unaffected.)UPDATE t … (SELECT … FROM t)is refused outright:ER_UPDATE_TABLE_USED(1093), "You can't specify target table 'crm_case' for update in FROM clause". The stamp's collision-exclusion guards now go through a derived table, which is plain ANSI and unchanged in meaning on the other two dialects.Why nobody saw it: a migration must never fail a boot, so every call site swallows the failure into a warning. On MySQL the symptom was a skipped repair in the log — not a wrong answer, and not the repair either. Declared (the module header names MySQL) ≠ enforced.
The fix — producer-side, and the seam carries the dialect
Producer-side repair in
metadata-protocol, as ruled; nothing in any driver was made tolerant of double-quoted identifiers.quoteIdent(name, client)— the same shape as Report the business identifiers already minted twice by the tenancy split — an operator-facing inventory, on installs the #8686 backfill cannot repair #8928's inpackages/cli/src/commands/migrate/duplicates.ts, backticks formysql/mysql2, ANSI otherwise.resolveSeedTenancySeam(engine)returns{ exec, client }andbackfillSeedTenancytakes that pair. A caller can no longer obtain the exec without the dialect beside it — the quoting helper alone would leave the same hole open for the next caller.resolveSeedTenancyExecstays exported and unchanged foros migrate duplicates, which resolves its own client.The client is read from the same driver object the exec came from (
driver.config.client). Verified against real drivers, not doubles:Step 2 — the sibling migrations, checked
Asked for explicitly by the card; both results are results.
partial-index-probe.ts— clean. It builds no identifier-quoted SQL of its own; the statements it runs (a bareDROP INDEX IF EXISTSon an index name, and the caller'sCREATE …) carry bare index names. Nothing to fix.sys-setting-identity-index.ts— clean of THIS defect, and deliberately so: it uses bare identifiers and its header explains why (MySQL takes the degradation path for that index anyway). One separate, smaller problem found there while checking, filed unassigned as sys_setting's degradation message hands MySQL operators a duplicate-probe statement that does not parse on MySQL (keyis reserved) #9434 and not touched here: the duplicate-probe statement it PRINTS for the operator names the columnkey, which is reserved on MySQL, so the remedy handed to the MySQL operator does not parse on MySQL (measured,ER_PARSE_ERROR). Printed only, never executed — no runtime path breaks.overlay-index.ts's equivalent probe was measured on the same server and runs fine.Migration gating was not touched — that is #9380's surface, and #9380 is not addressed here.
Verification
Union of gates run at
d79b35377c(the final commit).Live MySQL, after the fix — a new suite that RUNS the statements on a real server, gated on
OS_TEST_MYSQL_URL, with a non-vacuity assertion that the server is not runningANSI_QUOTES(on a server that had it, a green run would mean nothing — the exact condition the premise step ruled out):Reverse verification, twice — each half of the fix ablated alone, from the committed state:
ER_PARSE_ERRORonSELECT "tenant_id" FROM "_objectstack_sequences" WHERE 1 = 0;ER_UPDATE_TABLE_USEDon the backtick-spelledUPDATE.The remaining green in both runs is the
ANSI_QUOTESassertion, which is about the server and not about the fix. Restored ⇒ 5/5 green (above).Live PostgreSQL 16.13 — the same builders' ANSI output, executed statement by statement on a real server with
ON_ERROR_STOP=1: all seven run, and the semantics are right (UPDATE 1— the one movable row moved; the row colliding with an org-sideCASE-00001stayedNULLand was reported).SQLite —
packages/runtime's end-to-end integration test on a realSqlDriver: 8/8 pass.Suites
pnpm --filter @objectstack/metadata-protocol test— 120 passed | 1 skipped (121 files), 1662 passed | 5 skipped. The skip is the live-MySQL file with no URL provisioned: a named skip, never a silent pass.pnpm --filter @objectstack/runtime test— 165 files, 2467 passed.pnpm --filter @objectstack/cli exec vitest run src/commands/migrate— 7 files, 33 passed.tsc -p packages/metadata-protocol/tsconfig.json— 63 errors, exactly the frozen ledger count. No new entry, no ledger edit.Gates re-derived from the changed paths with
node scripts/pm/dispatch-gates.mjs, then run atd79b35377c:nul-bytes,cross-package-test-inputs,durability-log-level,node-version,required-contexts,shard-attestation,workflow-status-functions,stack-collection-maps,changeset-gate-self-tests,objectui-changeset,empty-changeset,adr-0087-registration,override-consistency,query-options-erasure,engine-double-contract,where-matcher,type-check-coverage,check-changeset-no-major,check-changeset-fixed,check-osv-exemptions,docs-audit/check-affected-docs— all pass. The ratchet,check:type-check-debt --re-measure, was run at the samed79b35377cover a fully built workspace closure: 33 ledger entries re-measured, 1926 raw tsc errors total, none above its recorded number.CI
The live suite is wired into the one job that already provisions a MySQL (
Temporal Conformance (live PG + MySQL)), running only that file, withOS_EXPECT_LIVE_DIALECT_MATRIX=1so a droppedenv:line becomes a red instead of quietly returning this seam to zero coverage. Without that step the new test would never run in CI and the fix would be a one-time measurement rather than a ratchet.Notes for review
backfillSeedTenancynow takes the seam object instead of a bare exec — a breaking change to an exported helper (changeset:minorformetadata-protocol). All three call sites are updated;resolveSeedTenancyExecis unchanged for the CLI.mysql2is added as a devDependency ofmetadata-protocolfor the live test only. No runtime dependency on a driver was introduced.quoteIdent— this one and Report the business identifiers already minted twice by the tenancy split — an operator-facing inventory, on installs the #8686 backfill cannot repair #8928's. A shared home would be reasonable (@objectstack/types, beside the other dialect-shaped helpers, is the natural candidate:metadata-protocolalready depends on it and the CLI reaches it transitively). Deliberately NOT done here — it is a second change touching Report the business identifiers already minted twice by the tenancy split — an operator-facing inventory, on installs the #8686 backfill cannot repair #8928's tested file and deserves its own decision.Generated by Claude Code