diff --git a/paimon-python/pypaimon/data/decimal.py b/paimon-python/pypaimon/data/decimal.py index 30b5101bb4d9..b5c74c0f05fe 100644 --- a/paimon-python/pypaimon/data/decimal.py +++ b/paimon-python/pypaimon/data/decimal.py @@ -137,9 +137,7 @@ def to_unscaled_bytes(self) -> bytes: if value != value.to_integral_exact(): raise ArithmeticError("Decimal does not exactly fit in long.") - unscaled = int(value) - length = self._get_compact_byte_length(unscaled) - return unscaled.to_bytes(length, byteorder="big", signed=True) + return _signed_big_endian_bytes(int(value)) def is_compact(self) -> bool: """ @@ -505,3 +503,8 @@ def _get_compact_byte_length(val: int) -> int: bits = (~val).bit_length() return bits // 8 + 1 + + +def _signed_big_endian_bytes(value: int) -> bytes: + length = Decimal._get_compact_byte_length(value) + return value.to_bytes(length, byteorder="big", signed=True) diff --git a/paimon-python/pypaimon/execution/python/globalindex/key_serializer.py b/paimon-python/pypaimon/execution/python/globalindex/key_serializer.py index e8c43c493799..d7347c4172f8 100644 --- a/paimon-python/pypaimon/execution/python/globalindex/key_serializer.py +++ b/paimon-python/pypaimon/execution/python/globalindex/key_serializer.py @@ -25,6 +25,7 @@ from typing import Callable, Tuple import struct +from pypaimon.data.decimal import _signed_big_endian_bytes from pypaimon.schema.data_types import AtomicType from pypaimon.schema.data_types import DataType @@ -327,24 +328,6 @@ def _decimal_unscaled(value: object, scale: int) -> int: return int(rounded.scaleb(scale, context=context)) -def _signed_big_endian_bytes(value: int) -> bytes: - length = max(1, (value.bit_length() + 8) // 8) - while True: - try: - encoded = value.to_bytes(length, byteorder='big', signed=True) - break - except OverflowError: - length += 1 - while len(encoded) > 1: - if encoded[0] == 0x00 and encoded[1] < 0x80: - encoded = encoded[1:] - elif encoded[0] == 0xFF and encoded[1] >= 0x80: - encoded = encoded[1:] - else: - break - return encoded - - def _date_to_epoch_days(value: object) -> int: if isinstance(value, datetime.datetime): value = value.date() diff --git a/paimon-python/pypaimon/execution/python/write/writer/format_row_writer.py b/paimon-python/pypaimon/execution/python/write/writer/format_row_writer.py index 249189d11128..3fb5f1c00e4f 100644 --- a/paimon-python/pypaimon/execution/python/write/writer/format_row_writer.py +++ b/paimon-python/pypaimon/execution/python/write/writer/format_row_writer.py @@ -24,6 +24,7 @@ import pyarrow as pa from pypaimon.common.delta_varint_compressor import DeltaVarintCompressor +from pypaimon.data.decimal import _signed_big_endian_bytes from pypaimon.schema.data_types import ( ArrayType, AtomicType, DataField, MapType, MultisetType, RowType, VectorType ) @@ -278,8 +279,7 @@ def _write_field(buf: _BlockBuffer, value: Any, data_type) -> None: if precision <= 18: buf.write_long_le(unscaled) else: - raw = unscaled.to_bytes( - (unscaled.bit_length() + 8) // 8, byteorder='big', signed=True) + raw = _signed_big_endian_bytes(unscaled) buf.write_bytes_with_length(raw) elif type_name.startswith('TIMESTAMP'): precision = _parse_timestamp_precision(type_name) diff --git a/paimon-python/pypaimon/table/row/generic_row.py b/paimon-python/pypaimon/table/row/generic_row.py index 31a7701edb9b..3012d65c42ea 100644 --- a/paimon-python/pypaimon/table/row/generic_row.py +++ b/paimon-python/pypaimon/table/row/generic_row.py @@ -23,6 +23,7 @@ from decimal import Decimal from typing import Any, List, Union +from pypaimon.data.decimal import _signed_big_endian_bytes from pypaimon.data.generic_variant import GenericVariant from pypaimon.schema.data_types import AtomicType, DataField, DataType from pypaimon.table.row.binary_row import BinaryRow @@ -528,12 +529,7 @@ def to_bytes(cls, row: Union[GenericRow, BinaryRow]) -> bytes: (absolute_offset << 32) | length) elif is_var_len_type or is_high_precision_decimal: if is_high_precision_decimal: - # Big-endian signed bytes - if unscaled_value == 0: - value_bytes = b'\x00' - else: - byte_length = (unscaled_value.bit_length() + 8) // 8 # +8 for sign bit - value_bytes = unscaled_value.to_bytes(byte_length, byteorder='big', signed=True) + value_bytes = _signed_big_endian_bytes(unscaled_value) elif any(type_name.startswith(p) for p in ['CHAR', 'VARCHAR', 'STRING']): value_bytes = str(value).encode('utf-8') elif type_name == 'BLOB': diff --git a/paimon-python/pypaimon/tests/decimal_key_serializer_test.py b/paimon-python/pypaimon/tests/decimal_key_serializer_test.py index 531efc9d75a2..af25ece22a50 100644 --- a/paimon-python/pypaimon/tests/decimal_key_serializer_test.py +++ b/paimon-python/pypaimon/tests/decimal_key_serializer_test.py @@ -66,6 +66,27 @@ def test_exact_bytes_and_decode_with_low_precision(self, precision, scale, value self.assertEqual(3, context.prec) self.assertFalse(any(context.flags.values())) + @parameterized.expand([ + (0, '00'), + (-1, 'ff'), + (127, '7f'), + (128, '0080'), + (-128, '80'), + (-129, 'ff7f'), + (-32768, '8000'), + (-8388608, '800000'), + ]) + def test_java_decimal_bytes(self, unscaled, expected): + value = Decimal(unscaled).scaleb(-2) + raw = bytes.fromhex(expected) + for precision in (18, 20): + serializer = create_serializer(AtomicType('DECIMAL(%s,2)' % precision)) + encoded = struct.pack(' unscaled = 555000 self.assertEqual(unscaled_value, 555000) + @parameterized.expand([ + (0, '00'), + (-1, 'ff'), + (127, '7f'), + (128, '0080'), + (-128, '80'), + (-129, 'ff7f'), + (-32768, '8000'), + (-8388608, '800000'), + ]) + def test_decimal_not_compact_java_bytes(self, unscaled, expected): + fields = [DataField(0, 'd', AtomicType('DECIMAL(20, 2)'))] + value = Decimal(unscaled).scaleb(-2) + raw = bytes.fromhex(expected) + prefix = struct.pack('>i', 1) + b'\x00' * 8 + expected_row = prefix + struct.pack('