mysql/json: read character data's numbers as the double MySQL stores - #20726
mysql/json: read character data's numbers as the double MySQL stores#20726arthurschreiber wants to merge 1 commit into
Conversation
Review ChecklistHello reviewers! 👋 Please follow this checklist when reviewing this Pull Request. General
Tests
Documentation
New flags
If a workflow is added or modified:
Backward compatibility
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## arthur/json-immutable-values #20726 +/- ##
=================================================================
+ Coverage 73.00% 86.40% +13.40%
=================================================================
Files 6 65 +59
Lines 1715 20918 +19203
=================================================================
+ Hits 1252 18075 +16823
- Misses 463 2843 +2380
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
MySQL parses JSON numbers at normal precision: the significand accumulates into a double and one multiplication scales it, landing an ULP or two from the nearest double for long significands and large exponents. Vitess read the same text correctly rounded, so the same expression could produce a different value evaluated at vtgate than pushed down to MySQL. Parser gains ParseCast and ParseCastBytes, which read numbers with the same conversion the validity check already transcribes, now returning the value. A float's kind settles at parse time, so reading it never writes back. The cast reading applies wherever mysqld would parse the same bytes as text: CAST to JSON, string arguments to JSON functions, and JSON-typed bind variables — a parameter reaches mysqld only as text for its document parser to read. JSON columns keep the exact reading: their text spells the double the tablet's mysqld already holds, and correct rounding reconstructs it losslessly, while re-reading it MySQL's way would move roughly a third of shortest-printed doubles by an ULP. Decimal() on a float derives from the double's shortest text, which is how MySQL's double2decimal converts (through my_gcvt), so comparisons, hashing and weight strings all follow the stored double. Verified over 4,649 documents against MySQL 8.0.45, 8.4.11 and 9.4.0 on arm64 and 8.4.11 on x86_64: zero validity or value mismatches, and bit-exact agreement with MySQL's vendored RapidJSON compiled the way MySQL builds it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
63277ea to
67b1fd0
Compare
| // so its numbers convert the way MySQL converts them. A JSON column's text, | ||
| // by contrast, spells the double the tablet's mysqld already holds, and | ||
| // converts back to exactly that double. | ||
| func valueToEvalBindVar(value sqltypes.Value, collation collations.TypedCollation, values *EnumSetValues) (eval, error) { |
There was a problem hiding this comment.
We should distinguish client parameters from internally generated bind variables here. A TypeJSON bind variable is not always character-origin: UncorrelatedSubquery, for example, wraps a typed result row with:
combinedVars[ps.SubqueryResult] =
sqltypes.ValueBindVariable(result.Rows[0][0])If that result is a JSON column, its text already spells the stored binary-JSON double exactly. Running it through ParseCastBytes can round it again; -5.3746011104623175e107, for example, moves from 0xd64d4ae72831c4f2 to 0xd64d4ae72831c4f3.
The cast-style parsing should happen specifically at the external-parameter boundary, while bind variables synthesized from typed result rows retain the exact/printed parsing path.
|
This PR is being marked as stale because it has been open for 30 days with no activity. To rectify, you may do any of the following:
If no action is taken within 7 days, this PR will be closed. |
Description
Stacked on #20722 and #20723 — only the last commit is new here; the base retargets down the stack as those merge.
#20722 made Vitess agree with MySQL on which JSON documents exist. This one makes it agree on what their numbers are worth. MySQL parses JSON numbers at normal precision (significand accumulated into a double, one multiplication to scale it), so for long significands and large exponents its stored double sits an ULP or two away from the correctly-rounded one Vitess produced — for 17+ significant digits that's a majority of numbers, so the same expression could produce a different value evaluated in vtgate than pushed down.
The parser gains
ParseCast/ParseCastBytes, which read numbers with the same conversion the validity check already transcribes, now returning the value. A float's kind settles at parse time, in keeping with #20723 — reading a value never writes back, and the mode is per-call, so a pooled parser cannot carry one caller's choice into the next parse. Where the cast reading applies is decided by one question: what would mysqld do with the same bytes?CAST(... AS JSON), string arguments to JSON functions, and JSON-typed bind variables — gets MySQL's conversion, because mysqld would parse that text with its document parser (a parameter can't reach mysqld any other way).Decimal()on a float now derives from the double's shortest round-trip text, which is what MySQL'sdouble2decimaldoes (it prints throughmy_gcvtand reads the digits back) — with that, JSON comparisons, hashing and weight strings all follow the stored double with no changes of their own.Verified over 4,649 documents against real MySQL 8.0.45, 8.4.11 and 9.4.0 on arm64 plus 8.4.11 on x86_64: zero validity or value mismatches, and bit-exact agreement with MySQL's vendored RapidJSON compiled the way MySQL builds it. The expected bits in the tests were confirmed inside the servers with
JSON_EXTRACT(...) = CAST(... AS DOUBLE)probes.Backport justification: on a supported release, the same expression answers differently depending on whether it runs in vtgate or is pushed down to MySQL — the divergence this PR removes is itself the bug, and it bites hardest when one query mixes both, like a scatter query with vtgate-side post-processing over documents MySQL parsed. Values move only for numbers with 16+ significant digits or extreme exponents, the same class whose validity #20722 already changes on those branches.
Related Issue(s)
Same class as #20720/#20724, in the value rather than in whether the document is valid. Stacked on #20722 and #20723.
Checklist
Deployment Notes
User-visible: numbers with 16+ significant digits or large exponents in JSON built from character data convert, compare, hash and print by MySQL's double instead of the nearest one. Numbers with up to 15 significant digits and moderate exponents don't move, and JSON column values are unaffected. Release note included in
changelog/25.0/25.0.0/summary.md.AI Disclosure
This PR was written primarily by Claude Code — investigation, implementation and tests — with direction and review from me.