You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by fo...@apache.org on 2026/09/01 19:46:25 UTC

(iceberg-python) branch main updated: fix(avro): read doubles at full precision in the Cython decoder (#3808)

This is an automated email from the ASF dual-hosted git repository.

Fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new d12f68042 fix(avro): read doubles at full precision in the Cython decoder (#3808)
d12f68042 is described below

commit d12f68042a459e5d4732bd6ee5a188e891b0dd44
Author: m0g3r <87...@users.noreply.github.com>
AuthorDate: Tue Sep 1 21:46:19 2026 +0200

    fix(avro): read doubles at full precision in the Cython decoder (#3808)
    
    `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.
    
    
    Claude-Session: https://claude.ai/code/session_01T2GWEoizz8ZGQbjatT8aZy
    
    Co-authored-by: Claude Opus 5 <no...@anthropic.com>
---
 pyiceberg/avro/decoder_fast.pyx |  2 +-
 tests/avro/test_decoder.py      | 17 +++++++++++++++++
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/pyiceberg/avro/decoder_fast.pyx b/pyiceberg/avro/decoder_fast.pyx
index 52caec330..ffd23dd97 100644
--- a/pyiceberg/avro/decoder_fast.pyx
+++ b/pyiceberg/avro/decoder_fast.pyx
@@ -138,7 +138,7 @@ cdef class CythonBinaryDecoder:
         """
         return float(STRUCT_FLOAT.unpack(self.read(4))[0])
 
-    cpdef float read_double(self):
+    cpdef double read_double(self):
         """Reads a value from the stream as a double.
 
         A double is written as 8 bytes.
diff --git a/tests/avro/test_decoder.py b/tests/avro/test_decoder.py
index 163ad8405..1cf834634 100644
--- a/tests/avro/test_decoder.py
+++ b/tests/avro/test_decoder.py
@@ -160,6 +160,23 @@ def test_read_double(decoder_class: Callable[[bytes], BinaryDecoder]) -> None:
     assert decoder.read_double() == 19.25
 
 
+@pytest.mark.parametrize("decoder_class", AVAILABLE_DECODERS)
+@pytest.mark.parametrize(
+    "value",
+    [
+        3.141592653589793,
+        429496729622.314,
+        0.1,
+        1.0000000000000002,  # smallest double above 1.0
+        1e308,  # overflows to inf in single precision
+        5e-324,  # underflows to 0.0 in single precision
+    ],
+)
+def test_read_double_keeps_full_precision(decoder_class: Callable[[bytes], BinaryDecoder], value: float) -> None:
+    decoder = decoder_class(struct.pack("<d", value))
+    assert decoder.read_double() == value
+
+
 @pytest.mark.parametrize("decoder_class", AVAILABLE_DECODERS)
 def test_skip_double(decoder_class: Callable[[bytes], BinaryDecoder]) -> None:
     decoder = decoder_class(b"\x00\x00\x00\x00\x00\x40\x33\x40")