From ea4ddc85c6fdd38be1b85ce6a871126d74b39246 Mon Sep 17 00:00:00 2001 From: linzch3 <20380442+linzch3@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:49:42 +0000 Subject: [PATCH 1/3] Fix TruncateTransform.satisfies_order_of for different widths satisfies_order_of read source_type, which is never set on TruncateTransform, so comparing two truncate transforms with different widths raised AttributeError. The source type is not known on the unbound transform, and a wider truncation only preserves the order of a narrower one for strings, so only equal transforms are reported as satisfying the order. Closes #3680 Generated-by: Grok Bot (AI coding assistant) --- pyiceberg/transforms.py | 13 +++---------- tests/test_transforms.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pyiceberg/transforms.py b/pyiceberg/transforms.py index 74925c695f..fe60f3b420 100644 --- a/pyiceberg/transforms.py +++ b/pyiceberg/transforms.py @@ -882,16 +882,9 @@ def truncate_func(v: Any) -> Any: return lambda v: truncate_func(v) if v is not None else None def satisfies_order_of(self, other: Transform[S, T]) -> bool: - if self == other: - return True - elif ( - isinstance(self.source_type, StringType) - and isinstance(other, TruncateTransform) - and isinstance(other.source_type, StringType) - ): - return self.width >= other.width - - return False + # A wider string truncation satisfies the order of a narrower one, but a wider integer or decimal + # truncation does not. The source type is not known here, so only equal transforms are matched. + return self == other def to_human_string(self, _: IcebergType, value: S | None) -> str: if value is None: diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 998d8a224d..7ed6b19df1 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -513,6 +513,20 @@ def test_truncate_method(type_var: PrimitiveType, value: Any, expected_human_str assert truncate_transform.satisfies_order_of(truncate_transform) +def test_truncate_satisfies_order_of_different_widths() -> None: + assert TruncateTransform(3).satisfies_order_of(TruncateTransform(3)) + assert not TruncateTransform(5).satisfies_order_of(TruncateTransform(3)) + assert not TruncateTransform(3).satisfies_order_of(TruncateTransform(5)) + assert not TruncateTransform(3).satisfies_order_of(IdentityTransform()) + assert not TruncateTransform(3).satisfies_order_of(BucketTransform(3)) + + # Ordering by truncate[5] does not imply ordering by truncate[3] for integers: 3 and 2 both truncate to 0 + # with width 5, but to 3 and 0 with width 3. + wide, narrow = TruncateTransform(5).transform(IntegerType()), TruncateTransform(3).transform(IntegerType()) + assert wide(3) == wide(2) + assert narrow(3) > narrow(2) + + def test_unknown_transform() -> None: unknown_transform = UnknownTransform("unknown") # type: ignore assert str(unknown_transform) == str(eval(repr(unknown_transform))) From 4e6ecc7f8ce4e877055051e1de261161b580c6f4 Mon Sep 17 00:00:00 2001 From: linzch3 <20380442+linzch3@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:03:15 +0000 Subject: [PATCH 2/3] Match the Java implementation of TruncateTransform.satisfies_order_of A truncate transform satisfies the order of another truncate transform when its width is greater than or equal to the other's, as in the Java Truncate implementation. This no longer reads the missing source_type attribute. Generated-by: Grok Bot (AI coding assistant) --- pyiceberg/transforms.py | 11 ++++++++--- tests/test_transforms.py | 8 +------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pyiceberg/transforms.py b/pyiceberg/transforms.py index fe60f3b420..45c8ffd196 100644 --- a/pyiceberg/transforms.py +++ b/pyiceberg/transforms.py @@ -882,9 +882,14 @@ def truncate_func(v: Any) -> Any: return lambda v: truncate_func(v) if v is not None else None def satisfies_order_of(self, other: Transform[S, T]) -> bool: - # A wider string truncation satisfies the order of a narrower one, but a wider integer or decimal - # truncation does not. The source type is not known here, so only equal transforms are matched. - return self == other + if self == other: + return True + elif isinstance(other, TruncateTransform): + # Matches the Java implementation: a truncation satisfies the order of any truncation with a + # width less than or equal to its own. + return self.width >= other.width + + return False def to_human_string(self, _: IcebergType, value: S | None) -> str: if value is None: diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 7ed6b19df1..2f9fab0951 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -515,17 +515,11 @@ def test_truncate_method(type_var: PrimitiveType, value: Any, expected_human_str def test_truncate_satisfies_order_of_different_widths() -> None: assert TruncateTransform(3).satisfies_order_of(TruncateTransform(3)) - assert not TruncateTransform(5).satisfies_order_of(TruncateTransform(3)) + assert TruncateTransform(5).satisfies_order_of(TruncateTransform(3)) assert not TruncateTransform(3).satisfies_order_of(TruncateTransform(5)) assert not TruncateTransform(3).satisfies_order_of(IdentityTransform()) assert not TruncateTransform(3).satisfies_order_of(BucketTransform(3)) - # Ordering by truncate[5] does not imply ordering by truncate[3] for integers: 3 and 2 both truncate to 0 - # with width 5, but to 3 and 0 with width 3. - wide, narrow = TruncateTransform(5).transform(IntegerType()), TruncateTransform(3).transform(IntegerType()) - assert wide(3) == wide(2) - assert narrow(3) > narrow(2) - def test_unknown_transform() -> None: unknown_transform = UnknownTransform("unknown") # type: ignore From b4622888b63045a4e0ee20eda1dc455e0b3bcbe4 Mon Sep 17 00:00:00 2001 From: linzch3 <20380442+linzch3@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:03:16 +0000 Subject: [PATCH 3/3] Port the truncate sort order tests from the Java reference implementation Mirror TestSortOrder#testSatisfiesTruncateFieldOrder at the transform level, since pyiceberg has no SortOrder.satisfies. Generated-by: AI-assisted --- tests/test_transforms.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 2f9fab0951..cc63aa8e35 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -513,12 +513,22 @@ def test_truncate_method(type_var: PrimitiveType, value: Any, expected_human_str assert truncate_transform.satisfies_order_of(truncate_transform) -def test_truncate_satisfies_order_of_different_widths() -> None: - assert TruncateTransform(3).satisfies_order_of(TruncateTransform(3)) - assert TruncateTransform(5).satisfies_order_of(TruncateTransform(3)) - assert not TruncateTransform(3).satisfies_order_of(TruncateTransform(5)) - assert not TruncateTransform(3).satisfies_order_of(IdentityTransform()) - assert not TruncateTransform(3).satisfies_order_of(BucketTransform(3)) +# Ported from TestSortOrder#testSatisfiesTruncateFieldOrder in the Java reference implementation: +# https://github.com/apache/iceberg/blob/main/core/src/test/java/org/apache/iceberg/TestSortOrder.java +@pytest.mark.parametrize( + "transform,other,expected", + [ + (IdentityTransform(), TruncateTransform(2), True), + (TruncateTransform(2), IdentityTransform(), False), + (TruncateTransform(4), IdentityTransform(), False), + (TruncateTransform(4), TruncateTransform(2), True), + (TruncateTransform(2), TruncateTransform(4), False), + (TruncateTransform(2), TruncateTransform(2), True), + (TruncateTransform(3), BucketTransform(3), False), + ], +) +def test_truncate_satisfies_order_of(transform: Transform[Any, Any], other: Transform[Any, Any], expected: bool) -> None: + assert transform.satisfies_order_of(other) is expected def test_unknown_transform() -> None: