Uh oh!
There was an error while loading. Please reload this page.
fix(driver-sql): declared booleans answer JSON booleans on MySQL's row-read doors (find/distinct/group keys) - #12019
Conversation
…row reads (#11782 scope: find/distinct/group keys) formatOutput's boolean read coercion and readPresentationKind's boolean arm were gated isSqlite-only, so a declared boolean answered 1/0 on MySQL (tinyint(1) via mysql2) through find(), distinct() and aggregate group keys while SQLite and Postgres answered true/false — and after #11635 the aggregate door and the row-read door gave opposite answers on the same MySQL connection. The boolean presentation now runs on the two dialects whose stored boolean is a number; Postgres stays ungated (native boolean). Measured live before/after on MySQL 8.0.46, PG 16.13 and embedded SQLite. Pinned by a cross-door agreement suite over the live dialect matrix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o
…sql-boolean-row-read
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8ee4089e093ff330c08154fc8f37c84a27f629a7 && git checkout 8ee4089e093ff330c08154fc8f37c84a27f629a7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1620c1de2805ade35890c38470a57c9b4fe3eb70 0091979a8e7dc90f15de8d27d222bec854e38d03 && git checkout -B drift-repro 1620c1de2805ade35890c38470a57c9b4fe3eb70 && git merge --no-ff 0091979a8e7dc90f15de8d27d222bec854e38d03
node scripts/docs-audit/affected-docs.mjs --json 1620c1de2805ade35890c38470a57c9b4fe3eb70
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11782
What this changes
formatOutput's boolean read coercion andreadPresentationKind's boolean arm were gatedisSqlite-only, so a declaredField.booleanleaked its storage form on MySQL:tinyint(1)reaches mysql2 as a JS number, and nothing downstream converted. The boolean presentation now runs on the two dialects whose stored boolean is a number — SQLite (INTEGER0/1) and MySQL (tinyint(1)). Postgres stores a realbooleanthat node-pg parses, so its stored form already IS the presented form and it deliberately stays outside the gate: its answers are byte-identical before and after.Contract surface — answer set per door, per dialect (dispatched at tier; stated explicitly)
Measured live before (
main@d63b014360) and after (this branch), on MySQL 8.0.46, PostgreSQL 16.13, embedded SQLite, through the driver boundary (driver.create(...)+ each read door — the card's own instrument):find()row valuetrue/false→ unchangedtrue/false→ unchanged1/0(number) →true/false(boolean)distinct()values[true,false]→ unchanged[true,false]→ unchanged[0,1](number) →[true,false](boolean)aggregate()group keystrue/false→ unchangedtrue/false→ unchanged1/0(number) →true/false(boolean)aggregate()min/maxfalse/true→ unchangedfalse/true→ unchangedfalse/true→ unchanged (#11635 already presented this door; it now short-circuits onreadPresentationKindinstead of the call-site fallback — same answers)NULLboolean, every doornull→ unchangednull→ unchangednull→ unchanged (absence is notfalse)number/stringcolumns, every doorThe aggregate
min/maxrow is the cross-door disagreement this card exists to close: since #11635,max(flag)answeredtrueon MySQL whilefind()on the same column over the same connection answered1. The group-key cells are the same gate consumed through the groupBy tracking sites — before this fix MySQL was also the one dialect whose group keys disagreed with SQLite/Postgres.distinct()was recorded on the card as UNMEASURED; it is measured here (before:[0,1]on MySQL) and decided alongsidefind(), through the shared gate.Scope fence honoured:
aggregate()'s lowering and its #11635 call-site fallback are behaviourally untouched (one comment updated to stay accurate). The fallback is NOT dead code after this change — it is what carries Postgres, wherecast(?? as int)makes the backend answer 1/0 while row reads need no presentation.Regions (declared by symbol — serial constraint vs #11876)
formatOutput— thebooleanFieldsread coercion, moved out of the SQLite-only block behind anisSqlite || isMysqlgate.readPresentationKind— the boolean arm now applies on SQLite and MySQL; the numeric arm stays SQLite-only (it exists for legacy TEXT-affinity columns, which only SQLite has).distinct()seam consumes the re-gatedreadPresentationKind; no code indistinct()itself changed.presentReadValue's doc and at the driver-sql: boolean aggregands need a lowering cast on PG (+ a MySQL min/max presentation check) — the ruledfalse/true+ arithmetic answers are unproducible on the PG face #11635 aggregatemin/maxtracking site (its prose described a "SQLite gate" that no longer exists as stated; no behaviour touched).Disjoint from #11876's regions (
createColumn/ varchar width mirror).origin/mainmerged before opening this PR (merge commit0091979a8e); no upstreamdriver-sqlmovement between base and merge (path-filtered log empty). Amariadbclient spelling stays outsideMYSQL_EMIT_CLIENTSand therefore outside this gate — that is the standing #11756 support-scope decision, not this card's.Evidence
sql-driver-11782-boolean-row-read-presentation.test.ts— 9 tests per dialect cell over the live matrix + the axis guard (28 total). Asserts cross-door agreement on the same column (find + distinct + group keys + min/max), per the triage note — a one-door pin passes on an implementation where the doors still disagree. Booleans asserted strictly (toBe(true);1is truthy and would pass atoBeTruthypin). Negative controls: declarednumber/stringcolumns andNULLpassthrough.expected 1 to be true // Object.is equality;Set{1, null, +0}≠Set{true, false, null}; group count under keytrueundefined). Mutation proven on disk before any result was read (anchored greps: union-gate spelling 2→0,#11782tags 4→0), restore undertrap … EXIT INT TERM, post-restore anchors 2/4 and cleangit status --porcelain. No build leg owed: the suite imports./sql-driver.jsrelative fromsrc, so the subject never resolves through a dependencyexportsmap — no dist is involved in the measurement.0091979a8eunder CI-parity skew (PGAsia/Shanghai, MySQL+08:00, processAmerica/New_York,OS_EXPECT_LIVE_DIALECT_MATRIX=1, both live servers attached): 138 files, 2778 passed, 1 skipped, 0 failed.@objectstack/metadata-protocolmigration suites): 10/10 green.pnpm --filter @objectstack/driver-sql typecheckgreen at0091979a8e(script echo verified).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack; the script derives the changeset itself from the merge-base; its stderr attribution line names this repo at this checkout): 14 path-matched + 6 convention-triggered (new test file) +check:nul-bytes. All 21 ran at0091979a8e(this PR's head), every exit captured before any pipe, allEXIT=0. Verdict lines from the gates' own output include:check-driver-conformance: OK — 45 covered cell(s), 0 in the DEBT ledger, 0 exempt;check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 346.6s, 1898 raw tsc error(s) total, none above its recorded number(the debt gate, run on the freshly built./packages/*closure);check-engine-double-contract: OK — 405 pinned, 133 in the DEBT ledger, 2 exempt;where-matcher conformance holds: 297 matcher(s) discovered, 297 answer the combinator battery correctly or refuse it loudly;check-nul-bytes: OK (scanned 6664 text file(s) … no raw ASCII control bytes);OK: 16 package(s) read outside themselves, all declared(cross-package-test-inputs);query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new..tsfiles — the changeset.mdreturns eslint's own "File ignored because no matching configuration was supplied";--format jsoncounts 2 files linted, 0 errors, 0 warnings; and this repo'seslint.config.mjsstates in its own docblock that type-aware linting is never enabled for any file, so a diff cannot move an untouched file's lint verdict.origin/maingained 3 commits past this branch's merge point (specmaxLength, release CI, a showcase test) — none touchpackages/drivers/driver-sql/or any file in this diff, and fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 has not landed, so the serial trigger ("merge again if fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 lands first") has not fired. The full package suite and typecheck above were re-run at0091979a8eafter that check.Changeset
@objectstack/driver-sqlpatch — user-visible presentation change on MySQL row-read doors.Generated by Claude Code