From 369cbe3aca73cf61739f37a9e6ee72c163141053 Mon Sep 17 00:00:00 2001 From: Aniket Singh Yadav Date: Fri, 3 Oct 2025 01:49:51 +0530 Subject: [PATCH 1/2] Make And expression JSON serializable using Pydantic --- pyiceberg/expressions/__init__.py | 12 ++++++++++-- tests/table/test_partitioning.py | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index a928b988fd..6eae6d008c 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -237,12 +237,19 @@ def as_bound(self) -> type[BoundReference]: 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,6 +261,7 @@ def __new__(cls, left: BooleanExpression, right: BooleanExpression, *rest: Boole return left else: obj = super().__new__(cls) + obj.__pydantic_fields_set__ = set() obj.left = left obj.right = right return obj @@ -264,7 +272,7 @@ def __eq__(self, other: Any) -> bool: def __str__(self) -> str: """Return the string representation of the And class.""" - return f"And(left={str(self.left)}, right={str(self.right)})" + return f"{str(self.__class__.__name__)}(left={repr(self.left)}, right={repr(self.right)})" def __repr__(self) -> str: """Return the string representation of the And class.""" diff --git a/tests/table/test_partitioning.py b/tests/table/test_partitioning.py index 0fe22391c0..8b7fff10f7 100644 --- a/tests/table/test_partitioning.py +++ b/tests/table/test_partitioning.py @@ -21,6 +21,7 @@ import pytest +from pyiceberg.expressions import And, EqualTo from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC, PartitionField, PartitionSpec from pyiceberg.schema import Schema from pyiceberg.transforms import ( @@ -125,6 +126,13 @@ def test_serialize_partition_spec() -> None: ) +def test_serialize_and_expression() -> None: + expr = And(EqualTo("foo", 1), EqualTo("bar", 2)) + assert expr.model_dump_json(by_alias=True) == ( + '{"type":"and","left":{"type":"equal_to","term":"foo","literal":1},"right":{"type":"equal_to","term":"bar","literal":2}}' + ) + + def test_deserialize_unpartition_spec() -> None: json_partition_spec = """{"spec-id":0,"fields":[]}""" spec = PartitionSpec.model_validate_json(json_partition_spec) From 7e8189a95efe173b3bccde7ebf30a4a43bab70a1 Mon Sep 17 00:00:00 2001 From: geruh Date: Tue, 25 Nov 2025 16:41:31 -0800 Subject: [PATCH 2/2] feat: make and expression serializable --- pyiceberg/expressions/__init__.py | 5 +---- tests/expressions/test_expressions.py | 9 +++++++++ tests/table/test_partitioning.py | 8 -------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index 6eae6d008c..71ee7cd98d 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -261,9 +261,6 @@ def __new__(cls, left: BooleanExpression, right: BooleanExpression, *rest: Boole return left else: obj = super().__new__(cls) - obj.__pydantic_fields_set__ = set() - obj.left = left - obj.right = right return obj def __eq__(self, other: Any) -> bool: @@ -272,7 +269,7 @@ def __eq__(self, other: Any) -> bool: def __str__(self) -> str: """Return the string representation of the And class.""" - return f"{str(self.__class__.__name__)}(left={repr(self.left)}, right={repr(self.right)})" + return f"And(left={str(self.left)}, right={str(self.right)})" def __repr__(self) -> str: """Return the string representation of the And class.""" diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py index f0d6cdbce2..252da478d8 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")) diff --git a/tests/table/test_partitioning.py b/tests/table/test_partitioning.py index 8b7fff10f7..0fe22391c0 100644 --- a/tests/table/test_partitioning.py +++ b/tests/table/test_partitioning.py @@ -21,7 +21,6 @@ import pytest -from pyiceberg.expressions import And, EqualTo from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC, PartitionField, PartitionSpec from pyiceberg.schema import Schema from pyiceberg.transforms import ( @@ -126,13 +125,6 @@ def test_serialize_partition_spec() -> None: ) -def test_serialize_and_expression() -> None: - expr = And(EqualTo("foo", 1), EqualTo("bar", 2)) - assert expr.model_dump_json(by_alias=True) == ( - '{"type":"and","left":{"type":"equal_to","term":"foo","literal":1},"right":{"type":"equal_to","term":"bar","literal":2}}' - ) - - def test_deserialize_unpartition_spec() -> None: json_partition_spec = """{"spec-id":0,"fields":[]}""" spec = PartitionSpec.model_validate_json(json_partition_spec)