diff --git a/pyiceberg/conversions.py b/pyiceberg/conversions.py index 6dd964f436..fde1885443 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/pyiceberg/expressions/literals.py b/pyiceberg/expressions/literals.py index 61581d9b3c..6f9b79e77f 100644 --- a/pyiceberg/expressions/literals.py +++ b/pyiceberg/expressions/literals.py @@ -522,7 +522,7 @@ def to(self, type_var: IcebergType) -> Literal: # type: ignore @to.register(DecimalType) def _(self, type_var: DecimalType) -> Literal[Decimal]: - if type_var.scale == abs(int(self.value.as_tuple().exponent)): + if type_var.scale == -int(self.value.as_tuple().exponent): return self raise ValueError(f"Could not convert {self.value} into a {type_var}") @@ -628,7 +628,7 @@ def _(self, _: UUIDType) -> Literal[bytes]: @to.register(DecimalType) def _(self, type_var: DecimalType) -> Literal[Decimal]: dec = Decimal(self.value) - scale = abs(int(dec.as_tuple().exponent)) + scale = -int(dec.as_tuple().exponent) if type_var.scale == scale: return DecimalLiteral(dec) else: diff --git a/tests/expressions/test_literals.py b/tests/expressions/test_literals.py index 9251e79a7d..9b73a5022d 100644 --- a/tests/expressions/test_literals.py +++ b/tests/expressions/test_literals.py @@ -291,6 +291,9 @@ def test_decimal_to_decimal_conversion() -> None: with pytest.raises(ValueError) as e: _ = lit.to(DecimalType(9, 3)) assert "Could not convert 34.11 into a decimal(9, 3)" in str(e.value) + with pytest.raises(ValueError) as e: + _ = literal(Decimal("1E+2")).to(DecimalType(9, 2)) + assert "Could not convert 1E+2 into a decimal(9, 2)" in str(e.value) def test_timestamp_to_date() -> None: @@ -947,6 +950,9 @@ def test_string_to_decimal_type_invalid_value() -> None: with pytest.raises(ValueError) as e: _ = literal("18.15").to(DecimalType(10, 0)) assert "Could not convert 18.15 into a decimal(10, 0), scales differ 0 <> 2" in str(e.value) + with pytest.raises(ValueError) as e: + _ = literal("1E+2").to(DecimalType(10, 2)) + assert "Could not convert 1E+2 into a decimal(10, 2), scales differ 2 <> -2" in str(e.value) def test_decimal_literal_increment() -> None: diff --git a/tests/test_conversions.py b/tests/test_conversions.py index e786ae0683..322e58a9b7 100644 --- a/tests/test_conversions.py +++ b/tests/test_conversions.py @@ -511,6 +511,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"),