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 5720f4a45b [#6526] feat(core): Support update properties operations 
for model alteration (#6912)
5720f4a45b is described below

commit 5720f4a45b55cc04e076b432006a1240e6793f23
Author: Lord of Abyss <[email protected]>
AuthorDate: Mon Apr 14 14:22:25 2025 +0800

    [#6526] feat(core): Support update properties operations for model 
alteration (#6912)
    
    ### What changes were proposed in this pull request?
    
    Support update properties operations for model alteration.
    
    ### Why are the changes needed?
    
    Fix: #6526
    
    ### Does this PR introduce _any_ user-facing change?
    
    server support update/set property or remove a property from a model.
    
    ### How was this patch tested?
    
    local host
    `bin/gcli.sh model set -m demo_metalake --name
    model_catalog.schema.model2 --property key1 --value val1`
    <img width="1614" alt="image"
    
src="https://github.com/user-attachments/assets/32946856-4b24-4e71-a7bd-4ecada2c7397";
    />
    
    `bin/gcli.sh model set -m demo_metalake --name
    model_catalog.schema.model2 --property key1 --value val2`
    <img width="1060" alt="image"
    
src="https://github.com/user-attachments/assets/608734c3-8c3a-48ee-a3df-39e07b336ab7";
    />
    
    `bin/gcli.sh model remove -m demo_metalake --name
    model_catalog.schema.model2 --property key1`
    <img width="1039" alt="image"
    
src="https://github.com/user-attachments/assets/29725d56-b420-4541-b6f8-505c529bbd57";
    />
---
 .../org/apache/gravitino/model/ModelChange.java    | 160 ++++++++++++++++++++-
 .../catalog/model/ModelCatalogOperations.java      |  24 +++-
 .../catalog/model/TestModelCatalogOperations.java  | 111 +++++++++++++-
 .../integration/test/ModelCatalogOperationsIT.java |  57 ++++++++
 .../apache/gravitino/cli/ModelCommandHandler.java  |  26 ++++
 .../apache/gravitino/cli/TestableCommandLine.java  |  23 +++
 .../cli/commands/RemoveModelProperty.java          |  94 ++++++++++++
 .../gravitino/cli/commands/SetModelProperty.java   |  99 +++++++++++++
 .../apache/gravitino/cli/TestModelCommands.java    |  63 ++++++++
 .../org/apache/gravitino/client/DTOConverters.java |   9 ++
 .../client-python/gravitino/api/model_change.py    | 115 +++++++++++++++
 .../gravitino/client/generic_model_catalog.py      |   8 ++
 .../gravitino/dto/requests/model_update_request.py |  38 +++++
 .../tests/integration/test_model_catalog.py        |  53 +++++++
 .../gravitino/dto/requests/ModelUpdateRequest.java |  69 ++++++++-
 .../catalog/TestModelOperationDispatcher.java      |  85 +++++++++++
 .../gravitino/connector/TestCatalogOperations.java |  15 +-
 .../server/web/rest/TestModelOperations.java       | 120 +++++++++++++++-
 18 files changed, 1160 insertions(+), 9 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/model/ModelChange.java 
b/api/src/main/java/org/apache/gravitino/model/ModelChange.java
index 5d40c78856..513ad569e3 100644
--- a/api/src/main/java/org/apache/gravitino/model/ModelChange.java
+++ b/api/src/main/java/org/apache/gravitino/model/ModelChange.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.model;
 
+import java.util.Objects;
 import org.apache.gravitino.annotation.Evolving;
 
 /**
@@ -37,6 +38,27 @@ public interface ModelChange {
     return new ModelChange.RenameModel(newName);
   }
 
+  /**
+   * Create a ModelChange for setting a property and value of a model.
+   *
+   * @param property The name of the property to be set.
+   * @param value The value to be set for the property.
+   * @return A ModelChange for the property set.
+   */
+  static ModelChange setProperty(String property, String value) {
+    return new ModelChange.SetProperty(property, value);
+  }
+
+  /**
+   * Create a ModelChange for removing a property from a model.
+   *
+   * @param property The name of the property to be removed from the model.
+   * @return A ModelChange for the property removal.
+   */
+  static ModelChange removeProperty(String property) {
+    return new ModelChange.RemoveProperty(property);
+  }
+
   /** A ModelChange to rename a model. */
   final class RenameModel implements ModelChange {
     private final String newName;
@@ -70,9 +92,9 @@ public interface ModelChange {
     @Override
     public boolean equals(Object obj) {
       if (obj == this) return true;
-      if (obj == null || getClass() != obj.getClass()) return false;
+      if (!(obj instanceof RenameModel)) return false;
       RenameModel other = (RenameModel) obj;
-      return newName.equals(other.newName);
+      return Objects.equals(newName, other.newName);
     }
 
     /**
@@ -97,4 +119,138 @@ public interface ModelChange {
       return "RenameModel " + newName;
     }
   }
+
+  /** A ModelChange to set a property and value of a model. */
+  final class SetProperty implements ModelChange {
+    private final String property;
+    private final String value;
+
+    /**
+     * Constructs a new {@link SetProperty} instance with the specified 
property name and value.
+     *
+     * @param property The name of the property to be set.
+     * @param value The value to be set for the property.
+     */
+    public SetProperty(String property, String value) {
+      this.property = property;
+      this.value = value;
+    }
+
+    /**
+     * Retrieves the name of the property to be set.
+     *
+     * @return The name of the property to be set.
+     */
+    public String property() {
+      return property;
+    }
+
+    /**
+     * Retrieves the value to be set for the property.
+     *
+     * @return The value to be set for the property.
+     */
+    public String value() {
+      return value;
+    }
+
+    /**
+     * Compares this SetProperty instance with another object for equality. 
Two instances are
+     * considered equal if they target the same property and set the same 
value.
+     *
+     * @param obj The object to compare with this instance.
+     * @return {@code true} if the given object represents the same property 
set; {@code false}
+     *     otherwise.
+     */
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) return true;
+      if (!(obj instanceof SetProperty)) return false;
+      SetProperty other = (SetProperty) obj;
+      return Objects.equals(property, other.property) && Objects.equals(value, 
other.value);
+    }
+
+    /**
+     * Generates a hash code for this SetProperty instance. The hash code is 
based on the property
+     * name and value to be set.
+     *
+     * @return A hash code value for this property set operation.
+     */
+    @Override
+    public int hashCode() {
+      return Objects.hash(property, value);
+    }
+
+    /**
+     * Provides a string representation of the SetProperty instance. This 
string format includes the
+     * class name followed by the property name and value to be set.
+     *
+     * @return A string summary of the property set operation.
+     */
+    @Override
+    public String toString() {
+      return "SETPROPERTY " + property + " " + value;
+    }
+  }
+
+  /** A ModelChange to remove a property from model. */
+  final class RemoveProperty implements ModelChange {
+    private final String property;
+
+    /**
+     * Constructs a new {@link RemoveProperty} instance with the specified 
property name.
+     *
+     * @param property The name of the property to be removed from the model.
+     */
+    public RemoveProperty(String property) {
+      this.property = property;
+    }
+
+    /**
+     * Retrieves the name of the property to be removed from the model.
+     *
+     * @return The name of the property for removal.
+     */
+    public String property() {
+      return property;
+    }
+
+    /**
+     * Compares this RemoveProperty instance with another object for equality. 
Two instances are
+     * considered equal if they target the same property for removal from the 
fileset.
+     *
+     * @param obj The object to compare with this instance.
+     * @return {@code true} if the given object represents the same property 
removal; {@code false}
+     *     otherwise.
+     */
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) return true;
+      if (!(obj instanceof RemoveProperty)) return false;
+      RemoveProperty other = (RemoveProperty) obj;
+      return Objects.equals(property, other.property);
+    }
+
+    /**
+     * Generates a hash code for this RemoveProperty instance. The hash code 
is based on the
+     * property name that is to be removed from the fileset.
+     *
+     * @return A hash code value for this property removal operation.
+     */
+    @Override
+    public int hashCode() {
+      return property.hashCode();
+    }
+
+    /**
+     * Provides a string representation of the RemoveProperty instance. This 
string format includes
+     * the class name followed by the property name to be removed.
+     *
+     * @return A string summary of the property removal operation.
+     */
+    @Override
+    public String toString() {
+      return "RemoveProperty " + property;
+    }
+  }
 }
diff --git 
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogOperations.java
 
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogOperations.java
index 4a47880812..6ad4acb2b0 100644
--- 
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogOperations.java
+++ 
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogOperations.java
@@ -306,10 +306,17 @@ public class ModelCatalogOperations extends 
ManagedSchemaOperations
     AuditInfo entityAuditInfo = modelEntity.auditInfo();
     Namespace entityNamespace = modelEntity.namespace();
     Integer entityLatestVersion = modelEntity.latestVersion();
+    String modifier = PrincipalUtils.getCurrentPrincipal().getName();
 
     for (ModelChange change : changes) {
       if (change instanceof ModelChange.RenameModel) {
         entityName = ((ModelChange.RenameModel) change).newName();
+      } else if (change instanceof ModelChange.SetProperty) {
+        ModelChange.SetProperty setPropertyChange = (ModelChange.SetProperty) 
change;
+        doSetProperty(entityProperties, setPropertyChange);
+      } else if (change instanceof ModelChange.RemoveProperty) {
+        ModelChange.RemoveProperty removePropertyChange = 
(ModelChange.RemoveProperty) change;
+        doRemoveProperty(entityProperties, removePropertyChange);
       } else {
         throw new IllegalArgumentException(
             "Unsupported model change: " + change.getClass().getSimpleName());
@@ -320,7 +327,13 @@ public class ModelCatalogOperations extends 
ManagedSchemaOperations
         .withName(entityName)
         .withId(entityId)
         .withComment(entityComment)
-        .withAuditInfo(entityAuditInfo)
+        .withAuditInfo(
+            AuditInfo.builder()
+                .withCreator(entityAuditInfo.creator())
+                .withCreateTime(entityAuditInfo.createTime())
+                .withLastModifier(modifier)
+                .withLastModifiedTime(Instant.now())
+                .build())
         .withNamespace(entityNamespace)
         .withProperties(entityProperties)
         .withLatestVersion(entityLatestVersion)
@@ -368,4 +381,13 @@ public class ModelCatalogOperations extends 
ManagedSchemaOperations
       throw new RuntimeException("Failed to delete model version " + ident, 
ioe);
     }
   }
+
+  private void doRemoveProperty(
+      Map<String, String> entityProperties, ModelChange.RemoveProperty change) 
{
+    entityProperties.remove(change.property());
+  }
+
+  private void doSetProperty(Map<String, String> entityProperties, 
ModelChange.SetProperty change) {
+    entityProperties.put(change.property(), change.value());
+  }
 }
diff --git 
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
 
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
index 83fcc1505f..bb6c010456 100644
--- 
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
+++ 
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
@@ -34,6 +34,7 @@ import static 
org.apache.gravitino.Configs.STORE_TRANSACTION_MAX_SKEW_TIME;
 import static org.apache.gravitino.Configs.VERSION_RETENTION_COUNT;
 import static org.mockito.Mockito.when;
 
+import com.google.common.collect.ImmutableMap;
 import java.io.File;
 import java.io.IOException;
 import java.time.Instant;
@@ -658,7 +659,7 @@ public class TestModelCatalogOperations {
   }
 
   @Test
-  public void testRename() {
+  public void testRenameModel() {
     String schemaName = randomSchemaName();
     createSchema(schemaName);
 
@@ -689,6 +690,114 @@ public class TestModelCatalogOperations {
     Assertions.assertEquals(properties, alteredModel.properties());
   }
 
+  @Test
+  void testAddModelProperty() {
+    String schemaName = randomSchemaName();
+    createSchema(schemaName);
+
+    String modelName = "model";
+    String comment = "comment";
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName, 
modelName);
+    StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> properties =
+        StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of("key1", 
"value1"));
+    Map<String, String> newProperties =
+        ImmutableMap.<String, String>builder().putAll(properties).put("key2", 
"value2").build();
+
+    // validate registered model
+    Model registeredModel = ops.registerModel(modelIdent, comment, properties);
+    Assertions.assertEquals(modelName, registeredModel.name());
+    Assertions.assertEquals(comment, registeredModel.comment());
+    Assertions.assertEquals(properties, registeredModel.properties());
+
+    // validate loaded model
+    Model loadedModel = ops.getModel(modelIdent);
+    Assertions.assertEquals(modelName, loadedModel.name());
+    Assertions.assertEquals(comment, loadedModel.comment());
+    Assertions.assertEquals(properties, loadedModel.properties());
+
+    ModelChange change = ModelChange.setProperty("key2", "value2");
+    Model alteredModel = ops.alterModel(modelIdent, change);
+
+    // validate altered model
+    Assertions.assertEquals(modelName, alteredModel.name());
+    Assertions.assertEquals(comment, alteredModel.comment());
+    Assertions.assertEquals(newProperties, alteredModel.properties());
+  }
+
+  @Test
+  void testUpdateModelProperty() {
+    String schemaName = randomSchemaName();
+    createSchema(schemaName);
+
+    String modelName = "model";
+    String comment = "comment";
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName, 
modelName);
+    StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> properties =
+        StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of("key1", 
"value1"));
+    Map<String, String> newProperties =
+        StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of("key1", 
"value2"));
+
+    // validate registered model
+    Model registeredModel = ops.registerModel(modelIdent, comment, properties);
+    Assertions.assertEquals(modelName, registeredModel.name());
+    Assertions.assertEquals(comment, registeredModel.comment());
+    Assertions.assertEquals(properties, registeredModel.properties());
+
+    // validate loaded model
+    Model loadedModel = ops.getModel(modelIdent);
+    Assertions.assertEquals(modelName, loadedModel.name());
+    Assertions.assertEquals(comment, loadedModel.comment());
+    Assertions.assertEquals(properties, loadedModel.properties());
+
+    ModelChange change = ModelChange.setProperty("key1", "value2");
+    Model alteredModel = ops.alterModel(modelIdent, change);
+
+    // validate altered model
+    Assertions.assertEquals(modelName, alteredModel.name());
+    Assertions.assertEquals(comment, alteredModel.comment());
+    Assertions.assertEquals(newProperties, alteredModel.properties());
+  }
+
+  @Test
+  void testRemoveModelProperty() {
+    String schemaName = randomSchemaName();
+    createSchema(schemaName);
+
+    String modelName = "model";
+    String comment = "comment";
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName, 
modelName);
+    StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> properties =
+        StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of("key1", 
"value1"));
+    Map<String, String> newProperties =
+        StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of());
+
+    // validate registered model
+    Model registeredModel = ops.registerModel(modelIdent, comment, properties);
+    Assertions.assertEquals(modelName, registeredModel.name());
+    Assertions.assertEquals(comment, registeredModel.comment());
+    Assertions.assertEquals(properties, registeredModel.properties());
+
+    // validate loaded model
+    Model loadedModel = ops.getModel(modelIdent);
+    Assertions.assertEquals(modelName, loadedModel.name());
+    Assertions.assertEquals(comment, loadedModel.comment());
+    Assertions.assertEquals(properties, loadedModel.properties());
+
+    ModelChange change = ModelChange.removeProperty("key1");
+    Model alteredModel = ops.alterModel(modelIdent, change);
+
+    // validate altered model
+    Assertions.assertEquals(modelName, alteredModel.name());
+    Assertions.assertEquals(comment, alteredModel.comment());
+    Assertions.assertEquals(newProperties, alteredModel.properties());
+  }
+
   private String randomSchemaName() {
     return "schema_" + UUID.randomUUID().toString().replace("-", "");
   }
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 cbea8e1d66..0cabc1495c 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
@@ -360,6 +360,63 @@ public class ModelCatalogOperationsIT extends BaseIT {
                 .alterModel(NameIdentifier.of(schemaName, null), updateName));
   }
 
+  @Test
+  void testRegisterAndAddModelProperty() {
+    String comment = "comment";
+    String modelName = RandomNameUtils.genRandomName("alter_name_model");
+    NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+    Map<String, String> properties = ImmutableMap.of("owner", "data-team", 
"key1", "val1");
+    Map<String, String> newProperties =
+        ImmutableMap.of("owner", "data-team", "key1", "val1", "key2", "val2");
+
+    Model createdModel =
+        gravitinoCatalog.asModelCatalog().registerModel(modelIdent, comment, 
properties);
+
+    ModelChange addProperty = ModelChange.setProperty("key2", "val2");
+    Model alteredModel = 
gravitinoCatalog.asModelCatalog().alterModel(modelIdent, addProperty);
+
+    Assertions.assertEquals(modelName, alteredModel.name());
+    Assertions.assertNotEquals(createdModel.properties(), 
alteredModel.properties());
+    Assertions.assertEquals(newProperties, alteredModel.properties());
+    Assertions.assertEquals(createdModel.comment(), alteredModel.comment());
+  }
+
+  @Test
+  void testRegisterAndUpdateModelProperty() {
+    String comment = "comment";
+    String modelName = RandomNameUtils.genRandomName("alter_name_model");
+    NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+    Map<String, String> properties = ImmutableMap.of("owner", "data-team", 
"key1", "val1");
+    Map<String, String> newProperties = ImmutableMap.of("owner", "data-team", 
"key1", "val3");
+
+    Model createdModel =
+        gravitinoCatalog.asModelCatalog().registerModel(modelIdent, comment, 
properties);
+    ModelChange addProperty = ModelChange.setProperty("key1", "val3");
+    Model alteredModel = 
gravitinoCatalog.asModelCatalog().alterModel(modelIdent, addProperty);
+
+    Assertions.assertEquals(modelName, alteredModel.name());
+    Assertions.assertEquals(newProperties, alteredModel.properties());
+    Assertions.assertEquals(createdModel.comment(), alteredModel.comment());
+  }
+
+  @Test
+  void testRegisterAndRemoveModelProperty() {
+    String comment = "comment";
+    String modelName = RandomNameUtils.genRandomName("alter_name_model");
+    NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+    Map<String, String> properties = ImmutableMap.of("owner", "data-team", 
"key1", "val1");
+    Map<String, String> newProperties = ImmutableMap.of("owner", "data-team");
+
+    Model createdModel =
+        gravitinoCatalog.asModelCatalog().registerModel(modelIdent, comment, 
properties);
+    ModelChange addProperty = ModelChange.removeProperty("key1");
+    Model alteredModel = 
gravitinoCatalog.asModelCatalog().alterModel(modelIdent, addProperty);
+
+    Assertions.assertEquals(modelName, alteredModel.name());
+    Assertions.assertEquals(newProperties, alteredModel.properties());
+    Assertions.assertEquals(createdModel.comment(), alteredModel.comment());
+  }
+
   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 5a417125bd..6bbe796dba 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
@@ -111,6 +111,14 @@ public class ModelCommandHandler extends CommandHandler {
         handleUpdateCommand();
         return true;
 
+      case CommandActions.SET:
+        handleSetCommand();
+        return true;
+
+      case CommandActions.REMOVE:
+        handleRemoveCommand();
+        return true;
+
       default:
         return false;
     }
@@ -178,4 +186,22 @@ public class ModelCommandHandler extends CommandHandler {
   private void handleListCommand() {
     gravitinoCommandLine.newListModel(context, metalake, catalog, 
schema).validate().handle();
   }
+
+  /** Handles the "SET" command. */
+  private void handleSetCommand() {
+    String property = line.getOptionValue(GravitinoOptions.PROPERTY);
+    String value = line.getOptionValue(GravitinoOptions.VALUE);
+    gravitinoCommandLine
+        .newSetModelProperty(context, metalake, catalog, schema, model, 
property, value)
+        .validate()
+        .handle();
+  }
+
+  private void handleRemoveCommand() {
+    String property = line.getOptionValue(GravitinoOptions.PROPERTY);
+    gravitinoCommandLine
+        .newRemoveModelProperty(context, metalake, catalog, schema, model, 
property)
+        .validate()
+        .handle();
+  }
 }
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 5ee96da889..9bf17aacd5 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
@@ -89,6 +89,7 @@ import org.apache.gravitino.cli.commands.RemoveAllTags;
 import org.apache.gravitino.cli.commands.RemoveCatalogProperty;
 import org.apache.gravitino.cli.commands.RemoveFilesetProperty;
 import org.apache.gravitino.cli.commands.RemoveMetalakeProperty;
+import org.apache.gravitino.cli.commands.RemoveModelProperty;
 import org.apache.gravitino.cli.commands.RemoveRoleFromGroup;
 import org.apache.gravitino.cli.commands.RemoveRoleFromUser;
 import org.apache.gravitino.cli.commands.RemoveSchemaProperty;
@@ -105,6 +106,7 @@ import org.apache.gravitino.cli.commands.ServerVersion;
 import org.apache.gravitino.cli.commands.SetCatalogProperty;
 import org.apache.gravitino.cli.commands.SetFilesetProperty;
 import org.apache.gravitino.cli.commands.SetMetalakeProperty;
+import org.apache.gravitino.cli.commands.SetModelProperty;
 import org.apache.gravitino.cli.commands.SetOwner;
 import org.apache.gravitino.cli.commands.SetSchemaProperty;
 import org.apache.gravitino.cli.commands.SetTableProperty;
@@ -883,6 +885,27 @@ public class TestableCommandLine {
     return new UpdateModelName(context, metalake, catalog, schema, model, 
rename);
   }
 
+  protected SetModelProperty newSetModelProperty(
+      CommandContext context,
+      String metalake,
+      String catalog,
+      String schema,
+      String model,
+      String property,
+      String value) {
+    return new SetModelProperty(context, metalake, catalog, schema, model, 
property, value);
+  }
+
+  protected RemoveModelProperty newRemoveModelProperty(
+      CommandContext context,
+      String metalake,
+      String catalog,
+      String schema,
+      String model,
+      String property) {
+    return new RemoveModelProperty(context, metalake, catalog, schema, model, 
property);
+  }
+
   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/RemoveModelProperty.java
 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RemoveModelProperty.java
new file mode 100644
index 0000000000..ae208a68b5
--- /dev/null
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RemoveModelProperty.java
@@ -0,0 +1,94 @@
+/*
+ * 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;
+
+/** Removes a property of a model. */
+public class RemoveModelProperty extends Command {
+  protected final String metalake;
+  protected final String catalog;
+  protected final String schema;
+  protected final String model;
+  protected final String property;
+
+  /**
+   * Constructs a new {@link RemoveModelProperty} 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 property The name of the property to remove
+   */
+  public RemoveModelProperty(
+      CommandContext context,
+      String metalake,
+      String catalog,
+      String schema,
+      String model,
+      String property) {
+    super(context);
+    this.metalake = metalake;
+    this.catalog = catalog;
+    this.schema = schema;
+    this.model = model;
+    this.property = property;
+  }
+
+  /** Removes a property of a model. */
+  @Override
+  public void handle() {
+    try {
+      NameIdentifier name = NameIdentifier.of(schema, model);
+      GravitinoClient client = buildClient(metalake);
+      ModelChange change = ModelChange.removeProperty(property);
+      client.loadCatalog(catalog).asModelCatalog().alterModel(name, change);
+    } 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(property + " property removed.");
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public Command validate() {
+    validateProperty(property);
+    return super.validate();
+  }
+}
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
new file mode 100644
index 0000000000..451e1f71c3
--- /dev/null
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+/** Set a property of a model. */
+public class SetModelProperty extends Command {
+
+  private final String metalake;
+  private final String catalog;
+  private final String schema;
+  private final String model;
+  private final String property;
+  private final String value;
+
+  /**
+   * Construct a new {@link SetModelProperty} 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 property The name of the property to set.
+   * @param value The value to set the property to.
+   */
+  public SetModelProperty(
+      CommandContext context,
+      String metalake,
+      String catalog,
+      String schema,
+      String model,
+      String property,
+      String value) {
+    super(context);
+    this.metalake = metalake;
+    this.catalog = catalog;
+    this.schema = schema;
+    this.model = model;
+    this.property = property;
+    this.value = value;
+  }
+
+  /** Set a property of a model. */
+  @Override
+  public void handle() {
+    try {
+      NameIdentifier name = NameIdentifier.of(schema, model);
+      GravitinoClient client = buildClient(metalake);
+      ModelChange change = ModelChange.setProperty(property, value);
+      client.loadCatalog(catalog).asModelCatalog().alterModel(name, change);
+    } 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_TABLE);
+    } catch (Exception exp) {
+      exitWithError(exp.getMessage());
+    }
+
+    printInformation(model + " property set.");
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public Command validate() {
+    validatePropertyAndValue(property, value);
+    return super.validate();
+  }
+}
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 1035418cb2..9f03769093 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,8 @@ 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.RemoveModelProperty;
+import org.apache.gravitino.cli.commands.SetModelProperty;
 import org.apache.gravitino.cli.commands.UpdateModelName;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -598,4 +600,65 @@ public class TestModelCommands {
     commandLine.handleCommandLine();
     verify(mockUpdate).handle();
   }
+
+  @Test
+  void testSetModelProperty() {
+    SetModelProperty mockSetProperty = mock(SetModelProperty.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.PROPERTY)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.PROPERTY)).thenReturn("key");
+    when(mockCommandLine.hasOption(GravitinoOptions.VALUE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.VALUE)).thenReturn("value");
+
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.MODEL, 
CommandActions.SET));
+
+    doReturn(mockSetProperty)
+        .when(commandLine)
+        .newSetModelProperty(
+            any(CommandContext.class),
+            eq("metalake_demo"),
+            eq("catalog"),
+            eq("schema"),
+            eq("model"),
+            eq("key"),
+            eq("value"));
+    doReturn(mockSetProperty).when(mockSetProperty).validate();
+    commandLine.handleCommandLine();
+    verify(mockSetProperty).handle();
+  }
+
+  @Test
+  void testRemoveModelProperty() {
+    RemoveModelProperty mockRemoveProperty = mock(RemoveModelProperty.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.PROPERTY)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.PROPERTY)).thenReturn("key");
+
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.MODEL, 
CommandActions.REMOVE));
+
+    doReturn(mockRemoveProperty)
+        .when(commandLine)
+        .newRemoveModelProperty(
+            any(CommandContext.class),
+            eq("metalake_demo"),
+            eq("catalog"),
+            eq("schema"),
+            eq("model"),
+            eq("key"));
+    doReturn(mockRemoveProperty).when(mockRemoveProperty).validate();
+    commandLine.handleCommandLine();
+    verify(mockRemoveProperty).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 8304750385..e76b1a0b1d 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
@@ -363,6 +363,15 @@ class DTOConverters {
       return new ModelUpdateRequest.RenameModelRequest(
           ((ModelChange.RenameModel) change).newName());
 
+    } else if (change instanceof ModelChange.RemoveProperty) {
+      return new ModelUpdateRequest.RemoveModelPropertyRequest(
+          ((ModelChange.RemoveProperty) change).property());
+
+    } else if (change instanceof ModelChange.SetProperty) {
+      return new ModelUpdateRequest.SetModelPropertyRequest(
+          ((ModelChange.SetProperty) change).property(),
+          ((ModelChange.SetProperty) change).value());
+
     } else {
       throw new IllegalArgumentException(
           "Unknown model change type: " + change.getClass().getSimpleName());
diff --git a/clients/client-python/gravitino/api/model_change.py 
b/clients/client-python/gravitino/api/model_change.py
index f2557cd2cc..99737d7823 100644
--- a/clients/client-python/gravitino/api/model_change.py
+++ b/clients/client-python/gravitino/api/model_change.py
@@ -34,6 +34,27 @@ class ModelChange(ABC):
         """
         return ModelChange.RenameModel(new_name)
 
+    @staticmethod
+    def set_property(pro, value):
+        """Creates a new model change to set the property and value pairs for 
the model.
+        Args:
+            property: The name of the property to be set.
+            value: The value of the property to be set.
+        Returns:
+            The model change.
+        """
+        return ModelChange.SetProperty(pro, value)
+
+    @staticmethod
+    def remove_property(pro):
+        """Creates a new model change to remove the property and value pairs 
for the model.
+        Args:
+            property: The name of the property to be removed.
+        Returns:
+            The model change.
+        """
+        return ModelChange.RemoveProperty(pro)
+
     class RenameModel:
         """A model change to rename the model."""
 
@@ -78,3 +99,97 @@ class ModelChange(ABC):
                 A string summary of this renaming operation.
             """
             return f"RENAMEMODEL {self.new_name()}"
+
+    class SetProperty:
+        """
+        A model change to set the property and value pairs for the model.
+        """
+
+        def __init__(self, pro, value):
+            self._property = pro
+            self._value = value
+
+        def property(self):
+            """Retrieves the name of the property to be set.
+            Returns:
+                The name of the property.
+            """
+            return self._property
+
+        def value(self):
+            """Retrieves the value of the property to be set.
+            Returns:
+                The value of the property.
+            """
+            return self._value
+
+        def __eq__(self, other) -> bool:
+            """Compares this SetProperty instance with another object for 
equality. Two instances are
+            considered equal if they designate the same property and value for 
the model.
+            Args:
+                other: The object to compare with this instance.
+            Returns:
+                true if the given object represents an identical model 
property setting operation; false otherwise.
+            """
+            if not isinstance(other, ModelChange.SetProperty):
+                return False
+            return self.property() == other.property() and self.value() == 
other.value()
+
+        def __hash__(self):
+            """Generates a hash code for this SetProperty instance. The hash 
code is primarily based on
+            the property and value for the model.
+            Returns:
+                A hash code value for this property setting operation.
+            """
+            return hash(self.property(), self.value())
+
+        def __str__(self):
+            """Provides a string representation of the SetProperty instance. 
This string includes the
+            class name followed by the property and value of the model.
+            Returns:
+                A string summary of this property setting operation.
+            """
+            return f"SETPROPERTY {self.property()}={self.value()}"
+
+    class RemoveProperty:
+        """
+        A model change to remove the property and value pairs for the model.
+        """
+
+        def __init__(self, pro):
+            self._property = pro
+
+        def property(self):
+            """Retrieves the name of the property to be removed.
+            Returns:
+                The name of the property.
+            """
+            return self._property
+
+        def __eq__(self, other) -> bool:
+            """Compares this RemoveProperty instance with another object for 
equality. Two instances are
+            considered equal if they designate the same property for the model.
+            Args:
+                other: The object to compare with this instance.
+            Returns:
+                true if the given object represents an identical model 
property removal operation; false otherwise.
+            """
+            if not isinstance(other, ModelChange.RemoveProperty):
+                return False
+            return self.property() == other.property()
+
+        def __hash__(self):
+            """Generates a hash code for this RemoveProperty instance. The 
hash code is primarily based on
+            the property for the model.
+            Returns:
+                A hash code value for this property removal operation.
+            """
+            return hash(self.property())
+
+        def __str__(self):
+            """Provides a string representation of the RemoveProperty 
instance. This string includes the
+            class name followed by the property of the model.
+            Returns:
+                A string summary of this property removal operation.
+            """
+            return f"REMOVEPROPERTY {self.property()}"
diff --git a/clients/client-python/gravitino/client/generic_model_catalog.py 
b/clients/client-python/gravitino/client/generic_model_catalog.py
index 1dbf96db56..79cfcdb2e9 100644
--- a/clients/client-python/gravitino/client/generic_model_catalog.py
+++ b/clients/client-python/gravitino/client/generic_model_catalog.py
@@ -429,6 +429,14 @@ class GenericModelCatalog(BaseSchemaCatalog):
         if isinstance(change, ModelChange.RenameModel):
             return ModelUpdateRequest.UpdateModelNameRequest(change.new_name())
 
+        if isinstance(change, ModelChange.SetProperty):
+            return ModelUpdateRequest.ModelSetPropertyRequest(
+                change.property(), change.value()
+            )
+
+        if isinstance(change, ModelChange.RemoveProperty):
+            return 
ModelUpdateRequest.ModelRemovePropertyRequest(change.property())
+
         raise ValueError(f"Unknown change type: {type(change).__name__}")
 
     def _check_model_namespace(self, namespace: 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
index 6310a47be0..5769f2015f 100644
--- a/clients/client-python/gravitino/dto/requests/model_update_request.py
+++ b/clients/client-python/gravitino/dto/requests/model_update_request.py
@@ -59,3 +59,41 @@ class ModelUpdateRequest:
 
         def model_change(self) -> ModelChange:
             return ModelChange.rename(self._new_name)
+
+    @dataclass
+    class ModelSetPropertyRequest(ModelUpdateRequestBase):
+        """Request to set model property"""
+
+        _property: Optional[str] = 
field(metadata=config(field_name="property"))
+        _value: Optional[str] = field(metadata=config(field_name="value"))
+
+        def __init__(self, pro: str, value: str):
+            super().__init__("setProperty")
+            self._property = pro
+            self._value = value
+
+        def validate(self):
+            if not self._property:
+                raise ValueError('"property" field is required')
+            if not self._value:
+                raise ValueError('"value" field is required')
+
+        def model_change(self) -> ModelChange:
+            return ModelChange.set_property(self._property, self._value)
+
+    @dataclass
+    class ModelRemovePropertyRequest(ModelUpdateRequestBase):
+        """Request to remove model property"""
+
+        _property: Optional[str] = 
field(metadata=config(field_name="property"))
+
+        def __init__(self, pro: str):
+            super().__init__("removeProperty")
+            self._property = pro
+
+        def validate(self):
+            if not self._property:
+                raise ValueError('"property" field is required')
+
+        def model_change(self) -> ModelChange:
+            return ModelChange.remove_property(self._property)
diff --git a/clients/client-python/tests/integration/test_model_catalog.py 
b/clients/client-python/tests/integration/test_model_catalog.py
index 9a9f0e2743..88ef813bd5 100644
--- a/clients/client-python/tests/integration/test_model_catalog.py
+++ b/clients/client-python/tests/integration/test_model_catalog.py
@@ -227,6 +227,59 @@ class TestModelCatalog(IntegrationTestEnv):
         self.assertEqual(0, renamed_model.latest_version())
         self.assertEqual(properties, renamed_model.properties())
 
+    def test_register_alter_model_with_set_property(self):
+        model_name = f"model_it_model{str(randint(0, 1000))}"
+        model_ident = NameIdentifier.of(self._schema_name, model_name)
+        comment = "comment"
+        properties = {"k1": "v1", "k2": "v2"}
+        self._catalog.as_model_catalog().register_model(
+            model_ident, comment, properties
+        )
+        origin_model = self._catalog.as_model_catalog().get_model(model_ident)
+
+        self.assertEqual(origin_model.name(), model_name)
+        self.assertEqual(origin_model.comment(), comment)
+        self.assertEqual(origin_model.latest_version(), 0)
+        self.assertEqual(origin_model.properties(), properties)
+
+        changes = [
+            ModelChange.set_property("k1", "v11"),
+            ModelChange.set_property("k3", "v3"),
+        ]
+
+        self._catalog.as_model_catalog().alter_model(model_ident, *changes)
+        update_property_model = 
self._catalog.as_model_catalog().get_model(model_ident)
+
+        self.assertEqual(update_property_model.name(), model_name)
+        self.assertEqual(update_property_model.comment(), comment)
+        self.assertEqual(update_property_model.latest_version(), 0)
+        self.assertEqual(
+            update_property_model.properties(), {"k1": "v11", "k2": "v2", 
"k3": "v3"}
+        )
+
+    def test_register_alter_model_with_remove_property(self):
+        model_name = f"model_it_model{str(randint(0, 1000))}"
+        model_ident = NameIdentifier.of(self._schema_name, model_name)
+        comment = "comment"
+        properties = {"k1": "v1", "k2": "v2"}
+
+        self._catalog.as_model_catalog().register_model(
+            model_ident, comment, properties
+        )
+        origin_model = self._catalog.as_model_catalog().get_model(model_ident)
+        self.assertEqual(origin_model.name(), model_name)
+        self.assertEqual(origin_model.comment(), comment)
+        self.assertEqual(origin_model.latest_version(), 0)
+        self.assertEqual(origin_model.properties(), properties)
+
+        changes = [ModelChange.remove_property("k1")]
+        self._catalog.as_model_catalog().alter_model(model_ident, *changes)
+        update_property_model = 
self._catalog.as_model_catalog().get_model(model_ident)
+        self.assertEqual(update_property_model.name(), model_name)
+        self.assertEqual(update_property_model.comment(), comment)
+        self.assertEqual(update_property_model.latest_version(), 0)
+        self.assertEqual(update_property_model.properties(), {"k2": "v2"})
+
     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/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java
index 9b36a25d2c..ad06d75597 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java
@@ -24,8 +24,10 @@ 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.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
+import lombok.NoArgsConstructor;
 import lombok.ToString;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.model.ModelChange;
@@ -35,7 +37,11 @@ import org.apache.gravitino.rest.RESTRequest;
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
 @JsonSubTypes({
-  @JsonSubTypes.Type(value = ModelUpdateRequest.RenameModelRequest.class, name 
= "rename")
+  @JsonSubTypes.Type(value = ModelUpdateRequest.RenameModelRequest.class, name 
= "rename"),
+  @JsonSubTypes.Type(
+      value = ModelUpdateRequest.RemoveModelPropertyRequest.class,
+      name = "removeProperty"),
+  @JsonSubTypes.Type(value = ModelUpdateRequest.SetModelPropertyRequest.class, 
name = "setProperty")
 })
 public interface ModelUpdateRequest extends RESTRequest {
 
@@ -90,4 +96,65 @@ public interface ModelUpdateRequest extends RESTRequest {
           StringUtils.isNotBlank(newName), "\"newName\" field is required and 
cannot be empty");
     }
   }
+
+  /** The model update request for set property of model. */
+  @EqualsAndHashCode
+  @AllArgsConstructor
+  @NoArgsConstructor(force = true)
+  @ToString
+  @Getter
+  class SetModelPropertyRequest implements ModelUpdateRequest {
+    @JsonProperty("property")
+    private final String property;
+
+    @JsonProperty("value")
+    private final String value;
+
+    /** {@inheritDoc} */
+    @Override
+    public ModelChange modelChange() {
+      return ModelChange.setProperty(property, value);
+    }
+
+    /**
+     * Validates the request, i.e., checks if the property and value are not 
empty and not null.
+     *
+     * @throws IllegalArgumentException If the request is invalid, this 
exception is thrown.
+     */
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(property), "\"property\" field is required 
and cannot be empty");
+      Preconditions.checkArgument(value != null, "\"value\" field is required 
and cannot be null");
+    }
+  }
+
+  /** The model update request for remove property from model. */
+  @EqualsAndHashCode
+  @AllArgsConstructor
+  @NoArgsConstructor(force = true)
+  @ToString
+  @Getter
+  class RemoveModelPropertyRequest implements ModelUpdateRequest {
+
+    @JsonProperty("property")
+    private final String property;
+
+    /** {@inheritDoc} */
+    @Override
+    public ModelChange modelChange() {
+      return ModelChange.removeProperty(property);
+    }
+
+    /**
+     * Validates the request, i.e., checks if the property is not empty.
+     *
+     * @throws IllegalArgumentException If the request is invalid, this 
exception is thrown.
+     */
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(property), "\"property\" field is required 
and cannot be empty");
+    }
+  }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
index dd1a1d48c3..24d48c7fed 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
@@ -278,6 +278,91 @@ public class TestModelOperationDispatcher extends 
TestOperationDispatcher {
     Assertions.assertEquals(model.properties(), alteredModel.properties());
   }
 
+  @Test
+  void testAddModelProperty() {
+    String schemaName = "schema";
+    String modelName = "test_update_model_property";
+    String modelComment = "model which tests update property";
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
schemaName);
+    schemaOperationDispatcher.createSchema(
+        schemaIdent, "schema comment", ImmutableMap.of("k1", "v1", "k2", 
"v2"));
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    Model model = modelOperationDispatcher.registerModel(modelIdent, 
modelComment, props);
+
+    // validate registered model
+    Assertions.assertEquals(modelName, model.name());
+    Assertions.assertEquals(modelComment, model.comment());
+    Assertions.assertEquals(props, model.properties());
+
+    ModelChange[] addProperty = new ModelChange[] 
{ModelChange.setProperty("k3", "v3")};
+    Model alteredModel = modelOperationDispatcher.alterModel(modelIdent, 
addProperty);
+
+    // validate updated model
+    Assertions.assertEquals(modelName, alteredModel.name());
+    Assertions.assertEquals(modelComment, alteredModel.comment());
+    Assertions.assertEquals(
+        ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), 
alteredModel.properties());
+  }
+
+  @Test
+  void testUpdateModelProperty() {
+    String schemaName = "test_update_model_property_schema";
+    String modelName = "test_update_model_property";
+    String modelComment = "model which tests update property";
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
schemaName);
+    schemaOperationDispatcher.createSchema(
+        schemaIdent, "schema comment", ImmutableMap.of("k1", "v1", "k2", 
"v2"));
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    Model model = modelOperationDispatcher.registerModel(modelIdent, 
modelComment, props);
+
+    // validate registered model
+    Assertions.assertEquals(modelName, model.name());
+    Assertions.assertEquals(modelComment, model.comment());
+    Assertions.assertEquals(props, model.properties());
+
+    ModelChange[] updateProperty = new ModelChange[] 
{ModelChange.setProperty("k1", "v3")};
+    Model alteredModel = modelOperationDispatcher.alterModel(modelIdent, 
updateProperty);
+
+    // validate updated model
+    Assertions.assertEquals(modelName, alteredModel.name());
+    Assertions.assertEquals(modelComment, alteredModel.comment());
+    Assertions.assertEquals(ImmutableMap.of("k1", "v3", "k2", "v2"), 
alteredModel.properties());
+  }
+
+  @Test
+  void testRemoveModelProperty() {
+    String schemaName = "test_remove_model_property_schema";
+    String modelName = "test_update_model_property";
+    String modelComment = "model which tests update property";
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
schemaName);
+    schemaOperationDispatcher.createSchema(
+        schemaIdent, "schema comment", ImmutableMap.of("k1", "v1", "k2", 
"v2"));
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    Model model = modelOperationDispatcher.registerModel(modelIdent, 
modelComment, props);
+
+    // validate registered model
+    Assertions.assertEquals(modelName, model.name());
+    Assertions.assertEquals(modelComment, model.comment());
+    Assertions.assertEquals(props, model.properties());
+
+    ModelChange[] removeProperty = new ModelChange[] 
{ModelChange.removeProperty("k1")};
+    Model alteredModel = modelOperationDispatcher.alterModel(modelIdent, 
removeProperty);
+
+    // validate updated model
+    Assertions.assertEquals(modelName, alteredModel.name());
+    Assertions.assertEquals(modelComment, alteredModel.comment());
+    Assertions.assertEquals(ImmutableMap.of("k2", "v2"), 
alteredModel.properties());
+  }
+
   private String randomSchemaName() {
     return "schema_" + UUID.randomUUID().toString().replace("-", "");
   }
diff --git 
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java 
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
index c0a3f87588..17d3f5c217 100644
--- 
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
+++ 
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
@@ -19,6 +19,7 @@
 package org.apache.gravitino.connector;
 
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import java.io.File;
 import java.io.IOException;
@@ -913,7 +914,9 @@ public class TestCatalogOperations
 
     TestModel model = models.get(ident);
     Map<String, String> newProps =
-        model.properties() != null ? Maps.newHashMap(model.properties()) : 
Maps.newHashMap();
+        model.properties() == null ? ImmutableMap.of() : new 
HashMap<>(model.properties());
+    String newComment = model.comment();
+    int newLatestVersion = model.latestVersion();
 
     NameIdentifier newIdent = ident;
     for (ModelChange change : changes) {
@@ -923,15 +926,21 @@ public class TestCatalogOperations
         if (models.containsKey(newIdent)) {
           throw new ModelAlreadyExistsException("Model %s already exists", 
ident);
         }
+      } else if (change instanceof ModelChange.RemoveProperty) {
+        ModelChange.RemoveProperty removeProperty = 
(ModelChange.RemoveProperty) change;
+        newProps.remove(removeProperty.property());
+      } else if (change instanceof ModelChange.SetProperty) {
+        ModelChange.SetProperty setProperty = (ModelChange.SetProperty) change;
+        newProps.put(setProperty.property(), setProperty.value());
       }
     }
     TestModel updatedModel =
         TestModel.builder()
             .withName(newIdent.name())
-            .withComment(model.comment())
+            .withComment(newComment)
             .withProperties(new HashMap<>(newProps))
             .withAuditInfo(updatedAuditInfo)
-            .withLatestVersion(model.latestVersion())
+            .withLatestVersion(newLatestVersion)
             .build();
 
     models.put(ident, updatedModel);
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 3d80600320..c3040a5acc 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
@@ -797,7 +797,7 @@ public class TestModelOperations extends JerseyTest {
   }
 
   @Test
-  public void testAlterModel() {
+  public void testRenameModel() {
     String oldName = "model1";
     String newName = "newModel1";
     String comment = "comment";
@@ -860,6 +860,113 @@ public class TestModelOperations extends JerseyTest {
     Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResp1.getCode());
   }
 
+  @Test
+  void testAddModelProperty() {
+    String modelName = "model1";
+    String comment = "comment";
+
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, modelName);
+    Model updatedModel =
+        mockModel(
+            modelName,
+            comment,
+            0,
+            ImmutableMap.<String, String>builder()
+                .putAll(properties)
+                .put("key2", "value2")
+                .build());
+
+    // Mock alterModel to return updated model
+    when(modelDispatcher.alterModel(
+            modelId, new ModelChange[] {ModelChange.setProperty("key2", 
"value2")}))
+        .thenReturn(updatedModel);
+
+    // Build update request
+    ModelUpdatesRequest req =
+        new ModelUpdatesRequest(
+            Collections.singletonList(
+                new ModelUpdateRequest.SetModelPropertyRequest("key2", 
"value2")));
+
+    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(modelName, modelResp.getModel().name());
+    Assertions.assertEquals(2, modelResp.getModel().properties().size());
+    Assertions.assertEquals("value2", 
modelResp.getModel().properties().get("key2"));
+  }
+
+  @Test
+  void testUpdateModelProperty() {
+    String modelName = "model1";
+    String comment = "comment";
+
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, modelName);
+    Model updatedModel = mockModel(modelName, comment, 0, 
ImmutableMap.of("key1", "updatedValue1"));
+
+    // Mock alterModel to return updated model
+    when(modelDispatcher.alterModel(modelId, ModelChange.setProperty("key1", 
"updatedValue1")))
+        .thenReturn(updatedModel);
+
+    // Build update request
+    ModelUpdatesRequest req =
+        new ModelUpdatesRequest(
+            Collections.singletonList(
+                new ModelUpdateRequest.SetModelPropertyRequest("key1", 
"updatedValue1")));
+
+    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(modelName, modelResp.getModel().name());
+    Assertions.assertEquals(1, modelResp.getModel().properties().size());
+    Assertions.assertEquals("updatedValue1", 
modelResp.getModel().properties().get("key1"));
+  }
+
+  @Test
+  void testRemoveModelProperty() {
+    String modelName = "model1";
+    String comment = "comment";
+
+    NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog, 
schema, modelName);
+    Model updatedModel = mockModel(modelName, comment, 0, ImmutableMap.of());
+
+    // Mock alterModel to return updated model
+    when(modelDispatcher.alterModel(modelId, 
ModelChange.removeProperty("key1")))
+        .thenReturn(updatedModel);
+
+    // Build update request
+    ModelUpdatesRequest req =
+        new ModelUpdatesRequest(
+            Collections.singletonList(new 
ModelUpdateRequest.RemoveModelPropertyRequest("key1")));
+
+    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(modelName, modelResp.getModel().name());
+    Assertions.assertEquals(0, modelResp.getModel().properties().size());
+    
Assertions.assertFalse(modelResp.getModel().properties().containsKey("key1"));
+  }
+
   private String modelPath() {
     return "/metalakes/" + metalake + "/catalogs/" + catalog + "/schemas/" + 
schema + "/models";
   }
@@ -874,6 +981,17 @@ public class TestModelOperations extends JerseyTest {
     return mockModel;
   }
 
+  private Model mockModel(
+      String modelName, String comment, int latestVersion, Map<String, String> 
properties) {
+    Model mockModel = mock(Model.class);
+    when(mockModel.name()).thenReturn(modelName);
+    when(mockModel.comment()).thenReturn(comment);
+    when(mockModel.latestVersion()).thenReturn(latestVersion);
+    when(mockModel.properties()).thenReturn(properties);
+    when(mockModel.auditInfo()).thenReturn(testAuditInfo);
+    return mockModel;
+  }
+
   private ModelVersion mockModelVersion(int version, String uri, String[] 
aliases, String comment) {
     ModelVersion mockModelVersion = mock(ModelVersion.class);
     when(mockModelVersion.version()).thenReturn(version);

Reply via email to