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.
    
    
![image](https://github.com/user-attachments/assets/e3a65d9b-3813-42a2-8c17-e1fc9eba7e9c)
    
    ---------
    
    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";
   }

Reply via email to