8000 fix(sqlite): type a scalar subquery's json nested in json soundly by colinhacks · Pull Request #51 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(sqlite): type a scalar subquery's json nested in json soundly - #51

Merged
colinhacks merged 2 commits into
mainfrom
sqlite-subquery-json
Oct 23, 2026
Merged

colinhacks merged 2 commits into
mainfrom
sqlite-subquery-json

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

SQLite 3.54.1, the SQLite inside Node 26.11's node:sqlite, drops the JSON subtype of a scalar subquery's value. So JSON built in a subquery and nested in other JSON comes back as text there, where SQLite 3.53 (better-sqlite3) nests it as JSON:

select json_object('n', (select json_object('a', 1)))
-- SQLite 3.53.4: {"n":{"a":1}}
-- SQLite 3.54.1: {"n":"{\"a\":1}"}

Reylo typed n as the object, which is wrong on 3.54. Neither 3.54.0's nor 3.54.1's release notes mention the change, so both behaviors are out there, depending on the driver's SQLite.

  • A scalar subquery's JSON nested in a JSON function is now unknown (engine/sqlite/src/sqlite.rs, carry): it may be the JSON or its text.
  • json((select …)) marks the subquery's text as JSON again on every SQLite, so it now takes the shape the subquery builds, nested or at the top of the select list. It's nullable where the subquery may find no row.
  • A scalar subquery selected as a column of its own is unaffected, since the run time parses its text either way. That's how the main docs' SQLite examples use it.

The SQLite docs page's "Built JSON inside built JSON" rule now says to wrap the subquery in json(…). Its new pair was run through .run() on node:sqlite (SQLite 3.54.1) and better-sqlite3 (3.53.4).

This also fixes tests/representation.test.ts › "SQLite: JSON a statement builds…", which fails on main under Node 26.11 (CI runs Node 24, so it stayed green): the test's profile and the driver matrix's S10j statement now wrap their subqueries in json().

Tests: the engine's unit tests (the old expectations moved to the unknown list, plus json() cases and a no-row case); the oracle case sqlite_json (profile wrapped, and a new subquery_json.sql); the full oracle with --strict-coverage; the wasm fingerprint; the loader tests under Node 26.11.1; and the driver matrix's node:sqlite and better-sqlite3@13 jobs under Node 26.11.1, S10j included.

Copilot AI balanced review requested due to automatic review settings October 22, 2026 23:39
@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 3:12am 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

The new "json() of a scalar subquery is NULL" rule fires for every JSON constructor whose first argument is a subquery, not just json(). Constructors that never return NULL get typed | null.

Reviewed changes

Reviewed the single commit, which makes a scalar subquery's JSON unknown when it is nested in other JSON, and types json((select …)) as the shape the subquery builds.

  • carry / json_fact for select: a scalar subquery's value is now Carry::Maybe (so unknown inside JSON) unless it is plainly not JSON, because SQLite 3.54.1 drops the subtype.
  • subquery_shape: json((select <constructor>)) takes the shape of the subquery's one column, through both built_text and shape_here. Compounds and VALUES are excluded.
  • Nullability of json((select …)): nullable when the subquery may find no row, both at the top level (type_of) and nested (json_fact).
  • Oracle, tests, docs, driver matrix: kept.sql moves to unknown, a new subquery_json.sql covers the json() forms, the profile statements wrap their subqueries in json(), and the SQLite docs' rule says to do the same. engine.wasm is rebuilt.

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

Comment thread engine/sqlite/src/sqlite.rs Resolved
SQLite 3.54.1, which Node 26.11's node:sqlite bundles, drops the JSON
subtype of a scalar subquery's value, so json_object('n', (select ...))
nests it as text where 3.53 nests it as JSON. Such a value is now
unknown, and json() around the subquery nests its JSON on every SQLite.
Only json() and jsonb() of a scalar subquery are NULL where it finds no row; json_array and the aggregates read that NULL as a null.
@colinhacks
colinhacks force-pushed the sqlite-subquery-json branch from 0d353ce to 5f165fe Compare October 23, 2026 03:11
@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.

Reviewed changes

Reviewed 5f165fe, which narrows the subquery-nullability rule from the previous review to json() and jsonb().

  • type_of "function" arm: a constructor's nullability now follows its subquery argument only when the call is json(). jsonb() never reaches this arm, because built_text doesn't cover it, but it already becomes nullable through Nulls::IfArgs.
  • json_fact shape branch: the same rule is now limited to json and jsonb, so json_array and json_group_array of a subquery stay non-null where they are nested.
  • Tests: added a top-level json_typed assertion for json_array, json_group_array and json_group_object, two kept-list rows, and an oracle statement, subquery_null.sql, whose expected types ([string | null], [text?]) match what SQLite returns: [null].

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

@colinhacks
colinhacks merged commit 0d1df64 into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the sqlite-subquery-json branch October 23, 2026 03:19
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