8000 fix(sqlite): type a scalar subquery's json inside other json as unknown by colinhacks · Pull Request #52 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(sqlite): type a scalar subquery's json inside other json as unknown - #52

Closed
colinhacks wants to merge 5 commits into
mainfrom
sqlite-subtype
Closed

colinhacks wants to merge 5 commits into
mainfrom
sqlite-subtype

Conversation

@colinhacks
@colinhacks colinhacks commented Oct 22, 2026 •
Copy link
Copy Markdown
Owner

What SQLite 3.54 changed

SQLite marks the JSON its functions build with a value subtype, and a JSON builder nests a value that carries it as JSON rather than as a string. SQLite 3.53.4 kept that subtype through a scalar subquery; SQLite 3.54.1 drops it. So a scalar subquery's JSON passed into another builder nests as JSON on 3.53 and as a string on 3.54, while Reylo typed it as the JSON it builds on both: on 3.54 (Node 26's node:sqlite, and every driver as it upgrades) the value contradicted its type. tests/representation.test.ts failed under Node 26.11 on main for this reason.

Measured with node:sqlite (SQLite 3.54.1) against better-sqlite3 (SQLite 3.53.4), 66 constructs. Every one that differs reads a scalar subquery:

Construct 3.54.1 3.53.4
json_object('p', (select json_group_array(1))) {"p":"[1]"} {"p":[1]}
json_object('p', (select json_array(1))), a correlated one, limit 1, group by, distinct, union all, values, nested string JSON
json_array((select …)), json_group_array((select …)), json_group_object, json_set/json_insert/json_replace string JSON
coalesce, ifnull, nullif, iif, case, likely over a scalar subquery string JSON
a scalar subquery of json_extract or -> string JSON
json_object('p', json((select json_group_array(1)))) {"p":[1]} {"p":[1]}
json_object('p', jsonb((select …))), a scalar subquery of jsonb_array JSON JSON
json_object('p', json((select json_array(x) from t where 0))) {"p":null} {"p":null}

Unchanged between the two: coalesce, case, iif, ifnull, nullif, likely, COLLATE, unary plus, a cast to text, min/max (scalar, aggregate, with a FILTER) over built JSON directly; a subquery's, WITH query's or view's column, lag and first_value (dropped on both); window calls; a sorted scalar subquery (dropped on both); and a scalar subquery returned as a column of its own, whose text Reylo parses by the column's type.

What the types are now

  • A scalar subquery's JSON inside another JSON builder, directly or through coalesce, case, iif and the rest, is unknown, the convention for a value that may or may not keep the marker.
  • json(…) or jsonb(…) of a value that is built JSON, with or without the marker (a scalar subquery, a subquery's or view's column, lag, a coalesce of them), is typed as the JSON it builds, and is nullable where the value is: json() parses the text back into the same JSON on every version. A column json((select json_group_array(…))) is now that array, parsed, where it was a string.
  • A scalar subquery returned as a column of its own keeps its type.
  • lag(x, n, d) and lead(x, n, d) are typed from both x and their default d. They had been typed from x alone, which json() now trusts: lag(json_array(id), 1, '{"a":1}') is string | null, not [number] | null.
  • The driver matrix's S10j wraps its scalar subqueries in json(). With bare subqueries it fails on SQLite 3.54.

The SQLite page's JSON section now says to wrap a scalar subquery in json(…) to nest it, and lists the scalar subquery among what loses the marker.

Test plan

  • cargo test -p reylo-engine-core -p reylo-engine-sqlite: pass (new cases for each construct above, json() over subqueries, columns and lag)
  • packages/sqlite/engine.wasm rebuilt from these sources in one run of engine/build.sh
  • cargo run -p reylo-oracle -- --strict-coverage on all 24 SQLite cases: 0 failed (sqlite_json blessed, and lag_default.sql added: the new scalar-subquery members are unknown; kept.sql and profile.sql wrap their subqueries in json() and keep their shapes)
  • nub run typecheck: pass
  • nub run test under Node 26.11 with Postgres 18 and MySQL 8.4 test servers: 209 pass, 0 fail, 8 skipped (labs not installed: connection tests and Jest). The representation test now runs the profile through json() on every driver (better-sqlite3, node:sqlite, libSQL, Turso, sqlite3) and checks that a bare subquery's member, typed unknown, is the array or its text
  • Driver matrix SQLite jobs (runner.ts --only), on Node 26.11.1 and Bun 1.4.3: S10j passes on every SQLite driver. Turso 0.4.4 skips it as before. With the old, bare query it fails on node:sqlite 26.11.1
  • nub run build and nub run capture in site/: pass, no captured type changed

Copilot AI balanced review requested due to automatic review settings October 22, 2026 23:46
@vercel
vercel Bot commented Oct 22, 2026 •
Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 23, 2026 12:13am UTC

Request Review

Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

json(…) now trusts type_of's built type, and type_of drops the default argument of lag/lead. So json(lag(x, 1, '<other json>') over …) nested in JSON gets a concrete type that its first row contradicts. On main this was unknown. Details inline.

Reviewed changes

The full PR: the SQLite engine's handling of scalar subqueries and json() re-parsing, the sqlite_json oracle case, the rebuilt artifact, the SQLite docs page and the representation test.

  • Scalar subquery's JSON is unknown inside other JSON: carry's select arm now returns Maybe instead of Yes, and json_fact's select arm becomes any. This matches 3.54.1 dropping the subtype.
  • json()/jsonb() of built JSON is that JSON again: the new Analyzer::reparsed types json(x) from type_of(x) whenever that type is built JSON, keeping x's nullability. built_text now returns a Fact, so a top-level json((select …)) column is that JSON, parsed, and nullable where the subquery is.
  • Oracle, docs and representation test: kept.sql and profile.sql wrap their subqueries in json(). dropped.sql adds the bare forms as unknown. The docs say to wrap a scalar subquery in json(…) to nest it. The test accepts either the array or its text for a bare subquery.
  • Artifact: packages/sqlite/engine.wasm matches its sources (tests/fingerprint.test.ts passes).

ℹ️ The driver matrix's builtJson still nests bare scalar subqueries

lab/driver-matrix/lib/harness.ts:313 builds 'scores', (select json_group_array(…)) and 'none', (select json_group_array(…)) without json(). Its check S10j (:897) expects arrays. On any SQLite 3.54 driver, both come back as strings, so S10j fails. The comment above the query says it follows the types tests/representation.test.ts pins, and that test now wraps its profile in json(). CI doesn't run the matrix, so nothing turns red until someone runs it.

Technical details
# Driver matrix builtJson nests bare scalar subqueries

## Affected sites
- lab/driver-matrix/lib/harness.ts:313 — `scores` and `none` are bare scalar subqueries inside `json_object`
- lab/driver-matrix/lib/harness.ts:897 — S10j expects `scores: [...]`, `none: []` on every driver

## Required outcome
- The query nests the subqueries the way the docs now recommend (`json((select …))`), so S10j holds on SQLite 3.53 and 3.54 drivers alike; `RESULTS.md` regenerated if the row changes

ℹ️ Artifact built alone

The PR body says packages/sqlite/engine.wasm was rebuilt with engine/build.sh sqlite. According to AGENTS.md, a single-artifact build has the right fingerprint but different bytes from a build of all three. Building all three in one run gives the bytes any checkout reproduces.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread engine/sqlite/src/sqlite.rs Resolved
@colinhacks
Copy link
Copy Markdown
Owner Author

Thanks. On the two notes in the review summary:

  • Driver matrix: fixed in 8301f23. builtJson wraps both scalar subqueries in json(), and S10j's README row says so. With the bare subqueries, S10j failed on every node:sqlite 26.11.1 shape (SQLite 3.54.1). With them wrapped, it passes on node:sqlite 26.11.1, better-sqlite3 13.0.3 and 12.11.1, every libSQL, Turso 0.6.1, 0.7.2 and 0.8.3 (0.4.4 skips as before), sqlite3, the sqlite wrapper, drizzle, bun:sqlite and Bun.sql (runner.ts --only, so RESULTS.md is not rewritten).
  • Artifact: packages/sqlite/engine.wasm is now built in one run of all three (engine/build.sh). Only the SQLite artifact is committed, since it is the only engine this change touches.

@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. Both points from the last review are fixed.

Reviewed changes

This review covers the two commits pushed since the last Pullfrog review, aba29f8 and 8301f23.

  • lag/lead typed from their default too: with three arguments, the window arm now types the call with shared over x and d, skipping a NULL literal default. Built JSON next to a text default becomes text, so json() of it is unknown. This fixes the nested case the last review flagged and the top-level column that was already wrong on main. The engine tests and the new oracle statement lag_default.sql cover the three-argument form at the top level and inside json_object(… json(…)), and engine.wasm was rebuilt with it.
  • Driver matrix's S10j nests through json(): builtJson now wraps both scalar subqueries in json(), so S10j holds on SQLite 3.53 and 3.54 drivers. The README row is updated. The Turso 0.4 skip still applies, since json(NULL) is still NULL.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

SQLite 3.54 drops the JSON subtype a scalar subquery kept on 3.53, so
its built JSON nests as a string inside another builder on one version
and as JSON on the other. json() around the value nests it on both, and
now keeps the shape it builds.
@colinhacks
Copy link
Copy Markdown
Owner Author

Closing: #51 landed the scalar-subquery fix and #49 the lag/lead default fix. Checked against main (b480b81): every construct that changes on SQLite 3.54 (coalesce/ifnull/nullif/iif/case/likely over a scalar subquery, a scalar subquery inside json_array/json_group_array/json_group_object, json_extract or -> in a scalar subquery) is already unknown, and json_set/json_insert/json_replace columns are strings, so nothing in this PR is still needed.

@colinhacks colinhacks closed this Oct 23, 2026
@colinhacks
colinhacks deleted the sqlite-subtype branch October 23, 2026 03:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0