Skip to content

fix(mysql2): decode JSON columns instead of returning null (#9349) - #9350

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/9349-mysql-json-columns
Sep 1, 2026
Merged

fix(mysql2): decode JSON columns instead of returning null (#9349)#9350
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/9349-mysql-json-columns

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #9349.

Every MySQL JSON column read back as null — through drizzle's typed builder, through db.execute, and through the raw driver alike. A VARCHAR in the same row decoded correctly, so this was specific to the JSON column type.

Cause

Two layers, and the second one is why the obvious fix doesn't work on its own:

  1. extract_raw_value has no "JSON" arm, so JSON falls to the catch-all, which tries try_get::<String> then try_get::<Vec<u8>>. Both fail their sqlx type check and the column becomes RawValue::Null.
  2. Underneath that, sqlx is built here without its json feature, so MySQL JSON has no Decode impl at all. I patched the arm first, rebuilt, and the value was still null — adding an arm alone changes nothing.

A note for whoever touches this next: there are two independent copies of extract_raw_value, one in perry-stdlib/src/mysql2/result.rs and one here. A createPool program runs this one. I patched the stdlib copy first and spent a rebuild cycle wondering why nothing changed.

The fix

  • Enable sqlx's json feature and decode to serde_json::Value.
  • Add a RawValue::Json variant and a "JSON" arm ahead of the catch-all.
  • Materialise it into a real JS object graph, not text — mysql2 in Node returns the decoded value, and drizzle's json() mapper plus every ordinary property access depend on that.

json_value_to_jsvalue builds the graph from the object/array primitives perry_ffi already exports. It does not hand the text to the runtime's parser: perry_ffi exports json_stringify and no counterpart, and adding a json_parse to the public ABI felt like a wider change than this bug warrants. Say the word if you'd rather have the ABI export and reuse the existing parser — it would be less code here.

serde_json's preserve_order keeps object keys in document order, matching Node. Without it the map is a BTreeMap and keys come back alphabetised.

Verification

Against MySQL 8 on 0.5.1519, output is now byte-identical to Node's:

doc    : {"on":true,"name":"läuft","tiers":[{"step":25,"upTo":500},{"step":100,"upTo":null}]}
arr    : [1,2.5,"x",null,true,{"k":"v"}]
scalar : 42 number
nul    : null
nested : 25 | bool true | utf8 läuft | null-in-obj null
entries: 3 keys
isArray: true len 6

Covering nested objects and arrays, floats, booleans, null inside an object, UTF-8, scalar documents, SQL NULL, Array.isArray and Object.entries. Before the change every one of those printed null.

Compiles clean on both main (sqlx 0.9.0) and an older base carrying sqlx 0.8.6.

Why it mattered

The failure was silent, because null is legal for a nullable JSON column. On a real deployment it surfaced four layers away as TypeError: Cannot convert undefined or null to object, having already wedged a scheduler loop — while the health endpoint kept answering ok, since it runs SELECT 1, the one query with no columns to decode.

The affected columns there were an auction's bid-increment ladder, an audit log's before/after snapshots and a shipment's tracking payload, so the visible symptom was not an error but wrong numbers and a blank audit trail.

Summary by CodeRabbit

  • Bug Fixes
    • MySQL JSON columns are now returned as parsed JavaScript values instead of NULL.
    • JSON data now round-trips correctly, matching expected mysql2 behavior.

)

Every MySQL JSON column read back as `null` — through drizzle's typed builder,
through `db.execute`, and through the raw driver alike. A VARCHAR in the same
row decoded correctly, so this was specific to the JSON column type.

`extract_raw_value` had no "JSON" arm, so JSON fell through to the catch-all,
which tries `try_get::<String>` and then `try_get::<Vec<u8>>`. Both fail their
sqlx type check, and the column became `RawValue::Null`.

Underneath that, sqlx was built here without its "json" feature, so MySQL JSON
had no Decode impl at all and adding an arm alone would not have helped. This
enables it and decodes to `serde_json::Value`.

The value is materialised into a real JS object graph rather than handed back
as text: mysql2 in Node returns the decoded value, and drizzle's `json()`
mapper — along with every ordinary property access — depends on that.
`perry_ffi` exports `json_stringify` and no counterpart, and adding a
`json_parse` to the public ABI seemed a wider change than this bug warrants,
so `json_value_to_jsvalue` builds it from the object and array primitives
`perry_ffi` already exports.

serde_json's "preserve_order" keeps object keys in document order, matching
Node. Without it the map is a BTreeMap and keys come back alphabetised.
Nothing should depend on JSON object key order, but silently reordering a
document that round-trips through a database is the kind of difference that
surfaces much later, in a diff nobody can explain.

Verified against MySQL 8 on 0.5.1519: output is now byte-identical to Node's
for nested objects and arrays, floats, booleans, nulls inside objects, UTF-8
strings, scalar documents, SQL NULL, `Array.isArray` and `Object.entries`.

Why it mattered: the failure was silent, because `null` is legal for a
nullable JSON column. On a real deployment it surfaced four layers away as
`TypeError: Cannot convert undefined or null to object`, having already wedged
a scheduler loop — while the health endpoint kept answering ok, since it runs
`SELECT 1`, the one query with no columns to decode.
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 162c1042-2bf8-4eed-b7e9-d3ba5799a96e

📥 Commits

Reviewing files that changed from the base of the PR and between c78cf37 and e12c88f.

📒 Files selected for processing (2)
  • crates/perry-ext-mysql2/Cargo.toml
  • crates/perry-ext-mysql2/src/lib.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The MySQL extension now decodes JSON columns with serde_json and recursively converts them into JavaScript values. Object key order is preserved. JSON extraction and conversion use newly enabled dependency features.

Changes

MySQL JSON support

Layer / File(s) Summary
Decode and materialise MySQL JSON values
crates/perry-ext-mysql2/Cargo.toml, crates/perry-ext-mysql2/src/lib.rs
The extension enables sqlx JSON support and serde_json with preserved object order. extract_raw_value stores decoded JSON in RawValue::Json. raw_value_to_jsvalue recursively converts JSON values into JavaScript values.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: 🔵 Low · up to e12c8

MySQL JSON columns will now return usable JavaScript values instead of null. Because large or deeply nested documents are converted synchronously, they could consume process CPU, memory, or stack and affect availability; the PR is mergeable with explicit owner awareness or follow-up on resource limits.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: decoding MySQL JSON columns instead of returning null.
Description check ✅ Passed The description is detailed and relevant. It explains the symptom, root causes, implementation, verification results, and linked issue. It does not follow the template headings exactly and does not in…
Linked Issues check ✅ Passed The changes address issue #9349. They enable SQL JSON decoding, add explicit JSON extraction, materialize nested JSON values as JavaScript objects and arrays, preserve key order, and support scalar va…
Out of Scope Changes check ✅ Passed The dependency changes and mysql2 implementation changes are directly related to restoring MySQL JSON reads. No unrelated code or scope expansion is evident.
Full details: Description check

Explanation

The description is detailed and relevant. It explains the symptom, root causes, implementation, verification results, and linked issue. It does not follow the template headings exactly and does not include the checklist, but the required information is mostly present.

Full details: Linked Issues check

Explanation

The changes address issue #9349. They enable SQL JSON decoding, add explicit JSON extraction, materialize nested JSON values as JavaScript objects and arrays, preserve key order, and support scalar values, nulls, UTF-8, and normal array/object operations.

Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug merged commit 46a7250 into PerryTS:main Sep 1, 2026
31 of 34 checks passed
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.

mysql2: every JSON column reads back as null (typed builder, db.execute and raw driver alike)

1 participant