From a3ed51dc4cd87deba2eed2d6da406b0a21e9a7db Mon Sep 17 00:00:00 2001 From: Shahzeb Ali <126076773+Shahzeb-DataEngineer@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:27:19 +0500 Subject: [PATCH] fix: raise when binding starts-with with an out-of-range literal LiteralPredicate.bind folded AboveMax/BelowMin literals to AlwaysTrue or AlwaysFalse for the comparison operations, but starts-with and not-starts-with fell through and built a bound predicate wrapping the sentinel itself, which then crashed evaluators. Raise a ValueError at bind time instead, mirroring the Java implementation, which throws for operations that have no defined fold. Signed-off-by: Shahzeb Ali <126076773+Shahzeb-DataEngineer@users.noreply.github.com> --- pyiceberg/expressions/__init__.py | 4 ++++ tests/expressions/test_expressions.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index ece0db82db..16e3aab753 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -915,11 +915,15 @@ def bind(self, schema: Schema, case_sensitive: bool = True) -> BoundLiteralPredi return AlwaysTrue() elif isinstance(self, (GreaterThan, GreaterThanOrEqual, EqualTo)): return AlwaysFalse() + else: + raise ValueError(f"Cannot bind {type(self).__name__} with AboveMax literal") elif isinstance(lit, BelowMin): if isinstance(self, (GreaterThan, GreaterThanOrEqual, NotEqualTo)): return AlwaysTrue() elif isinstance(self, (LessThan, LessThanOrEqual, EqualTo)): return AlwaysFalse() + else: + raise ValueError(f"Cannot bind {type(self).__name__} with BelowMin literal") return self.as_bound(bound_term, lit) # type: ignore diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py index 8ce48a6897..5a33db254c 100644 --- a/tests/expressions/test_expressions.py +++ b/tests/expressions/test_expressions.py @@ -295,6 +295,20 @@ def test_empty_bind_not_in(table_schema_simple: Schema) -> None: assert bound == AlwaysTrue() +@pytest.mark.parametrize( + "expression, match", + [ + (StartsWith("bar", "2147483648"), "AboveMax"), + (NotStartsWith("bar", "2147483648"), "AboveMax"), + (StartsWith("bar", "-2147483649"), "BelowMin"), + (NotStartsWith("bar", "-2147483649"), "BelowMin"), + ], +) +def test_bind_starts_with_out_of_range_literal(table_schema_simple: Schema, expression: BooleanExpression, match: str) -> None: + with pytest.raises(ValueError, match=match): + expression.bind(table_schema_simple) + + def test_bind_not_in_equal_term(table_schema_simple: Schema) -> None: bound = BoundNotIn( BoundReference(table_schema_simple.find_field(1), table_schema_simple.accessor_for_field(1)), {literal("hello")}