feat: Make `and` expression JSON serializable (#2784)
Related to: #2518, #2775
# Rationale for this change
This work was done by @Aniketsy, I just opened this to get the tests
passing, and we can merge for scan planning.
But, this PR allows `And` expressions to be deserialized from JSON
through Pydantic.
This PR aligns the `And` expression with the `Or`/`Not` pattern by
adding `IcebergBaseModel` as an inherited class. This gets teh And
expression into a proven serializable state, preparing it for the full
expression tree [de]serializability work in #2783.
## Are these changes tested?
Yes added a test and ensure that they align with EpressionParser in
Iceberg Java
## Are there any user-facing changes?
No this is just serialization
cc: @kevinjqliu @Fokko
---------
Co-authored-by: Aniket Singh Yadav <singhyadavaniket43@gmail.com>
diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py
index a928b98..71ee7cd 100644
--- a/pyiceberg/expressions/__init__.py
+++ b/pyiceberg/expressions/__init__.py
@@ -237,12 +237,19 @@
return BoundReference
-class And(BooleanExpression):
+class And(IcebergBaseModel, BooleanExpression):
"""AND operation expression - logical conjunction."""
+ model_config = ConfigDict(arbitrary_types_allowed=True)
+
+ type: TypingLiteral["and"] = Field(default="and", alias="type")
left: BooleanExpression
right: BooleanExpression
+ def __init__(self, left: BooleanExpression, right: BooleanExpression, *rest: BooleanExpression) -> None:
+ if isinstance(self, And) and not hasattr(self, "left") and not hasattr(self, "right"):
+ super().__init__(left=left, right=right)
+
def __new__(cls, left: BooleanExpression, right: BooleanExpression, *rest: BooleanExpression) -> BooleanExpression: # type: ignore
if rest:
return _build_balanced_tree(And, (left, right, *rest))
@@ -254,8 +261,6 @@
return left
else:
obj = super().__new__(cls)
- obj.left = left
- obj.right = right
return obj
def __eq__(self, other: Any) -> bool:
diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py
index f0d6cdb..252da47 100644
--- a/tests/expressions/test_expressions.py
+++ b/tests/expressions/test_expressions.py
@@ -725,6 +725,15 @@
null & "abc"
+def test_and_serialization() -> None:
+ expr = And(EqualTo("x", 1), GreaterThan("y", 2))
+
+ assert (
+ expr.model_dump_json()
+ == '{"type":"and","left":{"term":"x","type":"eq","value":1},"right":{"term":"y","type":"gt","value":2}}'
+ )
+
+
def test_or() -> None:
null = IsNull(Reference("a"))
nan = IsNaN(Reference("b"))