From 346dd520bf81a6a0b0d489d0ec3283303bc0c264 Mon Sep 17 00:00:00 2001 From: Rodrigo-Palma Date: Fri, 18 Sep 2026 00:08:23 -0300 Subject: [PATCH] fix(conversions): reject a Decimal whose scale is negative to_bytes compared abs(exponent) against the type scale, so a Decimal carrying a negative scale passed as if it had the matching positive one. The exponent is then dropped by decimal_to_unscaled, and the value is written four orders of magnitude off without an error: Decimal('1E+2') stored in a decimal(10, 2) reads back as 0.01. Decimal('100').normalize() produces exactly that form, and to_bytes writes the lower and upper bounds of a data file, so a wrong bound prunes files that do hold matching rows. Compare the signed scale instead. --- pyiceberg/conversions.py | 8 +++++--- tests/test_conversions.py | 7 +++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pyiceberg/conversions.py b/pyiceberg/conversions.py index 268cbb93ec..5b7cab560d 100644 --- a/pyiceberg/conversions.py +++ b/pyiceberg/conversions.py @@ -310,9 +310,11 @@ def _(primitive_type: DecimalType, value: Decimal) -> bytes: bytes: The byte representation of `value`. """ _, digits, exponent = value.as_tuple() - exponent = abs(int(exponent)) - if exponent != primitive_type.scale: - raise ValueError(f"Cannot serialize value, scale of value does not match type {primitive_type}: {exponent}") + # A Decimal carries the negated scale as its exponent, so a value with a negative + # scale (1E+2) must not be read as if it had the matching positive one + value_scale = -int(exponent) + if value_scale != primitive_type.scale: + raise ValueError(f"Cannot serialize value, scale of value does not match type {primitive_type}: {value_scale}") elif len(digits) > primitive_type.precision: raise ValueError( f"Cannot serialize value, precision of value is greater than precision of type {primitive_type}: {len(digits)}" diff --git a/tests/test_conversions.py b/tests/test_conversions.py index 9b73b2db8c..57e7f9f618 100644 --- a/tests/test_conversions.py +++ b/tests/test_conversions.py @@ -510,6 +510,13 @@ def __repr__(self) -> str: "primitive_type, value, expected_error_message", [ (DecimalType(7, 3), Decimal("123.4567"), "Cannot serialize value, scale of value does not match type decimal(7, 3): 4"), + # A Decimal with a negative scale must not pass as its positive counterpart + (DecimalType(10, 2), Decimal("1E+2"), "Cannot serialize value, scale of value does not match type decimal(10, 2): -2"), + ( + DecimalType(10, 2), + Decimal("100").normalize(), + "Cannot serialize value, scale of value does not match type decimal(10, 2): -2", + ), ( DecimalType(18, 8), Decimal("123456789.123456789"),