Skip to content

Commit 85e420d

Browse files
committed
fix: use nulls-first ordering for residual comparisons
1 parent d500f29 commit 85e420d

2 files changed

Lines changed: 11 additions & 12 deletions

File tree

pyiceberg/expressions/visitors.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,23 +1874,21 @@ def visit_is_nan(self, term: BoundTerm) -> BooleanExpression:
18741874

18751875
def visit_not_nan(self, term: BoundTerm) -> BooleanExpression:
18761876
val = term.eval(self.struct)
1877-
# Mirror _ExpressionEvaluator.visit_not_nan (val == val): only NaN fails not-NaN.
1878-
# A null (and any non-float value) is not NaN, so the predicate holds.
18791877
if isinstance(val, SupportsFloat) and math.isnan(val):
18801878
return self.visit_false()
18811879
else:
18821880
return self.visit_true()
18831881

18841882
def visit_less_than(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression:
18851883
value = term.eval(self.struct)
1886-
if value is not None and value < literal.value:
1884+
if value is None or value < literal.value:
18871885
return self.visit_true()
18881886
else:
18891887
return self.visit_false()
18901888

18911889
def visit_less_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression:
18921890
value = term.eval(self.struct)
1893-
if value is not None and value <= literal.value:
1891+
if value is None or value <= literal.value:
18941892
return self.visit_true()
18951893
else:
18961894
return self.visit_false()

tests/expressions/test_residual_evaluator.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -236,18 +236,19 @@ def test_is_not_nan() -> None:
236236
assert residual == AlwaysTrue()
237237

238238

239-
def test_comparison_residual_with_null_partition_value() -> None:
240-
# Regression test for https://github.com/apache/iceberg-python/issues/3498
241-
# A nullable identity-partitioned column whose partition value is None must not raise a
242-
# TypeError when compared against a literal; it should behave like row evaluation, where a
243-
# null value never satisfies an ordering predicate.
244-
schema = Schema(NestedField(50, "x", IntegerType(), required=False), NestedField(51, "hour", IntegerType()))
239+
def test_comparison_residuals_for_null_identity_partition() -> None:
240+
schema = Schema(NestedField(50, "x", IntegerType(), required=False))
245241
spec = PartitionSpec(PartitionField(50, 1050, IdentityTransform(), "x_part"))
246242

247-
for predicate in (LessThan("x", 1), LessThanOrEqual("x", 1), GreaterThan("x", 1), GreaterThanOrEqual("x", 1)):
243+
for predicate, expected in (
244+
(LessThan("x", 1), AlwaysTrue()),
245+
(LessThanOrEqual("x", 1), AlwaysTrue()),
246+
(GreaterThan("x", 1), AlwaysFalse()),
247+
(GreaterThanOrEqual("x", 1), AlwaysFalse()),
248+
):
248249
res_eval = residual_evaluator_of(spec=spec, expr=predicate, case_sensitive=True, schema=schema)
249250
residual = res_eval.residual_for(Record(None))
250-
assert residual == AlwaysFalse(), f"null partition value should not match {predicate}"
251+
assert residual == expected
251252

252253

253254
def test_not_in_timestamp() -> None:

0 commit comments

Comments
 (0)