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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 414080b718 [python] Preserve high-precision DECIMAL values in the row 
file format (#10124)
414080b718 is described below

commit 414080b71820570fe2a33063a5cb47049c2721c5
Author: jackylee <[email protected]>
AuthorDate: Thu Sep 24 10:46:26 2026 +0800

    [python] Preserve high-precision DECIMAL values in the row file format 
(#10124)
---
 .../pypaimon/read/reader/format_row_reader.py      | 10 ++--
 .../tests/test_format_row_reader_writer.py         | 62 ++++++++++++++++++++++
 .../pypaimon/write/writer/format_row_writer.py     | 17 +++---
 3 files changed, 77 insertions(+), 12 deletions(-)

diff --git a/paimon-python/pypaimon/read/reader/format_row_reader.py 
b/paimon-python/pypaimon/read/reader/format_row_reader.py
index 56ddd15fc0..21b7e75c3f 100644
--- a/paimon-python/pypaimon/read/reader/format_row_reader.py
+++ b/paimon-python/pypaimon/read/reader/format_row_reader.py
@@ -16,7 +16,7 @@
 # under the License.
 
 import struct
-from decimal import Decimal
+from decimal import Decimal, localcontext
 from typing import Any, List, Optional, Tuple
 
 import pyarrow as pa
@@ -483,11 +483,15 @@ def _read_field(decoder: _RowDecoder, data_type) -> Any:
             precision, scale = _parse_decimal_params(type_name)
             if precision <= 18:
                 unscaled = decoder.read_long()
-                return Decimal(unscaled) / Decimal(10 ** scale)
             else:
                 raw = decoder.read_bytes()
                 unscaled = int.from_bytes(raw, byteorder='big', signed=True)
-                return Decimal(unscaled) / Decimal(10 ** scale)
+            # Rescale under a context wide enough for the column: the default 
28-digit
+            # precision would round a DECIMAL(p) value with more than 28 
significant
+            # digits, silently corrupting it. Mirrors pypaimon/data/decimal.py.
+            with localcontext() as ctx:
+                ctx.prec = max(precision + abs(scale), 38)
+                return Decimal(unscaled).scaleb(-scale)
         elif type_name.startswith('TIMESTAMP'):
             precision = _parse_timestamp_precision(type_name)
             millis = decoder.read_long()
diff --git a/paimon-python/pypaimon/tests/test_format_row_reader_writer.py 
b/paimon-python/pypaimon/tests/test_format_row_reader_writer.py
index 2023dd3fdd..d8b5d9b7d1 100644
--- a/paimon-python/pypaimon/tests/test_format_row_reader_writer.py
+++ b/paimon-python/pypaimon/tests/test_format_row_reader_writer.py
@@ -88,6 +88,68 @@ class TestFormatRowReaderWriter:
         finally:
             os.unlink(path)
 
+    def test_high_precision_decimal(self):
+        # DECIMAL(p) with more than 28 significant digits overflows Python's 
default
+        # decimal context, which silently rounded the value on both write and 
read.
+        # Cover positive and negative (signed-byte path) high-precision values 
and a
+        # null, plus the 19..28 band and the compact p<=18 path.
+        fields = [
+            DataField(0, "d_int", AtomicType("DECIMAL(38, 0)")),
+            DataField(1, "d_frac", AtomicType("DECIMAL(38, 10)")),
+            DataField(2, "d_band", AtomicType("DECIMAL(28, 4)")),
+            DataField(3, "d_small", AtomicType("DECIMAL(18, 2)")),
+        ]
+        d_int = [Decimal("12345678901234567890123456789012345678"),
+                 Decimal("-12345678901234567890123456789012345678"), None]
+        d_frac = [Decimal("1234567890123456789012345678.9012345678"),
+                  Decimal("-1234567890123456789012345678.9012345678"), None]
+        d_band = [Decimal("123456789012345678901234.5678"),
+                  Decimal("-123456789012345678901234.5678"), None]
+        d_small = [Decimal("1234.56"), Decimal("-1234.56"), None]
+        data = pa.table({
+            "d_int": pa.array(d_int, type=pa.decimal128(38, 0)),
+            "d_frac": pa.array(d_frac, type=pa.decimal128(38, 10)),
+            "d_band": pa.array(d_band, type=pa.decimal128(28, 4)),
+            "d_small": pa.array(d_small, type=pa.decimal128(18, 2)),
+        })
+
+        with tempfile.NamedTemporaryFile(suffix=".row", delete=False) as tmp:
+            path = tmp.name
+
+        try:
+            _write_row_file(path, fields, data)
+            result = _read_row_file(path, fields)
+            assert result.column("d_int").to_pylist() == d_int
+            assert result.column("d_frac").to_pylist() == d_frac
+            assert result.column("d_band").to_pylist() == d_band
+            assert result.column("d_small").to_pylist() == d_small
+        finally:
+            os.unlink(path)
+
+    def test_high_precision_decimal_decoded_from_wire(self):
+        # Independent of the writer: decode a hand-built signed unscaled byte
+        # sequence (the row-file wire form shared with the Java 
implementation) and
+        # assert the exact Decimal, so a symmetric writer+reader scaling 
mistake
+        # cannot round-trip undetected.
+        from pypaimon.read.reader.format_row_reader import _read_field, 
_RowDecoder
+
+        def _varint(x):
+            out = bytearray()
+            while True:
+                b = x & 0x7F
+                x >>= 7
+                if x:
+                    out.append(b | 0x80)
+                else:
+                    out.append(b)
+                    return bytes(out)
+
+        unscaled = 12345678901234567890123456789012345678  # 38 significant 
digits
+        raw = unscaled.to_bytes((unscaled.bit_length() + 8) // 8, 'big', 
signed=True)
+        buf = _varint(len(raw)) + raw
+        got = _read_field(_RowDecoder(buf, 0), AtomicType("DECIMAL(38, 10)"))
+        assert got == Decimal("1234567890123456789012345678.9012345678")
+
     def test_all_primitive_types(self):
         fields = [
             DataField(0, "bool_col", AtomicType("BOOLEAN")),
diff --git a/paimon-python/pypaimon/write/writer/format_row_writer.py 
b/paimon-python/pypaimon/write/writer/format_row_writer.py
index f31c1eb58c..249189d111 100644
--- a/paimon-python/pypaimon/write/writer/format_row_writer.py
+++ b/paimon-python/pypaimon/write/writer/format_row_writer.py
@@ -18,7 +18,7 @@
 import datetime
 import re
 import struct
-from decimal import Decimal
+from decimal import Decimal, localcontext
 from typing import Any, List
 
 import pyarrow as pa
@@ -268,17 +268,16 @@ def _write_field(buf: _BlockBuffer, value: Any, 
data_type) -> None:
             buf.write_bytes_with_length(value)
         elif type_name.startswith('DECIMAL'):
             precision, scale = _parse_decimal_params(type_name)
+            dec = value if isinstance(value, Decimal) else Decimal(str(value))
+            # Compute the unscaled value under a context wide enough for the 
column:
+            # the default 28-digit precision would round a DECIMAL(p) value 
with more
+            # than 28 significant digits, silently corrupting it. Mirrors the 
read path.
+            with localcontext() as ctx:
+                ctx.prec = max(precision + abs(scale), 38)
+                unscaled = int(dec.scaleb(scale))
             if precision <= 18:
-                if isinstance(value, Decimal):
-                    unscaled = int(value * (10 ** scale))
-                else:
-                    unscaled = int(Decimal(str(value)) * (10 ** scale))
                 buf.write_long_le(unscaled)
             else:
-                if isinstance(value, Decimal):
-                    unscaled = int(value * (10 ** scale))
-                else:
-                    unscaled = int(Decimal(str(value)) * (10 ** scale))
                 raw = unscaled.to_bytes(
                     (unscaled.bit_length() + 8) // 8, byteorder='big', 
signed=True)
                 buf.write_bytes_with_length(raw)

Reply via email to