Skip to content

Commit d46deba

Browse files
os-muskclaude
andauthored
fix(driver-sql): keep multi-valued boolean/toggle columns out of the read-coercion registry (#17637)
* fix(driver-sql): keep multi-valued boolean/toggle columns out of the read-coercion registry `formatOutput`'s `booleanFields` pass does `data[field] = Boolean(data[field])` after the `jsonFields` pass has already parsed the cell into a real array. Every non-empty array is truthy, so a `multiple: true` boolean/toggle column presented a single `true` whatever the array held — a stored `[false]` read back as `true`, the opposite of what is stored, with no error anywhere. `&& !field.multiple` is the house spelling of both registration blocks, already written three times in each (`mediaCols`, `numericCols`, `numericValueCols`); `booleanCols.push(name)` was the single omission, in BOTH fills (`registerExternalObject` and `registerManagedObjectMetadata`) — repairing one would have left the other live. The carve-out is at the registry rather than at a reader because no reader of `booleanFields` needs a multi-valued column: the #11635 Postgres aggregate cast would emit `cast(?? as int)` over a json column, `readPresentationKind` hands the same one-boolean presenter to `aggregate()`/`distinct()` where it inverts identically, `formatOutput`'s row pass is the defect, and `isNonTextColumn` already carves multi-valued out at the reader so its answer is unchanged. Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <noreply@anthropic.com> * chore(changeset): patch @objectstack/driver-sql for the multi-valued boolean read inversion Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <noreply@anthropic.com> * fix(driver-sql): pin the live-Postgres distinct() divergence instead of asserting an answer it cannot give The suite's reader-2 row asserted that `distinct()` returns the stored array for a `multiple: true` toggle column. On live PostgreSQL that statement cannot execute at all: `multiple: true` is a `json` column, `json` defines no equality operator, and `SELECT DISTINCT` needs one — so the backend refuses with "could not identify an equality operator for type json". The row was written and validated against SQLite, the only cell provisioned locally, and first ran for real on the Temporal Conformance job. The divergence is class-wide and predates this branch. Measured on live PostgreSQL 16.13, two legs, `sql-driver.ts` blob verified on disk each time: leg A blob f7fe22f (guard present) toggles/flags/nums/tags_ -> SQLSTATE 42883 leg B blob a2b37dc (= merge base) toggles/flags/nums/tags_ -> SQLSTATE 42883 both legs scalar_flag -> [false] `nums` and `tags_` were never in `booleanFields`, so no part of this change can reach them, and they fail identically; a scalar boolean — a real `boolean` column rather than `json` — answers normally in the same run. So the row is replaced by a named divergence pinned on CLASS IDENTITY: the boolean cell must fail exactly as the untouched NUMBER control does. Pinned, not skipped, and not weakened — a future edit that broke the boolean cell for a reason of its own would stop matching the control and turn this red. The envelope is asserted on SQLSTATE rather than on ADR-0112 `code`/`status` because this door leaks the backend's own object (`status` undefined) — the gap #11455 closed for `aggregate()` and left open here. Filed as #17639; asserting a 500 would have pinned a fiction. Whole driver-sql suite re-run against live PostgreSQL under CI's own zone configuration (server Asia/Shanghai, process TZ=America/New_York): 179 passed | 3 skipped, zero failures. Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <noreply@anthropic.com> * test(driver-sql): execute reader 1 on live Postgres instead of pinning it at its registry input The four-reader enumeration pinned the #11635 Postgres aggregate cast at the one term that gates it, because no live PostgreSQL existed in the round's container. One is now provisioned, so the arm is measured by reading the statements the server actually received: guard present select max("flags") as "m" -> function max(json) does not exist guard reverted select max(cast("flags" as int)) as "m" -> cannot cast type json to integer So the registry entry really was buying that reader a `cast(?? as int)` over a `json` column, and the guard stops it being emitted. Both shapes are refused by the backend, so what moves is which refusal, never a correct answer becoming an error. The assertion added is the SCALAR half — `max(scalar_flag)` must still compute `1` — because that is the half a regression could silently take away: #11635 exists so a declared boolean can be aggregated on Postgres at all. Pinning the multi-valued half would assert one dialect error string against another and would go red the day the #17590 family is ruled. Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d07fc17 commit d46deba

3 files changed

Lines changed: 593 additions & 14 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/driver-sql': patch
3+
---
4+
5+
A `multiple: true` boolean/toggle column reads back as its stored array, not as a single inverted `true`
6+
7+
`formatOutput` runs its `jsonFields` pass first, which `JSON.parse`s the cell
8+
into a real array, and then its `booleanFields` pass did
9+
`data[field] = Boolean(data[field])`. Every non-empty array is truthy, so a
10+
`multiple: true` `boolean`/`toggle` column presented a single `true` whatever
11+
the array held — a stored `[false]` read back as **`true`**, the opposite of
12+
what is stored, with no error anywhere. `readPresentationKind` hands the same
13+
presenter to the `aggregate()` / `distinct()` doors, so the collapse was not
14+
confined to the row-read door.
15+
16+
**Fixed at the registry fill.** `&& !field.multiple` is the condition the three
17+
neighbouring pushes in both registration blocks already carry (`mediaCols`,
18+
`numericCols`, `numericValueCols`); `booleanCols.push(name)` was the single
19+
omission, in **both** fills (`registerExternalObject` and
20+
`registerManagedObjectMetadata`). A `multiple: true` boolean/toggle is a JSON
21+
column here, and its array is written faithfully — only the read collapsed it.
22+
23+
**What moves for a caller.** A `find()` / `aggregate()` / `distinct()` read of a
24+
`multiple: true` `boolean` or `toggle` column now returns the stored array of JS
25+
booleans (`[false]`, `[true, false]`) where it previously returned `true`. Code
26+
that consumed the old scalar was reading a value that did not reflect storage —
27+
including for an all-`false` array. Scalar `boolean`/`toggle` columns are
28+
unchanged and keep their stored-`1`/`0` → JS `true`/`false` coercion; the
29+
`multiple: true` number and `tags` classes were already correct and do not move.

0 commit comments

Comments
 (0)