You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ke...@apache.org on 2026/08/20 20:09:55 UTC
(iceberg-python) branch main updated: fix(decimal): use minimal byte length for negative powers of two (#3746)
This is an automated email from the ASF dual-hosted git repository.
kevinjqliu 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 8544bb409 fix(decimal): use minimal byte length for negative powers of two (#3746)
8544bb409 is described below
commit 8544bb40964cc9f6ae8f9ef92b605abf910ff0fe
Author: vishnu prakash <nv...@gmail.com>
AuthorDate: Fri Aug 21 01:39:50 2026 +0530
fix(decimal): use minimal byte length for negative powers of two (#3746)
* fix(decimal): use minimal byte length for negative powers of two
* test(decimal): add positive Decimal case to
bytes_required
* test(decimal): cover negative boundary bucketing
---------
Co-authored-by: Kevin Liu <ke...@gmail.com>
---
pyiceberg/utils/decimal.py | 13 +++++++++----
tests/test_transforms.py | 1 +
tests/utils/test_decimal.py | 20 +++++++++++++++++++-
3 files changed, 29 insertions(+), 5 deletions(-)
diff --git a/pyiceberg/utils/decimal.py b/pyiceberg/utils/decimal.py
index 5ef82640d..0ed2de1d5 100644
--- a/pyiceberg/utils/decimal.py
+++ b/pyiceberg/utils/decimal.py
@@ -58,11 +58,16 @@ def bytes_required(value: int | Decimal) -> int:
int: the minimum number of bytes needed to serialize the value.
"""
if isinstance(value, int):
- return (value.bit_length() + 8) // 8
+ unscaled = value
elif isinstance(value, Decimal):
- return (decimal_to_unscaled(value).bit_length() + 8) // 8
-
- raise ValueError(f"Unsupported value: {value}")
+ unscaled = decimal_to_unscaled(value)
+ else:
+ raise ValueError(f"Unsupported value: {value}")
+
+ # bit_length() ignores the sign, so -128 appears to need 9 signed bits instead of 8.
+ # Adding 1 before counting avoids the extra byte at negative boundaries.
+ num_bits = unscaled.bit_length() if unscaled >= 0 else (unscaled + 1).bit_length()
+ return (num_bits + 8) // 8
def decimal_to_bytes(value: Decimal, byte_length: int | None = None) -> bytes:
diff --git a/tests/test_transforms.py b/tests/test_transforms.py
index d296fcdb2..c977fcea1 100644
--- a/tests/test_transforms.py
+++ b/tests/test_transforms.py
@@ -187,6 +187,7 @@ def test_bucket_hash_values(test_input: Any, test_type: PrimitiveType, expected:
(BucketTransform(100).transform(TimeType()), 81068000000, 59),
(BucketTransform(100).transform(TimestampType()), 1510871468000000, 7),
(BucketTransform(100).transform(DecimalType(9, 2)), Decimal("14.20"), 59),
+ (BucketTransform(16).transform(DecimalType(5, 2)), Decimal("-1.28"), 13),
(BucketTransform(100).transform(StringType()), "iceberg", 89),
(
BucketTransform(100).transform(UUIDType()),
diff --git a/tests/utils/test_decimal.py b/tests/utils/test_decimal.py
index 3e67bf691..d50f7c570 100644
--- a/tests/utils/test_decimal.py
+++ b/tests/utils/test_decimal.py
@@ -18,7 +18,7 @@ from decimal import Decimal
import pytest
-from pyiceberg.utils.decimal import decimal_required_bytes, decimal_to_bytes
+from pyiceberg.utils.decimal import bytes_required, decimal_required_bytes, decimal_to_bytes
def test_decimal_required_bytes() -> None:
@@ -42,8 +42,26 @@ def test_decimal_required_bytes() -> None:
assert "(0, 40]" in str(exc_info.value)
+def test_bytes_required() -> None:
+ assert bytes_required(0) == 1
+ assert bytes_required(127) == 1
+ assert bytes_required(128) == 2
+ # Check negative signed-byte boundaries and their neighbors.
+ assert bytes_required(-127) == 1
+ assert bytes_required(-128) == 1
+ assert bytes_required(-129) == 2
+ assert bytes_required(-32768) == 2
+ assert bytes_required(-8388608) == 3
+ assert bytes_required(Decimal("1.27")) == 1
+ assert bytes_required(Decimal("-1.28")) == 1
+ assert bytes_required(Decimal("-327.68")) == 2
+
+
def test_decimal_to_bytes() -> None:
# Check the boundary between 2 and 3 bytes.
# 2 bytes has a minimum of -32,768 and a maximum value of 32,767 (inclusive).
assert decimal_to_bytes(Decimal("32767.")) == b"\x7f\xff"
assert decimal_to_bytes(Decimal("32768.")) == b"\x00\x80\x00"
+ # Unscaled values -128 and -32768 require no sign padding.
+ assert decimal_to_bytes(Decimal("-1.28")) == b"\x80"
+ assert decimal_to_bytes(Decimal("-327.68")) == b"\x80\x00"