fix(avro): read doubles at full precision in the Cython decoder - #3808
Open
m0g3r wants to merge 1 commit into
Open
fix(avro): read doubles at full precision in the Cython decoder#3808m0g3r wants to merge 1 commit into
m0g3r wants to merge 1 commit into
Conversation
`CythonBinaryDecoder.read_double` was declared `cpdef float`, which in Cython is the C single-precision type, so every Avro double decoded by the fast decoder was silently rounded to 32-bit precision. Values outside the single-precision range collapse entirely: 1e308 becomes inf and 5e-324 becomes 0.0. `new_decoder` returns the Cython decoder whenever the extension is built, so this is the default read path. It affects any double read from a manifest, most visibly identity partition values on a float/double column: writing a partition value of 429496729622.314 and reading the manifest back returns 429496729600.0. The pure-Python `StreamingBinaryDecoder` was always correct, and `read_float` is unaffected because a value decoded from four bytes is already representable as a C float. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T2GWEoizz8ZGQbjatT8aZy
ebyhr
reviewed
Aug 18, 2026
| 5e-324, # underflows to 0.0 in single precision | ||
| ], | ||
| ) | ||
| def test_read_double_keeps_full_precision(decoder_class: Callable[[bytes], BinaryDecoder], value: float) -> None: |
Member
There was a problem hiding this comment.
I don't think this is a valid regression test. It passes even if we revert decoder_fast.pyx's change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rationale for this change
CythonBinaryDecoder.read_doubleis declaredcpdef float read_double(self). In Cython,floatis the C single-precision type, so the correctly decoded 64-bit value returned bySTRUCT_DOUBLE.unpackis narrowed to 32 bits on the way out. Every Avrodoubleread through the fast decoder is silently rounded, and values outside the single-precision range collapse entirely.new_decoderreturnsCythonBinaryDecoderwhenever the extension is built, so this is the default read path; the pure-PythonStreamingBinaryDecoderfallback was always correct.The user-visible effect is on any
doubleread out of a manifest, most directly an identity partition value on afloat/doublecolumn. Writing a manifest with a partition value of429496729622.314and reading it back throughManifestFile.fetch_manifest_entryreturns429496729600.0onmain, and the exact value with this change.read_floatis left ascpdef float: a value decoded from four bytes is already exactly representable as a C float, so no precision is lost there.The bug dates back to the original Cython decoder (#8134, 2023). It was not caught by
tests/avro/test_decoder.py::test_read_doublebecause19.25is exactly representable in single precision.tests/avro/test_file.py::test_all_primitive_typesdoes round-trip adoublethat is not, but its assertion loop iteratesenumerate(all_primitives_schema.as_struct())— iterating the pydantic model yields its two model fields (type,fields), not the 13 schema fields — so only positions 0 and 1 were ever compared. I kept that out of this PR to keep it to one concern, and am happy to send the test fix as a follow-up (or fold it in here if you would rather).Are these changes tested?
Yes.
tests/avro/test_decoder.pygainstest_read_double_keeps_full_precision, parametrized over both decoder implementations and six doubles that are not representable in single precision, including the 1e308 overflow and 5e-324 underflow cases.Verified red/green by rebuilding the extension against the pre-change
decoder_fast.pyx:6 failed, 8 passed— every failure is theCythonBinaryDecoderparametrization, e.g.assert 0.0 == 5e-32456 passedintests/avro/test_decoder.pyFull local run on macOS/arm64, Python 3.13:
make lintall 12 hooks pass,make testgives3957 passed, 3 skipped, 1570 deselected.Integration tests (Spark/Docker) were not run locally.
Are there any user-facing changes?
Yes —
doublevalues read from Avro are no longer rounded to single precision. This is a bug fix; existing manifests do not need to be rewritten, since the data on disk was always correct and only the decode was lossy.Disclosure: this change was written with AI assistance (Claude Code). The bug was found by auditing the Cython decoder's C return types, then confirmed against the pure-Python decoder and end to end through a manifest write/read round trip.