Skip to content

[Decision] min/max over a boolean aggregand: pin the cross-driver JSON answer — 0/1 (SQL) vs false/true (both in-memory faces) #11249

Description

@os-zhuang

Split from #11152 (retriage, 2026-08-23): half (a) — sum/avg boolean conformance cases, settled by #11065's precedent — stays on #11152 and is dispatchable. This card carries half (b), the genuinely unpinned contract question, which is 协议变化类别 ⇒ manual floor.

The question

When an aggregation query takes min or max over a boolean column, what JSON value does the platform contract promise?

  • SQLite / the SQL family: MIN(col)/MAX(col) over a boolean answer 0 / 1.
  • objectql's in-memory fallback (in-memory-aggregation.ts) and driver-memory both reduce with </> and answer false / true.

Unlike #11065 (driver-memory was the lone outlier; two faces pinned the answer), here the two in-memory faces agree with each other and differ from SQL — precedent does not settle it. Whatever is chosen becomes the expected value in AGGREGATION_ROWS conformance cases and forces the losing side to convert.

[facets-block]

  • 实际业务需求:布尔列上的 min/max 在真实分析里语义是「是否存在任一 false / 任一 true」——用户拿到 0/1 或 false/true 都能用,但两驱动答案不同就是同一查询两个结果,由调用方看不见的驱动能力位决定;真实需求是一致,不是哪个字面量。
  • 项目长远合理性:平台契约面(REST/JSON)里布尔字段的读值恒为 true/false;聚合结果突然变 0/1 是 SQL 存储表示的泄漏。选 false/true 让契约与字段类型系统自洽;选 0/1 则是让存储实现定义契约。
  • 防 AI 写代码犯错:AI 写下游代码时会对布尔字段做 === true 判断——0/1 会静默永假。收敛到 false/true(SQL 侧结果在驱动层归一化)是结构上更难写错的一侧。
  • 创业阶段不扩散需求:两选项实现代价接近(都是在少数驱动侧加一层归一化);不存在「先不做」选项——[finding] AGGREGATION_ROWS has no boolean column, so the cross-driver aggregation conformance family cannot see a boolean aggregand on any face #11152 的列一加,期望值必须写死。

推荐 B(false/true,置信度高):四轴同向——契约按字段类型说话,SQL 驱动在聚合读出侧把 0/1 归一化为布尔;与布尔字段普通读取路径的既有归一化同族。本分析看不见什么:driver-sql 各方言布尔存储表示的现状差异(TINYINT/BOOLEAN/INTEGER),归一化实现成本以实测为准;PG 对 avg(boolean) 可能直接报错(#11152 half (a) 的实测项,读数会回贴该卡)。

裁决格式:回「A」(= 0/1,SQL 面答案为准)或「B」(= false/true,布尔类型面答案为准)。裁后:结论写进 #11152 的实施约束,min/max 案例随裁决入列;本卡关闭。

Related: #11152 (carrier of half (a)), #11065 (the avg cell that exposed the family), #11151 (driver-mongodb sibling, on hold).

Activity

  1. claude commented on Aug 23, 2026

    @claude
    Contributor

    Maintainer ruling recorded (atomic four-part write)

    Ruled 2026-08-23, live PM chat, verbatim: 「10950 不考虑存量,其他接受你的建议」 — this card falls under the accepted recommendations. Ruled: the contract answer is false / true — min/max are order statistics and return a member of the input domain; a boolean column answers booleans in JSON. SQL drivers convert 0/1 at the driver boundary. needs-user-decision → pm:queue in the same stroke. Implementation: pin the expected values in AGGREGATION_ROWS conformance cases (SQL side converts); contract-answer pin ⇒ treat as Clause-②: yes, contract-review tier.


    Generated by Claude Code

  2. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    The reading this card asked for — plus two corrections to its stated premises

    This card's own "本分析看不见什么" note says the PG reading would be taken on #11152 half (a) and posted back here. I was dispatched on #11152; here it is. ⛔ I have added no min/max cases and am not proposing a ruling — this is measurement only, so the maintainer decides on facts rather than on the premises below.

    Measured 2026-08-23 on base 5a916c4d4d: live PostgreSQL 16.13 (a cluster I provisioned and tore down), better-sqlite3 through the real SqlDriver, InMemoryDriver.aggregate(AST), applyInMemoryAggregation, and driver-mongodb's emitted pipeline through the in-process evaluator. Fixture: the six AGGREGATION_ROWS rows plus a boolean flag column.

    min / max over a boolean — what each face actually answers

    face min(flag) max(flag) JSON type
    driver-sql — SQLite false true boolean
    driver-sql — Postgres THROWS 42883 function min(boolean) does not exist THROWS 42883 —
    driver-memory false true boolean
    objectql in-memory fallback false true boolean
    driver-mongodb null null null

    Correction 1 — the SQL family does not answer 0/1 at the driver contract

    This card states:

    SQLite / the SQL family: MIN(col)/MAX(col) over a boolean answer 0 / 1.

    That is true of the raw SQL and false of what driver-sql hands back. SQLite does return 1/0, and then presentAggregateReadValues runs the min/max result columns through presentReadValue('boolean', v) — Boolean(value) — so the driver's aggregate() answers false / true. Measured, not read: min(flag) => [{"n":false}] types=["boolean"].

    So on the axis this decision actually pins — the JSON value the platform contract promises — three of the four working faces already agree on false/true, and option B costs no conversion on driver-sql/SQLite at all. The card's framing of "the two in-memory faces agree with each other and differ from SQL" does not survive the measurement.

    One dependency worth stating: that presentation is keyed off the field being declared type: 'boolean' in the object metadata. A harness that declares the conformance column as something else would see the raw 0/1 — so whichever way this is decided, the case table's harnesses must declare the column boolean.

    Correction 2 — Postgres is a third behaviour, not a third value

    Option A and option B both assume every face answers something. Postgres answers nothing: it has no min, max, sum or avg over boolean and errors with SQLSTATE 42883. count and count_distinct are fine (they are defined over any type).

    That matters here because whichever literal is chosen, driver-sql on Postgres cannot produce it without the lowering casting boolean to int — which is a driver change in the engine lane, not a fixture edit. A decision of "B, false/true" is therefore not free on the PG face; it implies a cast plus the existing boolean read-presentation.

    Two related facts filed separately while measuring: the PG failure escapes with no ADR-0112 envelope (status undefined, code = raw SQLSTATE) — #11455; and the aggregation conformance suite hard-codes better-sqlite3, which is why none of this was visible — #11456.

    For completeness — the sum/avg half this card split away

    Same measurement round, since it bears on whether half (a) is as settled as the split assumed: sum(flag) is 3 and avg(flag) is 0.5 on driver-sql/SQLite, driver-memory and objectql; 0 and null on driver-mongodb (#11151, on hold); and both throw on Postgres. Reported in full on #11152, which I returned blocked.


    Generated by Claude Code


    Generated by Claude Code

  3. os-warren commented on Aug 24, 2026

    @os-warren
    Collaborator

    Closing per this card's own pre-recorded disposition (spec seat, session_01Rxnd8cyFnoU8V5y21PaTsy, R3).

    Provenance, all three pieces: the instruction is this card's body clause 「裁后:结论写进 #11152 的实施约束,min/max 案例随裁决入列;本卡关闭」, activated by the maintainer ruling recorded in comment 5386670755 (2026-08-23, live PM chat, verbatim 「10950 不考虑存量,其他接受你的建议」 — ruled B: false/true, SQL drivers convert at the driver boundary).

    The 20:23Z measurement corrections (comment 5388270392) do NOT reopen the ruled answer — they correct the option-cost premises (driver-sql/SQLite already answers false/true via presentAggregateReadValues; PG produces nothing without a lowering cast; harnesses must declare the column type: 'boolean') and they are carried forward as implementation constraints, recorded on #11152 this stroke. The driver half the ruling now requires is filed as #11635 (engine lane, unassigned, pm:queue); the min/max cases enter #11152's scope per the ruling.

    State: pm:queue removed in the same stroke as closing (no pm:* residue on a closed card). Closed as completed — the decision this card carried is made and recorded.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions