From 7b22b187cd1101f002aa217607619ba05133111a Mon Sep 17 00:00:00 2001 From: Yuya Ebihara Date: Sun, 14 Jun 2026 13:44:31 +0900 Subject: [PATCH] Fix NotStartsWith residual evaluation to return correct result --- pyiceberg/expressions/visitors.py | 7 ++++--- tests/expressions/test_residual_evaluator.py | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pyiceberg/expressions/visitors.py b/pyiceberg/expressions/visitors.py index f3cfabf1a5..d9b51af08f 100644 --- a/pyiceberg/expressions/visitors.py +++ b/pyiceberg/expressions/visitors.py @@ -1905,10 +1905,11 @@ def visit_starts_with(self, term: BoundTerm, literal: LiteralValue) -> BooleanEx return AlwaysFalse() def visit_not_starts_with(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression: - if not self.visit_starts_with(term, literal): - return AlwaysTrue() - else: + starts_with_result = self.visit_starts_with(term, literal) + if isinstance(starts_with_result, AlwaysTrue): return AlwaysFalse() + else: + return AlwaysTrue() def visit_bound_predicate(self, predicate: BoundPredicate) -> BooleanExpression: """ diff --git a/tests/expressions/test_residual_evaluator.py b/tests/expressions/test_residual_evaluator.py index ba0a0da2e5..d64e2bf095 100644 --- a/tests/expressions/test_residual_evaluator.py +++ b/tests/expressions/test_residual_evaluator.py @@ -41,7 +41,7 @@ from pyiceberg.schema import Schema from pyiceberg.transforms import DayTransform, IdentityTransform from pyiceberg.typedef import Record -from pyiceberg.types import DoubleType, FloatType, IntegerType, NestedField, TimestampType +from pyiceberg.types import DoubleType, FloatType, IntegerType, NestedField, StringType, TimestampType def test_identity_transform_residual() -> None: @@ -249,3 +249,16 @@ def test_not_in_timestamp() -> None: ts_day += 3 # type: ignore residual = res_eval.residual_for(Record(ts_day)) assert residual == AlwaysTrue() + + +def test_not_starts_with() -> None: + schema = Schema(NestedField(1, "x", StringType())) + spec = PartitionSpec(PartitionField(1, 1001, IdentityTransform(), "x_part")) + + predicate = NotStartsWith("x", "a") + res_eval = residual_evaluator_of(spec=spec, expr=predicate, case_sensitive=True, schema=schema) + + assert res_eval.residual_for(Record("bb")) == AlwaysTrue() + assert res_eval.residual_for(Record("abc")) == AlwaysFalse() + assert res_eval.residual_for(Record("a")) == AlwaysFalse() + assert res_eval.residual_for(Record("zoo")) == AlwaysTrue()