This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 5720f4a45b [#6526] feat(core): Support update properties operations
for model alteration (#6912)
5720f4a45b is described below
commit 5720f4a45b55cc04e076b432006a1240e6793f23
Author: Lord of Abyss <[email protected]>
AuthorDate: Mon Apr 14 14:22:25 2025 +0800
[#6526] feat(core): Support update properties operations for model
alteration (#6912)
### What changes were proposed in this pull request?
Support update properties operations for model alteration.
### Why are the changes needed?
Fix: #6526
### Does this PR introduce _any_ user-facing change?
server support update/set property or remove a property from a model.
### How was this patch tested?
local host
`bin/gcli.sh model set -m demo_metalake --name
model_catalog.schema.model2 --property key1 --value val1`
<img width="1614" alt="image"
src="https://github.com/user-attachments/assets/32946856-4b24-4e71-a7bd-4ecada2c7397"
/>
`bin/gcli.sh model set -m demo_metalake --name
model_catalog.schema.model2 --property key1 --value val2`
<img width="1060" alt="image"
src="https://github.com/user-attachments/assets/608734c3-8c3a-48ee-a3df-39e07b336ab7"
/>
`bin/gcli.sh model remove -m demo_metalake --name
model_catalog.schema.model2 --property key1`
<img width="1039" alt="image"
src="https://github.com/user-attachments/assets/29725d56-b420-4541-b6f8-505c529bbd57"
/>
---
.../org/apache/gravitino/model/ModelChange.java | 160 ++++++++++++++++++++-
.../catalog/model/ModelCatalogOperations.java | 24 +++-
.../catalog/model/TestModelCatalogOperations.java | 111 +++++++++++++-
.../integration/test/ModelCatalogOperationsIT.java | 57 ++++++++
.../apache/gravitino/cli/ModelCommandHandler.java | 26 ++++
.../apache/gravitino/cli/TestableCommandLine.java | 23 +++
.../cli/commands/RemoveModelProperty.java | 94 ++++++++++++
.../gravitino/cli/commands/SetModelProperty.java | 99 +++++++++++++
.../apache/gravitino/cli/TestModelCommands.java | 63 ++++++++
.../org/apache/gravitino/client/DTOConverters.java | 9 ++
.../client-python/gravitino/api/model_change.py | 115 +++++++++++++++
.../gravitino/client/generic_model_catalog.py | 8 ++
.../gravitino/dto/requests/model_update_request.py | 38 +++++
.../tests/integration/test_model_catalog.py | 53 +++++++
.../gravitino/dto/requests/ModelUpdateRequest.java | 69 ++++++++-
.../catalog/TestModelOperationDispatcher.java | 85 +++++++++++
.../gravitino/connector/TestCatalogOperations.java | 15 +-
.../server/web/rest/TestModelOperations.java | 120 +++++++++++++++-
18 files changed, 1160 insertions(+), 9 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/model/ModelChange.java
b/api/src/main/java/org/apache/gravitino/model/ModelChange.java
index 5d40c78856..513ad569e3 100644
--- a/api/src/main/java/org/apache/gravitino/model/ModelChange.java
+++ b/api/src/main/java/org/apache/gravitino/model/ModelChange.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.model;
+import java.util.Objects;
import org.apache.gravitino.annotation.Evolving;
/**
@@ -37,6 +38,27 @@ public interface ModelChange {
return new ModelChange.RenameModel(newName);
}
+ /**
+ * Create a ModelChange for setting a property and value of a model.
+ *
+ * @param property The name of the property to be set.
+ * @param value The value to be set for the property.
+ * @return A ModelChange for the property set.
+ */
+ static ModelChange setProperty(String property, String value) {
+ return new ModelChange.SetProperty(property, value);
+ }
+
+ /**
+ * Create a ModelChange for removing a property from a model.
+ *
+ * @param property The name of the property to be removed from the model.
+ * @return A ModelChange for the property removal.
+ */
+ static ModelChange removeProperty(String property) {
+ return new ModelChange.RemoveProperty(property);
+ }
+
/** A ModelChange to rename a model. */
final class RenameModel implements ModelChange {
private final String newName;
@@ -70,9 +92,9 @@ public interface ModelChange {
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
- if (obj == null || getClass() != obj.getClass()) return false;
+ if (!(obj instanceof RenameModel)) return false;
RenameModel other = (RenameModel) obj;
- return newName.equals(other.newName);
+ return Objects.equals(newName, other.newName);
}
/**
@@ -97,4 +119,138 @@ public interface ModelChange {
return "RenameModel " + newName;
}
}
+
+ /** A ModelChange to set a property and value of a model. */
+ final class SetProperty implements ModelChange {
+ private final String property;
+ private final String value;
+
+ /**
+ * Constructs a new {@link SetProperty} instance with the specified
property name and value.
+ *
+ * @param property The name of the property to be set.
+ * @param value The value to be set for the property.
+ */
+ public SetProperty(String property, String value) {
+ this.property = property;
+ this.value = value;
+ }
+
+ /**
+ * Retrieves the name of the property to be set.
+ *
+ * @return The name of the property to be set.
+ */
+ public String property() {
+ return property;
+ }
+
+ /**
+ * Retrieves the value to be set for the property.
+ *
+ * @return The value to be set for the property.
+ */
+ public String value() {
+ return value;
+ }
+
+ /**
+ * Compares this SetProperty instance with another object for equality.
Two instances are
+ * considered equal if they target the same property and set the same
value.
+ *
+ * @param obj The object to compare with this instance.
+ * @return {@code true} if the given object represents the same property
set; {@code false}
+ * otherwise.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (!(obj instanceof SetProperty)) return false;
+ SetProperty other = (SetProperty) obj;
+ return Objects.equals(property, other.property) && Objects.equals(value,
other.value);
+ }
+
+ /**
+ * Generates a hash code for this SetProperty instance. The hash code is
based on the property
+ * name and value to be set.
+ *
+ * @return A hash code value for this property set operation.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(property, value);
+ }
+
+ /**
+ * Provides a string representation of the SetProperty instance. This
string format includes the
+ * class name followed by the property name and value to be set.
+ *
+ * @return A string summary of the property set operation.
+ */
+ @Override
+ public String toString() {
+ return "SETPROPERTY " + property + " " + value;
+ }
+ }
+
+ /** A ModelChange to remove a property from model. */
+ final class RemoveProperty implements ModelChange {
+ private final String property;
+
+ /**
+ * Constructs a new {@link RemoveProperty} instance with the specified
property name.
+ *
+ * @param property The name of the property to be removed from the model.
+ */
+ public RemoveProperty(String property) {
+ this.property = property;
+ }
+
+ /**
+ * Retrieves the name of the property to be removed from the model.
+ *
+ * @return The name of the property for removal.
+ */
+ public String property() {
+ return property;
+ }
+
+ /**
+ * Compares this RemoveProperty instance with another object for equality.
Two instances are
+ * considered equal if they target the same property for removal from the
fileset.
+ *
+ * @param obj The object to compare with this instance.
+ * @return {@code true} if the given object represents the same property
removal; {@code false}
+ * otherwise.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (!(obj instanceof RemoveProperty)) return false;
+ RemoveProperty other = (RemoveProperty) obj;
+ return Objects.equals(property, other.property);
+ }
+
+ /**
+ * Generates a hash code for this RemoveProperty instance. The hash code
is based on the
+ * property name that is to be removed from the fileset.
+ *
+ * @return A hash code value for this property removal operation.
+ */
+ @Override
+ public int hashCode() {
+ return property.hashCode();
+ }
+
+ /**
+ * Provides a string representation of the RemoveProperty instance. This
string format includes
+ * the class name followed by the property name to be removed.
+ *
+ * @return A string summary of the property removal operation.
+ */
+ @Override
+ public String toString() {
+ return "RemoveProperty " + property;
+ }
+ }
}
diff --git
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogOperations.java
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogOperations.java
index 4a47880812..6ad4acb2b0 100644
---
a/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogOperations.java
+++
b/catalogs/catalog-model/src/main/java/org/apache/gravitino/catalog/model/ModelCatalogOperations.java
@@ -306,10 +306,17 @@ public class ModelCatalogOperations extends
ManagedSchemaOperations
AuditInfo entityAuditInfo = modelEntity.auditInfo();
Namespace entityNamespace = modelEntity.namespace();
Integer entityLatestVersion = modelEntity.latestVersion();
+ String modifier = PrincipalUtils.getCurrentPrincipal().getName();
for (ModelChange change : changes) {
if (change instanceof ModelChange.RenameModel) {
entityName = ((ModelChange.RenameModel) change).newName();
+ } else if (change instanceof ModelChange.SetProperty) {
+ ModelChange.SetProperty setPropertyChange = (ModelChange.SetProperty)
change;
+ doSetProperty(entityProperties, setPropertyChange);
+ } else if (change instanceof ModelChange.RemoveProperty) {
+ ModelChange.RemoveProperty removePropertyChange =
(ModelChange.RemoveProperty) change;
+ doRemoveProperty(entityProperties, removePropertyChange);
} else {
throw new IllegalArgumentException(
"Unsupported model change: " + change.getClass().getSimpleName());
@@ -320,7 +327,13 @@ public class ModelCatalogOperations extends
ManagedSchemaOperations
.withName(entityName)
.withId(entityId)
.withComment(entityComment)
- .withAuditInfo(entityAuditInfo)
+ .withAuditInfo(
+ AuditInfo.builder()
+ .withCreator(entityAuditInfo.creator())
+ .withCreateTime(entityAuditInfo.createTime())
+ .withLastModifier(modifier)
+ .withLastModifiedTime(Instant.now())
+ .build())
.withNamespace(entityNamespace)
.withProperties(entityProperties)
.withLatestVersion(entityLatestVersion)
@@ -368,4 +381,13 @@ public class ModelCatalogOperations extends
ManagedSchemaOperations
throw new RuntimeException("Failed to delete model version " + ident,
ioe);
}
}
+
+ private void doRemoveProperty(
+ Map<String, String> entityProperties, ModelChange.RemoveProperty change)
{
+ entityProperties.remove(change.property());
+ }
+
+ private void doSetProperty(Map<String, String> entityProperties,
ModelChange.SetProperty change) {
+ entityProperties.put(change.property(), change.value());
+ }
}
diff --git
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
index 83fcc1505f..bb6c010456 100644
---
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
+++
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
@@ -34,6 +34,7 @@ import static
org.apache.gravitino.Configs.STORE_TRANSACTION_MAX_SKEW_TIME;
import static org.apache.gravitino.Configs.VERSION_RETENTION_COUNT;
import static org.mockito.Mockito.when;
+import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
@@ -658,7 +659,7 @@ public class TestModelCatalogOperations {
}
@Test
- public void testRename() {
+ public void testRenameModel() {
String schemaName = randomSchemaName();
createSchema(schemaName);
@@ -689,6 +690,114 @@ public class TestModelCatalogOperations {
Assertions.assertEquals(properties, alteredModel.properties());
}
+ @Test
+ void testAddModelProperty() {
+ String schemaName = randomSchemaName();
+ createSchema(schemaName);
+
+ String modelName = "model";
+ String comment = "comment";
+ NameIdentifier modelIdent =
+ NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName,
modelName);
+ StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+ Map<String, String> properties =
+ StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of("key1",
"value1"));
+ Map<String, String> newProperties =
+ ImmutableMap.<String, String>builder().putAll(properties).put("key2",
"value2").build();
+
+ // validate registered model
+ Model registeredModel = ops.registerModel(modelIdent, comment, properties);
+ Assertions.assertEquals(modelName, registeredModel.name());
+ Assertions.assertEquals(comment, registeredModel.comment());
+ Assertions.assertEquals(properties, registeredModel.properties());
+
+ // validate loaded model
+ Model loadedModel = ops.getModel(modelIdent);
+ Assertions.assertEquals(modelName, loadedModel.name());
+ Assertions.assertEquals(comment, loadedModel.comment());
+ Assertions.assertEquals(properties, loadedModel.properties());
+
+ ModelChange change = ModelChange.setProperty("key2", "value2");
+ Model alteredModel = ops.alterModel(modelIdent, change);
+
+ // validate altered model
+ Assertions.assertEquals(modelName, alteredModel.name());
+ Assertions.assertEquals(comment, alteredModel.comment());
+ Assertions.assertEquals(newProperties, alteredModel.properties());
+ }
+
+ @Test
+ void testUpdateModelProperty() {
+ String schemaName = randomSchemaName();
+ createSchema(schemaName);
+
+ String modelName = "model";
+ String comment = "comment";
+ NameIdentifier modelIdent =
+ NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName,
modelName);
+ StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+ Map<String, String> properties =
+ StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of("key1",
"value1"));
+ Map<String, String> newProperties =
+ StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of("key1",
"value2"));
+
+ // validate registered model
+ Model registeredModel = ops.registerModel(modelIdent, comment, properties);
+ Assertions.assertEquals(modelName, registeredModel.name());
+ Assertions.assertEquals(comment, registeredModel.comment());
+ Assertions.assertEquals(properties, registeredModel.properties());
+
+ // validate loaded model
+ Model loadedModel = ops.getModel(modelIdent);
+ Assertions.assertEquals(modelName, loadedModel.name());
+ Assertions.assertEquals(comment, loadedModel.comment());
+ Assertions.assertEquals(properties, loadedModel.properties());
+
+ ModelChange change = ModelChange.setProperty("key1", "value2");
+ Model alteredModel = ops.alterModel(modelIdent, change);
+
+ // validate altered model
+ Assertions.assertEquals(modelName, alteredModel.name());
+ Assertions.assertEquals(comment, alteredModel.comment());
+ Assertions.assertEquals(newProperties, alteredModel.properties());
+ }
+
+ @Test
+ void testRemoveModelProperty() {
+ String schemaName = randomSchemaName();
+ createSchema(schemaName);
+
+ String modelName = "model";
+ String comment = "comment";
+ NameIdentifier modelIdent =
+ NameIdentifierUtil.ofModel(METALAKE_NAME, CATALOG_NAME, schemaName,
modelName);
+ StringIdentifier stringId = StringIdentifier.fromId(idGenerator.nextId());
+ Map<String, String> properties =
+ StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of("key1",
"value1"));
+ Map<String, String> newProperties =
+ StringIdentifier.newPropertiesWithId(stringId, ImmutableMap.of());
+
+ // validate registered model
+ Model registeredModel = ops.registerModel(modelIdent, comment, properties);
+ Assertions.assertEquals(modelName, registeredModel.name());
+ Assertions.assertEquals(comment, registeredModel.comment());
+ Assertions.assertEquals(properties, registeredModel.properties());
+
+ // validate loaded model
+ Model loadedModel = ops.getModel(modelIdent);
+ Assertions.assertEquals(modelName, loadedModel.name());
+ Assertions.assertEquals(comment, loadedModel.comment());
+ Assertions.assertEquals(properties, loadedModel.properties());
+
+ ModelChange change = ModelChange.removeProperty("key1");
+ Model alteredModel = ops.alterModel(modelIdent, change);
+
+ // validate altered model
+ Assertions.assertEquals(modelName, alteredModel.name());
+ Assertions.assertEquals(comment, alteredModel.comment());
+ Assertions.assertEquals(newProperties, alteredModel.properties());
+ }
+
private String randomSchemaName() {
return "schema_" + UUID.randomUUID().toString().replace("-", "");
}
diff --git
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java
index cbea8e1d66..0cabc1495c 100644
---
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java
+++
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java
@@ -360,6 +360,63 @@ public class ModelCatalogOperationsIT extends BaseIT {
.alterModel(NameIdentifier.of(schemaName, null), updateName));
}
+ @Test
+ void testRegisterAndAddModelProperty() {
+ String comment = "comment";
+ String modelName = RandomNameUtils.genRandomName("alter_name_model");
+ NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+ Map<String, String> properties = ImmutableMap.of("owner", "data-team",
"key1", "val1");
+ Map<String, String> newProperties =
+ ImmutableMap.of("owner", "data-team", "key1", "val1", "key2", "val2");
+
+ Model createdModel =
+ gravitinoCatalog.asModelCatalog().registerModel(modelIdent, comment,
properties);
+
+ ModelChange addProperty = ModelChange.setProperty("key2", "val2");
+ Model alteredModel =
gravitinoCatalog.asModelCatalog().alterModel(modelIdent, addProperty);
+
+ Assertions.assertEquals(modelName, alteredModel.name());
+ Assertions.assertNotEquals(createdModel.properties(),
alteredModel.properties());
+ Assertions.assertEquals(newProperties, alteredModel.properties());
+ Assertions.assertEquals(createdModel.comment(), alteredModel.comment());
+ }
+
+ @Test
+ void testRegisterAndUpdateModelProperty() {
+ String comment = "comment";
+ String modelName = RandomNameUtils.genRandomName("alter_name_model");
+ NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+ Map<String, String> properties = ImmutableMap.of("owner", "data-team",
"key1", "val1");
+ Map<String, String> newProperties = ImmutableMap.of("owner", "data-team",
"key1", "val3");
+
+ Model createdModel =
+ gravitinoCatalog.asModelCatalog().registerModel(modelIdent, comment,
properties);
+ ModelChange addProperty = ModelChange.setProperty("key1", "val3");
+ Model alteredModel =
gravitinoCatalog.asModelCatalog().alterModel(modelIdent, addProperty);
+
+ Assertions.assertEquals(modelName, alteredModel.name());
+ Assertions.assertEquals(newProperties, alteredModel.properties());
+ Assertions.assertEquals(createdModel.comment(), alteredModel.comment());
+ }
+
+ @Test
+ void testRegisterAndRemoveModelProperty() {
+ String comment = "comment";
+ String modelName = RandomNameUtils.genRandomName("alter_name_model");
+ NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+ Map<String, String> properties = ImmutableMap.of("owner", "data-team",
"key1", "val1");
+ Map<String, String> newProperties = ImmutableMap.of("owner", "data-team");
+
+ Model createdModel =
+ gravitinoCatalog.asModelCatalog().registerModel(modelIdent, comment,
properties);
+ ModelChange addProperty = ModelChange.removeProperty("key1");
+ Model alteredModel =
gravitinoCatalog.asModelCatalog().alterModel(modelIdent, addProperty);
+
+ Assertions.assertEquals(modelName, alteredModel.name());
+ Assertions.assertEquals(newProperties, alteredModel.properties());
+ Assertions.assertEquals(createdModel.comment(), alteredModel.comment());
+ }
+
private void createMetalake() {
GravitinoMetalake[] gravitinoMetalakes = client.listMetalakes();
Assertions.assertEquals(0, gravitinoMetalakes.length);
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/ModelCommandHandler.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/ModelCommandHandler.java
index 5a417125bd..6bbe796dba 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/ModelCommandHandler.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/ModelCommandHandler.java
@@ -111,6 +111,14 @@ public class ModelCommandHandler extends CommandHandler {
handleUpdateCommand();
return true;
+ case CommandActions.SET:
+ handleSetCommand();
+ return true;
+
+ case CommandActions.REMOVE:
+ handleRemoveCommand();
+ return true;
+
default:
return false;
}
@@ -178,4 +186,22 @@ public class ModelCommandHandler extends CommandHandler {
private void handleListCommand() {
gravitinoCommandLine.newListModel(context, metalake, catalog,
schema).validate().handle();
}
+
+ /** Handles the "SET" command. */
+ private void handleSetCommand() {
+ String property = line.getOptionValue(GravitinoOptions.PROPERTY);
+ String value = line.getOptionValue(GravitinoOptions.VALUE);
+ gravitinoCommandLine
+ .newSetModelProperty(context, metalake, catalog, schema, model,
property, value)
+ .validate()
+ .handle();
+ }
+
+ private void handleRemoveCommand() {
+ String property = line.getOptionValue(GravitinoOptions.PROPERTY);
+ gravitinoCommandLine
+ .newRemoveModelProperty(context, metalake, catalog, schema, model,
property)
+ .validate()
+ .handle();
+ }
}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
index 5ee96da889..9bf17aacd5 100644
---
a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
@@ -89,6 +89,7 @@ import org.apache.gravitino.cli.commands.RemoveAllTags;
import org.apache.gravitino.cli.commands.RemoveCatalogProperty;
import org.apache.gravitino.cli.commands.RemoveFilesetProperty;
import org.apache.gravitino.cli.commands.RemoveMetalakeProperty;
+import org.apache.gravitino.cli.commands.RemoveModelProperty;
import org.apache.gravitino.cli.commands.RemoveRoleFromGroup;
import org.apache.gravitino.cli.commands.RemoveRoleFromUser;
import org.apache.gravitino.cli.commands.RemoveSchemaProperty;
@@ -105,6 +106,7 @@ import org.apache.gravitino.cli.commands.ServerVersion;
import org.apache.gravitino.cli.commands.SetCatalogProperty;
import org.apache.gravitino.cli.commands.SetFilesetProperty;
import org.apache.gravitino.cli.commands.SetMetalakeProperty;
+import org.apache.gravitino.cli.commands.SetModelProperty;
import org.apache.gravitino.cli.commands.SetOwner;
import org.apache.gravitino.cli.commands.SetSchemaProperty;
import org.apache.gravitino.cli.commands.SetTableProperty;
@@ -883,6 +885,27 @@ public class TestableCommandLine {
return new UpdateModelName(context, metalake, catalog, schema, model,
rename);
}
+ protected SetModelProperty newSetModelProperty(
+ CommandContext context,
+ String metalake,
+ String catalog,
+ String schema,
+ String model,
+ String property,
+ String value) {
+ return new SetModelProperty(context, metalake, catalog, schema, model,
property, value);
+ }
+
+ protected RemoveModelProperty newRemoveModelProperty(
+ CommandContext context,
+ String metalake,
+ String catalog,
+ String schema,
+ String model,
+ String property) {
+ return new RemoveModelProperty(context, metalake, catalog, schema, model,
property);
+ }
+
protected DeleteModel newDeleteModel(
CommandContext context, String metalake, String catalog, String schema,
String model) {
return new DeleteModel(context, metalake, catalog, schema, model);
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RemoveModelProperty.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RemoveModelProperty.java
new file mode 100644
index 0000000000..ae208a68b5
--- /dev/null
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RemoveModelProperty.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.cli.commands;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.cli.CommandContext;
+import org.apache.gravitino.cli.ErrorMessages;
+import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.exceptions.NoSuchCatalogException;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchModelException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.model.ModelChange;
+
+/** Removes a property of a model. */
+public class RemoveModelProperty extends Command {
+ protected final String metalake;
+ protected final String catalog;
+ protected final String schema;
+ protected final String model;
+ protected final String property;
+
+ /**
+ * Constructs a new {@link RemoveModelProperty} instance.
+ *
+ * @param context The command context
+ * @param metalake The name of the metalake
+ * @param catalog The name of the catalog
+ * @param schema The name of the schema
+ * @param model The name of the model
+ * @param property The name of the property to remove
+ */
+ public RemoveModelProperty(
+ CommandContext context,
+ String metalake,
+ String catalog,
+ String schema,
+ String model,
+ String property) {
+ super(context);
+ this.metalake = metalake;
+ this.catalog = catalog;
+ this.schema = schema;
+ this.model = model;
+ this.property = property;
+ }
+
+ /** Removes a property of a model. */
+ @Override
+ public void handle() {
+ try {
+ NameIdentifier name = NameIdentifier.of(schema, model);
+ GravitinoClient client = buildClient(metalake);
+ ModelChange change = ModelChange.removeProperty(property);
+ client.loadCatalog(catalog).asModelCatalog().alterModel(name, change);
+ } catch (NoSuchMetalakeException err) {
+ exitWithError(ErrorMessages.UNKNOWN_METALAKE);
+ } catch (NoSuchCatalogException err) {
+ exitWithError(ErrorMessages.UNKNOWN_CATALOG);
+ } catch (NoSuchSchemaException err) {
+ exitWithError(ErrorMessages.UNKNOWN_SCHEMA);
+ } catch (NoSuchModelException err) {
+ exitWithError(ErrorMessages.UNKNOWN_MODEL);
+ } catch (Exception exp) {
+ exitWithError(exp.getMessage());
+ }
+
+ printInformation(property + " property removed.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Command validate() {
+ validateProperty(property);
+ return super.validate();
+ }
+}
diff --git
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
new file mode 100644
index 0000000000..451e1f71c3
--- /dev/null
+++
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/SetModelProperty.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.cli.commands;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.cli.CommandContext;
+import org.apache.gravitino.cli.ErrorMessages;
+import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.exceptions.NoSuchCatalogException;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchModelException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.model.ModelChange;
+
+/** Set a property of a model. */
+public class SetModelProperty extends Command {
+
+ private final String metalake;
+ private final String catalog;
+ private final String schema;
+ private final String model;
+ private final String property;
+ private final String value;
+
+ /**
+ * Construct a new {@link SetModelProperty} instance.
+ *
+ * @param context The command context.
+ * @param metalake The name of the metalake.
+ * @param catalog The name of the catalog.
+ * @param schema The name of the schema.
+ * @param model The name of the model.
+ * @param property The name of the property to set.
+ * @param value The value to set the property to.
+ */
+ public SetModelProperty(
+ CommandContext context,
+ String metalake,
+ String catalog,
+ String schema,
+ String model,
+ String property,
+ String value) {
+ super(context);
+ this.metalake = metalake;
+ this.catalog = catalog;
+ this.schema = schema;
+ this.model = model;
+ this.property = property;
+ this.value = value;
+ }
+
+ /** Set a property of a model. */
+ @Override
+ public void handle() {
+ try {
+ NameIdentifier name = NameIdentifier.of(schema, model);
+ GravitinoClient client = buildClient(metalake);
+ ModelChange change = ModelChange.setProperty(property, value);
+ client.loadCatalog(catalog).asModelCatalog().alterModel(name, change);
+ } catch (NoSuchMetalakeException err) {
+ exitWithError(ErrorMessages.UNKNOWN_METALAKE);
+ } catch (NoSuchCatalogException err) {
+ exitWithError(ErrorMessages.UNKNOWN_CATALOG);
+ } catch (NoSuchSchemaException err) {
+ exitWithError(ErrorMessages.UNKNOWN_SCHEMA);
+ } catch (NoSuchModelException err) {
+ exitWithError(ErrorMessages.UNKNOWN_TABLE);
+ } catch (Exception exp) {
+ exitWithError(exp.getMessage());
+ }
+
+ printInformation(model + " property set.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Command validate() {
+ validatePropertyAndValue(property, value);
+ return super.validate();
+ }
+}
diff --git
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommands.java
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommands.java
index 1035418cb2..9f03769093 100644
--- a/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommands.java
+++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommands.java
@@ -46,6 +46,8 @@ import org.apache.gravitino.cli.commands.ListModel;
import org.apache.gravitino.cli.commands.ModelAudit;
import org.apache.gravitino.cli.commands.ModelDetails;
import org.apache.gravitino.cli.commands.RegisterModel;
+import org.apache.gravitino.cli.commands.RemoveModelProperty;
+import org.apache.gravitino.cli.commands.SetModelProperty;
import org.apache.gravitino.cli.commands.UpdateModelName;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -598,4 +600,65 @@ public class TestModelCommands {
commandLine.handleCommandLine();
verify(mockUpdate).handle();
}
+
+ @Test
+ void testSetModelProperty() {
+ SetModelProperty mockSetProperty = mock(SetModelProperty.class);
+
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+ when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true);
+
when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema.model");
+
when(mockCommandLine.hasOption(GravitinoOptions.PROPERTY)).thenReturn(true);
+
when(mockCommandLine.getOptionValue(GravitinoOptions.PROPERTY)).thenReturn("key");
+ when(mockCommandLine.hasOption(GravitinoOptions.VALUE)).thenReturn(true);
+
when(mockCommandLine.getOptionValue(GravitinoOptions.VALUE)).thenReturn("value");
+
+ GravitinoCommandLine commandLine =
+ spy(
+ new GravitinoCommandLine(
+ mockCommandLine, mockOptions, CommandEntities.MODEL,
CommandActions.SET));
+
+ doReturn(mockSetProperty)
+ .when(commandLine)
+ .newSetModelProperty(
+ any(CommandContext.class),
+ eq("metalake_demo"),
+ eq("catalog"),
+ eq("schema"),
+ eq("model"),
+ eq("key"),
+ eq("value"));
+ doReturn(mockSetProperty).when(mockSetProperty).validate();
+ commandLine.handleCommandLine();
+ verify(mockSetProperty).handle();
+ }
+
+ @Test
+ void testRemoveModelProperty() {
+ RemoveModelProperty mockRemoveProperty = mock(RemoveModelProperty.class);
+
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+ when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true);
+
when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema.model");
+
when(mockCommandLine.hasOption(GravitinoOptions.PROPERTY)).thenReturn(true);
+
when(mockCommandLine.getOptionValue(GravitinoOptions.PROPERTY)).thenReturn("key");
+
+ GravitinoCommandLine commandLine =
+ spy(
+ new GravitinoCommandLine(
+ mockCommandLine, mockOptions, CommandEntities.MODEL,
CommandActions.REMOVE));
+
+ doReturn(mockRemoveProperty)
+ .when(commandLine)
+ .newRemoveModelProperty(
+ any(CommandContext.class),
+ eq("metalake_demo"),
+ eq("catalog"),
+ eq("schema"),
+ eq("model"),
+ eq("key"));
+ doReturn(mockRemoveProperty).when(mockRemoveProperty).validate();
+ commandLine.handleCommandLine();
+ verify(mockRemoveProperty).handle();
+ }
}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
index 8304750385..e76b1a0b1d 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
@@ -363,6 +363,15 @@ class DTOConverters {
return new ModelUpdateRequest.RenameModelRequest(
((ModelChange.RenameModel) change).newName());
+ } else if (change instanceof ModelChange.RemoveProperty) {
+ return new ModelUpdateRequest.RemoveModelPropertyRequest(
+ ((ModelChange.RemoveProperty) change).property());
+
+ } else if (change instanceof ModelChange.SetProperty) {
+ return new ModelUpdateRequest.SetModelPropertyRequest(
+ ((ModelChange.SetProperty) change).property(),
+ ((ModelChange.SetProperty) change).value());
+
} else {
throw new IllegalArgumentException(
"Unknown model change type: " + change.getClass().getSimpleName());
diff --git a/clients/client-python/gravitino/api/model_change.py
b/clients/client-python/gravitino/api/model_change.py
index f2557cd2cc..99737d7823 100644
--- a/clients/client-python/gravitino/api/model_change.py
+++ b/clients/client-python/gravitino/api/model_change.py
@@ -34,6 +34,27 @@ class ModelChange(ABC):
"""
return ModelChange.RenameModel(new_name)
+ @staticmethod
+ def set_property(pro, value):
+ """Creates a new model change to set the property and value pairs for
the model.
+ Args:
+ property: The name of the property to be set.
+ value: The value of the property to be set.
+ Returns:
+ The model change.
+ """
+ return ModelChange.SetProperty(pro, value)
+
+ @staticmethod
+ def remove_property(pro):
+ """Creates a new model change to remove the property and value pairs
for the model.
+ Args:
+ property: The name of the property to be removed.
+ Returns:
+ The model change.
+ """
+ return ModelChange.RemoveProperty(pro)
+
class RenameModel:
"""A model change to rename the model."""
@@ -78,3 +99,97 @@ class ModelChange(ABC):
A string summary of this renaming operation.
"""
return f"RENAMEMODEL {self.new_name()}"
+
+ class SetProperty:
+ """
+ A model change to set the property and value pairs for the model.
+ """
+
+ def __init__(self, pro, value):
+ self._property = pro
+ self._value = value
+
+ def property(self):
+ """Retrieves the name of the property to be set.
+ Returns:
+ The name of the property.
+ """
+ return self._property
+
+ def value(self):
+ """Retrieves the value of the property to be set.
+ Returns:
+ The value of the property.
+ """
+ return self._value
+
+ def __eq__(self, other) -> bool:
+ """Compares this SetProperty instance with another object for
equality. Two instances are
+ considered equal if they designate the same property and value for
the model.
+ Args:
+ other: The object to compare with this instance.
+ Returns:
+ true if the given object represents an identical model
property setting operation; false otherwise.
+ """
+ if not isinstance(other, ModelChange.SetProperty):
+ return False
+ return self.property() == other.property() and self.value() ==
other.value()
+
+ def __hash__(self):
+ """Generates a hash code for this SetProperty instance. The hash
code is primarily based on
+ the property and value for the model.
+ Returns:
+ A hash code value for this property setting operation.
+ """
+ return hash(self.property(), self.value())
+
+ def __str__(self):
+ """Provides a string representation of the SetProperty instance.
This string includes the
+ class name followed by the property and value of the model.
+ Returns:
+ A string summary of this property setting operation.
+ """
+ return f"SETPROPERTY {self.property()}={self.value()}"
+
+ class RemoveProperty:
+ """
+ A model change to remove the property and value pairs for the model.
+ """
+
+ def __init__(self, pro):
+ self._property = pro
+
+ def property(self):
+ """Retrieves the name of the property to be removed.
+ Returns:
+ The name of the property.
+ """
+ return self._property
+
+ def __eq__(self, other) -> bool:
+ """Compares this RemoveProperty instance with another object for
equality. Two instances are
+ considered equal if they designate the same property for the model.
+ Args:
+ other: The object to compare with this instance.
+ Returns:
+ true if the given object represents an identical model
property removal operation; false otherwise.
+ """
+ if not isinstance(other, ModelChange.RemoveProperty):
+ return False
+ return self.property() == other.property()
+
+ def __hash__(self):
+ """Generates a hash code for this RemoveProperty instance. The
hash code is primarily based on
+ the property for the model.
+ Returns:
+ A hash code value for this property removal operation.
+ """
+ return hash(self.property())
+
+ def __str__(self):
+ """Provides a string representation of the RemoveProperty
instance. This string includes the
+ class name followed by the property of the model.
+ Returns:
+ A string summary of this property removal operation.
+ """
+ return f"REMOVEPROPERTY {self.property()}"
diff --git a/clients/client-python/gravitino/client/generic_model_catalog.py
b/clients/client-python/gravitino/client/generic_model_catalog.py
index 1dbf96db56..79cfcdb2e9 100644
--- a/clients/client-python/gravitino/client/generic_model_catalog.py
+++ b/clients/client-python/gravitino/client/generic_model_catalog.py
@@ -429,6 +429,14 @@ class GenericModelCatalog(BaseSchemaCatalog):
if isinstance(change, ModelChange.RenameModel):
return ModelUpdateRequest.UpdateModelNameRequest(change.new_name())
+ if isinstance(change, ModelChange.SetProperty):
+ return ModelUpdateRequest.ModelSetPropertyRequest(
+ change.property(), change.value()
+ )
+
+ if isinstance(change, ModelChange.RemoveProperty):
+ return
ModelUpdateRequest.ModelRemovePropertyRequest(change.property())
+
raise ValueError(f"Unknown change type: {type(change).__name__}")
def _check_model_namespace(self, namespace: Namespace):
diff --git
a/clients/client-python/gravitino/dto/requests/model_update_request.py
b/clients/client-python/gravitino/dto/requests/model_update_request.py
index 6310a47be0..5769f2015f 100644
--- a/clients/client-python/gravitino/dto/requests/model_update_request.py
+++ b/clients/client-python/gravitino/dto/requests/model_update_request.py
@@ -59,3 +59,41 @@ class ModelUpdateRequest:
def model_change(self) -> ModelChange:
return ModelChange.rename(self._new_name)
+
+ @dataclass
+ class ModelSetPropertyRequest(ModelUpdateRequestBase):
+ """Request to set model property"""
+
+ _property: Optional[str] =
field(metadata=config(field_name="property"))
+ _value: Optional[str] = field(metadata=config(field_name="value"))
+
+ def __init__(self, pro: str, value: str):
+ super().__init__("setProperty")
+ self._property = pro
+ self._value = value
+
+ def validate(self):
+ if not self._property:
+ raise ValueError('"property" field is required')
+ if not self._value:
+ raise ValueError('"value" field is required')
+
+ def model_change(self) -> ModelChange:
+ return ModelChange.set_property(self._property, self._value)
+
+ @dataclass
+ class ModelRemovePropertyRequest(ModelUpdateRequestBase):
+ """Request to remove model property"""
+
+ _property: Optional[str] =
field(metadata=config(field_name="property"))
+
+ def __init__(self, pro: str):
+ super().__init__("removeProperty")
+ self._property = pro
+
+ def validate(self):
+ if not self._property:
+ raise ValueError('"property" field is required')
+
+ def model_change(self) -> ModelChange:
+ return ModelChange.remove_property(self._property)
diff --git a/clients/client-python/tests/integration/test_model_catalog.py
b/clients/client-python/tests/integration/test_model_catalog.py
index 9a9f0e2743..88ef813bd5 100644
--- a/clients/client-python/tests/integration/test_model_catalog.py
+++ b/clients/client-python/tests/integration/test_model_catalog.py
@@ -227,6 +227,59 @@ class TestModelCatalog(IntegrationTestEnv):
self.assertEqual(0, renamed_model.latest_version())
self.assertEqual(properties, renamed_model.properties())
+ def test_register_alter_model_with_set_property(self):
+ model_name = f"model_it_model{str(randint(0, 1000))}"
+ model_ident = NameIdentifier.of(self._schema_name, model_name)
+ comment = "comment"
+ properties = {"k1": "v1", "k2": "v2"}
+ self._catalog.as_model_catalog().register_model(
+ model_ident, comment, properties
+ )
+ origin_model = self._catalog.as_model_catalog().get_model(model_ident)
+
+ self.assertEqual(origin_model.name(), model_name)
+ self.assertEqual(origin_model.comment(), comment)
+ self.assertEqual(origin_model.latest_version(), 0)
+ self.assertEqual(origin_model.properties(), properties)
+
+ changes = [
+ ModelChange.set_property("k1", "v11"),
+ ModelChange.set_property("k3", "v3"),
+ ]
+
+ self._catalog.as_model_catalog().alter_model(model_ident, *changes)
+ update_property_model =
self._catalog.as_model_catalog().get_model(model_ident)
+
+ self.assertEqual(update_property_model.name(), model_name)
+ self.assertEqual(update_property_model.comment(), comment)
+ self.assertEqual(update_property_model.latest_version(), 0)
+ self.assertEqual(
+ update_property_model.properties(), {"k1": "v11", "k2": "v2",
"k3": "v3"}
+ )
+
+ def test_register_alter_model_with_remove_property(self):
+ model_name = f"model_it_model{str(randint(0, 1000))}"
+ model_ident = NameIdentifier.of(self._schema_name, model_name)
+ comment = "comment"
+ properties = {"k1": "v1", "k2": "v2"}
+
+ self._catalog.as_model_catalog().register_model(
+ model_ident, comment, properties
+ )
+ origin_model = self._catalog.as_model_catalog().get_model(model_ident)
+ self.assertEqual(origin_model.name(), model_name)
+ self.assertEqual(origin_model.comment(), comment)
+ self.assertEqual(origin_model.latest_version(), 0)
+ self.assertEqual(origin_model.properties(), properties)
+
+ changes = [ModelChange.remove_property("k1")]
+ self._catalog.as_model_catalog().alter_model(model_ident, *changes)
+ update_property_model =
self._catalog.as_model_catalog().get_model(model_ident)
+ self.assertEqual(update_property_model.name(), model_name)
+ self.assertEqual(update_property_model.comment(), comment)
+ self.assertEqual(update_property_model.latest_version(), 0)
+ self.assertEqual(update_property_model.properties(), {"k2": "v2"})
+
def test_link_get_model_version(self):
model_name = "model_it_model" + str(randint(0, 1000))
model_ident = NameIdentifier.of(self._schema_name, model_name)
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java
index 9b36a25d2c..ad06d75597 100644
---
a/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/ModelUpdateRequest.java
@@ -24,8 +24,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.common.base.Preconditions;
+import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
+import lombok.NoArgsConstructor;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.model.ModelChange;
@@ -35,7 +37,11 @@ import org.apache.gravitino.rest.RESTRequest;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
- @JsonSubTypes.Type(value = ModelUpdateRequest.RenameModelRequest.class, name
= "rename")
+ @JsonSubTypes.Type(value = ModelUpdateRequest.RenameModelRequest.class, name
= "rename"),
+ @JsonSubTypes.Type(
+ value = ModelUpdateRequest.RemoveModelPropertyRequest.class,
+ name = "removeProperty"),
+ @JsonSubTypes.Type(value = ModelUpdateRequest.SetModelPropertyRequest.class,
name = "setProperty")
})
public interface ModelUpdateRequest extends RESTRequest {
@@ -90,4 +96,65 @@ public interface ModelUpdateRequest extends RESTRequest {
StringUtils.isNotBlank(newName), "\"newName\" field is required and
cannot be empty");
}
}
+
+ /** The model update request for set property of model. */
+ @EqualsAndHashCode
+ @AllArgsConstructor
+ @NoArgsConstructor(force = true)
+ @ToString
+ @Getter
+ class SetModelPropertyRequest implements ModelUpdateRequest {
+ @JsonProperty("property")
+ private final String property;
+
+ @JsonProperty("value")
+ private final String value;
+
+ /** {@inheritDoc} */
+ @Override
+ public ModelChange modelChange() {
+ return ModelChange.setProperty(property, value);
+ }
+
+ /**
+ * Validates the request, i.e., checks if the property and value are not
empty and not null.
+ *
+ * @throws IllegalArgumentException If the request is invalid, this
exception is thrown.
+ */
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" field is required
and cannot be empty");
+ Preconditions.checkArgument(value != null, "\"value\" field is required
and cannot be null");
+ }
+ }
+
+ /** The model update request for remove property from model. */
+ @EqualsAndHashCode
+ @AllArgsConstructor
+ @NoArgsConstructor(force = true)
+ @ToString
+ @Getter
+ class RemoveModelPropertyRequest implements ModelUpdateRequest {
+
+ @JsonProperty("property")
+ private final String property;
+
+ /** {@inheritDoc} */
+ @Override
+ public ModelChange modelChange() {
+ return ModelChange.removeProperty(property);
+ }
+
+ /**
+ * Validates the request, i.e., checks if the property is not empty.
+ *
+ * @throws IllegalArgumentException If the request is invalid, this
exception is thrown.
+ */
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" field is required
and cannot be empty");
+ }
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
index dd1a1d48c3..24d48c7fed 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
@@ -278,6 +278,91 @@ public class TestModelOperationDispatcher extends
TestOperationDispatcher {
Assertions.assertEquals(model.properties(), alteredModel.properties());
}
+ @Test
+ void testAddModelProperty() {
+ String schemaName = "schema";
+ String modelName = "test_update_model_property";
+ String modelComment = "model which tests update property";
+ NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog,
schemaName);
+ schemaOperationDispatcher.createSchema(
+ schemaIdent, "schema comment", ImmutableMap.of("k1", "v1", "k2",
"v2"));
+
+ NameIdentifier modelIdent =
+ NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ Model model = modelOperationDispatcher.registerModel(modelIdent,
modelComment, props);
+
+ // validate registered model
+ Assertions.assertEquals(modelName, model.name());
+ Assertions.assertEquals(modelComment, model.comment());
+ Assertions.assertEquals(props, model.properties());
+
+ ModelChange[] addProperty = new ModelChange[]
{ModelChange.setProperty("k3", "v3")};
+ Model alteredModel = modelOperationDispatcher.alterModel(modelIdent,
addProperty);
+
+ // validate updated model
+ Assertions.assertEquals(modelName, alteredModel.name());
+ Assertions.assertEquals(modelComment, alteredModel.comment());
+ Assertions.assertEquals(
+ ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"),
alteredModel.properties());
+ }
+
+ @Test
+ void testUpdateModelProperty() {
+ String schemaName = "test_update_model_property_schema";
+ String modelName = "test_update_model_property";
+ String modelComment = "model which tests update property";
+ NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog,
schemaName);
+ schemaOperationDispatcher.createSchema(
+ schemaIdent, "schema comment", ImmutableMap.of("k1", "v1", "k2",
"v2"));
+
+ NameIdentifier modelIdent =
+ NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ Model model = modelOperationDispatcher.registerModel(modelIdent,
modelComment, props);
+
+ // validate registered model
+ Assertions.assertEquals(modelName, model.name());
+ Assertions.assertEquals(modelComment, model.comment());
+ Assertions.assertEquals(props, model.properties());
+
+ ModelChange[] updateProperty = new ModelChange[]
{ModelChange.setProperty("k1", "v3")};
+ Model alteredModel = modelOperationDispatcher.alterModel(modelIdent,
updateProperty);
+
+ // validate updated model
+ Assertions.assertEquals(modelName, alteredModel.name());
+ Assertions.assertEquals(modelComment, alteredModel.comment());
+ Assertions.assertEquals(ImmutableMap.of("k1", "v3", "k2", "v2"),
alteredModel.properties());
+ }
+
+ @Test
+ void testRemoveModelProperty() {
+ String schemaName = "test_remove_model_property_schema";
+ String modelName = "test_update_model_property";
+ String modelComment = "model which tests update property";
+ NameIdentifier schemaIdent = NameIdentifier.of(metalake, catalog,
schemaName);
+ schemaOperationDispatcher.createSchema(
+ schemaIdent, "schema comment", ImmutableMap.of("k1", "v1", "k2",
"v2"));
+
+ NameIdentifier modelIdent =
+ NameIdentifierUtil.ofModel(metalake, catalog, schemaName, modelName);
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ Model model = modelOperationDispatcher.registerModel(modelIdent,
modelComment, props);
+
+ // validate registered model
+ Assertions.assertEquals(modelName, model.name());
+ Assertions.assertEquals(modelComment, model.comment());
+ Assertions.assertEquals(props, model.properties());
+
+ ModelChange[] removeProperty = new ModelChange[]
{ModelChange.removeProperty("k1")};
+ Model alteredModel = modelOperationDispatcher.alterModel(modelIdent,
removeProperty);
+
+ // validate updated model
+ Assertions.assertEquals(modelName, alteredModel.name());
+ Assertions.assertEquals(modelComment, alteredModel.comment());
+ Assertions.assertEquals(ImmutableMap.of("k2", "v2"),
alteredModel.properties());
+ }
+
private String randomSchemaName() {
return "schema_" + UUID.randomUUID().toString().replace("-", "");
}
diff --git
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
index c0a3f87588..17d3f5c217 100644
---
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
+++
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.connector;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.io.File;
import java.io.IOException;
@@ -913,7 +914,9 @@ public class TestCatalogOperations
TestModel model = models.get(ident);
Map<String, String> newProps =
- model.properties() != null ? Maps.newHashMap(model.properties()) :
Maps.newHashMap();
+ model.properties() == null ? ImmutableMap.of() : new
HashMap<>(model.properties());
+ String newComment = model.comment();
+ int newLatestVersion = model.latestVersion();
NameIdentifier newIdent = ident;
for (ModelChange change : changes) {
@@ -923,15 +926,21 @@ public class TestCatalogOperations
if (models.containsKey(newIdent)) {
throw new ModelAlreadyExistsException("Model %s already exists",
ident);
}
+ } else if (change instanceof ModelChange.RemoveProperty) {
+ ModelChange.RemoveProperty removeProperty =
(ModelChange.RemoveProperty) change;
+ newProps.remove(removeProperty.property());
+ } else if (change instanceof ModelChange.SetProperty) {
+ ModelChange.SetProperty setProperty = (ModelChange.SetProperty) change;
+ newProps.put(setProperty.property(), setProperty.value());
}
}
TestModel updatedModel =
TestModel.builder()
.withName(newIdent.name())
- .withComment(model.comment())
+ .withComment(newComment)
.withProperties(new HashMap<>(newProps))
.withAuditInfo(updatedAuditInfo)
- .withLatestVersion(model.latestVersion())
+ .withLatestVersion(newLatestVersion)
.build();
models.put(ident, updatedModel);
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
index 3d80600320..c3040a5acc 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestModelOperations.java
@@ -797,7 +797,7 @@ public class TestModelOperations extends JerseyTest {
}
@Test
- public void testAlterModel() {
+ public void testRenameModel() {
String oldName = "model1";
String newName = "newModel1";
String comment = "comment";
@@ -860,6 +860,113 @@ public class TestModelOperations extends JerseyTest {
Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp1.getCode());
}
+ @Test
+ void testAddModelProperty() {
+ String modelName = "model1";
+ String comment = "comment";
+
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, modelName);
+ Model updatedModel =
+ mockModel(
+ modelName,
+ comment,
+ 0,
+ ImmutableMap.<String, String>builder()
+ .putAll(properties)
+ .put("key2", "value2")
+ .build());
+
+ // Mock alterModel to return updated model
+ when(modelDispatcher.alterModel(
+ modelId, new ModelChange[] {ModelChange.setProperty("key2",
"value2")}))
+ .thenReturn(updatedModel);
+
+ // Build update request
+ ModelUpdatesRequest req =
+ new ModelUpdatesRequest(
+ Collections.singletonList(
+ new ModelUpdateRequest.SetModelPropertyRequest("key2",
"value2")));
+
+ Response resp =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ ModelResponse modelResp = resp.readEntity(ModelResponse.class);
+ Assertions.assertEquals(comment, modelResp.getModel().comment());
+ Assertions.assertEquals(modelName, modelResp.getModel().name());
+ Assertions.assertEquals(2, modelResp.getModel().properties().size());
+ Assertions.assertEquals("value2",
modelResp.getModel().properties().get("key2"));
+ }
+
+ @Test
+ void testUpdateModelProperty() {
+ String modelName = "model1";
+ String comment = "comment";
+
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, modelName);
+ Model updatedModel = mockModel(modelName, comment, 0,
ImmutableMap.of("key1", "updatedValue1"));
+
+ // Mock alterModel to return updated model
+ when(modelDispatcher.alterModel(modelId, ModelChange.setProperty("key1",
"updatedValue1")))
+ .thenReturn(updatedModel);
+
+ // Build update request
+ ModelUpdatesRequest req =
+ new ModelUpdatesRequest(
+ Collections.singletonList(
+ new ModelUpdateRequest.SetModelPropertyRequest("key1",
"updatedValue1")));
+
+ Response resp =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ ModelResponse modelResp = resp.readEntity(ModelResponse.class);
+ Assertions.assertEquals(comment, modelResp.getModel().comment());
+ Assertions.assertEquals(modelName, modelResp.getModel().name());
+ Assertions.assertEquals(1, modelResp.getModel().properties().size());
+ Assertions.assertEquals("updatedValue1",
modelResp.getModel().properties().get("key1"));
+ }
+
+ @Test
+ void testRemoveModelProperty() {
+ String modelName = "model1";
+ String comment = "comment";
+
+ NameIdentifier modelId = NameIdentifierUtil.ofModel(metalake, catalog,
schema, modelName);
+ Model updatedModel = mockModel(modelName, comment, 0, ImmutableMap.of());
+
+ // Mock alterModel to return updated model
+ when(modelDispatcher.alterModel(modelId,
ModelChange.removeProperty("key1")))
+ .thenReturn(updatedModel);
+
+ // Build update request
+ ModelUpdatesRequest req =
+ new ModelUpdatesRequest(
+ Collections.singletonList(new
ModelUpdateRequest.RemoveModelPropertyRequest("key1")));
+
+ Response resp =
+ target(modelPath())
+ .path("model1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ ModelResponse modelResp = resp.readEntity(ModelResponse.class);
+ Assertions.assertEquals(comment, modelResp.getModel().comment());
+ Assertions.assertEquals(modelName, modelResp.getModel().name());
+ Assertions.assertEquals(0, modelResp.getModel().properties().size());
+
Assertions.assertFalse(modelResp.getModel().properties().containsKey("key1"));
+ }
+
private String modelPath() {
return "/metalakes/" + metalake + "/catalogs/" + catalog + "/schemas/" +
schema + "/models";
}
@@ -874,6 +981,17 @@ public class TestModelOperations extends JerseyTest {
return mockModel;
}
+ private Model mockModel(
+ String modelName, String comment, int latestVersion, Map<String, String>
properties) {
+ Model mockModel = mock(Model.class);
+ when(mockModel.name()).thenReturn(modelName);
+ when(mockModel.comment()).thenReturn(comment);
+ when(mockModel.latestVersion()).thenReturn(latestVersion);
+ when(mockModel.properties()).thenReturn(properties);
+ when(mockModel.auditInfo()).thenReturn(testAuditInfo);
+ return mockModel;
+ }
+
private ModelVersion mockModelVersion(int version, String uri, String[]
aliases, String comment) {
ModelVersion mockModelVersion = mock(ModelVersion.class);
when(mockModelVersion.version()).thenReturn(version);