From 43774b779e7ea6493aa143424a0603ddaa34fd0f Mon Sep 17 00:00:00 2001 From: Artem Titoulenko Date: Mon, 18 Aug 2025 19:53:01 -0400 Subject: [PATCH 1/5] Allow snapshot-id in assert-ref-snapshot-id requirement to serialize to null in json, fix #2342 --- pyiceberg/table/update/__init__.py | 9 ++++++++- tests/table/test_init.py | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pyiceberg/table/update/__init__.py b/pyiceberg/table/update/__init__.py index 3f7d43f0ef..297607c45e 100644 --- a/pyiceberg/table/update/__init__.py +++ b/pyiceberg/table/update/__init__.py @@ -736,7 +736,7 @@ class AssertRefSnapshotId(ValidatableTableRequirement): type: Literal["assert-ref-snapshot-id"] = Field(default="assert-ref-snapshot-id") ref: str = Field(...) - snapshot_id: Optional[int] = Field(default=None, alias="snapshot-id") + snapshot_id: Optional[int] = Field(default=None, alias="snapshot-id", exclude=False) def validate(self, base_metadata: Optional[TableMetadata]) -> None: if base_metadata is None: @@ -756,6 +756,13 @@ def validate(self, base_metadata: Optional[TableMetadata]) -> None: elif self.snapshot_id is not None: raise CommitFailedException(f"Requirement failed: branch or tag {self.ref} is missing, expected {self.snapshot_id}") + # override the override method, allowing None to serialize to `null` instead of being omitted. + def model_dump_json( + self, exclude_none: bool = False, exclude: Optional[Set[str]] = None, by_alias: bool = True, **kwargs: Any + ) -> str: + return super().model_dump_json( + exclude_none=exclude_none, exclude=self._exclude_private_properties(exclude), by_alias=by_alias, **kwargs + ) class AssertLastAssignedFieldId(ValidatableTableRequirement): """The table's last assigned column id must match the requirement's `last-assigned-field-id`.""" diff --git a/tests/table/test_init.py b/tests/table/test_init.py index 748a77eee0..1cdcf34018 100644 --- a/tests/table/test_init.py +++ b/tests/table/test_init.py @@ -1039,6 +1039,11 @@ def test_assert_ref_snapshot_id(table_v2: Table) -> None: ): AssertRefSnapshotId(ref="test", snapshot_id=3055729675574597004).validate(base_metadata) + expected_json = """{"type":"assert-ref-snapshot-id","ref":"main","snapshot-id":null}""" + assert_json = AssertRefSnapshotId(ref="main").model_dump_json() + print(assert_json) + assert assert_json == expected_json + def test_assert_last_assigned_field_id(table_v2: Table) -> None: base_metadata = table_v2.metadata From fc39204836d6d5bcefbf85291099f1bc635ee9b2 Mon Sep 17 00:00:00 2001 From: Artem Titoulenko Date: Mon, 18 Aug 2025 20:00:25 -0400 Subject: [PATCH 2/5] remove exclude from Field --- pyiceberg/table/update/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiceberg/table/update/__init__.py b/pyiceberg/table/update/__init__.py index 297607c45e..65a440a28e 100644 --- a/pyiceberg/table/update/__init__.py +++ b/pyiceberg/table/update/__init__.py @@ -736,7 +736,7 @@ class AssertRefSnapshotId(ValidatableTableRequirement): type: Literal["assert-ref-snapshot-id"] = Field(default="assert-ref-snapshot-id") ref: str = Field(...) - snapshot_id: Optional[int] = Field(default=None, alias="snapshot-id", exclude=False) + snapshot_id: Optional[int] = Field(default=None, alias="snapshot-id") def validate(self, base_metadata: Optional[TableMetadata]) -> None: if base_metadata is None: From 4a62692ee4eca0cba340636d2ab0b6ce9aa1a8ee Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Tue, 19 Aug 2025 14:31:03 +0200 Subject: [PATCH 3/5] Simplify --- pyiceberg/table/update/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyiceberg/table/update/__init__.py b/pyiceberg/table/update/__init__.py index 65a440a28e..247c060eff 100644 --- a/pyiceberg/table/update/__init__.py +++ b/pyiceberg/table/update/__init__.py @@ -760,9 +760,8 @@ def validate(self, base_metadata: Optional[TableMetadata]) -> None: def model_dump_json( self, exclude_none: bool = False, exclude: Optional[Set[str]] = None, by_alias: bool = True, **kwargs: Any ) -> str: - return super().model_dump_json( - exclude_none=exclude_none, exclude=self._exclude_private_properties(exclude), by_alias=by_alias, **kwargs - ) + # `snapshot-id` is required in json response, even if null + return super().model_dump_json(exclude_none=False) class AssertLastAssignedFieldId(ValidatableTableRequirement): """The table's last assigned column id must match the requirement's `last-assigned-field-id`.""" From cf5a1f3c300e1a08b8bc7b4fca5b994712c9ef8f Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Tue, 19 Aug 2025 14:31:17 +0200 Subject: [PATCH 4/5] Simplify Co-authored-by: Kevin Liu --- tests/table/test_init.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/table/test_init.py b/tests/table/test_init.py index 1cdcf34018..9d284e77f4 100644 --- a/tests/table/test_init.py +++ b/tests/table/test_init.py @@ -1039,10 +1039,8 @@ def test_assert_ref_snapshot_id(table_v2: Table) -> None: ): AssertRefSnapshotId(ref="test", snapshot_id=3055729675574597004).validate(base_metadata) - expected_json = """{"type":"assert-ref-snapshot-id","ref":"main","snapshot-id":null}""" - assert_json = AssertRefSnapshotId(ref="main").model_dump_json() - print(assert_json) - assert assert_json == expected_json + expected_json = '{"type":"assert-ref-snapshot-id","ref":"main","snapshot-id":null}' + assert AssertRefSnapshotId(ref="main").model_dump_json() == expected_json def test_assert_last_assigned_field_id(table_v2: Table) -> None: From 499b8af4ea43b5fff6df32bc274314f40a6c7416 Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Tue, 19 Aug 2025 14:45:30 +0200 Subject: [PATCH 5/5] Make the linter happy --- pyiceberg/table/update/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyiceberg/table/update/__init__.py b/pyiceberg/table/update/__init__.py index 247c060eff..dc941c34ee 100644 --- a/pyiceberg/table/update/__init__.py +++ b/pyiceberg/table/update/__init__.py @@ -21,7 +21,7 @@ from abc import ABC, abstractmethod from datetime import datetime from functools import singledispatch -from typing import TYPE_CHECKING, Annotated, Any, Dict, Generic, List, Literal, Optional, Tuple, TypeVar, Union, cast +from typing import TYPE_CHECKING, Annotated, Any, Dict, Generic, List, Literal, Optional, Set, Tuple, TypeVar, Union, cast from pydantic import Field, field_validator, model_validator @@ -760,9 +760,10 @@ def validate(self, base_metadata: Optional[TableMetadata]) -> None: def model_dump_json( self, exclude_none: bool = False, exclude: Optional[Set[str]] = None, by_alias: bool = True, **kwargs: Any ) -> str: - # `snapshot-id` is required in json response, even if null + # `snapshot-id` is required in json response, even if null return super().model_dump_json(exclude_none=False) + class AssertLastAssignedFieldId(ValidatableTableRequirement): """The table's last assigned column id must match the requirement's `last-assigned-field-id`."""