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 <[email protected]>
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] <[email protected]>
Signed-off-by: George T. C. Lai <[email protected]>
Co-authored-by: roryqi <[email protected]>
Co-authored-by: dependabot[bot]
<49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mchades <[email protected]>
Co-authored-by: George T. C. Lai <[email protected]>
Co-authored-by: yangyang zhong <[email protected]>
---
.../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";
}