fix(mysql2): decode JSON columns instead of returning null (#9349) - #9350
Conversation
) 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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe MySQL extension now decodes ChangesMySQL JSON support
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: 🔵 Low · up to 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)
✅ Passed checks (4 passed)
Full details: Description checkExplanation 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 checkExplanation The changes address issue Full details: Docstring CoverageExplanation 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.)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Fixes #9349.
Every MySQL
JSONcolumn read back asnull— through drizzle's typed builder, throughdb.execute, and through the raw driver alike. AVARCHARin 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:
extract_raw_valuehas no"JSON"arm, so JSON falls to the catch-all, which triestry_get::<String>thentry_get::<Vec<u8>>. Both fail their sqlx type check and the column becomesRawValue::Null.jsonfeature, so MySQL JSON has noDecodeimpl 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 inperry-stdlib/src/mysql2/result.rsand one here. AcreatePoolprogram runs this one. I patched the stdlib copy first and spent a rebuild cycle wondering why nothing changed.The fix
jsonfeature and decode toserde_json::Value.RawValue::Jsonvariant and a"JSON"arm ahead of the catch-all.json()mapper plus every ordinary property access depend on that.json_value_to_jsvaluebuilds the graph from the object/array primitivesperry_ffialready exports. It does not hand the text to the runtime's parser:perry_ffiexportsjson_stringifyand no counterpart, and adding ajson_parseto 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'spreserve_orderkeeps object keys in document order, matching Node. Without it the map is aBTreeMapand keys come back alphabetised.Verification
Against MySQL 8 on 0.5.1519, output is now byte-identical to Node's:
Covering nested objects and arrays, floats, booleans,
nullinside an object, UTF-8, scalar documents, SQLNULL,Array.isArrayandObject.entries. Before the change every one of those printednull.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
nullis legal for a nullable JSON column. On a real deployment it surfaced four layers away asTypeError: Cannot convert undefined or null to object, having already wedged a scheduler loop — while the health endpoint kept answeringok, since it runsSELECT 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
NULL.