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 068ee5d4 Refactor `Metadata` in `Transaction` (#1903)
068ee5d4 is described below

commit 068ee5d43a5a3b1346b6ef37921d479bc5168061
Author: Fokko Driesprong <[email protected]>
AuthorDate: Fri Apr 18 12:18:57 2025 +0200

    Refactor `Metadata` in `Transaction` (#1903)
    
    # Rationale for this change
    
    Today, we have a copy of the `TableMetadata` on the `Table` and the
    `Transaction`. This PR changes that logic to re-use the one on the
    table, and add the changes to the one on the `Transaction`.
    
    This also allows us to stack changes, for example, to first change a
    schema, and then write data with the new schema right away.
    
    Also a prerequisite for
    https://github.com/apache/iceberg-python/pull/1772
    
    # Are these changes tested?
    
    Includes a new test :)
    
    # Are there any user-facing changes?
    
    <!-- In the case of user-facing changes, please add the changelog label.
    -->
---
 pyiceberg/table/__init__.py                  |  8 +++----
 pyiceberg/table/update/__init__.py           |  8 ++++++-
 tests/integration/test_rest_schema.py        |  4 +---
 tests/integration/test_writes/test_writes.py | 34 ++++++++++++++++++++++++++++
 4 files changed, 46 insertions(+), 8 deletions(-)

diff --git a/pyiceberg/table/__init__.py b/pyiceberg/table/__init__.py
index 8dd52c4b..9e9de52d 100644
--- a/pyiceberg/table/__init__.py
+++ b/pyiceberg/table/__init__.py
@@ -244,7 +244,6 @@ class TableProperties:
 
 class Transaction:
     _table: Table
-    table_metadata: TableMetadata
     _autocommit: bool
     _updates: Tuple[TableUpdate, ...]
     _requirements: Tuple[TableRequirement, ...]
@@ -256,12 +255,15 @@ class Transaction:
             table: The table that will be altered.
             autocommit: Option to automatically commit the changes when they 
are staged.
         """
-        self.table_metadata = table.metadata
         self._table = table
         self._autocommit = autocommit
         self._updates = ()
         self._requirements = ()
 
+    @property
+    def table_metadata(self) -> TableMetadata:
+        return update_table_metadata(self._table.metadata, self._updates)
+
     def __enter__(self) -> Transaction:
         """Start a transaction to update the table."""
         return self
@@ -287,8 +289,6 @@ class Transaction:
             if type(new_requirement) not in existing_requirements:
                 self._requirements = self._requirements + (new_requirement,)
 
-        self.table_metadata = update_table_metadata(self.table_metadata, 
updates)
-
         if self._autocommit:
             self.commit_transaction()
             self._updates = ()
diff --git a/pyiceberg/table/update/__init__.py 
b/pyiceberg/table/update/__init__.py
index f60ac1e3..4905c31b 100644
--- a/pyiceberg/table/update/__init__.py
+++ b/pyiceberg/table/update/__init__.py
@@ -360,7 +360,8 @@ def _(update: SetCurrentSchemaUpdate, base_metadata: 
TableMetadata, context: _Ta
 @_apply_table_update.register(AddPartitionSpecUpdate)
 def _(update: AddPartitionSpecUpdate, base_metadata: TableMetadata, context: 
_TableMetadataUpdateContext) -> TableMetadata:
     for spec in base_metadata.partition_specs:
-        if spec.spec_id == update.spec.spec_id:
+        # Only raise in case of a discrepancy
+        if spec.spec_id == update.spec.spec_id and spec != update.spec:
             raise ValueError(f"Partition spec with id {spec.spec_id} already 
exists: {spec}")
 
     metadata_updates: Dict[str, Any] = {
@@ -525,6 +526,11 @@ def _(update: RemoveSnapshotRefUpdate, base_metadata: 
TableMetadata, context: _T
 
 @_apply_table_update.register(AddSortOrderUpdate)
 def _(update: AddSortOrderUpdate, base_metadata: TableMetadata, context: 
_TableMetadataUpdateContext) -> TableMetadata:
+    for sort in base_metadata.sort_orders:
+        # Only raise in case of a discrepancy
+        if sort.order_id == update.sort_order.order_id and sort != 
update.sort_order:
+            raise ValueError(f"Sort-order with id {sort.order_id} already 
exists: {sort}")
+
     context.add_update(update)
     return base_metadata.model_copy(
         update={
diff --git a/tests/integration/test_rest_schema.py 
b/tests/integration/test_rest_schema.py
index 6a704839..fd975d81 100644
--- a/tests/integration/test_rest_schema.py
+++ b/tests/integration/test_rest_schema.py
@@ -154,7 +154,7 @@ def test_schema_evolution_via_transaction(catalog: Catalog) 
-> None:
         NestedField(field_id=4, name="col_integer", field_type=IntegerType(), 
required=False),
     )
 
-    with pytest.raises(CommitFailedException) as exc_info:
+    with pytest.raises(CommitFailedException, match="Requirement failed: 
current schema id has changed: expected 2, found 3"):
         with tbl.transaction() as tx:
             # Start a new update
             schema_update = tx.update_schema()
@@ -165,8 +165,6 @@ def test_schema_evolution_via_transaction(catalog: Catalog) 
-> None:
             # stage another update in the transaction
             schema_update.add_column("col_double", DoubleType()).commit()
 
-    assert "Requirement failed: current schema changed: expected id 2 != 3" in 
str(exc_info.value)
-
     assert tbl.schema() == Schema(
         NestedField(field_id=1, name="col_uuid", field_type=UUIDType(), 
required=False),
         NestedField(field_id=2, name="col_fixed", field_type=FixedType(25), 
required=False),
diff --git a/tests/integration/test_writes/test_writes.py 
b/tests/integration/test_writes/test_writes.py
index 372c0a01..46d54f04 100644
--- a/tests/integration/test_writes/test_writes.py
+++ b/tests/integration/test_writes/test_writes.py
@@ -1776,3 +1776,37 @@ def test_write_optional_list(session_catalog: Catalog) 
-> None:
     session_catalog.load_table(identifier).append(df_2)
 
     assert len(session_catalog.load_table(identifier).scan().to_arrow()) == 4
+
+
[email protected]
[email protected]("format_version", [1, 2])
+def test_evolve_and_write(
+    spark: SparkSession, session_catalog: Catalog, arrow_table_with_null: 
pa.Table, format_version: int
+) -> None:
+    identifier = "default.test_evolve_and_write"
+    tbl = _create_table(session_catalog, identifier, 
properties={"format-version": format_version}, schema=Schema())
+    other_table = session_catalog.load_table(identifier)
+
+    numbers = pa.array([1, 2, 3, 4], type=pa.int32())
+
+    with tbl.update_schema() as upd:
+        # This is not known by other_table
+        upd.add_column("id", IntegerType())
+
+    with other_table.transaction() as tx:
+        # Refreshes the underlying metadata, and the schema
+        other_table.refresh()
+        tx.append(
+            pa.Table.from_arrays(
+                [
+                    numbers,
+                ],
+                schema=pa.schema(
+                    [
+                        pa.field("id", pa.int32(), nullable=True),
+                    ]
+                ),
+            )
+        )
+
+    assert 
session_catalog.load_table(identifier).scan().to_arrow().column(0).combine_chunks()
 == numbers

Reply via email to