This is an automated email from the ASF dual-hosted git repository. jshao pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push: new f7d3431e9e [#6234] feat(core): Support rename operations for model alteration. (#6796) f7d3431e9e is described below commit f7d3431e9ef2b4a18a5cbc5f0c0edc9c0d44501b Author: Lord of Abyss <103809695+abyss-l...@users.noreply.github.com> AuthorDate: Sat Apr 5 01:24:19 2025 +0800 [#6234] feat(core): Support rename operations for model alteration. (#6796) ### What changes were proposed in this pull request? - [x] PR1: Add ModelChange API interface, Implement the rename logic in model catalog and JDBC backend logic. - [x] PR2: Add REST endpoint to support model change. - [x] PR3: Add Java client, python client and CLI module for model rename. - [ ] PR4: update docs. - [x] PR5: update model-related event. ### Why are the changes needed? Fix: #6234 ### Does this PR introduce _any_ user-facing change? The model can now be renamed via both the REST API and the CLI. ### How was this patch tested? local test.  --------- Signed-off-by: dependabot[bot] <supp...@github.com> Signed-off-by: George T. C. Lai <tsungchih...@gmail.com> Co-authored-by: roryqi <h...@datastrato.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: mchades <liminghu...@datastrato.com> Co-authored-by: George T. C. Lai <tsungchih...@gmail.com> Co-authored-by: yangyang zhong <35210666+hdyg...@users.noreply.github.com> --- .../integration/test/ModelCatalogOperationsIT.java | 32 ++++++++ .../apache/gravitino/cli/ModelCommandHandler.java | 30 ++++--- .../apache/gravitino/cli/TestableCommandLine.java | 11 +++ .../gravitino/cli/commands/UpdateModelName.java | 90 +++++++++++++++++++++ .../apache/gravitino/cli/TestModelCommands.java | 30 +++++++ .../org/apache/gravitino/client/DTOConverters.java | 13 +++ .../gravitino/client/GenericModelCatalog.java | 28 ++++++- .../gravitino/client/TestGenericModelCatalog.java | 63 +++++++++++++++ .../client-python/gravitino/api/model_change.py | 80 +++++++++++++++++++ .../gravitino/client/generic_model_catalog.py | 42 +++++++++- .../gravitino/dto/requests/model_update_request.py | 61 ++++++++++++++ .../dto/requests/model_updates_request.py | 47 +++++++++++ .../tests/integration/test_model_catalog.py | 36 +++++++-- .../tests/unittests/test_model_catalog_api.py | 35 +++++++- .../gravitino/dto/requests/ModelUpdateRequest.java | 93 ++++++++++++++++++++++ .../dto/requests/ModelUpdatesRequest.java | 60 ++++++++++++++ .../gravitino/server/web/rest/ModelOperations.java | 37 +++++++++ .../server/web/rest/TestModelOperations.java | 68 ++++++++++++++++ 18 files changed, 836 insertions(+), 20 deletions(-) diff --git a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java index 6e7adac551..cbea8e1d66 100644 --- a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java +++ b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java @@ -38,6 +38,7 @@ import org.apache.gravitino.exceptions.NoSuchModelVersionException; import org.apache.gravitino.exceptions.NoSuchSchemaException; import org.apache.gravitino.integration.test.util.BaseIT; import org.apache.gravitino.model.Model; +import org.apache.gravitino.model.ModelChange; import org.apache.gravitino.model.ModelVersion; import org.apache.gravitino.utils.RandomNameUtils; import org.junit.jupiter.api.AfterAll; @@ -328,6 +329,37 @@ public class ModelCatalogOperationsIT extends BaseIT { Assertions.assertEquals(0, modelVersionsAfterDeleteAll.length); } + @Test + public void testRegisterAndUpdateModel() { + String comment = "comment"; + String modelName = RandomNameUtils.genRandomName("alter_name_model"); + String newName = RandomNameUtils.genRandomName("new_name"); + NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName); + Map<String, String> properties = ImmutableMap.of("owner", "data-team"); + + Model createdModel = + gravitinoCatalog.asModelCatalog().registerModel(modelIdent, comment, properties); + + ModelChange updateName = ModelChange.rename(newName); + Model alteredModel = gravitinoCatalog.asModelCatalog().alterModel(modelIdent, updateName); + + Assertions.assertEquals(newName, alteredModel.name()); + Assertions.assertEquals(createdModel.properties(), alteredModel.properties()); + Assertions.assertEquals(createdModel.comment(), alteredModel.comment()); + + NameIdentifier nonExistIdent = NameIdentifier.of(schemaName, "non_exist_model"); + Assertions.assertThrows( + NoSuchModelException.class, + () -> gravitinoCatalog.asModelCatalog().alterModel(nonExistIdent, updateName)); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + gravitinoCatalog + .asModelCatalog() + .alterModel(NameIdentifier.of(schemaName, null), updateName)); + } + private void createMetalake() { GravitinoMetalake[] gravitinoMetalakes = client.listMetalakes(); Assertions.assertEquals(0, gravitinoMetalakes.length); diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/ModelCommandHandler.java b/clients/cli/src/main/java/org/apache/gravitino/cli/ModelCommandHandler.java index 9bab9355a6..5a417125bd 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/ModelCommandHandler.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/ModelCommandHandler.java @@ -152,16 +152,26 @@ public class ModelCommandHandler extends CommandHandler { /** Handles the "UPDATE" command. */ private void handleUpdateCommand() { - String[] alias = line.getOptionValues(GravitinoOptions.ALIAS); - String uri = line.getOptionValue(GravitinoOptions.URI); - String linkComment = line.getOptionValue(GravitinoOptions.COMMENT); - String[] linkProperties = line.getOptionValues(CommandActions.PROPERTIES); - Map<String, String> linkPropertityMap = new Properties().parse(linkProperties); - gravitinoCommandLine - .newLinkModel( - context, metalake, catalog, schema, model, uri, alias, linkComment, linkPropertityMap) - .validate() - .handle(); + if (line.hasOption(GravitinoOptions.URI)) { + String[] alias = line.getOptionValues(GravitinoOptions.ALIAS); + String uri = line.getOptionValue(GravitinoOptions.URI); + String linkComment = line.getOptionValue(GravitinoOptions.COMMENT); + String[] linkProperties = line.getOptionValues(CommandActions.PROPERTIES); + Map<String, String> linkPropertityMap = new Properties().parse(linkProperties); + gravitinoCommandLine + .newLinkModel( + context, metalake, catalog, schema, model, uri, alias, linkComment, linkPropertityMap) + .validate() + .handle(); + } + + if (line.hasOption(GravitinoOptions.RENAME)) { + String newName = line.getOptionValue(GravitinoOptions.RENAME); + gravitinoCommandLine + .newUpdateModelName(context, metalake, catalog, schema, model, newName) + .validate() + .handle(); + } } /** Handles the "LIST" command. */ diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java index dcb8e1bc24..5ee96da889 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java @@ -132,6 +132,7 @@ import org.apache.gravitino.cli.commands.UpdateFilesetComment; import org.apache.gravitino.cli.commands.UpdateFilesetName; import org.apache.gravitino.cli.commands.UpdateMetalakeComment; import org.apache.gravitino.cli.commands.UpdateMetalakeName; +import org.apache.gravitino.cli.commands.UpdateModelName; import org.apache.gravitino.cli.commands.UpdateTableComment; import org.apache.gravitino.cli.commands.UpdateTableName; import org.apache.gravitino.cli.commands.UpdateTagComment; @@ -872,6 +873,16 @@ public class TestableCommandLine { return new RegisterModel(context, metalake, catalog, schema, model, comment, properties); } + protected UpdateModelName newUpdateModelName( + CommandContext context, + String metalake, + String catalog, + String schema, + String model, + String rename) { + return new UpdateModelName(context, metalake, catalog, schema, model, rename); + } + protected DeleteModel newDeleteModel( CommandContext context, String metalake, String catalog, String schema, String model) { return new DeleteModel(context, metalake, catalog, schema, model); diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UpdateModelName.java b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UpdateModelName.java new file mode 100644 index 0000000000..853aaf35ce --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UpdateModelName.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.gravitino.cli.commands; + +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.cli.CommandContext; +import org.apache.gravitino.cli.ErrorMessages; +import org.apache.gravitino.client.GravitinoClient; +import org.apache.gravitino.exceptions.NoSuchCatalogException; +import org.apache.gravitino.exceptions.NoSuchMetalakeException; +import org.apache.gravitino.exceptions.NoSuchModelException; +import org.apache.gravitino.exceptions.NoSuchSchemaException; +import org.apache.gravitino.model.ModelChange; + +/** Update the name of a model. */ +public class UpdateModelName extends Command { + protected final String metalake; + protected final String catalog; + protected final String schema; + protected final String model; + protected final String name; + + /** + * Constructs a new {@link UpdateModelName} instance. + * + * @param context The command context. + * @param metalake The name of the metalake. + * @param catalog The name of the catalog. + * @param schema The name of the schema. + * @param model The name of the model. + * @param name The new model name. + */ + public UpdateModelName( + CommandContext context, + String metalake, + String catalog, + String schema, + String model, + String name) { + super(context); + this.metalake = metalake; + this.catalog = catalog; + this.schema = schema; + this.model = model; + this.name = name; + } + + /** Update the name of a model. */ + @Override + public void handle() { + NameIdentifier modelIdent; + + try { + modelIdent = NameIdentifier.of(schema, model); + GravitinoClient client = buildClient(metalake); + ModelChange renameChange = ModelChange.rename(name); + + client.loadCatalog(catalog).asModelCatalog().alterModel(modelIdent, renameChange); + } catch (NoSuchMetalakeException err) { + exitWithError(ErrorMessages.UNKNOWN_METALAKE); + } catch (NoSuchCatalogException err) { + exitWithError(ErrorMessages.UNKNOWN_CATALOG); + } catch (NoSuchSchemaException err) { + exitWithError(ErrorMessages.UNKNOWN_SCHEMA); + } catch (NoSuchModelException err) { + exitWithError(ErrorMessages.UNKNOWN_MODEL); + } catch (Exception exp) { + exitWithError(exp.getMessage()); + } + + printInformation(model + " name changed."); + } +} diff --git a/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommands.java b/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommands.java index 6f5e607c91..1035418cb2 100644 --- a/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommands.java +++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommands.java @@ -46,6 +46,7 @@ import org.apache.gravitino.cli.commands.ListModel; import org.apache.gravitino.cli.commands.ModelAudit; import org.apache.gravitino.cli.commands.ModelDetails; import org.apache.gravitino.cli.commands.RegisterModel; +import org.apache.gravitino.cli.commands.UpdateModelName; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -568,4 +569,33 @@ public class TestModelCommands { commandLine.handleCommandLine(); verify(linkModelMock).handle(); } + + @Test + void testUpdateModelNameCommand() { + UpdateModelName mockUpdate = mock(UpdateModelName.class); + when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo"); + when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema.model"); + when(mockCommandLine.hasOption(GravitinoOptions.RENAME)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.RENAME)).thenReturn("new_model_name"); + + GravitinoCommandLine commandLine = + spy( + new GravitinoCommandLine( + mockCommandLine, mockOptions, CommandEntities.MODEL, CommandActions.UPDATE)); + + doReturn(mockUpdate) + .when(commandLine) + .newUpdateModelName( + any(CommandContext.class), + eq("metalake_demo"), + eq("catalog"), + eq("schema"), + eq("model"), + eq("new_model_name")); + doReturn(mockUpdate).when(mockUpdate).validate(); + commandLine.handleCommandLine(); + verify(mockUpdate).handle(); + } } diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java index c9a88239f5..8304750385 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java @@ -38,12 +38,14 @@ import org.apache.gravitino.dto.authorization.SecurableObjectDTO; import org.apache.gravitino.dto.requests.CatalogUpdateRequest; import org.apache.gravitino.dto.requests.FilesetUpdateRequest; import org.apache.gravitino.dto.requests.MetalakeUpdateRequest; +import org.apache.gravitino.dto.requests.ModelUpdateRequest; import org.apache.gravitino.dto.requests.SchemaUpdateRequest; import org.apache.gravitino.dto.requests.TableUpdateRequest; import org.apache.gravitino.dto.requests.TagUpdateRequest; import org.apache.gravitino.dto.requests.TopicUpdateRequest; import org.apache.gravitino.file.FilesetChange; import org.apache.gravitino.messaging.TopicChange; +import org.apache.gravitino.model.ModelChange; import org.apache.gravitino.rel.Column; import org.apache.gravitino.rel.TableChange; import org.apache.gravitino.rel.expressions.Expression; @@ -355,4 +357,15 @@ class DTOConverters { "Unknown change type: " + change.getClass().getSimpleName()); } } + + static ModelUpdateRequest toModelUpdateRequest(ModelChange change) { + if (change instanceof ModelChange.RenameModel) { + return new ModelUpdateRequest.RenameModelRequest( + ((ModelChange.RenameModel) change).newName()); + + } else { + throw new IllegalArgumentException( + "Unknown model change type: " + change.getClass().getSimpleName()); + } + } } diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericModelCatalog.java b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericModelCatalog.java index f579a79900..5a58275bd0 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericModelCatalog.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericModelCatalog.java @@ -22,7 +22,9 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.gravitino.Catalog; import org.apache.gravitino.NameIdentifier; @@ -30,6 +32,8 @@ import org.apache.gravitino.Namespace; import org.apache.gravitino.dto.AuditDTO; import org.apache.gravitino.dto.CatalogDTO; import org.apache.gravitino.dto.requests.ModelRegisterRequest; +import org.apache.gravitino.dto.requests.ModelUpdateRequest; +import org.apache.gravitino.dto.requests.ModelUpdatesRequest; import org.apache.gravitino.dto.requests.ModelVersionLinkRequest; import org.apache.gravitino.dto.responses.BaseResponse; import org.apache.gravitino.dto.responses.DropResponse; @@ -253,8 +257,28 @@ class GenericModelCatalog extends BaseSchemaCatalog implements ModelCatalog { @Override public Model alterModel(NameIdentifier ident, ModelChange... changes) throws NoSuchModelException, IllegalArgumentException { - // TODO: implement alterModel - return null; + checkModelNameIdentifier(ident); + + // Convert ModelChange objects to DTO requests + List<ModelUpdateRequest> updateRequests = + Arrays.stream(changes) + .map(DTOConverters::toModelUpdateRequest) + .collect(Collectors.toList()); + + ModelUpdatesRequest req = new ModelUpdatesRequest(updateRequests); + req.validate(); + + Namespace modelFullNs = modelFullNamespace(ident.namespace()); + ModelResponse resp = + restClient.put( + formatModelRequestPath(modelFullNs) + "/" + RESTUtils.encodeString(ident.name()), + req, + ModelResponse.class, + Collections.emptyMap(), + ErrorHandlers.modelErrorHandler()); + + resp.validate(); + return new GenericModel(resp.getModel(), restClient, modelFullNs); } /** @return A new builder instance for {@link GenericModelCatalog}. */ diff --git a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericModelCatalog.java b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericModelCatalog.java index a3575988fc..583ff3a257 100644 --- a/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericModelCatalog.java +++ b/clients/client-java/src/test/java/org/apache/gravitino/client/TestGenericModelCatalog.java @@ -19,6 +19,7 @@ package org.apache.gravitino.client; import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableList; import java.time.Instant; import java.util.Collections; import java.util.Map; @@ -32,6 +33,8 @@ import org.apache.gravitino.dto.model.ModelDTO; import org.apache.gravitino.dto.model.ModelVersionDTO; import org.apache.gravitino.dto.requests.CatalogCreateRequest; import org.apache.gravitino.dto.requests.ModelRegisterRequest; +import org.apache.gravitino.dto.requests.ModelUpdateRequest; +import org.apache.gravitino.dto.requests.ModelUpdatesRequest; import org.apache.gravitino.dto.requests.ModelVersionLinkRequest; import org.apache.gravitino.dto.responses.BaseResponse; import org.apache.gravitino.dto.responses.CatalogResponse; @@ -507,6 +510,66 @@ public class TestGenericModelCatalog extends TestBase { "internal error"); } + @Test + public void testAlterModel() throws JsonProcessingException { + String comment = "comment"; + String schema = "schema1"; + String oldName = "model1"; + String newName = "new_model"; + NameIdentifier modelId = NameIdentifier.of(schema, oldName); + String modelPath = + withSlash( + GenericModelCatalog.formatModelRequestPath( + Namespace.of(METALAKE_NAME, CATALOG_NAME, schema)) + + "/" + + modelId.name()); + + // Test rename the model + ModelUpdateRequest.RenameModelRequest renameModelReq = + new ModelUpdateRequest.RenameModelRequest(newName); + ModelDTO renamedDTO = mockModelDTO(newName, 0, comment, Collections.emptyMap()); + ModelResponse commentResp = new ModelResponse(renamedDTO); + buildMockResource( + Method.PUT, + modelPath, + new ModelUpdatesRequest(ImmutableList.of(renameModelReq)), + commentResp, + HttpStatus.SC_OK); + Model renamedModel = catalog.asModelCatalog().alterModel(modelId, renameModelReq.modelChange()); + compareModel(renamedDTO, renamedModel); + + Assertions.assertEquals(newName, renamedModel.name()); + Assertions.assertEquals(comment, renamedModel.comment()); + Assertions.assertEquals(Collections.emptyMap(), renamedModel.properties()); + + // Test NoSuchModelException + ErrorResponse notFoundResp = + ErrorResponse.notFound(NoSuchModelException.class.getSimpleName(), "model not found"); + buildMockResource( + Method.PUT, + modelPath, + new ModelUpdatesRequest(ImmutableList.of(renameModelReq)), + notFoundResp, + HttpStatus.SC_NOT_FOUND); + Assertions.assertThrows( + NoSuchModelException.class, + () -> catalog.asModelCatalog().alterModel(modelId, renameModelReq.modelChange()), + "model not found"); + + // Test RuntimeException + ErrorResponse internalErrorResp = ErrorResponse.internalError("internal error"); + buildMockResource( + Method.PUT, + modelPath, + new ModelUpdatesRequest(ImmutableList.of(renameModelReq)), + internalErrorResp, + HttpStatus.SC_INTERNAL_SERVER_ERROR); + Assertions.assertThrows( + RuntimeException.class, + () -> catalog.asModelCatalog().alterModel(modelId, renameModelReq.modelChange()), + "internal error"); + } + private ModelDTO mockModelDTO( String modelName, int latestVersion, String comment, Map<String, String> properties) { return ModelDTO.builder() diff --git a/clients/client-python/gravitino/api/model_change.py b/clients/client-python/gravitino/api/model_change.py new file mode 100644 index 0000000000..f2557cd2cc --- /dev/null +++ b/clients/client-python/gravitino/api/model_change.py @@ -0,0 +1,80 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from abc import ABC + + +class ModelChange(ABC): + """ + A model change is a change to a model. It can be used to rename a model, update the comment + of a model, set a property and value pair for a model, or remove a property from a model. + """ + + @staticmethod + def rename(new_name): + """Creates a new model change to rename the model. + Args: + new_name: The new name of the model. + Returns: + The model change. + """ + return ModelChange.RenameModel(new_name) + + class RenameModel: + """A model change to rename the model.""" + + def __init__(self, new_name): + self._new_name = new_name + + def new_name(self): + """Retrieves the new name set for the model. + Returns: + The new name of the model. + """ + return self._new_name + + def __eq__(self, other) -> bool: + """Compares this RenameModel instance with another object for equality. Two instances are + considered equal if they designate the same new name for the model. + + Args: + other: The object to compare with this instance. + + Returns: + true if the given object represents an identical model renaming operation; false otherwise. + """ + if not isinstance(other, ModelChange.RenameModel): + return False + return self.new_name() == other.new_name() + + def __hash__(self): + """Generates a hash code for this RenameModel instance. The hash code is primarily based on + the new name for the model. + + Returns: + A hash code value for this renaming operation. + """ + return hash(self.new_name()) + + def __str__(self): + """Provides a string representation of the RenameModel instance. This string includes the + class name followed by the new name of the model. + + Returns: + A string summary of this renaming operation. + """ + return f"RENAMEMODEL {self.new_name()}" diff --git a/clients/client-python/gravitino/client/generic_model_catalog.py b/clients/client-python/gravitino/client/generic_model_catalog.py index 17d6ed0f1c..1dbf96db56 100644 --- a/clients/client-python/gravitino/client/generic_model_catalog.py +++ b/clients/client-python/gravitino/client/generic_model_catalog.py @@ -17,15 +17,17 @@ from typing import Dict, List -from gravitino.name_identifier import NameIdentifier from gravitino.api.catalog import Catalog from gravitino.api.model.model import Model from gravitino.api.model.model_version import ModelVersion +from gravitino.api.model_change import ModelChange from gravitino.client.base_schema_catalog import BaseSchemaCatalog from gravitino.client.generic_model import GenericModel from gravitino.client.generic_model_version import GenericModelVersion from gravitino.dto.audit_dto import AuditDTO from gravitino.dto.requests.model_register_request import ModelRegisterRequest +from gravitino.dto.requests.model_update_request import ModelUpdateRequest +from gravitino.dto.requests.model_updates_request import ModelUpdatesRequest from gravitino.dto.requests.model_version_link_request import ModelVersionLinkRequest from gravitino.dto.responses.base_response import BaseResponse from gravitino.dto.responses.drop_response import DropResponse @@ -34,6 +36,7 @@ from gravitino.dto.responses.model_response import ModelResponse from gravitino.dto.responses.model_version_list_response import ModelVersionListResponse from gravitino.dto.responses.model_vesion_response import ModelVersionResponse from gravitino.exceptions.handlers.model_error_handler import MODEL_ERROR_HANDLER +from gravitino.name_identifier import NameIdentifier from gravitino.namespace import Namespace from gravitino.rest.rest_utils import encode_string from gravitino.utils import HTTPClient @@ -269,6 +272,36 @@ class GenericModelCatalog(BaseSchemaCatalog): return GenericModelVersion(model_version_resp.model_version()) + def alter_model(self, model_ident: NameIdentifier, *changes: ModelChange) -> Model: + """Alter the schema by applying the changes. + Args: + model_ident: The identifier of the model. + changes: The changes to apply to the model. + Raises: + NoSuchSchemaException: If the schema does not exist. + IllegalArgumentException: If the changes are invalid. + Returns: + The updated schema object. + """ + self._check_model_ident(model_ident) + model_full_ns = self._model_full_namespace(model_ident.namespace()) + + update_requests = [ + GenericModelCatalog.to_model_update_request(change) for change in changes + ] + + req = ModelUpdatesRequest(update_requests) + req.validate() + + resp = self.rest_client.put( + f"{self._format_model_request_path(model_full_ns)}/{encode_string(model_ident.name())}", + req, + error_handler=MODEL_ERROR_HANDLER, + ) + model_response = ModelResponse.from_json(resp.body, infer_missing=True) + model_response.validate() + return GenericModel(model_response.model()) + def link_model_version( self, model_ident: NameIdentifier, @@ -391,6 +424,13 @@ class GenericModelCatalog(BaseSchemaCatalog): self.link_model_version(ident, uri, aliases, comment, properties) return model + @staticmethod + def to_model_update_request(change: ModelChange): + if isinstance(change, ModelChange.RenameModel): + return ModelUpdateRequest.UpdateModelNameRequest(change.new_name()) + + raise ValueError(f"Unknown change type: {type(change).__name__}") + def _check_model_namespace(self, namespace: Namespace): """Check the validity of the model namespace. diff --git a/clients/client-python/gravitino/dto/requests/model_update_request.py b/clients/client-python/gravitino/dto/requests/model_update_request.py new file mode 100644 index 0000000000..6310a47be0 --- /dev/null +++ b/clients/client-python/gravitino/dto/requests/model_update_request.py @@ -0,0 +1,61 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from abc import abstractmethod +from dataclasses import dataclass, field +from typing import Optional + +from dataclasses_json import config + +from gravitino.api.model_change import ModelChange +from gravitino.rest.rest_message import RESTRequest + + +@dataclass +class ModelUpdateRequestBase(RESTRequest): + """Base class for all model update requests.""" + + _type: str = field(metadata=config(field_name="@type")) + + def __init__(self, action_type: str): + self._type = action_type + + @abstractmethod + def model_change(self) -> ModelChange: + """Convert to model change operation""" + pass + + +class ModelUpdateRequest: + """Namespace for all model update request types.""" + + @dataclass + class UpdateModelNameRequest(ModelUpdateRequestBase): + """Request to update model name""" + + _new_name: Optional[str] = field(metadata=config(field_name="newName")) + + def __init__(self, new_name: str): + super().__init__("rename") + self._new_name = new_name + + def validate(self): + if not self._new_name: + raise ValueError('"new_name" field is required') + + def model_change(self) -> ModelChange: + return ModelChange.rename(self._new_name) diff --git a/clients/client-python/gravitino/dto/requests/model_updates_request.py b/clients/client-python/gravitino/dto/requests/model_updates_request.py new file mode 100644 index 0000000000..bc708f0642 --- /dev/null +++ b/clients/client-python/gravitino/dto/requests/model_updates_request.py @@ -0,0 +1,47 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from dataclasses import dataclass, field +from typing import List + +from dataclasses_json import config + +from gravitino.dto.requests.model_update_request import ModelUpdateRequest +from gravitino.rest.rest_message import RESTRequest + + +@dataclass +class ModelUpdatesRequest(RESTRequest): + """Represents a collection of model metadata update operations to be applied atomically.""" + + _updates: List[ModelUpdateRequest] = field( + metadata=config(field_name="updates"), default_factory=list + ) + + def __init__(self, updates: List[ModelUpdateRequest] = None): + self._updates = updates + + def validate(self): + """Validates the update requests. + Raises: + ValueError: If the updates list is empty or contains invalid requests. + """ + if not self._updates: + raise ValueError("At least one model update must be specified") + + for update in self._updates: + update.validate() diff --git a/clients/client-python/tests/integration/test_model_catalog.py b/clients/client-python/tests/integration/test_model_catalog.py index 35ebfdc472..9a9f0e2743 100644 --- a/clients/client-python/tests/integration/test_model_catalog.py +++ b/clients/client-python/tests/integration/test_model_catalog.py @@ -16,20 +16,20 @@ # under the License. from random import randint -from gravitino import GravitinoAdminClient, GravitinoClient, Catalog, NameIdentifier +from gravitino import Catalog, GravitinoAdminClient, GravitinoClient, NameIdentifier +from gravitino.api.model_change import ModelChange from gravitino.exceptions.base import ( ModelAlreadyExistsException, - NoSuchSchemaException, - NoSuchModelException, ModelVersionAliasesAlreadyExistException, + NoSuchModelException, NoSuchModelVersionException, + NoSuchSchemaException, ) from gravitino.namespace import Namespace from tests.integration.integration_test_env import IntegrationTestEnv class TestModelCatalog(IntegrationTestEnv): - _metalake_name: str = "model_it_metalake" + str(randint(0, 1000)) _catalog_name: str = "model_it_catalog" + str(randint(0, 1000)) _schema_name: str = "model_it_schema" + str(randint(0, 1000)) @@ -132,7 +132,6 @@ class TestModelCatalog(IntegrationTestEnv): ) def test_register_list_models(self): - model_name1 = "model_it_model1" + str(randint(0, 1000)) model_name2 = "model_it_model2" + str(randint(0, 1000)) model_ident1 = NameIdentifier.of(self._schema_name, model_name1) @@ -201,6 +200,33 @@ class TestModelCatalog(IntegrationTestEnv): ) ) + def test_register_alter_model(self): + model_name = f"model_it_model{str(randint(0, 1000))}" + model_new_name = f"model_it_model_new{str(randint(0, 1000))}" + model_ident = NameIdentifier.of(self._schema_name, model_name) + renamed_ident = NameIdentifier.of(self._schema_name, model_new_name) + comment = "comment" + properties = {"k1": "v1", "k2": "v2"} + + self._catalog.as_model_catalog().register_model( + model_ident, comment, properties + ) + + renamed_model = self._catalog.as_model_catalog().get_model(model_ident) + self.assertEqual(model_name, renamed_model.name()) + self.assertEqual(comment, renamed_model.comment()) + self.assertEqual(0, renamed_model.latest_version()) + self.assertEqual(properties, renamed_model.properties()) + + changes = [ModelChange.rename(model_new_name)] + + self._catalog.as_model_catalog().alter_model(model_ident, *changes) + renamed_model = self._catalog.as_model_catalog().get_model(renamed_ident) + self.assertEqual(model_new_name, renamed_model.name()) + self.assertEqual(comment, renamed_model.comment()) + self.assertEqual(0, renamed_model.latest_version()) + self.assertEqual(properties, renamed_model.properties()) + def test_link_get_model_version(self): model_name = "model_it_model" + str(randint(0, 1000)) model_ident = NameIdentifier.of(self._schema_name, model_name) diff --git a/clients/client-python/tests/unittests/test_model_catalog_api.py b/clients/client-python/tests/unittests/test_model_catalog_api.py index 91d5d5ec78..721cc6756a 100644 --- a/clients/client-python/tests/unittests/test_model_catalog_api.py +++ b/clients/client-python/tests/unittests/test_model_catalog_api.py @@ -19,9 +19,10 @@ import unittest from http.client import HTTPResponse from unittest.mock import Mock, patch -from gravitino import NameIdentifier, GravitinoClient +from gravitino import GravitinoClient, NameIdentifier from gravitino.api.model.model import Model from gravitino.api.model.model_version import ModelVersion +from gravitino.api.model_change import ModelChange from gravitino.dto.audit_dto import AuditDTO from gravitino.dto.model_dto import ModelDTO from gravitino.dto.model_version_dto import ModelVersionDTO @@ -37,7 +38,6 @@ from tests.unittests import mock_base @mock_base.mock_data class TestModelCatalogApi(unittest.TestCase): - _metalake_name: str = "metalake_demo" _catalog_name: str = "model_catalog" @@ -163,6 +163,37 @@ class TestModelCatalogApi(unittest.TestCase): ) self._compare_models(model_dto, model) + def test_alter_model(self, *mock_method): + gravitino_client = GravitinoClient( + uri="http://localhost:8090", metalake_name=self._metalake_name + ) + catalog = gravitino_client.load_catalog(self._catalog_name) + + model_ident = NameIdentifier.of("schema", "model1") + + model_dto = ModelDTO( + _name="model2", + _comment="this is test", + _properties={"k": "v"}, + _latest_version=0, + _audit=AuditDTO(_creator="test", _create_time="2022-01-01T00:00:00Z"), + ) + + ## test with response + model_resp = ModelResponse(_model=model_dto, _code=0) + json_str = model_resp.to_json() + mock_resp = self._mock_http_response(json_str) + + with patch( + "gravitino.utils.http_client.HTTPClient.put", + return_value=mock_resp, + ): + model = catalog.as_model_catalog().alter_model( + model_ident, ModelChange.rename("model2") + ) + + self._compare_models(model_dto, model) + def test_delete_model(self, *mock_method): gravitino_client = GravitinoClient( uri="http://localhost:8090", metalake_name=self._metalake_name diff --git a/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java b/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java new file mode 100644 index 0000000000..9b36a25d2c --- /dev/null +++ b/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.gravitino.dto.requests; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.google.common.base.Preconditions; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.model.ModelChange; +import org.apache.gravitino.rest.RESTRequest; + +/** Request to update a model. */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY) +@JsonSubTypes({ + @JsonSubTypes.Type(value = ModelUpdateRequest.RenameModelRequest.class, name = "rename") +}) +public interface ModelUpdateRequest extends RESTRequest { + + /** + * Returns the model change. + * + * @return the model change. + */ + ModelChange modelChange(); + + /** The model update request for rename model. */ + @EqualsAndHashCode + @ToString + class RenameModelRequest implements ModelUpdateRequest { + + @Getter + @JsonProperty("newName") + private final String newName; + + /** + * Returns the model change. + * + * @return An instance of ModelChange. + */ + @Override + public ModelChange modelChange() { + return ModelChange.rename(newName); + } + + /** + * Constructor for RenameModelRequest. + * + * @param newName the new name of the model + */ + public RenameModelRequest(String newName) { + this.newName = newName; + } + + /** Default constructor for Jackson deserialization. */ + public RenameModelRequest() { + this(null); + } + + /** + * Validates the request. + * + * @throws IllegalArgumentException If the request is invalid, this exception is thrown. + */ + @Override + public void validate() throws IllegalArgumentException { + Preconditions.checkArgument( + StringUtils.isNotBlank(newName), "\"newName\" field is required and cannot be empty"); + } + } +} diff --git a/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdatesRequest.java b/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdatesRequest.java new file mode 100644 index 0000000000..ba30149008 --- /dev/null +++ b/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdatesRequest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.gravitino.dto.requests; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.apache.gravitino.rest.RESTRequest; + +/** Represents a request to update a model. */ +@Getter +@EqualsAndHashCode +@ToString +public class ModelUpdatesRequest implements RESTRequest { + + @JsonProperty("updates") + private final List<ModelUpdateRequest> updates; + + /** + * Creates a new {@link ModelUpdatesRequest} instance. + * + * @param updates The updates to apply to the model. + */ + public ModelUpdatesRequest(List<ModelUpdateRequest> updates) { + this.updates = updates; + } + + /** This is the constructor that is used by Jackson deserializer */ + public ModelUpdatesRequest() { + this(null); + } + /** + * Validates the request. + * + * @throws IllegalArgumentException If the request is invalid, this exception is thrown. + */ + @Override + public void validate() throws IllegalArgumentException { + updates.forEach(RESTRequest::validate); + } +} diff --git a/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java b/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java index e4b80d0526..7c4ff37c58 100644 --- a/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java +++ b/server/src/main/java/org/apache/gravitino/server/web/rest/ModelOperations.java @@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletRequest; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -34,6 +35,8 @@ import org.apache.gravitino.NameIdentifier; import org.apache.gravitino.Namespace; import org.apache.gravitino.catalog.ModelDispatcher; import org.apache.gravitino.dto.requests.ModelRegisterRequest; +import org.apache.gravitino.dto.requests.ModelUpdateRequest; +import org.apache.gravitino.dto.requests.ModelUpdatesRequest; import org.apache.gravitino.dto.requests.ModelVersionLinkRequest; import org.apache.gravitino.dto.responses.BaseResponse; import org.apache.gravitino.dto.responses.DropResponse; @@ -44,6 +47,7 @@ import org.apache.gravitino.dto.responses.ModelVersionResponse; import org.apache.gravitino.dto.util.DTOConverters; import org.apache.gravitino.metrics.MetricNames; import org.apache.gravitino.model.Model; +import org.apache.gravitino.model.ModelChange; import org.apache.gravitino.model.ModelVersion; import org.apache.gravitino.server.web.Utils; import org.apache.gravitino.utils.NameIdentifierUtil; @@ -401,6 +405,39 @@ public class ModelOperations { } } + @PUT + @Path("{model}") + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "alter-model." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "alter-model", absolute = true) + public Response alterModel( + @PathParam("metalake") String metalake, + @PathParam("catalog") String catalog, + @PathParam("schema") String schema, + @PathParam("model") String model, + ModelUpdatesRequest request) { + LOG.info("Received alter model request: {}.{}.{}.{}", metalake, catalog, schema, model); + try { + return Utils.doAs( + httpRequest, + () -> { + request.validate(); + NameIdentifier ident = NameIdentifierUtil.ofModel(metalake, catalog, schema, model); + ModelChange[] changes = + request.getUpdates().stream() + .map(ModelUpdateRequest::modelChange) + .toArray(ModelChange[]::new); + Model m = modelDispatcher.alterModel(ident, changes); + Response response = Utils.ok(new ModelResponse(DTOConverters.toDTO(m))); + LOG.info("Model altered: {}.{}.{}.{}", metalake, catalog, schema, m.name()); + return response; + }); + + } catch (Exception e) { + return ExceptionHandlers.handleModelException(OperationType.ALTER, model, schema, e); + } + } + private String versionString(String model, int version) { return model + " version(" + version + ")"; } diff --git a/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java b/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java index c383a07a46..3d80600320 100644 --- a/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java +++ b/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.time.Instant; +import java.util.Collections; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.client.Entity; @@ -36,6 +37,8 @@ import org.apache.gravitino.NameIdentifier; import org.apache.gravitino.Namespace; import org.apache.gravitino.catalog.ModelDispatcher; import org.apache.gravitino.dto.requests.ModelRegisterRequest; +import org.apache.gravitino.dto.requests.ModelUpdateRequest; +import org.apache.gravitino.dto.requests.ModelUpdatesRequest; import org.apache.gravitino.dto.requests.ModelVersionLinkRequest; import org.apache.gravitino.dto.responses.BaseResponse; import org.apache.gravitino.dto.responses.DropResponse; @@ -50,6 +53,7 @@ import org.apache.gravitino.exceptions.NoSuchModelException; import org.apache.gravitino.exceptions.NoSuchSchemaException; import org.apache.gravitino.meta.AuditInfo; import org.apache.gravitino.model.Model; +import org.apache.gravitino.model.ModelChange; import org.apache.gravitino.model.ModelVersion; import org.apache.gravitino.rest.RESTUtils; import org.apache.gravitino.utils.NameIdentifierUtil; @@ -792,6 +796,70 @@ public class TestModelOperations extends JerseyTest { Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp2.getType()); } + @Test + public void testAlterModel() { + String oldName = "model1"; + String newName = "newModel1"; + String comment = "comment"; + + NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, schema, oldName); + Model updatedModel = mockModel(newName, comment, 0); + + // Mock alterModel to return updated model + when(modelDispatcher.alterModel(modelId, new ModelChange[] {ModelChange.rename(newName)})) + .thenReturn(updatedModel); + + // Build update request + ModelUpdatesRequest req = + new ModelUpdatesRequest( + Collections.singletonList(new ModelUpdateRequest.RenameModelRequest(newName))); + + Response resp = + target(modelPath()) + .path("model1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + ModelResponse modelResp = resp.readEntity(ModelResponse.class); + Assertions.assertEquals(comment, modelResp.getModel().comment()); + Assertions.assertEquals(newName, modelResp.getModel().name()); + + // Test NoSuchModelException + doThrow(new NoSuchModelException("mock error")) + .when(modelDispatcher) + .alterModel(modelId, new ModelChange[] {ModelChange.rename(newName)}); + + Response resp1 = + target(modelPath()) + .path("model1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resp1.getStatus()); + ErrorResponse errorResp = resp1.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, errorResp.getCode()); + + // Test RuntimeException + doThrow(new RuntimeException("mock error")) + .when(modelDispatcher) + .alterModel(modelId, new ModelChange[] {ModelChange.rename(newName)}); + + Response resp2 = + target(modelPath()) + .path("model1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp2.getStatus()); + ErrorResponse errorResp1 = resp2.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResp1.getCode()); + } + private String modelPath() { return "/metalakes/" + metalake + "/catalogs/" + catalog + "/schemas/" + schema + "/models"; }