diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index 2d7333838c..4b173bcbc2 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -447,7 +447,20 @@ def bind(self, schema: Schema, case_sensitive: bool = True) -> BooleanExpression def as_bound(self) -> Type[BoundPredicate[L]]: ... -class UnaryPredicate(UnboundPredicate[Any], ABC): +class UnaryPredicate(IcebergBaseModel, UnboundPredicate[Any], ABC): + type: str + + model_config = {"arbitrary_types_allowed": True} + + def __init__(self, term: Union[str, UnboundTerm[Any]]): + unbound = _to_unbound_term(term) + super().__init__(term=unbound) + + def __str__(self) -> str: + """Return the string representation of the UnaryPredicate class.""" + # Sort to make it deterministic + return f"{str(self.__class__.__name__)}(term={str(self.term)})" + def bind(self, schema: Schema, case_sensitive: bool = True) -> BoundUnaryPredicate[Any]: bound_term = self.term.bind(schema, case_sensitive) return self.as_bound(bound_term) @@ -506,6 +519,8 @@ def as_unbound(self) -> Type[NotNull]: class IsNull(UnaryPredicate): + type: str = "is-null" + def __invert__(self) -> NotNull: """Transform the Expression into its negated version.""" return NotNull(self.term) @@ -516,6 +531,8 @@ def as_bound(self) -> Type[BoundIsNull[L]]: class NotNull(UnaryPredicate): + type: str = "not-null" + def __invert__(self) -> IsNull: """Transform the Expression into its negated version.""" return IsNull(self.term) @@ -558,6 +575,8 @@ def as_unbound(self) -> Type[NotNaN]: class IsNaN(UnaryPredicate): + type: str = "is-nan" + def __invert__(self) -> NotNaN: """Transform the Expression into its negated version.""" return NotNaN(self.term) @@ -568,6 +587,8 @@ def as_bound(self) -> Type[BoundIsNaN[L]]: class NotNaN(UnaryPredicate): + type: str = "not-nan" + def __invert__(self) -> IsNaN: """Transform the Expression into its negated version.""" return IsNaN(self.term) diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py index 63673fdaeb..bfed8ec2b0 100644 --- a/tests/expressions/test_expressions.py +++ b/tests/expressions/test_expressions.py @@ -694,7 +694,7 @@ def test_and() -> None: assert and_ == pickle.loads(pickle.dumps(and_)) with pytest.raises(ValueError, match="Expected BooleanExpression, got: abc"): - null & "abc" # type: ignore + null & "abc" def test_or() -> None: @@ -711,7 +711,7 @@ def test_or() -> None: assert or_ == pickle.loads(pickle.dumps(or_)) with pytest.raises(ValueError, match="Expected BooleanExpression, got: abc"): - null | "abc" # type: ignore + null | "abc" def test_not() -> None: @@ -780,6 +780,16 @@ def test_not_null() -> None: assert non_null == pickle.loads(pickle.dumps(non_null)) +def test_serialize_is_null() -> None: + pred = IsNull(term="foo") + assert pred.model_dump_json() == '{"term":"foo","type":"is-null"}' + + +def test_serialize_not_null() -> None: + pred = NotNull(term="foo") + assert pred.model_dump_json() == '{"term":"foo","type":"not-null"}' + + def test_bound_is_nan(accessor: Accessor) -> None: # We need a FloatType here term = BoundReference[float](