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 76e2ee9680 [#6816] feat(server): Support update properties for model 
version (#6996)
76e2ee9680 is described below

commit 76e2ee96803149832c99703cedcbde4113a4d031
Author: Lord of Abyss <[email protected]>
AuthorDate: Fri Apr 18 14:47:10 2025 +0800

    [#6816] feat(server): Support update properties for model version (#6996)
    
    ### What changes were proposed in this pull request?
    
    Support update properties for model version
    
    ### Why are the changes needed?
    
    Fix: #6816
    
    ### Does this PR introduce _any_ user-facing change?
    
    user now can set or remove a property from a model version.
    
    ### How was this patch tested?
    
    local test.
    
    #### Add property
    
    `bin/gcli.sh model set -m demo_metalake --name
    model_catalog.schema.model2 --alias test --property "new_property"
    --value "new_value"`
    
    <img width="1050" alt="image"
    
src="https://github.com/user-attachments/assets/646091da-896f-47b8-8673-7ec082eb4eb5";
    />
    
    #### update property
    
    `bin/gcli.sh model set -m demo_metalake --name
    model_catalog.schema.model2 --alias test --property "new_property"
    --value "update_value"`
    
    #### Remove property
    
    `bin/gcli.sh model remove -m demo_metalake --name
    model_catalog.schema.model2 --alias test --property "new_property"`
    
    <img width="1050" alt="image"
    
src="https://github.com/user-attachments/assets/a38a7bf6-f24f-4678-8a8b-005762b0b127";
    />
---
 .../apache/gravitino/model/ModelVersionChange.java | 155 +++++++++++++++
 .../gravitino/model/TestModelVersionChange.java    |  92 +++++++++
 .../catalog/model/ModelCatalogOperations.java      |  19 ++
 .../catalog/model/TestModelCatalogOperations.java  | 215 +++++++++++++++++++++
 .../integration/test/ModelCatalogOperationsIT.java | 144 +++++++++++++-
 .../org/apache/gravitino/cli/ErrorMessages.java    |   1 +
 .../apache/gravitino/cli/ModelCommandHandler.java  |  62 ++++--
 .../apache/gravitino/cli/TestableCommandLine.java  |  29 +++
 ...operty.java => RemoveModelVersionProperty.java} |  60 +++---
 .../gravitino/cli/commands/SetModelProperty.java   |   5 +-
 ...lProperty.java => SetModelVersionProperty.java} |  52 +++--
 .../apache/gravitino/cli/TestModelCommands.java    | 185 +++++++++++++++++-
 .../org/apache/gravitino/client/DTOConverters.java |   9 +
 .../gravitino/api/model_version_change.py          | 112 +++++++++++
 .../gravitino/client/generic_model_catalog.py      |  11 +-
 .../dto/requests/model_version_update_request.py   |  46 +++++
 .../tests/integration/test_model_catalog.py        |  47 +++++
 .../dto/requests/ModelVersionUpdateRequest.java    |  70 ++++++-
 .../catalog/TestModelOperationDispatcher.java      | 156 +++++++++++++++
 .../gravitino/connector/TestCatalogOperations.java |   9 +
 .../service/TestModelVersionMetaService.java       |  84 ++++++++
 21 files changed, 1502 insertions(+), 61 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/model/ModelVersionChange.java 
b/api/src/main/java/org/apache/gravitino/model/ModelVersionChange.java
index 2b18bccb26..9a35c28f85 100644
--- a/api/src/main/java/org/apache/gravitino/model/ModelVersionChange.java
+++ b/api/src/main/java/org/apache/gravitino/model/ModelVersionChange.java
@@ -40,6 +40,27 @@ public interface ModelVersionChange {
     return new ModelVersionChange.UpdateComment(newComment);
   }
 
+  /**
+   * Create a ModelVersionChange for setting a property of a model version.
+   *
+   * @param property name of the property to be set
+   * @param value value to be set for the property
+   * @return A new ModelVersionChange instance for setting a property of a 
model version
+   */
+  static ModelVersionChange setProperty(String property, String value) {
+    return new ModelVersionChange.SetProperty(property, value);
+  }
+
+  /**
+   * Create a ModelVersionChange for removing a property from a model version.
+   *
+   * @param property The name of the property to be removed.
+   * @return The new ModelVersionChange instance for removing a property from 
a model version
+   */
+  static ModelVersionChange removeProperty(String property) {
+    return new ModelVersionChange.RemoveProperty(property);
+  }
+
   /** A ModelVersionChange to update the model version comment. */
   final class UpdateComment implements ModelVersionChange {
 
@@ -101,4 +122,138 @@ public interface ModelVersionChange {
       return "UpdateComment " + newComment;
     }
   }
+
+  /** A ModelVersionChange to set a property of a model version. */
+  final class SetProperty implements ModelVersionChange {
+    private final String property;
+    private final String value;
+
+    /**
+     * Creates a new {@link SetProperty} instance with the specified property 
name and value.
+     *
+     * @param property name of the property to be set
+     * @param value value to be set for the property
+     */
+    public SetProperty(String property, String value) {
+      this.property = property;
+      this.value = value;
+    }
+
+    /**
+     * Returns the name of the property to be set.
+     *
+     * @return the name of the property to be set
+     */
+    public String property() {
+      return property;
+    }
+
+    /**
+     * Returns 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 ModelVersionChange.SetProperty)) return false;
+      ModelVersionChange.SetProperty other = (ModelVersionChange.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 ModelVersionChange to remove a property from a model version. */
+  final class RemoveProperty implements ModelVersionChange {
+    private final String property;
+
+    /**
+     * Creates a new {@link RemoveProperty} instance with the specified 
property name.
+     *
+     * @param property name of the property to be removed
+     */
+    public RemoveProperty(String property) {
+      this.property = property;
+    }
+
+    /**
+     * Returns the name of the property to be removed.
+     *
+     * @return the name of the property to be removed
+     */
+    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.
+     *
+     * @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 ModelVersionChange.RemoveProperty)) return false;
+      ModelVersionChange.RemoveProperty other = 
(ModelVersionChange.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 to be removed.
+     *
+     * @return A hash code value for this property removal operation.
+     */
+    @Override
+    public int hashCode() {
+      return Objects.hash(property);
+    }
+
+    /**
+     * 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/api/src/test/java/org/apache/gravitino/model/TestModelVersionChange.java 
b/api/src/test/java/org/apache/gravitino/model/TestModelVersionChange.java
index bc4e9bb668..f978437f27 100644
--- a/api/src/test/java/org/apache/gravitino/model/TestModelVersionChange.java
+++ b/api/src/test/java/org/apache/gravitino/model/TestModelVersionChange.java
@@ -63,4 +63,96 @@ public class TestModelVersionChange {
     Assertions.assertNotEquals(modelVersionChange1.hashCode(), 
modelVersionChange3.hashCode());
     Assertions.assertNotEquals(modelVersionChange2.hashCode(), 
modelVersionChange3.hashCode());
   }
+
+  @Test
+  void testCreateSetPropertyChangeUseStaticMethod() {
+    String property = "property";
+    String value = "value";
+    ModelVersionChange modelVersionChange = 
ModelVersionChange.setProperty(property, value);
+
+    Assertions.assertEquals(ModelVersionChange.SetProperty.class, 
modelVersionChange.getClass());
+
+    ModelVersionChange.SetProperty setPropertyChange =
+        (ModelVersionChange.SetProperty) modelVersionChange;
+    Assertions.assertEquals(property, setPropertyChange.property());
+    Assertions.assertEquals(value, setPropertyChange.value());
+    Assertions.assertEquals("SETPROPERTY " + property + " " + value, 
setPropertyChange.toString());
+  }
+
+  @Test
+  void testCreateSetPropertyChangeUseConstructor() {
+    String property = "property";
+    String value = "value";
+    ModelVersionChange modelVersionChange = new 
ModelVersionChange.SetProperty(property, value);
+
+    Assertions.assertEquals(ModelVersionChange.SetProperty.class, 
modelVersionChange.getClass());
+
+    ModelVersionChange.SetProperty setPropertyChange =
+        (ModelVersionChange.SetProperty) modelVersionChange;
+    Assertions.assertEquals(property, setPropertyChange.property());
+    Assertions.assertEquals(value, setPropertyChange.value());
+    Assertions.assertEquals("SETPROPERTY " + property + " " + value, 
setPropertyChange.toString());
+  }
+
+  @Test
+  void testSetPropertyChangeEquals() {
+    String property1 = "property1";
+    String value1 = "value1";
+    String property2 = "property2";
+    ModelVersionChange modelVersionChange1 = 
ModelVersionChange.setProperty(property1, value1);
+    ModelVersionChange modelVersionChange2 = 
ModelVersionChange.setProperty(property1, value1);
+    ModelVersionChange modelVersionChange3 = 
ModelVersionChange.setProperty(property2, value1);
+
+    Assertions.assertEquals(modelVersionChange1, modelVersionChange2);
+    Assertions.assertNotEquals(modelVersionChange1, modelVersionChange3);
+    Assertions.assertNotEquals(modelVersionChange2, modelVersionChange3);
+
+    Assertions.assertEquals(modelVersionChange1.hashCode(), 
modelVersionChange2.hashCode());
+    Assertions.assertNotEquals(modelVersionChange1.hashCode(), 
modelVersionChange3.hashCode());
+    Assertions.assertNotEquals(modelVersionChange2.hashCode(), 
modelVersionChange3.hashCode());
+  }
+
+  @Test
+  void testCreateRemovePropertyChangeUseStaticMethod() {
+    String property = "property";
+    ModelVersionChange modelVersionChange = 
ModelVersionChange.removeProperty(property);
+
+    Assertions.assertEquals(ModelVersionChange.RemoveProperty.class, 
modelVersionChange.getClass());
+
+    ModelVersionChange.RemoveProperty removePropertyChange =
+        (ModelVersionChange.RemoveProperty) modelVersionChange;
+    Assertions.assertEquals(property, removePropertyChange.property());
+    Assertions.assertEquals("REMOVEPROPERTY " + property, 
removePropertyChange.toString());
+  }
+
+  @Test
+  void testCreateRemovePropertyChangeUseConstructor() {
+    String property = "property";
+    ModelVersionChange modelVersionChange = new 
ModelVersionChange.RemoveProperty(property);
+
+    Assertions.assertEquals(ModelVersionChange.RemoveProperty.class, 
modelVersionChange.getClass());
+
+    ModelVersionChange.RemoveProperty removePropertyChange =
+        (ModelVersionChange.RemoveProperty) modelVersionChange;
+    Assertions.assertEquals(property, removePropertyChange.property());
+    Assertions.assertEquals("REMOVEPROPERTY " + property, 
removePropertyChange.toString());
+  }
+
+  @Test
+  void testRemovePropertyChangeEquals() {
+    String property1 = "property1";
+    String property2 = "property2";
+
+    ModelVersionChange modelVersionChange1 = 
ModelVersionChange.removeProperty(property1);
+    ModelVersionChange modelVersionChange2 = 
ModelVersionChange.removeProperty(property1);
+    ModelVersionChange modelVersionChange3 = 
ModelVersionChange.removeProperty(property2);
+
+    Assertions.assertEquals(modelVersionChange1, modelVersionChange2);
+    Assertions.assertNotEquals(modelVersionChange1, modelVersionChange3);
+    Assertions.assertNotEquals(modelVersionChange2, modelVersionChange3);
+
+    Assertions.assertEquals(modelVersionChange1.hashCode(), 
modelVersionChange2.hashCode());
+    Assertions.assertNotEquals(modelVersionChange1.hashCode(), 
modelVersionChange3.hashCode());
+    Assertions.assertNotEquals(modelVersionChange2.hashCode(), 
modelVersionChange3.hashCode());
+  }
 }
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 bba059308b..d0c9b89cb6 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
@@ -416,6 +416,15 @@ public class ModelCatalogOperations extends 
ManagedSchemaOperations
       if (change instanceof ModelVersionChange.UpdateComment) {
         entityComment = ((ModelVersionChange.UpdateComment) 
change).newComment();
 
+      } else if (change instanceof ModelVersionChange.SetProperty) {
+        ModelVersionChange.SetProperty setPropertyChange = 
(ModelVersionChange.SetProperty) change;
+        doSetProperty(entityProperties, setPropertyChange);
+
+      } else if (change instanceof ModelVersionChange.RemoveProperty) {
+        ModelVersionChange.RemoveProperty removePropertyChange =
+            (ModelVersionChange.RemoveProperty) change;
+        doRemoveProperty(entityProperties, removePropertyChange);
+
       } else {
         throw new IllegalArgumentException(
             "Unsupported model version change: " + 
change.getClass().getSimpleName());
@@ -489,4 +498,14 @@ public class ModelCatalogOperations extends 
ManagedSchemaOperations
   private void doSetProperty(Map<String, String> entityProperties, 
ModelChange.SetProperty change) {
     entityProperties.put(change.property(), change.value());
   }
+
+  private void doSetProperty(
+      Map<String, String> entityProperties, ModelVersionChange.SetProperty 
change) {
+    entityProperties.put(change.property(), change.value());
+  }
+
+  private void doRemoveProperty(
+      Map<String, String> entityProperties, ModelVersionChange.RemoveProperty 
change) {
+    entityProperties.remove(change.property());
+  }
 }
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 2bb88497bc..c84e2fc9cf 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
@@ -35,6 +35,7 @@ import static 
org.apache.gravitino.Configs.VERSION_RETENTION_COUNT;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
 import java.io.File;
 import java.io.IOException;
 import java.time.Instant;
@@ -895,6 +896,220 @@ public class TestModelCatalogOperations {
     Assertions.assertEquals(versionProperties, 
updatedModelVersion.properties());
   }
 
+  @Test
+  void testSetAndUpdateModelVersionProperty() {
+    String schemaName = randomSchemaName();
+    createSchema(schemaName);
+
+    String modelName = "model1";
+    String modelComment = "model1 comment";
+
+    String versionComment = "version1 comment";
+    String versionUri = "model_version_path";
+    String[] versionAliases = new String[] {"alias1", "alias2"};
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName, 
modelName);
+    StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> properties = 
StringIdentifier.newPropertiesWithId(stringId, null);
+
+    ops.registerModel(modelIdent, modelComment, properties);
+    StringIdentifier versionId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> versionProperties =
+        StringIdentifier.newPropertiesWithId(
+            versionId, ImmutableMap.of("key1", "value1", "key2", "value2"));
+    Map<String, String> tmpMap = Maps.newHashMap(versionProperties);
+    tmpMap.put("key3", "value3");
+    tmpMap.put("key1", "new value");
+    Map<String, String> newProperties = ImmutableMap.copyOf(tmpMap);
+
+    ops.linkModelVersion(modelIdent, versionUri, versionAliases, 
versionComment, versionProperties);
+
+    // validate loaded model
+    Model loadedModel = ops.getModel(modelIdent);
+    Assertions.assertEquals(1, loadedModel.latestVersion());
+
+    // validate loaded version
+    ModelVersion loadedVersion = ops.getModelVersion(modelIdent, 0);
+    Assertions.assertEquals(0, loadedVersion.version());
+    Assertions.assertArrayEquals(versionAliases, loadedVersion.aliases());
+    Assertions.assertEquals(versionComment, loadedVersion.comment());
+    Assertions.assertEquals(versionUri, loadedVersion.uri());
+    Assertions.assertEquals(versionProperties, loadedVersion.properties());
+
+    // set property via version and validate
+    ModelVersionChange updatePropertyChange = 
ModelVersionChange.setProperty("key1", "new value");
+    ModelVersionChange addPropertyChange = 
ModelVersionChange.setProperty("key3", "value3");
+
+    ModelVersion updatedModelVersion =
+        ops.alterModelVersion(modelIdent, 0, updatePropertyChange, 
addPropertyChange);
+
+    Assertions.assertEquals(0, updatedModelVersion.version());
+    Assertions.assertEquals(versionUri, updatedModelVersion.uri());
+    Assertions.assertEquals(versionComment, updatedModelVersion.comment());
+    Assertions.assertArrayEquals(versionAliases, 
updatedModelVersion.aliases());
+    Assertions.assertEquals(newProperties, updatedModelVersion.properties());
+  }
+
+  @Test
+  void testSetAndUpdateModelVersionPropertyByAlias() {
+    String schemaName = randomSchemaName();
+    createSchema(schemaName);
+
+    String modelName = "model1";
+    String modelComment = "model1 comment";
+
+    String versionComment = "version1 comment";
+    String versionUri = "model_version_path";
+    String[] versionAliases = new String[] {"alias1", "alias2"};
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName, 
modelName);
+    StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> properties = 
StringIdentifier.newPropertiesWithId(stringId, null);
+
+    ops.registerModel(modelIdent, modelComment, properties);
+    StringIdentifier versionId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> versionProperties =
+        StringIdentifier.newPropertiesWithId(
+            versionId, ImmutableMap.of("key1", "value1", "key2", "value2"));
+    Map<String, String> tmpMap = Maps.newHashMap(versionProperties);
+    tmpMap.put("key3", "value3");
+    tmpMap.put("key1", "new value");
+    Map<String, String> newProperties = ImmutableMap.copyOf(tmpMap);
+
+    ops.linkModelVersion(modelIdent, versionUri, versionAliases, 
versionComment, versionProperties);
+
+    // validate loaded model
+    Model loadedModel = ops.getModel(modelIdent);
+    Assertions.assertEquals(1, loadedModel.latestVersion());
+
+    // validate loaded version
+    ModelVersion loadedVersion = ops.getModelVersion(modelIdent, 
versionAliases[0]);
+    Assertions.assertEquals(0, loadedVersion.version());
+    Assertions.assertArrayEquals(versionAliases, loadedVersion.aliases());
+    Assertions.assertEquals(versionComment, loadedVersion.comment());
+    Assertions.assertEquals(versionUri, loadedVersion.uri());
+    Assertions.assertEquals(versionProperties, loadedVersion.properties());
+
+    // set property via version and validate
+    ModelVersionChange updatePropertyChange = 
ModelVersionChange.setProperty("key1", "new value");
+    ModelVersionChange addPropertyChange = 
ModelVersionChange.setProperty("key3", "value3");
+
+    ModelVersion updatedModelVersion =
+        ops.alterModelVersion(
+            modelIdent, versionAliases[0], updatePropertyChange, 
addPropertyChange);
+
+    Assertions.assertEquals(0, updatedModelVersion.version());
+    Assertions.assertEquals(versionUri, updatedModelVersion.uri());
+    Assertions.assertEquals(versionComment, updatedModelVersion.comment());
+    Assertions.assertArrayEquals(versionAliases, 
updatedModelVersion.aliases());
+    Assertions.assertEquals(newProperties, updatedModelVersion.properties());
+  }
+
+  @Test
+  void testRemoveModelVersionProperty() {
+    String schemaName = randomSchemaName();
+    createSchema(schemaName);
+
+    String modelName = "model1";
+    String modelComment = "model1 comment";
+
+    String versionComment = "version1 comment";
+    String versionUri = "model_version_path";
+    String[] versionAliases = new String[] {"alias1", "alias2"};
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName, 
modelName);
+    StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> properties = 
StringIdentifier.newPropertiesWithId(stringId, null);
+
+    ops.registerModel(modelIdent, modelComment, properties);
+    StringIdentifier versionId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> versionProperties =
+        StringIdentifier.newPropertiesWithId(
+            versionId, ImmutableMap.of("key1", "value1", "key2", "value2"));
+    Map<String, String> newVersionProperties =
+        StringIdentifier.newPropertiesWithId(versionId, 
ImmutableMap.of("key1", "value1"));
+
+    ops.linkModelVersion(modelIdent, versionUri, versionAliases, 
versionComment, versionProperties);
+
+    // validate loaded model
+    Model loadedModel = ops.getModel(modelIdent);
+    Assertions.assertEquals(1, loadedModel.latestVersion());
+
+    // validate loaded version
+    ModelVersion loadedVersion = ops.getModelVersion(modelIdent, 0);
+    Assertions.assertEquals(0, loadedVersion.version());
+    Assertions.assertArrayEquals(versionAliases, loadedVersion.aliases());
+    Assertions.assertEquals(versionComment, loadedVersion.comment());
+    Assertions.assertEquals(versionUri, loadedVersion.uri());
+    Assertions.assertEquals(versionProperties, loadedVersion.properties());
+
+    // set property via version and validate
+    ModelVersionChange removeProperty = 
ModelVersionChange.removeProperty("key2");
+
+    ModelVersion updatedModelVersion = ops.alterModelVersion(modelIdent, 0, 
removeProperty);
+
+    Assertions.assertEquals(0, updatedModelVersion.version());
+    Assertions.assertEquals(versionUri, updatedModelVersion.uri());
+    Assertions.assertEquals(versionComment, updatedModelVersion.comment());
+    Assertions.assertArrayEquals(versionAliases, 
updatedModelVersion.aliases());
+    Assertions.assertEquals(newVersionProperties, 
updatedModelVersion.properties());
+  }
+
+  @Test
+  void testRemoveModelVersionPropertyByAlias() {
+    String schemaName = randomSchemaName();
+    createSchema(schemaName);
+
+    String modelName = "model1";
+    String modelComment = "model1 comment";
+
+    String versionComment = "version1 comment";
+    String versionUri = "model_version_path";
+    String[] versionAliases = new String[] {"alias1", "alias2"};
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName, 
modelName);
+    StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> properties = 
StringIdentifier.newPropertiesWithId(stringId, null);
+
+    ops.registerModel(modelIdent, modelComment, properties);
+    StringIdentifier versionId = StringIdentifier.fromId(idGenerator.nextId());
+    Map<String, String> versionProperties =
+        StringIdentifier.newPropertiesWithId(
+            versionId, ImmutableMap.of("key1", "value1", "key2", "value2"));
+    Map<String, String> newVersionProperties =
+        StringIdentifier.newPropertiesWithId(versionId, 
ImmutableMap.of("key1", "value1"));
+
+    ops.linkModelVersion(modelIdent, versionUri, versionAliases, 
versionComment, versionProperties);
+
+    // validate loaded model
+    Model loadedModel = ops.getModel(modelIdent);
+    Assertions.assertEquals(1, loadedModel.latestVersion());
+
+    // validate loaded version
+    ModelVersion loadedVersion = ops.getModelVersion(modelIdent, 
versionAliases[0]);
+    Assertions.assertEquals(0, loadedVersion.version());
+    Assertions.assertArrayEquals(versionAliases, loadedVersion.aliases());
+    Assertions.assertEquals(versionComment, loadedVersion.comment());
+    Assertions.assertEquals(versionUri, loadedVersion.uri());
+    Assertions.assertEquals(versionProperties, loadedVersion.properties());
+
+    // set property via version and validate
+    ModelVersionChange removeProperty = 
ModelVersionChange.removeProperty("key2");
+
+    ModelVersion updatedModelVersion =
+        ops.alterModelVersion(modelIdent, versionAliases[0], removeProperty);
+
+    Assertions.assertEquals(0, updatedModelVersion.version());
+    Assertions.assertEquals(versionUri, updatedModelVersion.uri());
+    Assertions.assertEquals(versionComment, updatedModelVersion.comment());
+    Assertions.assertArrayEquals(versionAliases, 
updatedModelVersion.aliases());
+    Assertions.assertEquals(newVersionProperties, 
updatedModelVersion.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 1b582672fb..d75c21f371 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
@@ -331,7 +331,7 @@ public class ModelCatalogOperationsIT extends BaseIT {
   }
 
   @Test
-  void testLinkAndUpdateModelVersionCommentViaVersion() {
+  void testLinkAndUpdateModelVersionComment() {
     String modelName = RandomNameUtils.genRandomName("model1");
     Map<String, String> properties = ImmutableMap.of("key1", "val1", "key2", 
"val2");
     NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
@@ -482,6 +482,148 @@ public class ModelCatalogOperationsIT extends BaseIT {
     Assertions.assertEquals(createdModel.comment(), alteredModel.comment());
   }
 
+  @Test
+  void testLinkAndSetModelVersionProperties() {
+    String modelName = RandomNameUtils.genRandomName("model1");
+    String[] aliases = {"alias1"};
+    Map<String, String> properties = ImmutableMap.of("key1", "val1", "key2", 
"val2");
+    NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+    Map<String, String> newProperties =
+        ImmutableMap.of("key1", "new value", "key2", "val2", "key3", "val3");
+
+    gravitinoCatalog.asModelCatalog().registerModel(modelIdent, null, null);
+
+    gravitinoCatalog
+        .asModelCatalog()
+        .linkModelVersion(modelIdent, "uri", aliases, "comment", properties);
+
+    ModelVersion modelVersion = 
gravitinoCatalog.asModelCatalog().getModelVersion(modelIdent, 0);
+
+    Assertions.assertEquals(0, modelVersion.version());
+    Assertions.assertEquals("uri", modelVersion.uri());
+    Assertions.assertArrayEquals(aliases, modelVersion.aliases());
+    Assertions.assertEquals("comment", modelVersion.comment());
+    Assertions.assertEquals(properties, modelVersion.properties());
+
+    ModelVersionChange[] changes = {
+      ModelVersionChange.setProperty("key1", "new value"),
+      ModelVersionChange.setProperty("key3", "val3")
+    };
+    ModelVersion updatedModelVersion =
+        gravitinoCatalog.asModelCatalog().alterModelVersion(modelIdent, 0, 
changes);
+
+    Assertions.assertEquals(modelVersion.version(), 
updatedModelVersion.version());
+    Assertions.assertEquals(modelVersion.uri(), updatedModelVersion.uri());
+    Assertions.assertArrayEquals(modelVersion.aliases(), 
updatedModelVersion.aliases());
+    Assertions.assertEquals(modelVersion.comment(), 
updatedModelVersion.comment());
+    Assertions.assertEquals(newProperties, updatedModelVersion.properties());
+  }
+
+  @Test
+  void testLinkAndSetModelVersionPropertiesByAlias() {
+    String modelName = RandomNameUtils.genRandomName("model1");
+    String[] aliases = {"alias1"};
+    Map<String, String> properties = ImmutableMap.of("key1", "val1", "key2", 
"val2");
+    NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+    Map<String, String> newProperties =
+        ImmutableMap.of("key1", "new value", "key2", "val2", "key3", "val3");
+
+    gravitinoCatalog.asModelCatalog().registerModel(modelIdent, null, null);
+
+    gravitinoCatalog
+        .asModelCatalog()
+        .linkModelVersion(modelIdent, "uri", aliases, "comment", properties);
+
+    ModelVersion modelVersion =
+        gravitinoCatalog.asModelCatalog().getModelVersion(modelIdent, 
aliases[0]);
+
+    Assertions.assertEquals(0, modelVersion.version());
+    Assertions.assertEquals("uri", modelVersion.uri());
+    Assertions.assertArrayEquals(aliases, modelVersion.aliases());
+    Assertions.assertEquals("comment", modelVersion.comment());
+    Assertions.assertEquals(properties, modelVersion.properties());
+
+    ModelVersionChange[] changes = {
+      ModelVersionChange.setProperty("key1", "new value"),
+      ModelVersionChange.setProperty("key3", "val3")
+    };
+    ModelVersion updatedModelVersion =
+        gravitinoCatalog.asModelCatalog().alterModelVersion(modelIdent, 
aliases[0], changes);
+
+    Assertions.assertEquals(modelVersion.version(), 
updatedModelVersion.version());
+    Assertions.assertEquals(modelVersion.uri(), updatedModelVersion.uri());
+    Assertions.assertArrayEquals(modelVersion.aliases(), 
updatedModelVersion.aliases());
+    Assertions.assertEquals(modelVersion.comment(), 
updatedModelVersion.comment());
+    Assertions.assertEquals(newProperties, updatedModelVersion.properties());
+  }
+
+  @Test
+  void testLinkAndRemoveModelVersionProperties() {
+    String modelName = RandomNameUtils.genRandomName("model1");
+    String[] aliases = {"alias1"};
+    Map<String, String> properties = ImmutableMap.of("key1", "val1", "key2", 
"val2");
+    NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+    Map<String, String> newProperties = ImmutableMap.of("key2", "val2");
+
+    gravitinoCatalog.asModelCatalog().registerModel(modelIdent, null, null);
+
+    gravitinoCatalog
+        .asModelCatalog()
+        .linkModelVersion(modelIdent, "uri", aliases, "comment", properties);
+
+    ModelVersion modelVersion = 
gravitinoCatalog.asModelCatalog().getModelVersion(modelIdent, 0);
+
+    Assertions.assertEquals(0, modelVersion.version());
+    Assertions.assertEquals("uri", modelVersion.uri());
+    Assertions.assertArrayEquals(aliases, modelVersion.aliases());
+    Assertions.assertEquals("comment", modelVersion.comment());
+    Assertions.assertEquals(properties, modelVersion.properties());
+
+    ModelVersionChange change = ModelVersionChange.removeProperty("key1");
+    ModelVersion updatedModelVersion =
+        gravitinoCatalog.asModelCatalog().alterModelVersion(modelIdent, 0, 
change);
+
+    Assertions.assertEquals(modelVersion.version(), 
updatedModelVersion.version());
+    Assertions.assertEquals(modelVersion.uri(), updatedModelVersion.uri());
+    Assertions.assertArrayEquals(modelVersion.aliases(), 
updatedModelVersion.aliases());
+    Assertions.assertEquals(modelVersion.comment(), 
updatedModelVersion.comment());
+    Assertions.assertEquals(newProperties, updatedModelVersion.properties());
+  }
+
+  @Test
+  void testLinkAndRemoveModelVersionPropertiesByAlias() {
+    String modelName = RandomNameUtils.genRandomName("model1");
+    String[] aliases = {"alias1"};
+    Map<String, String> properties = ImmutableMap.of("key1", "val1", "key2", 
"val2");
+    NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+    Map<String, String> newProperties = ImmutableMap.of("key2", "val2");
+
+    gravitinoCatalog.asModelCatalog().registerModel(modelIdent, null, null);
+
+    gravitinoCatalog
+        .asModelCatalog()
+        .linkModelVersion(modelIdent, "uri", aliases, "comment", properties);
+
+    ModelVersion modelVersion =
+        gravitinoCatalog.asModelCatalog().getModelVersion(modelIdent, 
aliases[0]);
+
+    Assertions.assertEquals(0, modelVersion.version());
+    Assertions.assertEquals("uri", modelVersion.uri());
+    Assertions.assertArrayEquals(aliases, modelVersion.aliases());
+    Assertions.assertEquals("comment", modelVersion.comment());
+    Assertions.assertEquals(properties, modelVersion.properties());
+
+    ModelVersionChange change = ModelVersionChange.removeProperty("key1");
+    ModelVersion updatedModelVersion =
+        gravitinoCatalog.asModelCatalog().alterModelVersion(modelIdent, 
aliases[0], change);
+
+    Assertions.assertEquals(modelVersion.version(), 
updatedModelVersion.version());
+    Assertions.assertEquals(modelVersion.uri(), updatedModelVersion.uri());
+    Assertions.assertArrayEquals(modelVersion.aliases(), 
updatedModelVersion.aliases());
+    Assertions.assertEquals(modelVersion.comment(), 
updatedModelVersion.comment());
+    Assertions.assertEquals(newProperties, updatedModelVersion.properties());
+  }
+
   private void createMetalake() {
     GravitinoMetalake[] gravitinoMetalakes = client.listMetalakes();
     Assertions.assertEquals(0, gravitinoMetalakes.length);
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
index cc36dd66b7..2f937dd4be 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
@@ -83,6 +83,7 @@ public class ErrorMessages {
   public static final String UNKNOWN_GROUP = "Unknown group.";
   public static final String UNKNOWN_METALAKE = "Unknown metalake name.";
   public static final String UNKNOWN_MODEL = "Unknown model name.";
+  public static final String UNKNOWN_MODEL_VERSION = "Unknown model version.";
   public static final String UNKNOWN_PRIVILEGE = "Unknown privilege";
   public static final String UNKNOWN_ROLE = "Unknown role.";
   public static final String UNKNOWN_SCHEMA = "Unknown schema name.";
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 28cb743186..ec58f98aa6 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
@@ -185,14 +185,8 @@ public class ModelCommandHandler extends CommandHandler {
         && line.hasOption(GravitinoOptions.COMMENT)
         && (line.hasOption(GravitinoOptions.ALIAS) || 
line.hasOption(GravitinoOptions.VERSION))) {
       String comment = line.getOptionValue(GravitinoOptions.COMMENT);
-      Integer version =
-          line.hasOption(GravitinoOptions.VERSION)
-              ? Integer.parseInt(line.getOptionValue(GravitinoOptions.VERSION))
-              : null;
-      String alias =
-          line.hasOption(GravitinoOptions.ALIAS)
-              ? getOneAlias(line.getOptionValues(GravitinoOptions.ALIAS))
-              : null;
+      Integer version = getVersionFromLine(line);
+      String alias = getAliasFromLine(line);
 
       gravitinoCommandLine
           .newUpdateModelVersionComment(
@@ -211,18 +205,42 @@ public class ModelCommandHandler extends CommandHandler {
   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();
+
+    if (line.hasOption(GravitinoOptions.ALIAS) || 
line.hasOption(GravitinoOptions.VERSION)) {
+      Integer version = getVersionFromLine(line);
+      String alias = getAliasFromLine(line);
+      gravitinoCommandLine
+          .newSetModelVersionProperty(
+              context, metalake, catalog, schema, model, version, alias, 
property, value)
+          .validate()
+          .handle();
+    } else {
+      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();
+
+    if (line.hasOption(GravitinoOptions.ALIAS) || 
line.hasOption(GravitinoOptions.VERSION)) {
+      Integer version = getVersionFromLine(line);
+      String alias = getAliasFromLine(line);
+
+      gravitinoCommandLine
+          .newRemoveModelVersionProperty(
+              context, metalake, catalog, schema, model, version, alias, 
property)
+          .validate()
+          .handle();
+    } else {
+
+      gravitinoCommandLine
+          .newRemoveModelProperty(context, metalake, catalog, schema, model, 
property)
+          .validate()
+          .handle();
+    }
   }
 
   private String getOneAlias(String[] aliases) {
@@ -232,4 +250,16 @@ public class ModelCommandHandler extends CommandHandler {
     }
     return aliases[0];
   }
+
+  private Integer getVersionFromLine(CommandLine line) {
+    return line.hasOption(GravitinoOptions.VERSION)
+        ? Integer.parseInt(line.getOptionValue(GravitinoOptions.VERSION))
+        : null;
+  }
+
+  private String getAliasFromLine(CommandLine line) {
+    return line.hasOption(GravitinoOptions.ALIAS)
+        ? getOneAlias(line.getOptionValues(GravitinoOptions.ALIAS))
+        : null;
+  }
 }
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 86acd5b0de..4c25781865 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
@@ -90,6 +90,7 @@ 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.RemoveModelVersionProperty;
 import org.apache.gravitino.cli.commands.RemoveRoleFromGroup;
 import org.apache.gravitino.cli.commands.RemoveRoleFromUser;
 import org.apache.gravitino.cli.commands.RemoveSchemaProperty;
@@ -107,6 +108,7 @@ 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.SetModelVersionProperty;
 import org.apache.gravitino.cli.commands.SetOwner;
 import org.apache.gravitino.cli.commands.SetSchemaProperty;
 import org.apache.gravitino.cli.commands.SetTableProperty;
@@ -910,6 +912,33 @@ public class TestableCommandLine {
     return new SetModelProperty(context, metalake, catalog, schema, model, 
property, value);
   }
 
+  protected RemoveModelVersionProperty newRemoveModelVersionProperty(
+      CommandContext context,
+      String metalake,
+      String catalog,
+      String schema,
+      String model,
+      Integer version,
+      String alias,
+      String property) {
+    return new RemoveModelVersionProperty(
+        context, metalake, catalog, schema, model, version, alias, property);
+  }
+
+  protected SetModelVersionProperty newSetModelVersionProperty(
+      CommandContext context,
+      String metalake,
+      String catalog,
+      String schema,
+      String model,
+      Integer version,
+      String alias,
+      String property,
+      String value) {
+    return new SetModelVersionProperty(
+        context, metalake, catalog, schema, model, version, alias, property, 
value);
+  }
+
   protected RemoveModelProperty newRemoveModelProperty(
       CommandContext context,
       String metalake,
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/RemoveModelVersionProperty.java
similarity index 59%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/RemoveModelVersionProperty.java
index 451e1f71c3..864e36f025 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RemoveModelVersionProperty.java
@@ -26,55 +26,64 @@ 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.NoSuchModelVersionException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
-import org.apache.gravitino.model.ModelChange;
-
-/** Set a property of a model. */
-public class SetModelProperty extends Command {
+import org.apache.gravitino.model.ModelVersionChange;
 
+/** Remove a property from a model version. */
+public class RemoveModelVersionProperty extends Command {
   private final String metalake;
   private final String catalog;
   private final String schema;
   private final String model;
+  private final Integer version;
+  private final String alias;
   private final String property;
-  private final String value;
 
   /**
-   * Construct a new {@link SetModelProperty} instance.
+   * Constructs a new {@link RemoveModelVersionProperty} 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.
+   * @param metalake The metalake name.
+   * @param catalog The catalog name.
+   * @param schema The schema name.
+   * @param model The model name.
+   * @param version The model version, if specified, otherwise null.
+   * @param alias The alias name of the model version, if specified, otherwise 
null.
+   * @param property The property name to remove.
    */
-  public SetModelProperty(
+  public RemoveModelVersionProperty(
       CommandContext context,
       String metalake,
       String catalog,
       String schema,
       String model,
-      String property,
-      String value) {
+      Integer version,
+      String alias,
+      String property) {
     super(context);
     this.metalake = metalake;
     this.catalog = catalog;
     this.schema = schema;
     this.model = model;
+    this.version = version;
+    this.alias = alias;
     this.property = property;
-    this.value = value;
   }
 
-  /** Set a property of a model. */
+  /** Remove a property from a model version. */
   @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);
+      ModelVersionChange change = ModelVersionChange.removeProperty(property);
+
+      if (version != null) {
+        client.loadCatalog(catalog).asModelCatalog().alterModelVersion(name, 
version, change);
+      } else {
+        client.loadCatalog(catalog).asModelCatalog().alterModelVersion(name, 
alias, change);
+      }
     } catch (NoSuchMetalakeException err) {
       exitWithError(ErrorMessages.UNKNOWN_METALAKE);
     } catch (NoSuchCatalogException err) {
@@ -82,18 +91,25 @@ public class SetModelProperty extends Command {
     } catch (NoSuchSchemaException err) {
       exitWithError(ErrorMessages.UNKNOWN_SCHEMA);
     } catch (NoSuchModelException err) {
-      exitWithError(ErrorMessages.UNKNOWN_TABLE);
+      exitWithError(ErrorMessages.UNKNOWN_MODEL);
+    } catch (NoSuchModelVersionException err) {
+      exitWithError(ErrorMessages.UNKNOWN_MODEL_VERSION);
     } catch (Exception exp) {
       exitWithError(exp.getMessage());
     }
 
-    printInformation(model + " property set.");
+    if (alias != null) {
+      printInformation(model + " version " + alias + "property " + property + 
" property removed.");
+    } else {
+      printInformation(
+          model + " version " + version + "property " + property + " property 
removed.");
+    }
   }
 
   /** {@inheritDoc} */
   @Override
   public Command validate() {
-    validatePropertyAndValue(property, value);
+    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
index 451e1f71c3..10f6f692f2 100644
--- 
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
@@ -26,6 +26,7 @@ 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.NoSuchModelVersionException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.model.ModelChange;
 
@@ -82,7 +83,9 @@ public class SetModelProperty extends Command {
     } catch (NoSuchSchemaException err) {
       exitWithError(ErrorMessages.UNKNOWN_SCHEMA);
     } catch (NoSuchModelException err) {
-      exitWithError(ErrorMessages.UNKNOWN_TABLE);
+      exitWithError(ErrorMessages.UNKNOWN_MODEL);
+    } catch (NoSuchModelVersionException err) {
+      exitWithError(ErrorMessages.UNKNOWN_MODEL_VERSION);
     } catch (Exception exp) {
       exitWithError(exp.getMessage());
     }
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/SetModelVersionProperty.java
similarity index 62%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelVersionProperty.java
index 451e1f71c3..568ebeee68 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelVersionProperty.java
@@ -26,36 +26,42 @@ 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.NoSuchModelVersionException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
-import org.apache.gravitino.model.ModelChange;
-
-/** Set a property of a model. */
-public class SetModelProperty extends Command {
+import org.apache.gravitino.model.ModelVersionChange;
 
+/** Set or update a property of a model version. */
+public class SetModelVersionProperty extends Command {
   private final String metalake;
   private final String catalog;
   private final String schema;
   private final String model;
+  private final Integer version;
+  private final String alias;
   private final String property;
   private final String value;
 
   /**
-   * Construct a new {@link SetModelProperty} instance.
+   * Constructs a new {@link SetModelVersionProperty} 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.
+   * @param metalake The metalake name.
+   * @param catalog The catalog name.
+   * @param schema The schema name.
+   * @param model The model name.
+   * @param version The model version, if specified, otherwise null.
+   * @param alias The model alias, if specified, otherwise null.
+   * @param property The property name.
+   * @param value The property value.
    */
-  public SetModelProperty(
+  public SetModelVersionProperty(
       CommandContext context,
       String metalake,
       String catalog,
       String schema,
       String model,
+      Integer version,
+      String alias,
       String property,
       String value) {
     super(context);
@@ -63,18 +69,24 @@ public class SetModelProperty extends Command {
     this.catalog = catalog;
     this.schema = schema;
     this.model = model;
+    this.version = version;
+    this.alias = alias;
     this.property = property;
     this.value = value;
   }
 
-  /** Set a property of a model. */
+  /** Set or update a property of a model version. */
   @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);
+      ModelVersionChange change = ModelVersionChange.setProperty(property, 
value);
+      if (version != null) {
+        client.loadCatalog(catalog).asModelCatalog().alterModelVersion(name, 
version, change);
+      } else {
+        client.loadCatalog(catalog).asModelCatalog().alterModelVersion(name, 
alias, change);
+      }
     } catch (NoSuchMetalakeException err) {
       exitWithError(ErrorMessages.UNKNOWN_METALAKE);
     } catch (NoSuchCatalogException err) {
@@ -82,12 +94,18 @@ public class SetModelProperty extends Command {
     } catch (NoSuchSchemaException err) {
       exitWithError(ErrorMessages.UNKNOWN_SCHEMA);
     } catch (NoSuchModelException err) {
-      exitWithError(ErrorMessages.UNKNOWN_TABLE);
+      exitWithError(ErrorMessages.UNKNOWN_MODEL);
+    } catch (NoSuchModelVersionException err) {
+      exitWithError(ErrorMessages.UNKNOWN_MODEL_VERSION);
     } catch (Exception exp) {
       exitWithError(exp.getMessage());
     }
 
-    printInformation(model + " property set.");
+    if (alias != null) {
+      printInformation(model + " version " + alias + " property set.");
+    } else {
+      printInformation(model + " version " + version + " property set.");
+    }
   }
 
   /** {@inheritDoc} */
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 35b9f62698..e4014e1c1b 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
@@ -47,7 +47,9 @@ 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.RemoveModelVersionProperty;
 import org.apache.gravitino.cli.commands.SetModelProperty;
+import org.apache.gravitino.cli.commands.SetModelVersionProperty;
 import org.apache.gravitino.cli.commands.UpdateModelName;
 import org.apache.gravitino.cli.commands.UpdateModelVersionComment;
 import org.junit.jupiter.api.AfterEach;
@@ -734,7 +736,6 @@ public class TestModelCommands {
 
   @Test
   void testUpdateModelVersionCommentCommandByAliasAndVersion() {
-    Main.useExit = false;
     
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
     
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
     when(mockCommandLine.hasOption(GravitinoOptions.ALIAS)).thenReturn(true);
@@ -752,4 +753,186 @@ public class TestModelCommands {
 
     Assertions.assertThrows(RuntimeException.class, 
commandLine::handleCommandLine);
   }
+
+  @Test
+  void testSetModelVersionProperty() {
+    SetModelVersionProperty mockSetProperty = 
mock(SetModelVersionProperty.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.VERSION)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.VERSION)).thenReturn("1");
+    
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(mockSetProperty).validate();
+    doReturn(mockSetProperty)
+        .when(commandLine)
+        .newSetModelVersionProperty(
+            any(CommandContext.class),
+            eq("metalake_demo"),
+            eq("catalog"),
+            eq("schema"),
+            eq("model"),
+            any(),
+            any(),
+            eq("key"),
+            eq("value"));
+    commandLine.handleCommandLine();
+    verify(mockSetProperty).handle();
+  }
+
+  @Test
+  void testSetModelVersionPropertyByAlias() {
+    SetModelVersionProperty mockSetProperty = 
mock(SetModelVersionProperty.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.ALIAS)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.ALIAS))
+        .thenReturn(new String[] {"aliasA"});
+    
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(mockSetProperty).validate();
+    doReturn(mockSetProperty)
+        .when(commandLine)
+        .newSetModelVersionProperty(
+            any(CommandContext.class),
+            eq("metalake_demo"),
+            eq("catalog"),
+            eq("schema"),
+            eq("model"),
+            any(),
+            any(),
+            eq("key"),
+            eq("value"));
+    commandLine.handleCommandLine();
+    verify(mockSetProperty).handle();
+  }
+
+  @Test
+  void testSetModelVersionPropertyByAliasAndVersion() {
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.ALIAS)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.ALIAS))
+        .thenReturn(new String[] {"aliasA"});
+    when(mockCommandLine.hasOption(GravitinoOptions.VERSION)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.VERSION)).thenReturn("1");
+    
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));
+
+    Assertions.assertThrows(RuntimeException.class, 
commandLine::handleCommandLine);
+  }
+
+  @Test
+  void testRemoveModelVersionProperty() {
+    RemoveModelVersionProperty mockRemoveProperty = 
mock(RemoveModelVersionProperty.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.VERSION)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.VERSION)).thenReturn("1");
+    
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(mockRemoveProperty).validate();
+    doReturn(mockRemoveProperty)
+        .when(commandLine)
+        .newRemoveModelVersionProperty(
+            any(CommandContext.class),
+            eq("metalake_demo"),
+            eq("catalog"),
+            eq("schema"),
+            eq("model"),
+            any(),
+            any(),
+            eq("key"));
+    commandLine.handleCommandLine();
+    verify(mockRemoveProperty).handle();
+  }
+
+  @Test
+  void testRemoveModelVersionPropertyByAlias() {
+    RemoveModelVersionProperty mockRemoveProperty = 
mock(RemoveModelVersionProperty.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.ALIAS)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.ALIAS))
+        .thenReturn(new String[] {"aliasA"});
+    
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(mockRemoveProperty).validate();
+    doReturn(mockRemoveProperty)
+        .when(commandLine)
+        .newRemoveModelVersionProperty(
+            any(CommandContext.class),
+            eq("metalake_demo"),
+            eq("catalog"),
+            eq("schema"),
+            eq("model"),
+            any(),
+            any(),
+            eq("key"));
+    commandLine.handleCommandLine();
+    verify(mockRemoveProperty).handle();
+  }
+
+  @Test
+  void testRemoveModelVersionPropertyByAliasAndVersion() {
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.ALIAS)).thenReturn(true);
+    when(mockCommandLine.getOptionValues(GravitinoOptions.ALIAS))
+        .thenReturn(new String[] {"aliasA"});
+    when(mockCommandLine.hasOption(GravitinoOptions.VERSION)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.VERSION)).thenReturn("1");
+    
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));
+
+    Assertions.assertThrows(RuntimeException.class, 
commandLine::handleCommandLine);
+  }
 }
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 ceffd38a94..48ddd9df6e 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
@@ -391,6 +391,15 @@ class DTOConverters {
       return new ModelVersionUpdateRequest.UpdateModelVersionComment(
           ((ModelVersionChange.UpdateComment) change).newComment());
 
+    } else if (change instanceof ModelVersionChange.SetProperty) {
+      return new ModelVersionUpdateRequest.SetModelVersionPropertyRequest(
+          ((ModelVersionChange.SetProperty) change).property(),
+          ((ModelVersionChange.SetProperty) change).value());
+
+    } else if (change instanceof ModelVersionChange.RemoveProperty) {
+      return new ModelVersionUpdateRequest.RemoveModelVersionPropertyRequest(
+          ((ModelVersionChange.RemoveProperty) change).property());
+
     } else {
       throw new IllegalArgumentException(
           "Unknown model version change type: " + 
change.getClass().getSimpleName());
diff --git a/clients/client-python/gravitino/api/model_version_change.py 
b/clients/client-python/gravitino/api/model_version_change.py
index 0c555e983b..56550df874 100644
--- a/clients/client-python/gravitino/api/model_version_change.py
+++ b/clients/client-python/gravitino/api/model_version_change.py
@@ -34,6 +34,27 @@ class ModelVersionChange(ABC):
         """
         return ModelVersionChange.UpdateComment(comment)
 
+    @staticmethod
+    def set_property(key, value):
+        """Creates a new model version change to set a property and value pair 
for the model version.
+        Args:
+            key: The key of the property.
+            value: The value of the property.
+        Returns:
+            The model version change.
+        """
+        return ModelVersionChange.SetProperty(key, value)
+
+    @staticmethod
+    def remove_property(key: str):
+        """Creates a new model version change to remove a property from the 
model version.
+        Args:
+            key: The key of the property.
+        Returns:
+            The model version change.
+        """
+        return ModelVersionChange.RemoveProperty(key)
+
     class UpdateComment:
         """A model version change to update the comment of the model 
version."""
 
@@ -76,3 +97,94 @@ class ModelVersionChange(ABC):
                 A string summary of this comment update operation.
             """
             return f"UpdateComment {self._new_comment}"
+
+    class SetProperty:
+        """A model version change to set a property and value pair for the 
model version."""
+
+        def __init__(self, key: str, value: str):
+            self._key = key
+            self._value = value
+
+        def property(self) -> str:
+            """Retrieves the name of the property.
+            Returns:
+                The name of the property.
+            """
+            return self._key
+
+        def value(self) -> str:
+            """Retrieves the value of the property.
+            Returns:
+                The value of the property.
+            """
+            return self._value
+
+        def __eq__(self, other):
+            """Compares this SetProperty instance with another object for 
equality. Two instances are
+            considered equal if they designate the same key and value pair for 
the model version.
+            Args:
+                other: The object to compare with this instance.
+            Returns:
+                true if the given object represents an identical model version 
property set operation;  false otherwise.
+            """
+            if not isinstance(other, ModelVersionChange.SetProperty):
+                return False
+            return self.key() == other.key() and self.value() == other.value()
+
+        def __hash__(self):
+            """Generates a hash code for this SetProperty instance. The hash 
code is primarily based on
+            the key and value pair for the model version.
+            Returns:
+                A hash code value for this property set operation.
+            """
+            return hash((self.key(), self.value()))
+
+        def __str__(self):
+            """Provides a string representation of the SetProperty instance. 
This string includes the
+            class name followed by the key and value pair for the model 
version.
+            Returns:
+                A string summary of this property set operation.
+            """
+            return f"SetProperty {self.key()}={self.value()}"
+
+    class RemoveProperty:
+        """A model version change to remove a property from the model 
version."""
+
+        def __init__(self, key: str):
+            self._key = key
+
+        def property(self) -> str:
+            """Retrieves the name of the property.
+            Returns:
+                The name of the property.
+            """
+            return self._key
+
+        def __eq__(self, other):
+            """Compares this RemoveProperty instance with another object for 
equality. Two instances are
+            considered equal if they designate the same key for the model 
version.
+            Args:
+                other: The object to compare with this instance.
+            Returns:
+                true if the given object represents an identical model version 
property remove operation;
+                false otherwise.
+            """
+            if not isinstance(other, ModelVersionChange.RemoveProperty):
+                return False
+            return self.key() == other.key()
+
+        def __hash__(self):
+            """Generates a hash code for this RemoveProperty instance. The 
hash code is primarily based on
+            the key for the model version.
+            Returns:
+                A hash code value for this property remove operation.
+            """
+            return hash(self.key())
+
+        def __str__(self):
+            """Provides a string representation of the RemoveProperty 
instance. This string includes the
+            class name followed by the key for the model version.
+            Returns:
+                A string summary of this property remove operation.
+            """
+            return f"RemoveProperty {self.key()}"
diff --git a/clients/client-python/gravitino/client/generic_model_catalog.py 
b/clients/client-python/gravitino/client/generic_model_catalog.py
index 554b788e54..b304c5115d 100644
--- a/clients/client-python/gravitino/client/generic_model_catalog.py
+++ b/clients/client-python/gravitino/client/generic_model_catalog.py
@@ -532,8 +532,15 @@ class GenericModelCatalog(BaseSchemaCatalog):
                 change.new_comment()
             )
 
-        if isinstance(change, ModelVersionChange.RemoveComment):
-            return ModelVersionUpdateRequest.RemoveModelVersionComment()
+        if isinstance(change, ModelVersionChange.SetProperty):
+            return ModelVersionUpdateRequest.SetModelVersionPropertyRequest(
+                change.property(), change.value()
+            )
+
+        if isinstance(change, ModelVersionChange.RemoveProperty):
+            return ModelVersionUpdateRequest.RemoveModelVersionPropertyRequest(
+                change.property()
+            )
 
         raise ValueError(f"Unknown change type: {type(change).__name__}")
 
diff --git 
a/clients/client-python/gravitino/dto/requests/model_version_update_request.py 
b/clients/client-python/gravitino/dto/requests/model_version_update_request.py
index a9a5b9c1c4..948f92359b 100644
--- 
a/clients/client-python/gravitino/dto/requests/model_version_update_request.py
+++ 
b/clients/client-python/gravitino/dto/requests/model_version_update_request.py
@@ -61,3 +61,49 @@ class ModelVersionUpdateRequest:
 
         def model_version_change(self):
             return ModelVersionChange.update_comment(self._new_comment)
+
+    @dataclass
+    class SetModelVersionPropertyRequest(ModelVersionUpdateRequestBase):
+        """Request to update model version properties"""
+
+        _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_version_change(self) -> ModelVersionChange:
+            return ModelVersionChange.set_property(self._property, self._value)
+
+    @dataclass
+    class RemoveModelVersionPropertyRequest(ModelVersionUpdateRequestBase):
+        """Request to remove model version properties"""
+
+        _property: Optional[str] = 
field(metadata=config(field_name="property"))
+
+        def __init__(self, pro: str):
+            super().__init__("removeProperty")
+            self._property = pro
+
+        def key(self):
+            """
+            Returns the key of the property to remove.
+            Returns:
+                str: The key of the property to remove.
+            """
+            return self._property
+
+        def validate(self):
+            if not self._property:
+                raise ValueError('"property" field is required')
+
+        def model_version_change(self):
+            return ModelVersionChange.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 85211ccd65..54392ae9f3 100644
--- a/clients/client-python/tests/integration/test_model_catalog.py
+++ b/clients/client-python/tests/integration/test_model_catalog.py
@@ -328,6 +328,53 @@ class TestModelCatalog(IntegrationTestEnv):
         self.assertEqual({"k1": "v1", "k2": "v2"}, 
updated_model_version.properties())
         self.assertEqual("uri", updated_model_version.uri())
 
+    def test_link_update_model_version_property(self):
+        model_name = f"model_it_model{str(randint(0, 1000))}"
+        model_ident = NameIdentifier.of(self._schema_name, model_name)
+        aliases = ["alias1", "alias2"]
+        comment = "comment"
+        properties = {"k1": "v1", "k2": "v2"}
+        self._catalog.as_model_catalog().register_model(
+            model_ident, comment, properties
+        )
+
+        self._catalog.as_model_catalog().link_model_version(
+            model_ident,
+            uri="uri",
+            aliases=aliases,
+            comment="comment",
+            properties={"k1": "v1", "k2": "v2"},
+        )
+
+        original_model_version = 
self._catalog.as_model_catalog().get_model_version(
+            model_ident, 0
+        )
+
+        self.assertEqual(0, original_model_version.version())
+        self.assertEqual("uri", original_model_version.uri())
+        self.assertEqual(["alias1", "alias2"], 
original_model_version.aliases())
+        self.assertEqual("comment", original_model_version.comment())
+        self.assertEqual({"k1": "v1", "k2": "v2"}, 
original_model_version.properties())
+
+        changes = [
+            ModelVersionChange.set_property("k1", "v11"),
+            ModelVersionChange.set_property("k3", "v3"),
+            ModelVersionChange.remove_property("k2"),
+        ]
+
+        self._catalog.as_model_catalog().alter_model_version(model_ident, 0, 
*changes)
+        update_property_model = (
+            self._catalog.as_model_catalog().get_model_version_by_alias(
+                model_ident, aliases[0]
+            )
+        )
+
+        self.assertEqual(update_property_model.version(), 0)
+        self.assertEqual(update_property_model.uri(), "uri")
+        self.assertEqual(update_property_model.comment(), comment)
+        self.assertEqual(update_property_model.aliases(), aliases)
+        self.assertEqual(update_property_model.properties(), {"k1": "v11", 
"k3": "v3"})
+
     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/ModelVersionUpdateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelVersionUpdateRequest.java
index 1026bd82ae..364d0bc9c4 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/requests/ModelVersionUpdateRequest.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelVersionUpdateRequest.java
@@ -23,11 +23,13 @@ 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.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.model.ModelVersionChange;
 import org.apache.gravitino.rest.RESTRequest;
 
@@ -37,7 +39,13 @@ import org.apache.gravitino.rest.RESTRequest;
 @JsonSubTypes({
   @JsonSubTypes.Type(
       value = ModelVersionUpdateRequest.UpdateModelVersionComment.class,
-      name = "updateComment")
+      name = "updateComment"),
+  @JsonSubTypes.Type(
+      value = ModelVersionUpdateRequest.SetModelVersionPropertyRequest.class,
+      name = "setProperty"),
+  @JsonSubTypes.Type(
+      value = 
ModelVersionUpdateRequest.RemoveModelVersionPropertyRequest.class,
+      name = "removeProperty")
 })
 public interface ModelVersionUpdateRequest extends RESTRequest {
 
@@ -68,4 +76,64 @@ public interface ModelVersionUpdateRequest extends 
RESTRequest {
     @Override
     public void validate() throws IllegalArgumentException {}
   }
+
+  /** Request to set Properties of a model version */
+  @EqualsAndHashCode
+  @AllArgsConstructor
+  @NoArgsConstructor(force = true)
+  @ToString
+  @Getter
+  class SetModelVersionPropertyRequest implements ModelVersionUpdateRequest {
+    @JsonProperty("property")
+    private final String property;
+
+    @JsonProperty("value")
+    private final String value;
+
+    /** {@inheritDoc} */
+    @Override
+    public ModelVersionChange modelVersionChange() {
+      return ModelVersionChange.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");
+    }
+  }
+
+  /** Request to remove a property from a model version. */
+  @EqualsAndHashCode
+  @AllArgsConstructor
+  @NoArgsConstructor(force = true)
+  @ToString
+  @Getter
+  class RemoveModelVersionPropertyRequest implements ModelVersionUpdateRequest 
{
+    @JsonProperty("property")
+    private final String property;
+
+    /** {@inheritDoc} */
+    @Override
+    public ModelVersionChange modelVersionChange() {
+      return ModelVersionChange.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 115350dbcc..5e27a77ef1 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
@@ -398,6 +398,162 @@ public class TestModelOperationDispatcher extends 
TestOperationDispatcher {
     Assertions.assertEquals(modelVersion.properties(), 
alteredModelVersion.properties());
   }
 
+  @Test
+  void testUpdateAndAddModelVersionProperty() {
+    String schemaName = randomSchemaName();
+    String schemaComment = "schema which tests update";
+
+    String modelName = randomModelName();
+    String modelComment = "model which tests update";
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    Map<String, String> newProps = ImmutableMap.of("k1", "new value", "k2", 
"v2", "k3", "v3");
+
+    String versionUri = "s3://test-bucket/test-path/model.json";
+    String[] versionAliases = {"alias1", "alias2"};
+    String versionComment = "version which tests update";
+
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
schemaName);
+    schemaOperationDispatcher.createSchema(schemaIdent, schemaComment, props);
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+    modelOperationDispatcher.registerModel(modelIdent, modelComment, props);
+
+    modelOperationDispatcher.linkModelVersion(
+        modelIdent, versionUri, versionAliases, versionComment, props);
+
+    ModelVersionChange[] changes =
+        new ModelVersionChange[] {
+          ModelVersionChange.setProperty("k1", "new value"),
+          ModelVersionChange.setProperty("k3", "v3")
+        };
+    ModelVersion modelVersion = 
modelOperationDispatcher.getModelVersion(modelIdent, 0);
+    ModelVersion alteredModelVersion =
+        modelOperationDispatcher.alterModelVersion(modelIdent, 0, changes);
+
+    Assertions.assertEquals(modelVersion.uri(), alteredModelVersion.uri());
+    Assertions.assertEquals(modelVersion.version(), 
alteredModelVersion.version());
+    Assertions.assertEquals(modelVersion.aliases(), 
alteredModelVersion.aliases());
+    Assertions.assertEquals(modelVersion.comment(), 
alteredModelVersion.comment());
+    Assertions.assertEquals(newProps, alteredModelVersion.properties());
+  }
+
+  @Test
+  void testUpdateAndAddModelVersionPropertyByAlias() {
+    String schemaName = randomSchemaName();
+    String schemaComment = "schema which tests update";
+
+    String modelName = randomModelName();
+    String modelComment = "model which tests update";
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    Map<String, String> newProps = ImmutableMap.of("k1", "new value", "k2", 
"v2", "k3", "v3");
+
+    String versionUri = "s3://test-bucket/test-path/model.json";
+    String[] versionAliases = {"alias1", "alias2"};
+    String versionComment = "version which tests update";
+
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
schemaName);
+    schemaOperationDispatcher.createSchema(schemaIdent, schemaComment, props);
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+    modelOperationDispatcher.registerModel(modelIdent, modelComment, props);
+
+    modelOperationDispatcher.linkModelVersion(
+        modelIdent, versionUri, versionAliases, versionComment, props);
+
+    ModelVersionChange[] changes =
+        new ModelVersionChange[] {
+          ModelVersionChange.setProperty("k1", "new value"),
+          ModelVersionChange.setProperty("k3", "v3")
+        };
+    ModelVersion modelVersion =
+        modelOperationDispatcher.getModelVersion(modelIdent, 
versionAliases[0]);
+    ModelVersion alteredModelVersion =
+        modelOperationDispatcher.alterModelVersion(modelIdent, 
versionAliases[0], changes);
+
+    Assertions.assertEquals(modelVersion.uri(), alteredModelVersion.uri());
+    Assertions.assertEquals(modelVersion.version(), 
alteredModelVersion.version());
+    Assertions.assertEquals(modelVersion.aliases(), 
alteredModelVersion.aliases());
+    Assertions.assertEquals(modelVersion.comment(), 
alteredModelVersion.comment());
+    Assertions.assertEquals(newProps, alteredModelVersion.properties());
+  }
+
+  @Test
+  void testRemoveModelVersionProperty() {
+    String schemaName = randomSchemaName();
+    String schemaComment = "schema which tests update";
+
+    String modelName = randomModelName();
+    String modelComment = "model which tests update";
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    Map<String, String> newProps = ImmutableMap.of("k2", "v2");
+
+    String versionUri = "s3://test-bucket/test-path/model.json";
+    String[] versionAliases = {"alias1", "alias2"};
+    String versionComment = "version which tests update";
+
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
schemaName);
+    schemaOperationDispatcher.createSchema(schemaIdent, schemaComment, props);
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+    modelOperationDispatcher.registerModel(modelIdent, modelComment, props);
+
+    modelOperationDispatcher.linkModelVersion(
+        modelIdent, versionUri, versionAliases, versionComment, props);
+
+    ModelVersionChange change = ModelVersionChange.removeProperty("k1");
+
+    ModelVersion modelVersion = 
modelOperationDispatcher.getModelVersion(modelIdent, 0);
+    ModelVersion alteredModelVersion =
+        modelOperationDispatcher.alterModelVersion(modelIdent, 0, change);
+
+    Assertions.assertEquals(modelVersion.uri(), alteredModelVersion.uri());
+    Assertions.assertEquals(modelVersion.version(), 
alteredModelVersion.version());
+    Assertions.assertEquals(modelVersion.aliases(), 
alteredModelVersion.aliases());
+    Assertions.assertEquals(modelVersion.comment(), 
alteredModelVersion.comment());
+    Assertions.assertEquals(newProps, alteredModelVersion.properties());
+  }
+
+  @Test
+  void testRemoveModelVersionPropertyByAlias() {
+    String schemaName = randomSchemaName();
+    String schemaComment = "schema which tests update";
+
+    String modelName = randomModelName();
+    String modelComment = "model which tests update";
+    Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+    Map<String, String> newProps = ImmutableMap.of("k2", "v2");
+
+    String versionUri = "s3://test-bucket/test-path/model.json";
+    String[] versionAliases = {"alias1", "alias2"};
+    String versionComment = "version which tests update";
+
+    NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog, 
schemaName);
+    schemaOperationDispatcher.createSchema(schemaIdent, schemaComment, props);
+
+    NameIdentifier modelIdent =
+        NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+    modelOperationDispatcher.registerModel(modelIdent, modelComment, props);
+
+    modelOperationDispatcher.linkModelVersion(
+        modelIdent, versionUri, versionAliases, versionComment, props);
+
+    ModelVersionChange change = ModelVersionChange.removeProperty("k1");
+
+    ModelVersion modelVersion =
+        modelOperationDispatcher.getModelVersion(modelIdent, 
versionAliases[0]);
+    ModelVersion alteredModelVersion =
+        modelOperationDispatcher.alterModelVersion(modelIdent, 
versionAliases[0], change);
+
+    Assertions.assertEquals(modelVersion.uri(), alteredModelVersion.uri());
+    Assertions.assertEquals(modelVersion.version(), 
alteredModelVersion.version());
+    Assertions.assertEquals(modelVersion.aliases(), 
alteredModelVersion.aliases());
+    Assertions.assertEquals(modelVersion.comment(), 
alteredModelVersion.comment());
+    Assertions.assertEquals(newProps, alteredModelVersion.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 1588ba0e27..94dcd420f0 100644
--- 
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
+++ 
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
@@ -1046,6 +1046,15 @@ public class TestCatalogOperations
       if (change instanceof ModelVersionChange.UpdateComment) {
         newComment = ((ModelVersionChange.UpdateComment) change).newComment();
 
+      } else if (change instanceof ModelVersionChange.RemoveProperty) {
+        ModelVersionChange.RemoveProperty removeProperty =
+            (ModelVersionChange.RemoveProperty) change;
+        newProps.remove(removeProperty.property());
+
+      } else if (change instanceof ModelVersionChange.SetProperty) {
+        ModelVersionChange.SetProperty setProperty = 
(ModelVersionChange.SetProperty) change;
+        newProps.put(setProperty.property(), setProperty.value());
+
       } else {
         throw new IllegalArgumentException("Unsupported model change: " + 
change);
       }
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestModelVersionMetaService.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestModelVersionMetaService.java
index f1206f7959..4284537d76 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestModelVersionMetaService.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestModelVersionMetaService.java
@@ -548,6 +548,90 @@ public class TestModelVersionMetaService extends 
TestJDBCBackend {
                     updateCommentUpdater));
   }
 
+  @Test
+  void testAlterModelVersionProperties() throws IOException {
+    createParentEntities(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME, auditInfo);
+
+    Map<String, String> properties = ImmutableMap.of("k1", "v1", "k2", "v2");
+    String modelName = randomModelName();
+    String modelComment = "model1 comment";
+    String modelVersionUri = "S3://test/path/to/model/version";
+    List<String> modelVersionAliases = ImmutableList.of("alias1", "alias2");
+    String modelVersionComment = "test comment";
+    Map<String, String> updatedProperties =
+        ImmutableMap.of("k1", "new value", "k2", "v2", "k3", "v3");
+    int version = 0;
+
+    ModelEntity modelEntity =
+        createModelEntity(
+            RandomIdGenerator.INSTANCE.nextId(),
+            MODEL_NS,
+            modelName,
+            modelComment,
+            0,
+            properties,
+            auditInfo);
+
+    ModelVersionEntity modelVersionEntity =
+        createModelVersionEntity(
+            modelEntity.nameIdentifier(),
+            version,
+            modelVersionUri,
+            modelVersionAliases,
+            modelVersionComment,
+            properties,
+            auditInfo);
+
+    ModelVersionEntity updatedModelVersionEntity =
+        createModelVersionEntity(
+            modelVersionEntity.modelIdentifier(),
+            modelVersionEntity.version(),
+            modelVersionEntity.uri(),
+            modelVersionEntity.aliases(),
+            modelVersionEntity.comment(),
+            updatedProperties,
+            modelVersionEntity.auditInfo());
+
+    Assertions.assertDoesNotThrow(
+        () -> ModelMetaService.getInstance().insertModel(modelEntity, false));
+
+    Assertions.assertDoesNotThrow(
+        () -> 
ModelVersionMetaService.getInstance().insertModelVersion(modelVersionEntity));
+
+    Function<ModelVersionEntity, ModelVersionEntity> updatePropertiesUpdater =
+        oldModelVersionEntity -> updatedModelVersionEntity;
+
+    ModelVersionEntity alteredModelVersionEntity =
+        ModelVersionMetaService.getInstance()
+            .updateModelVersion(modelVersionEntity.nameIdentifier(), 
updatePropertiesUpdater);
+
+    Assertions.assertEquals(updatedModelVersionEntity, 
alteredModelVersionEntity);
+
+    // Test update a non-exist model
+    Assertions.assertThrows(
+        NoSuchEntityException.class,
+        () ->
+            ModelVersionMetaService.getInstance()
+                .updateModelVersion(
+                    NameIdentifierUtil.ofModelVersion(
+                        METALAKE_NAME,
+                        CATALOG_NAME,
+                        SCHEMA_NAME,
+                        "non_exist_model",
+                        "non_exist_version"),
+                    updatePropertiesUpdater));
+
+    // Test update a non-exist model version
+    Assertions.assertThrows(
+        NoSuchEntityException.class,
+        () ->
+            ModelVersionMetaService.getInstance()
+                .updateModelVersion(
+                    NameIdentifierUtil.ofModelVersion(
+                        METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME, modelName, 
"non_exist_version"),
+                    updatePropertiesUpdater));
+  }
+
   private NameIdentifier getModelVersionIdent(NameIdentifier modelIdent, int 
version) {
     List<String> parts = Lists.newArrayList(modelIdent.namespace().levels());
     parts.add(modelIdent.name());


Reply via email to