Skip to content

Add rename_view to REST Catalog #2149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions pyiceberg/catalog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,21 @@ def drop_view(self, identifier: Union[str, Identifier]) -> None:
NoSuchViewError: If a view with the given name does not exist.
"""

@abstractmethod
def rename_view(self, from_identifier: Union[str, Identifier], to_identifier: Union[str, Identifier]) -> None:
"""Rename a fully classified view name.

Args:
from_identifier (str | Identifier): Existing view identifier.
to_identifier (str | Identifier): New view identifier.

Returns:
Table: the updated table instance with its metadata.

Raises:
NoSuchViewError: If a table with the name does not exist.
"""

@staticmethod
def identifier_to_tuple(identifier: Union[str, Identifier]) -> Identifier:
"""Parse an identifier to a tuple.
Expand Down
3 changes: 3 additions & 0 deletions pyiceberg/catalog/dynamodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,9 @@ def drop_view(self, identifier: Union[str, Identifier]) -> None:
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
raise NotImplementedError

def rename_view(self, from_identifier: Union[str, Identifier], to_identifier: Union[str, Identifier]) -> None:
raise NotImplementedError

def _get_iceberg_table_item(self, database_name: str, table_name: str) -> Dict[str, Any]:
try:
return self._get_dynamo_item(identifier=f"{database_name}.{table_name}", namespace=database_name)
Expand Down
3 changes: 3 additions & 0 deletions pyiceberg/catalog/glue.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,9 @@ def drop_view(self, identifier: Union[str, Identifier]) -> None:
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
raise NotImplementedError

def rename_view(self, from_identifier: Union[str, Identifier], to_identifier: Union[str, Identifier]) -> None:
raise NotImplementedError

@staticmethod
def __is_iceberg_table(table: "TableTypeDef") -> bool:
return table.get("Parameters", {}).get(TABLE_TYPE, "").lower() == ICEBERG
Expand Down
3 changes: 3 additions & 0 deletions pyiceberg/catalog/hive.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,9 @@ def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
def view_exists(self, identifier: Union[str, Identifier]) -> bool:
raise NotImplementedError

def rename_view(self, from_identifier: Union[str, Identifier], to_identifier: Union[str, Identifier]) -> None:
raise NotImplementedError

def _create_lock_request(self, database_name: str, table_name: str) -> LockRequest:
lock_component: LockComponent = LockComponent(
level=LockLevel.TABLE, type=LockType.EXCLUSIVE, dbname=database_name, tablename=table_name, isTransactional=True
Expand Down
3 changes: 3 additions & 0 deletions pyiceberg/catalog/noop.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,6 @@ def view_exists(self, identifier: Union[str, Identifier]) -> bool:

def drop_view(self, identifier: Union[str, Identifier]) -> None:
raise NotImplementedError

def rename_view(self, from_identifier: Union[str, Identifier], to_identifier: Union[str, Identifier]) -> None:
raise NotImplementedError
14 changes: 14 additions & 0 deletions pyiceberg/catalog/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
NoSuchViewError,
TableAlreadyExistsError,
UnauthorizedError,
ViewAlreadyExistsError,
)
from pyiceberg.io import AWS_ACCESS_KEY_ID, AWS_REGION, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC, PartitionSpec, assign_fresh_partition_spec_ids
Expand Down Expand Up @@ -102,6 +103,7 @@ class Endpoints:
list_views: str = "namespaces/{namespace}/views"
drop_view: str = "namespaces/{namespace}/views/{view}"
view_exists: str = "namespaces/{namespace}/views/{view}"
rename_view: str = "views/rename"


class IdentifierKind(Enum):
Expand Down Expand Up @@ -857,3 +859,15 @@ def drop_view(self, identifier: Union[str]) -> None:
response.raise_for_status()
except HTTPError as exc:
_handle_non_200_response(exc, {404: NoSuchViewError})

@retry(**_RETRY_ARGS)
def rename_view(self, from_identifier: Union[str, Identifier], to_identifier: Union[str, Identifier]) -> None:
payload = {
"source": self._split_identifier_for_json(from_identifier),
"destination": self._split_identifier_for_json(to_identifier),
}
response = self._session.post(self.url(Endpoints.rename_view), json=payload)
try:
response.raise_for_status()
except HTTPError as exc:
_handle_non_200_response(exc, {404: NoSuchViewError, 409: ViewAlreadyExistsError})
3 changes: 3 additions & 0 deletions pyiceberg/catalog/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,3 +730,6 @@ def view_exists(self, identifier: Union[str, Identifier]) -> bool:

def drop_view(self, identifier: Union[str, Identifier]) -> None:
raise NotImplementedError

def rename_view(self, from_identifier: Union[str, Identifier], to_identifier: Union[str, Identifier]) -> None:
raise NotImplementedError
4 changes: 4 additions & 0 deletions pyiceberg/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class NoSuchViewError(Exception):
"""Raises when the view can't be found in the REST catalog."""


class ViewAlreadyExistsError(Exception):
"""Raises when the view being created already exists in the REST catalog."""


class NoSuchIdentifierError(Exception):
"""Raises when the identifier can't be found in the REST catalog."""

Expand Down
63 changes: 63 additions & 0 deletions tests/catalog/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
OAuthError,
ServerError,
TableAlreadyExistsError,
ViewAlreadyExistsError,
)
from pyiceberg.io import load_file_io
from pyiceberg.partitioning import PartitionField, PartitionSpec
Expand Down Expand Up @@ -1621,3 +1622,65 @@ def test_drop_view_204(rest_mock: Mocker) -> None:
request_headers=TEST_HEADERS,
)
RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).drop_view(("some_namespace", "some_view"))


def test_rename_view_204(rest_mock: Mocker) -> None:
from_identifier = ("some_namespace", "old_view")
to_identifier = ("some_namespace", "new_view")
rest_mock.post(
f"{TEST_URI}v1/views/rename",
json={
"source": {"namespace": ["some_namespace"], "name": "old_view"},
"destination": {"namespace": ["some_namespace"], "name": "new_view"},
},
status_code=204,
request_headers=TEST_HEADERS,
)
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
catalog.rename_view(from_identifier, to_identifier)
assert (
rest_mock.last_request.text
== """{"source": {"namespace": ["some_namespace"], "name": "old_view"}, "destination": {"namespace": ["some_namespace"], "name": "new_view"}}"""
)


def test_rename_view_404(rest_mock: Mocker) -> None:
from_identifier = ("some_namespace", "non_existent_view")
to_identifier = ("some_namespace", "new_view")
rest_mock.post(
f"{TEST_URI}v1/views/rename",
json={
"error": {
"message": "View does not exist: some_namespace.non_existent_view",
"type": "NoSuchViewException",
"code": 404,
}
},
status_code=404,
request_headers=TEST_HEADERS,
)
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
with pytest.raises(NoSuchViewError) as exc_info:
catalog.rename_view(from_identifier, to_identifier)
assert "View does not exist: some_namespace.non_existent_view" in str(exc_info.value)


def test_rename_view_409(rest_mock: Mocker) -> None:
from_identifier = ("some_namespace", "old_view")
to_identifier = ("some_namespace", "existing_view")
rest_mock.post(
f"{TEST_URI}v1/views/rename",
json={
"error": {
"message": "View already exists: some_namespace.existing_view",
"type": "ViewAlreadyExistsException",
"code": 409,
}
},
status_code=409,
request_headers=TEST_HEADERS,
)
catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN)
with pytest.raises(ViewAlreadyExistsError) as exc_info:
catalog.rename_view(from_identifier, to_identifier)
assert "View already exists: some_namespace.existing_view" in str(exc_info.value)