You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by gi...@apache.org on 2026/09/13 18:21:48 UTC

(iceberg-python) branch main updated: REST: Add support for unregister_table (#3419)

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

github-merge-queue[bot] 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 630661f07 REST: Add support for unregister_table (#3419)
630661f07 is described below

commit 630661f07e2197c07abc99827e5b0a3c8ed652b3
Author: Yuya Ebihara <eb...@gmail.com>
AuthorDate: Sun Sep 13 18:07:16 2026 +0000

    REST: Add support for unregister_table (#3419)
---
 pyiceberg/catalog/rest/__init__.py | 38 +++++++++++++++++++++++++++++++++++
 tests/catalog/test_rest.py         | 41 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 79 insertions(+)

diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py
index b6c7720b0..d8f58773e 100644
--- a/pyiceberg/catalog/rest/__init__.py
+++ b/pyiceberg/catalog/rest/__init__.py
@@ -166,6 +166,7 @@ class Endpoints:
     load_credentials: str = "namespaces/{namespace}/tables/{table}/credentials"
     update_table: str = "namespaces/{namespace}/tables/{table}"
     drop_table: str = "namespaces/{namespace}/tables/{table}"
+    unregister_table: str = "namespaces/{namespace}/tables/{table}/unregister"
     table_exists: str = "namespaces/{namespace}/tables/{table}"
     get_token: str = "oauth/tokens"
     rename_table: str = "tables/rename"
@@ -201,6 +202,7 @@ class Capability:
     V1_DELETE_TABLE = Endpoint(http_method=HttpMethod.DELETE, path=f"{API_PREFIX}/{Endpoints.drop_table}")
     V1_RENAME_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.rename_table}")
     V1_REGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.register_table}")
+    V1_UNREGISTER_TABLE = Endpoint(http_method=HttpMethod.POST, path=f"{API_PREFIX}/{Endpoints.unregister_table}")
     V1_LOAD_CREDENTIALS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.load_credentials}")
 
     V1_LIST_VIEWS = Endpoint(http_method=HttpMethod.GET, path=f"{API_PREFIX}/{Endpoints.list_views}")
@@ -397,6 +399,16 @@ class RegisterTableRequest(IcebergBaseModel):
     overwrite: bool
 
 
+class UnregisterTableResult(IcebergBaseModel):
+    """Result of unregistering a table.
+
+    Contains the last metadata location and table metadata at the time of unregistration.
+    """
+
+    metadata_location: str = Field(..., alias="metadata-location")
+    metadata: TableMetadata
+
+
 class RegisterViewRequest(IcebergBaseModel):
     name: str
     metadata_location: str = Field(..., alias="metadata-location")
@@ -1348,6 +1360,32 @@ class RestCatalog(Catalog):
         table_response = TableResponse.model_validate_json(response.text)
         return self._response_to_table(self.identifier_to_tuple(identifier), table_response)
 
+    @retry(**_RETRY_ARGS)
+    def unregister_table(self, identifier: str | Identifier) -> tuple[str, TableMetadata]:
+        """Unregister a table from the catalog without removing data or metadata files.
+
+        Args:
+            identifier (Union[str, Identifier]): Table identifier for the table
+
+        Returns:
+            tuple[str, TableMetadata]: The last metadata location and corresponding table metadata
+
+        Raises:
+            NoSuchTableError: If the table does not exist
+        """
+        self._check_endpoint(Capability.V1_UNREGISTER_TABLE)
+        namespace_and_table = self._split_identifier_for_path(identifier)
+        response = self._session.post(
+            self.url(Endpoints.unregister_table, prefixed=True, **namespace_and_table),
+        )
+        try:
+            response.raise_for_status()
+        except HTTPError as exc:
+            _handle_non_200_response(exc, {404: NoSuchTableError})
+
+        result = UnregisterTableResult.model_validate_json(response.content)
+        return (result.metadata_location, result.metadata)
+
     @retry(**_RETRY_ARGS)
     @override
     def list_tables(self, namespace: str | Identifier) -> list[Identifier]:
diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py
index 1b6315aa5..a918829c2 100644
--- a/tests/catalog/test_rest.py
+++ b/tests/catalog/test_rest.py
@@ -111,6 +111,7 @@ TEST_SUPPORTED_ENDPOINTS = [
     Capability.V1_DELETE_TABLE,
     Capability.V1_RENAME_TABLE,
     Capability.V1_REGISTER_TABLE,
+    Capability.V1_UNREGISTER_TABLE,
     Capability.V1_LOAD_CREDENTIALS,
     Capability.V1_LIST_VIEWS,
     Capability.V1_LOAD_VIEW,
@@ -1991,6 +1992,46 @@ def test_register_table_overwrite(
     assert actual.name() == expected.name()
 
 
+def test_unregister_table_200(
+    rest_mock: Mocker, table_schema_simple: Schema, example_table_metadata_no_snapshot_v1_rest_json: dict[str, Any]
+) -> None:
+    unregister_response = {
+        "metadata-location": "s3://warehouse/database/table/metadata.json",
+        "metadata": example_table_metadata_no_snapshot_v1_rest_json["metadata"],
+    }
+    rest_mock.post(
+        f"{TEST_URI}v1/namespaces/default/tables/my_table/unregister",
+        json=unregister_response,
+        status_code=200,
+        request_headers=TEST_HEADERS,
+    )
+    catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
+    metadata_location, metadata = catalog.unregister_table(identifier=("default", "my_table"))
+
+    assert metadata_location == "s3://warehouse/database/table/metadata.json"
+    assert metadata.model_dump() == TableMetadataV1(**example_table_metadata_no_snapshot_v1_rest_json["metadata"]).model_dump()
+
+
+def test_unregister_table_404(rest_mock: Mocker) -> None:
+    rest_mock.post(
+        f"{TEST_URI}v1/namespaces/default/tables/does_not_exist/unregister",
+        json={
+            "error": {
+                "message": "Table does not exist: default.does_not_exist",
+                "type": "NoSuchTableException",
+                "code": 404,
+            }
+        },
+        status_code=404,
+        request_headers=TEST_HEADERS,
+    )
+
+    catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
+    with pytest.raises(NoSuchTableError) as e:
+        catalog.unregister_table(identifier=("default", "does_not_exist"))
+    assert "Table does not exist" in str(e.value)
+
+
 def test_delete_namespace_204(rest_mock: Mocker) -> None:
     namespace = "example"
     rest_mock.delete(