This is an automated email from the ASF dual-hosted git repository.

fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new d1826f11 feat: Make `and` expression JSON serializable (#2784)
d1826f11 is described below

commit d1826f116d86a5ba5519695f1498a9112cd2babb
Author: Drew Gallardo <[email protected]>
AuthorDate: Wed Nov 26 00:26:08 2025 -0800

    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 <[email protected]>
---
 pyiceberg/expressions/__init__.py     | 11 ++++++++---
 tests/expressions/test_expressions.py |  9 +++++++++
 2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/pyiceberg/expressions/__init__.py 
b/pyiceberg/expressions/__init__.py
index a928b988..71ee7cd9 100644
--- a/pyiceberg/expressions/__init__.py
+++ b/pyiceberg/expressions/__init__.py
@@ -237,12 +237,19 @@ class Reference(UnboundTerm, IcebergRootModel[str]):
         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 @@ class And(BooleanExpression):
             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 f0d6cdbc..252da478 100644
--- a/tests/expressions/test_expressions.py
+++ b/tests/expressions/test_expressions.py
@@ -725,6 +725,15 @@ def test_and() -> None:
         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"))

Reply via email to