From 09fb73d89b4c1c5a1cbaec71fc425d48ce40b93c Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sat, 18 Jul 2026 10:00:13 -0700 Subject: [PATCH 1/2] Fix TruncateTransform.satisfies_order_of crashing on different widths Signed-off-by: Sai Asish Y --- pyiceberg/transforms.py | 6 +----- tests/test_transforms.py | 6 ++++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pyiceberg/transforms.py b/pyiceberg/transforms.py index cd0d7cebcb..bf1a930f32 100644 --- a/pyiceberg/transforms.py +++ b/pyiceberg/transforms.py @@ -897,11 +897,7 @@ def truncate_func(v: Any) -> Any: 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) - ): + elif isinstance(other, TruncateTransform): return self.width >= other.width return False diff --git a/tests/test_transforms.py b/tests/test_transforms.py index d296fcdb21..8229f8606f 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -512,6 +512,12 @@ 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(5).satisfies_order_of(TruncateTransform(3)) + assert not TruncateTransform(3).satisfies_order_of(TruncateTransform(5)) + assert not TruncateTransform(5).satisfies_order_of(BucketTransform(3)) + + def test_unknown_transform() -> None: unknown_transform = UnknownTransform("unknown") # type: ignore assert str(unknown_transform) == str(eval(repr(unknown_transform))) From 9e664383c016e08872dd7a21a2b785e417c20d61 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 29 Sep 2026 12:00:39 -0700 Subject: [PATCH 2/2] Use conservative ordering for unbound truncate transforms Generated-by: OpenAI Codex --- pyiceberg/transforms.py | 9 ++--- tests/test_transforms.py | 73 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/pyiceberg/transforms.py b/pyiceberg/transforms.py index bf1a930f32..f8cfabecae 100644 --- a/pyiceberg/transforms.py +++ b/pyiceberg/transforms.py @@ -895,12 +895,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(other, TruncateTransform): - return self.width >= other.width - - return False + # Unequal widths have different ordering semantics for numeric and prefix types. + # Without a source type, only equal transforms are guaranteed to satisfy the order. + 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 8229f8606f..040b2016bc 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -512,10 +512,77 @@ 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(5).satisfies_order_of(TruncateTransform(3)) - assert not TruncateTransform(3).satisfies_order_of(TruncateTransform(5)) +@pytest.mark.parametrize("width,other_width", [(5, 3), (3, 5), (4, 2), (2, 4), (1, 5), (5, 1)]) +def test_truncate_satisfies_order_of_different_widths(width: int, other_width: int) -> None: + assert not TruncateTransform(width).satisfies_order_of(TruncateTransform(other_width)) + + +def test_truncate_satisfies_order_of_equal_widths() -> None: + assert TruncateTransform(5).satisfies_order_of(TruncateTransform(5)) assert not TruncateTransform(5).satisfies_order_of(BucketTransform(3)) + assert not TruncateTransform(5).satisfies_order_of(IdentityTransform()) + + +@pytest.mark.parametrize( + "source,values,expected_width_five,expected_width_three", + [ + (IntegerType(), [3, 2], [0, 0], [3, 0]), + (IntegerType(), [-3, -4], [-5, -5], [-3, -6]), + (LongType(), [3, 2], [0, 0], [3, 0]), + (LongType(), [-3, -4], [-5, -5], [-3, -6]), + ( + DecimalType(3, 2), + [Decimal("0.03"), Decimal("0.02")], + [Decimal("0.00"), Decimal("0.00")], + [Decimal("0.03"), Decimal("0.00")], + ), + ( + DecimalType(3, 2), + [Decimal("-0.03"), Decimal("-0.04")], + [Decimal("-0.05"), Decimal("-0.05")], + [Decimal("-0.03"), Decimal("-0.06")], + ), + ], +) +def test_truncate_satisfies_order_of_numeric_ties( + source: PrimitiveType, + values: list[Any], + expected_width_five: list[Any], + expected_width_three: list[Any], +) -> None: + width_five = TruncateTransform(5) + width_three = TruncateTransform(3) + keys_five = list(map(width_five.transform(source), values)) + keys_three = list(map(width_three.transform(source), values)) + assert keys_five == expected_width_five + assert keys_three == expected_width_three + assert keys_five[0] == keys_five[1] + assert keys_three[0] > keys_three[1] + assert not width_five.satisfies_order_of(width_three) + + +@pytest.mark.parametrize( + "source,values", + [(StringType(), ["abc2", "abc1"]), (BinaryType(), [b"abc2", b"abc1"])], +) +def test_truncate_satisfies_order_of_prefix_ties(source: PrimitiveType, values: list[Any]) -> None: + width_three = TruncateTransform(3) + width_five = TruncateTransform(5) + keys_three = list(map(width_three.transform(source), values)) + keys_five = list(map(width_five.transform(source), values)) + assert keys_three[0] == keys_three[1] + assert keys_five[0] > keys_five[1] + assert not width_three.satisfies_order_of(width_five) + + +def test_truncate_satisfies_order_of_is_independent_of_source_calls() -> None: + transform = TruncateTransform(5) + other = TruncateTransform(3) + for source in (StringType(), IntegerType(), DecimalType(3, 2)): + transform.transform(source) + other.transform(source) + assert not transform.satisfies_order_of(other) + assert transform.satisfies_order_of(TruncateTransform(5)) def test_unknown_transform() -> None: