Skip to content

Commit

Permalink
feat: implement rename_table
Browse files Browse the repository at this point in the history
  • Loading branch information
felixscherz committed Jan 18, 2025
1 parent 6752429 commit 04d616d
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 5 deletions.
14 changes: 14 additions & 0 deletions moto/s3tables/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ def __init__(self) -> None:
super().__init__(self.msg)


class NothingToRename(BadRequestException):
msg = "Neither a new namespace name nor a new table name is specified."

def __init__(self) -> None:
super().__init__(self.msg)


class NotFoundException(JsonRESTError):
code = 404

Expand All @@ -59,6 +66,13 @@ def __init__(self) -> None:
super().__init__(self.msg)


class DestinationNamespaceDoesNotExist(NotFoundException):
msg = "The specified destination namespace does not exist."

def __init__(self) -> None:
super().__init__(self.msg)


class TableDoesNotExist(NotFoundException):
msg = "The specified table does not exist."

Expand Down
41 changes: 41 additions & 0 deletions moto/s3tables/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
from moto.s3.models import FakeBucket
from moto.s3tables.exceptions import (
ConflictException,
DestinationNamespaceDoesNotExist,
InvalidContinuationToken,
InvalidNamespaceName,
InvalidTableBucketName,
InvalidTableName,
NotFoundException,
NothingToRename,
TableAlreadyExists,
TableDoesNotExist,
VersionTokenMismatch,
Expand Down Expand Up @@ -75,6 +77,7 @@ def __init__(
self.partition = partition
self.created_by = created_by
self.format = format
self.type = type
self.version_token = self._generate_version_token()
self.creation_date = datetime.datetime.now(tz=datetime.timezone.utc)
self.last_modified = self.creation_date
Expand Down Expand Up @@ -122,6 +125,11 @@ def update_metadata_location(
self.metadata_location = metadata_location
self.version_token = self._generate_version_token()

def rename(self, new_name: str, by: str) -> None:
_validate_table_name(new_name)
self.name = new_name
self.was_modified(by)


class Namespace:
def __init__(self, name: str, account_id: str, created_by: str):
Expand Down Expand Up @@ -437,6 +445,39 @@ def update_table_metadata_location(

raise TableDoesNotExist()

def rename_table(
self,
table_bucket_arn: str,
namespace: str,
name: str,
new_namespace_name: Optional[str] = None,
new_name: Optional[str] = None,
version_token: Optional[str] = None,
) -> None:
if not new_namespace_name and not new_name:
raise NothingToRename()
destination_namespace = new_namespace_name if new_namespace_name else namespace
destination_name = new_name if new_name else name
_validate_table_name(destination_name)

bucket = self.table_buckets.get(table_bucket_arn)
if not bucket or destination_namespace not in bucket.namespaces:
raise DestinationNamespaceDoesNotExist()
if namespace not in bucket.namespaces or (
name not in bucket.namespaces[namespace].tables
):
raise TableDoesNotExist()
table = bucket.namespaces[namespace].tables[name]
if version_token and not version_token == table.version_token:
raise VersionTokenMismatch()

if destination_name in bucket.namespaces[destination_namespace].tables:
raise TableAlreadyExists()

table = bucket.namespaces[namespace].tables.pop(name)
table.rename(new_name=destination_name, by=self.account_id)
bucket.namespaces[destination_namespace].tables[destination_name] = table


s3tables_backends = BackendDict(
S3TablesBackend,
Expand Down
26 changes: 21 additions & 5 deletions moto/s3tables/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,24 +188,23 @@ def get_table(self) -> TYPE_RESPONSE:
namespace=namespace,
name=name,
)

return (
200,
self.default_response_headers,
json.dumps(
dict(
name=table.name,
type="",
type=table.type,
tableARN=table.arn,
namespace=namespace,
namespace=[namespace],
versionToken=table.version_token,
metadataLocation=table.metadata_location,
warehouseLocation=table.warehouse_location,
createdAt=table.creation_date.isoformat(),
createdBy=table.account_id,
managedByService=None,
managedByService=table.managed_by_service,
modifiedAt=table.last_modified.isoformat(),
modifiedBy="",
modifiedBy=table.modified_by,
ownerAccountId=table.account_id,
format=table.format,
)
Expand Down Expand Up @@ -304,3 +303,20 @@ def update_table_metadata_location(self) -> TYPE_RESPONSE:
)
),
)

def rename_table(self) -> TYPE_RESPONSE:
_, table_bucket_arn, namespace, name, _ = self.raw_path.lstrip("/").split("/")
table_bucket_arn = unquote(table_bucket_arn)
body = json.loads(self.body)
version_token = body.get("versionToken")
new_namespace_name = body.get("newNamespaceName")
new_name = body.get("newName")
self.s3tables_backend.rename_table(
table_bucket_arn=table_bucket_arn,
namespace=namespace,
name=name,
new_namespace_name=new_namespace_name,
new_name=new_name,
version_token=version_token,
)
return 200, {}, ""
1 change: 1 addition & 0 deletions moto/s3tables/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
"{0}/tables/(?P<tableBucketARN_pt_1>[^/]+)/(?P<tableBucketARN_pt_2>[^/]+)/(?P<namespace>[^/]+)/(?P<name>[^/]+)$": S3TablesResponse.dispatch,
"{0}/tables/(?P<tableBucketARN>[^/]+)/(?P<namespace>[^/]+)/(?P<name>[^/]+)/metadata-location$": S3TablesResponse.dispatch,
"{0}/tables/(?P<tableBucketARN_pt_1>[^/]+)/(?P<tableBucketARN_pt_2>[^/]+)/(?P<namespace>[^/]+)/(?P<name>[^/]+)/metadata-location$": S3TablesResponse.dispatch,
"{0}/tables/(?P<tableBucketARN>[^/]+)/(?P<namespace>[^/]+)/(?P<name>[^/]+)/rename$": S3TablesResponse.dispatch,
}
69 changes: 69 additions & 0 deletions tests/test_s3tables/test_s3tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,72 @@ def test_underlying_table_storage_does_not_support_delete_object() -> None:
s3.put_object(Bucket=bucket_name, Key="test", Body=b"{}")
with pytest.raises(s3.exceptions.ClientError):
s3.delete_object(Bucket=bucket_name, Key="test")


@mock_aws
def test_rename_table() -> None:
client = boto3.client("s3tables", region_name="us-east-2")
arn = client.create_table_bucket(name="foo")["arn"]
client.create_namespace(tableBucketARN=arn, namespace=["bar"])
resp = client.create_table(
tableBucketARN=arn, namespace="bar", name="baz", format="ICEBERG"
)

client.create_namespace(tableBucketARN=arn, namespace=["bar-two"])
client.rename_table(
tableBucketARN=arn,
namespace="bar",
name="baz",
newNamespaceName="bar-two",
newName="baz-two",
versionToken=resp["versionToken"],
)
assert (
client.get_table(tableBucketARN=arn, namespace="bar-two", name="baz-two")[
"name"
]
== "baz-two"
)
assert client.get_table(tableBucketARN=arn, namespace="bar-two", name="baz-two")[
"namespace"
] == ["bar-two"]


@mock_aws
def test_rename_table_fails_when_destination_namespace_does_not_exist() -> None:
client = boto3.client("s3tables", region_name="us-east-2")
arn = client.create_table_bucket(name="foo")["arn"]
client.create_namespace(tableBucketARN=arn, namespace=["bar"])
resp = client.create_table(
tableBucketARN=arn, namespace="bar", name="baz", format="ICEBERG"
)

with pytest.raises(client.exceptions.NotFoundException) as ctx:
client.rename_table(
tableBucketARN=arn,
namespace="bar",
name="baz",
newNamespaceName="bar-two",
newName="baz-two",
versionToken=resp["versionToken"],
)
assert ctx.match("The specified destination namespace does not exist.")


@mock_aws
def test_rename_table_fails_when_no_updates_are_specified() -> None:
client = boto3.client("s3tables", region_name="us-east-2")
arn = client.create_table_bucket(name="foo")["arn"]
client.create_namespace(tableBucketARN=arn, namespace=["bar"])
resp = client.create_table(
tableBucketARN=arn, namespace="bar", name="baz", format="ICEBERG"
)

with pytest.raises(client.exceptions.BadRequestException) as ctx:
client.rename_table(
tableBucketARN=arn,
namespace="bar",
name="baz",
versionToken=resp["versionToken"],
)
assert ctx.match("Neither a new namespace name nor a new table name is specified.")

0 comments on commit 04d616d

Please sign in to comment.