Skip to content

fix(driver-sql): boolean aggregands answer the ruled #11249 contract — PG lowering cast + all-dialect min/max JSON-boolean presentation - #11785

Draft
huangyiirene wants to merge 2 commits into
mainfrom
claude/issue-11635-boolean-aggregand-pg-cast
Draft

fix(driver-sql): boolean aggregands answer the ruled #11249 contract — PG lowering cast + all-dialect min/max JSON-boolean presentation#11785
huangyiirene wants to merge 2 commits into
mainfrom
claude/issue-11635-boolean-aggregand-pg-cast

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#11635

The ruling this implements

#11249 (maintainer 2026-08-23, comment 5386670755, verbatim and untranslated: 「10950 不考虑存量,其他接受你的建议」): min/max over a boolean aggregand answer false/true in JSON — order statistics return a member of the input domain, SQL drivers convert at the driver boundary; sum/avg answer arithmetic (3 / 0.5 on the 3-true/3-false fixture, the settled #11065 family shape). Cast, not refuse: a face that refuses cannot satisfy the ruled contract. This is declared = enforced restoration of an already-ruled contract, not a new capability.

Premise re-measured on current main (2a6122bd9d), before building

Both card measurements reproduced through SqlDriver.aggregate() on the live servers this container runs at CI parity (PG 16.13 timezone=Asia/Shanghai, MySQL 8.0.46 time_zone=+08:00, process TZ=America/New_York):

The change (2 seams in sql-driver.ts + tests)

  1. PG lowering cast — in aggregate()'s statement builder: sum/avg/min/max over a field in booleanFields emit func(cast("col" as int)) on Postgres only. The column stays a knex identifier binding. count/count_distinct are deliberately not cast (both lower to count, defined over boolean everywhere; the acceptance keeps them unchanged). No new dialect predicate — the existing isPostgres getter selects the branch (read-coupling with driver-sql: the isSqlite/isPostgres/isMysql getters recognise fewer knex client spellings than knex accepts, so a valid client: 'postgres' silently loses every dialect-specific behaviour #11550 noted below; its getters were not edited).
  2. All-dialect min/max boolean presentation — the min/max result-column tracking now presents a declared-boolean aggregand as 'boolean' on every dialect, not only under readPresentationKind's SQLite gate: MySQL's tinyint(1) and PG's casted int both answer 1/0 from the backend and leave the driver as JSON false/true. null (empty window) passes through untouched — min/max over no rows still answer null, matching emptyGroupValueFor.
  3. Tests — new sql-driver-11635-boolean-aggregand-answers.test.ts pins the ruled answers on all three dialect cells (AGGREGATION_ROWS + a flag boolean column, 3 true / 3 false, declared type: 'boolean'; east group all-true so grouped min=true catches whole-table/sticky presentation). The driver-sql (PG): sum/avg/min/max over a boolean column throw the raw PostgreSQL 42883 with no ADR-0112 envelope (status undefined) #11455 suite's PG-only 42883 block is retired per its own retirement clause ("if [finding] AGGREGATION_ROWS has no boolean column, so the cross-driver aggregation conformance family cannot see a boolean aggregand on any face #11152/[Decision] min/max over a boolean aggregand: pin the cross-driver JSON answer — 0/1 (SQL) vs false/true (both in-memory faces) #11249 rule that the lowering should CAST, these cases stop failing and this block is RETIRED by that card") — the four 42883 pins, the fence case, and its log case are replaced by a tombstone naming the ruling; its count/count_distinct controls live on in the new suite; the all-dialect terminal-envelope sweep (missing table) is untouched. No pin was deleted to make room — the retirement is the honest update the ruling forces, and this paragraph is its declaration.

Acceptance numbers, measured through SqlDriver.aggregate()

live PG 16.13live MySQL 8.0.46SQLite
sum(flag)"3" (Number 3)"3" (Number 3)3
avg(flag)"0.50000000000000000000" (Number 0.5)"0.5000" (Number 0.5)0.5
min(flag)false (JSON boolean)falsefalse
max(flag)true (JSON boolean)truetrue
count(flag)"6" — unchanged6 — unchanged6
count_distinct(flag)"2" — unchanged2 — unchanged2

Contract surface (clause ②) — the answer set and its JSON types, named

Verification

  • New suite: 28/28 green across sqlite + live postgres + live mysql with OS_EXPECT_LIVE_DIALECT_MATRIX=1 (no cell skipped).
  • Full @objectstack/driver-sql suite with both live servers: 133 files, 2696 passed | 1 skipped — verify-lock VERDICT command-exit 0. pnpm --filter @objectstack/driver-sql typecheck VERDICT command-exit 0.
  • Reverse verification (fix committed first; mutation = git restore --source=2a6122bd9d of sql-driver.ts, confirmed on disk by marker greps — castBooleanAggregand count 0 and the pre-fix bare-lowering line back — with a trap ... EXIT INT TERM restore): predicted 9 failed / 19 passed with PG reds by THROW and MySQL reds on VALUES; measured exactly that — PG: 6 red, all DATABASE_ERROR 42883 (sum/avg/min/max/grouped/empty-window); MySQL: 3 red on values (expected +0 to be false, expected 1 to be true, grouped {lo:1,hi:1}); SQLite all green. Restore confirmed (marker count back to 2, git status clean). The suite imports the driver by relative path, so vitest reads src — no build stands between the mutation and the measurement.
  • Gates at final head 05c95a7b85 (union re-run after the last commit): all derived + convention families green (exit 0 each): check:changeset-gate-self-tests, check:objectui-changeset, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:cross-package-test-inputs, check:where-matcher, check:nul-bytes, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-plugin-teardown-shape, check-affected-docs, release-rehearsal-clone --self-test, check:driver-conformance. Declared narrowing: check:type-check-debt's --re-measure half was not run locally (needs the full workspace build) — its population is the DEBT/TEST_DEBT ledgers in check-type-check-coverage.mjs, @objectstack/driver-sql is in none of them, driver-sql's own tsc program (src/**/*, tests included) covers every file this PR touches and exits 0, and the diff moves no export surface, so no ledgered package's program reads anything this PR changed; CI runs the full re-measure regardless.
  • Driver-conformance lane numbers: before 45 covered / 0 DEBT / 0 exempt → after 45 covered / 0 DEBT / 0 exempt (unchanged — this card adds per-driver pins, not a new conformance case-set enrollment).

Sequencing

Out of scope, filed


Generated by Claude Code

…max as JSON booleans on every dialect
Implements the #11249 ruling at the driver boundary: sum/avg over a
declared boolean answer arithmetic (3 / 0.5 on the 3-true/3-false
fixture) and min/max answer false/true in JSON — order statistics
return a member of the input domain.
- Postgres: the lowering now emits cast("flag" as int) for
sum/avg/min/max over a declared boolean column (PG defines no
arithmetic/order aggregates over boolean; SQLSTATE 42883, measured
on PG 16.13). count/count_distinct are deliberately not cast.
- Every dialect: min/max results over a declared boolean are tracked
for boolean read-presentation, closing the MySQL gap where
tinyint(1) answered 0/1 through the driver boundary (measured on
MySQL 8.0.46). NULL (empty window) passes through untouched.
- The #11455 suite's PG-only 42883 block is retired per its own
retirement clause; the ruled answers are pinned on all three
dialects by sql-driver-11635-boolean-aggregand-answers.test.ts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VK8rFDtg8eREaxBGX99Csn
@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 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)

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 8b41bcd85160ac1c475d26b2968d74f84954bbfapackageMentionDocs.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@huangyiirene@claude