From 0fe3bd9e9c109686b82326a50b83e16737ee1be9 Mon Sep 17 00:00:00 2001 From: Joseph Yaksich <294273268+gitcommit90@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:11:15 +0000 Subject: [PATCH 1/2] fix: reject DecimalType with precision outside [1, 38] The Iceberg spec states precision must be 38 or less [1]. The Python implementation silently accepted any precision, including values like decimal(39, 0) that cannot be represented in decimal fixed-byte storage. This aligns the Python library with the Java reference implementation which raises IllegalArgumentException for precision > 38. Changes: - Add a validation check in DecimalType.__init__ that raises ValidationError when precision < 1 or precision > 38 - Add four tests covering: precision=39 (direct), precision=0 (direct), precision=38 (boundary, must pass), and precision=39 via deserialization Fixes #3583 [1] https://iceberg.apache.org/spec/#primitive-types --- pyiceberg/types.py | 2 ++ tests/test_types.py | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/pyiceberg/types.py b/pyiceberg/types.py index 3c98215366..2449421841 100644 --- a/pyiceberg/types.py +++ b/pyiceberg/types.py @@ -324,6 +324,8 @@ class DecimalType(PrimitiveType): root: tuple[int, int] def __init__(self, precision: int, scale: int) -> None: + if not (1 <= precision <= 38): + raise ValidationError(f"Precision must be between 1 and 38 (inclusive), got: {precision}") super().__init__(root=(precision, scale)) @model_serializer diff --git a/tests/test_types.py b/tests/test_types.py index 5e95687ba2..2f8a27eecc 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -521,6 +521,32 @@ def test_deserialization_decimal_failure() -> None: assert "Could not parse decimal(abc, def) into a DecimalType" in str(exc_info.value) +def test_decimal_precision_above_38_raises() -> None: + """Precision > 38 must be rejected per the Iceberg spec (https://iceberg.apache.org/spec/#primitive-types).""" + with pytest.raises(ValidationError, match="Precision must be between 1 and 38"): + DecimalType(39, 0) + + +def test_decimal_precision_zero_raises() -> None: + """Precision 0 is not valid; minimum precision is 1.""" + with pytest.raises(ValidationError, match="Precision must be between 1 and 38"): + DecimalType(0, 0) + + +def test_decimal_precision_boundary_38_accepted() -> None: + """Precision == 38 is the maximum allowed and must be accepted.""" + d = DecimalType(38, 0) + assert d.precision == 38 + assert d.scale == 0 + + +def test_decimal_deserialization_precision_above_38_raises() -> None: + """Deserialization of decimal(39, 0) must also raise a ValidationError.""" + with pytest.raises(Exception, match="Precision must be between 1 and 38"): + DecimalType.model_validate_json('"decimal(39, 0)"') + + + def test_str_decimal() -> None: assert str(DecimalType(19, 25)) == "decimal(19, 25)" From 35b04394a6828f290a094d7587149c271871f390 Mon Sep 17 00:00:00 2001 From: Joseph Yaksich <294273268+gitcommit90@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:03:58 +0000 Subject: [PATCH 2/2] style: ruff format (CI lint hook) --- pyiceberg/table/__init__.py | 5 +++-- tests/test_types.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyiceberg/table/__init__.py b/pyiceberg/table/__init__.py index 63b87d290e..50596ff25f 100644 --- a/pyiceberg/table/__init__.py +++ b/pyiceberg/table/__init__.py @@ -2471,8 +2471,9 @@ def plan_files(self) -> Iterable[FileScanTask]: options=self.options, ).plan_files( manifests=manifests, - manifest_entry_filter=lambda manifest_entry: manifest_entry.snapshot_id in append_snapshot_ids - and manifest_entry.status == ManifestEntryStatus.ADDED, + manifest_entry_filter=lambda manifest_entry: ( + manifest_entry.snapshot_id in append_snapshot_ids and manifest_entry.status == ManifestEntryStatus.ADDED + ), ) def to_arrow(self) -> pa.Table: diff --git a/tests/test_types.py b/tests/test_types.py index 2f8a27eecc..76353f4a19 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -546,7 +546,6 @@ def test_decimal_deserialization_precision_above_38_raises() -> None: DecimalType.model_validate_json('"decimal(39, 0)"') - def test_str_decimal() -> None: assert str(DecimalType(19, 25)) == "decimal(19, 25)"