This is an automated email from the ASF dual-hosted git repository.

fanng 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 77136b692 [#5601] feat(core): Add model entity and model dispatcher in 
Gravitino (#5662)
77136b692 is described below

commit 77136b692fc73f2500552305f9fe775e3e008ef3
Author: Jerry Shao <[email protected]>
AuthorDate: Tue Nov 26 21:39:07 2024 +0800

    [#5601] feat(core): Add model entity and model dispatcher in Gravitino 
(#5662)
    
    ### What changes were proposed in this pull request?
    
    1. Define and add the model namespace, name identifier, entity definition 
in Gravitino.
    2. Add the basic dispatcher framework in Gravitino.
    
    ### Why are the changes needed?
    
    This is the second PR to support model management in Gravitino.
    
    Fix: #5601
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Add UTs to verify part of the code.
---
 .../src/main/java/org/apache/gravitino/Entity.java |   5 +
 .../java/org/apache/gravitino/GravitinoEnv.java    |  21 +++
 .../gravitino/catalog/CapabilityHelpers.java       |   3 +-
 .../apache/gravitino/catalog/ModelDispatcher.java  |  23 +++
 .../catalog/ModelNormalizeDispatcher.java          | 167 ++++++++++++++++++
 .../catalog/ModelOperationDispatcher.java          | 110 ++++++++++++
 .../gravitino/connector/capability/Capability.java |   3 +-
 .../org/apache/gravitino/meta/ModelEntity.java     | 189 +++++++++++++++++++++
 .../apache/gravitino/meta/ModelVersionEntity.java  | 174 +++++++++++++++++++
 .../gravitino/storage/relational/JDBCBackend.java  |   5 +
 .../apache/gravitino/utils/MetadataObjectUtil.java |   7 +
 .../apache/gravitino/utils/NameIdentifierUtil.java |  58 +++++++
 .../org/apache/gravitino/utils/NamespaceUtil.java  |  52 ++++++
 .../org/apache/gravitino/meta/TestModelEntity.java | 116 +++++++++++++
 .../gravitino/meta/TestModelVersionEntity.java     | 107 ++++++++++++
 .../gravitino/utils/TestMetadataObjectUtil.java    |  10 ++
 .../gravitino/utils/TestNameIdentifierUtil.java    |  28 +++
 .../apache/gravitino/utils/TestNamespaceUtil.java  |  17 ++
 18 files changed, 1093 insertions(+), 2 deletions(-)

diff --git a/core/src/main/java/org/apache/gravitino/Entity.java 
b/core/src/main/java/org/apache/gravitino/Entity.java
index 96ccc40ae..102be5036 100644
--- a/core/src/main/java/org/apache/gravitino/Entity.java
+++ b/core/src/main/java/org/apache/gravitino/Entity.java
@@ -69,6 +69,8 @@ public interface Entity extends Serializable {
     GROUP("gr", 8),
     ROLE("ro", 9),
     TAG("ta", 10),
+    MODEL("mo", 11),
+    MODEL_VERSION("mv", 12),
 
     AUDIT("au", 65534);
 
@@ -109,12 +111,15 @@ public interface Entity extends Serializable {
         case TABLE:
         case FILESET:
         case TOPIC:
+        case MODEL:
         case USER:
         case GROUP:
         case ROLE:
           return ImmutableList.of(METALAKE, CATALOG, SCHEMA);
         case COLUMN:
           return ImmutableList.of(METALAKE, CATALOG, SCHEMA, TABLE);
+        case MODEL_VERSION:
+          return ImmutableList.of(METALAKE, CATALOG, SCHEMA, MODEL);
         default:
           throw new IllegalArgumentException("Unknown entity type: " + 
entityType);
       }
diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java 
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index 859b74674..1cad967a9 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -31,6 +31,9 @@ import 
org.apache.gravitino.catalog.CatalogNormalizeDispatcher;
 import org.apache.gravitino.catalog.FilesetDispatcher;
 import org.apache.gravitino.catalog.FilesetNormalizeDispatcher;
 import org.apache.gravitino.catalog.FilesetOperationDispatcher;
+import org.apache.gravitino.catalog.ModelDispatcher;
+import org.apache.gravitino.catalog.ModelNormalizeDispatcher;
+import org.apache.gravitino.catalog.ModelOperationDispatcher;
 import org.apache.gravitino.catalog.PartitionDispatcher;
 import org.apache.gravitino.catalog.PartitionNormalizeDispatcher;
 import org.apache.gravitino.catalog.PartitionOperationDispatcher;
@@ -98,6 +101,8 @@ public class GravitinoEnv {
 
   private TopicDispatcher topicDispatcher;
 
+  private ModelDispatcher modelDispatcher;
+
   private MetalakeDispatcher metalakeDispatcher;
 
   private AccessControlDispatcher accessControlDispatcher;
@@ -207,6 +212,15 @@ public class GravitinoEnv {
     return tableDispatcher;
   }
 
+  /**
+   * Get the ModelDispatcher associated with the Gravitino environment.
+   *
+   * @return The ModelDispatcher instance.
+   */
+  public ModelDispatcher modelDispatcher() {
+    return modelDispatcher;
+  }
+
   /**
    * Get the PartitionDispatcher associated with the Gravitino environment.
    *
@@ -440,6 +454,13 @@ public class GravitinoEnv {
         new TopicNormalizeDispatcher(topicHookDispatcher, catalogManager);
     this.topicDispatcher = new TopicEventDispatcher(eventBus, 
topicNormalizeDispatcher);
 
+    // TODO(jerryshao). Add Hook and event dispatcher support for Model.
+    ModelOperationDispatcher modelOperationDispatcher =
+        new ModelOperationDispatcher(catalogManager, entityStore, idGenerator);
+    ModelNormalizeDispatcher modelNormalizeDispatcher =
+        new ModelNormalizeDispatcher(modelOperationDispatcher, catalogManager);
+    this.modelDispatcher = modelNormalizeDispatcher;
+
     // Create and initialize access control related modules
     boolean enableAuthorization = config.get(Configs.ENABLE_AUTHORIZATION);
     if (enableAuthorization) {
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java 
b/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
index 266b4a401..172d4b89d 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
@@ -130,7 +130,8 @@ public class CapabilityHelpers {
     String catalog = namespace.level(1);
     if (identScope == Capability.Scope.TABLE
         || identScope == Capability.Scope.FILESET
-        || identScope == Capability.Scope.TOPIC) {
+        || identScope == Capability.Scope.TOPIC
+        || identScope == Capability.Scope.MODEL) {
       String schema = namespace.level(namespace.length() - 1);
       schema = applyCaseSensitiveOnName(Capability.Scope.SCHEMA, schema, 
capabilities);
       return Namespace.of(metalake, catalog, schema);
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ModelDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/ModelDispatcher.java
new file mode 100644
index 000000000..067fd6ba2
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/catalog/ModelDispatcher.java
@@ -0,0 +1,23 @@
+/*
+ * 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.catalog;
+
+import org.apache.gravitino.model.ModelCatalog;
+
+public interface ModelDispatcher extends ModelCatalog {}
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
new file mode 100644
index 000000000..ea4933c3c
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
@@ -0,0 +1,167 @@
+/*
+ * 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.catalog;
+
+import static org.apache.gravitino.catalog.CapabilityHelpers.applyCapabilities;
+import static 
org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitive;
+import static org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
+
+import java.util.Map;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.exceptions.ModelAlreadyExistsException;
+import 
org.apache.gravitino.exceptions.ModelVersionAliasesAlreadyExistException;
+import org.apache.gravitino.exceptions.NoSuchModelException;
+import org.apache.gravitino.exceptions.NoSuchModelVersionException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.model.ModelVersion;
+
+public class ModelNormalizeDispatcher implements ModelDispatcher {
+  private final CatalogManager catalogManager;
+  private final ModelDispatcher dispatcher;
+
+  public ModelNormalizeDispatcher(ModelDispatcher dispatcher, CatalogManager 
catalogManager) {
+    this.dispatcher = dispatcher;
+    this.catalogManager = catalogManager;
+  }
+
+  @Override
+  public NameIdentifier[] listModels(Namespace namespace) throws 
NoSuchSchemaException {
+    // The constraints of the name spec may be more strict than underlying 
catalog,
+    // and for compatibility reasons, we only apply case-sensitive 
capabilities here.
+    Namespace caseSensitiveNs = normalizeCaseSensitive(namespace);
+    NameIdentifier[] identifiers = dispatcher.listModels(caseSensitiveNs);
+    return normalizeCaseSensitive(identifiers);
+  }
+
+  @Override
+  public Model getModel(NameIdentifier ident) throws NoSuchModelException {
+    // The constraints of the name spec may be more strict than underlying 
catalog,
+    // and for compatibility reasons, we only apply case-sensitive 
capabilities here.
+    return dispatcher.getModel(normalizeCaseSensitive(ident));
+  }
+
+  @Override
+  public boolean modelExists(NameIdentifier ident) {
+    // The constraints of the name spec may be more strict than underlying 
catalog,
+    // and for compatibility reasons, we only apply case-sensitive 
capabilities here.
+    return dispatcher.modelExists(normalizeCaseSensitive(ident));
+  }
+
+  @Override
+  public Model registerModel(NameIdentifier ident, String comment, Map<String, 
String> properties)
+      throws ModelAlreadyExistsException {
+    return dispatcher.registerModel(normalizeNameIdentifier(ident), comment, 
properties);
+  }
+
+  @Override
+  public Model registerModel(
+      NameIdentifier ident,
+      String uri,
+      String[] aliases,
+      String comment,
+      Map<String, String> properties)
+      throws ModelAlreadyExistsException, 
ModelVersionAliasesAlreadyExistException {
+    return dispatcher.registerModel(
+        normalizeNameIdentifier(ident), uri, aliases, comment, properties);
+  }
+
+  @Override
+  public boolean deleteModel(NameIdentifier ident) {
+    // The constraints of the name spec may be more strict than underlying 
catalog,
+    // and for compatibility reasons, we only apply case-sensitive 
capabilities here.
+    return dispatcher.deleteModel(normalizeCaseSensitive(ident));
+  }
+
+  @Override
+  public int[] listModelVersions(NameIdentifier ident) throws 
NoSuchModelException {
+    return dispatcher.listModelVersions(normalizeCaseSensitive(ident));
+  }
+
+  @Override
+  public ModelVersion getModelVersion(NameIdentifier ident, int version)
+      throws NoSuchModelVersionException {
+    return dispatcher.getModelVersion(normalizeCaseSensitive(ident), version);
+  }
+
+  @Override
+  public ModelVersion getModelVersion(NameIdentifier ident, String alias)
+      throws NoSuchModelVersionException {
+    return dispatcher.getModelVersion(normalizeCaseSensitive(ident), alias);
+  }
+
+  @Override
+  public boolean modelVersionExists(NameIdentifier ident, int version) {
+    return dispatcher.modelVersionExists(normalizeCaseSensitive(ident), 
version);
+  }
+
+  @Override
+  public boolean modelVersionExists(NameIdentifier ident, String alias) {
+    return dispatcher.modelVersionExists(normalizeCaseSensitive(ident), alias);
+  }
+
+  @Override
+  public ModelVersion linkModelVersion(
+      NameIdentifier ident,
+      String uri,
+      String[] aliases,
+      String comment,
+      Map<String, String> properties)
+      throws NoSuchModelException, ModelVersionAliasesAlreadyExistException {
+    return dispatcher.linkModelVersion(
+        normalizeCaseSensitive(ident), uri, aliases, comment, properties);
+  }
+
+  @Override
+  public boolean deleteModelVersion(NameIdentifier ident, int version) {
+    return dispatcher.deleteModelVersion(normalizeCaseSensitive(ident), 
version);
+  }
+
+  @Override
+  public boolean deleteModelVersion(NameIdentifier ident, String alias) {
+    return dispatcher.deleteModelVersion(normalizeCaseSensitive(ident), alias);
+  }
+
+  private Namespace normalizeCaseSensitive(Namespace namespace) {
+    Capability capabilities = 
getCapability(NameIdentifier.of(namespace.levels()), catalogManager);
+    return applyCaseSensitive(namespace, Capability.Scope.MODEL, capabilities);
+  }
+
+  private NameIdentifier normalizeCaseSensitive(NameIdentifier ident) {
+    Capability capabilities = getCapability(ident, catalogManager);
+    return applyCaseSensitive(ident, Capability.Scope.MODEL, capabilities);
+  }
+
+  private NameIdentifier[] normalizeCaseSensitive(NameIdentifier[] idents) {
+    if (ArrayUtils.isEmpty(idents)) {
+      return idents;
+    }
+
+    Capability capabilities = getCapability(idents[0], catalogManager);
+    return applyCaseSensitive(idents, Capability.Scope.MODEL, capabilities);
+  }
+
+  private NameIdentifier normalizeNameIdentifier(NameIdentifier ident) {
+    Capability capability = getCapability(ident, catalogManager);
+    return applyCapabilities(ident, Capability.Scope.MODEL, capability);
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ModelOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/catalog/ModelOperationDispatcher.java
new file mode 100644
index 000000000..e8739673d
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/ModelOperationDispatcher.java
@@ -0,0 +1,110 @@
+/*
+ * 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.catalog;
+
+import java.util.Map;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.exceptions.ModelAlreadyExistsException;
+import 
org.apache.gravitino.exceptions.ModelVersionAliasesAlreadyExistException;
+import org.apache.gravitino.exceptions.NoSuchModelException;
+import org.apache.gravitino.exceptions.NoSuchModelVersionException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.model.ModelVersion;
+import org.apache.gravitino.storage.IdGenerator;
+
+public class ModelOperationDispatcher extends OperationDispatcher implements 
ModelDispatcher {
+
+  public ModelOperationDispatcher(
+      CatalogManager catalogManager, EntityStore store, IdGenerator 
idGenerator) {
+    super(catalogManager, store, idGenerator);
+  }
+
+  @Override
+  public NameIdentifier[] listModels(Namespace namespace) throws 
NoSuchSchemaException {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public Model getModel(NameIdentifier ident) throws NoSuchModelException {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public Model registerModel(NameIdentifier ident, String comment, Map<String, 
String> properties)
+      throws ModelAlreadyExistsException {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public Model registerModel(
+      NameIdentifier ident,
+      String uri,
+      String[] aliases,
+      String comment,
+      Map<String, String> properties)
+      throws ModelAlreadyExistsException, 
ModelVersionAliasesAlreadyExistException {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public boolean deleteModel(NameIdentifier ident) {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public int[] listModelVersions(NameIdentifier ident) throws 
NoSuchModelException {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public ModelVersion getModelVersion(NameIdentifier ident, int version)
+      throws NoSuchModelVersionException {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public ModelVersion getModelVersion(NameIdentifier ident, String alias)
+      throws NoSuchModelVersionException {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public ModelVersion linkModelVersion(
+      NameIdentifier ident,
+      String uri,
+      String[] aliases,
+      String comment,
+      Map<String, String> properties)
+      throws NoSuchModelException, ModelVersionAliasesAlreadyExistException {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public boolean deleteModelVersion(NameIdentifier ident, int version) {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Override
+  public boolean deleteModelVersion(NameIdentifier ident, String alias) {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/connector/capability/Capability.java 
b/core/src/main/java/org/apache/gravitino/connector/capability/Capability.java
index 54d3ac543..37d517f2d 100644
--- 
a/core/src/main/java/org/apache/gravitino/connector/capability/Capability.java
+++ 
b/core/src/main/java/org/apache/gravitino/connector/capability/Capability.java
@@ -39,7 +39,8 @@ public interface Capability {
     COLUMN,
     FILESET,
     TOPIC,
-    PARTITION
+    PARTITION,
+    MODEL
   }
 
   /**
diff --git a/core/src/main/java/org/apache/gravitino/meta/ModelEntity.java 
b/core/src/main/java/org/apache/gravitino/meta/ModelEntity.java
new file mode 100644
index 000000000..130c5b36a
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/meta/ModelEntity.java
@@ -0,0 +1,189 @@
+/*
+ * 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.meta;
+
+import com.google.common.collect.Maps;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import lombok.ToString;
+import org.apache.gravitino.Auditable;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.Field;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.Namespace;
+
+@ToString
+public class ModelEntity implements Entity, Auditable, HasIdentifier {
+
+  public static final Field ID =
+      Field.required("id", Long.class, "The unique id of the model entity.");
+  public static final Field NAME =
+      Field.required("name", String.class, "The name of the model entity.");
+  public static final Field COMMENT =
+      Field.optional("comment", String.class, "The comment or description of 
the model entity.");
+  public static final Field LATEST_VERSION =
+      Field.required("latest_version", Integer.class, "The latest version of 
the model entity.");
+  public static final Field PROPERTIES =
+      Field.optional("properties", Map.class, "The properties of the model 
entity.");
+  public static final Field AUDIT_INFO =
+      Field.required("audit_info", AuditInfo.class, "The audit details of the 
model entity.");
+
+  private Long id;
+
+  private String name;
+
+  private Namespace namespace;
+
+  private String comment;
+
+  private Integer latestVersion;
+
+  private AuditInfo auditInfo;
+
+  private Map<String, String> properties;
+
+  private ModelEntity() {}
+
+  @Override
+  public Map<Field, Object> fields() {
+    Map<Field, Object> fields = Maps.newHashMap();
+    fields.put(ID, id);
+    fields.put(NAME, name);
+    fields.put(COMMENT, comment);
+    fields.put(LATEST_VERSION, latestVersion);
+    fields.put(PROPERTIES, properties);
+    fields.put(AUDIT_INFO, auditInfo);
+
+    return Collections.unmodifiableMap(fields);
+  }
+
+  @Override
+  public String name() {
+    return name;
+  }
+
+  @Override
+  public Long id() {
+    return id;
+  }
+
+  @Override
+  public Namespace namespace() {
+    return namespace;
+  }
+
+  public String comment() {
+    return comment;
+  }
+
+  public Integer latestVersion() {
+    return latestVersion;
+  }
+
+  public Map<String, String> properties() {
+    return properties;
+  }
+
+  @Override
+  public AuditInfo auditInfo() {
+    return auditInfo;
+  }
+
+  @Override
+  public EntityType type() {
+    return EntityType.MODEL;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+
+    if (!(o instanceof ModelEntity)) {
+      return false;
+    }
+
+    ModelEntity that = (ModelEntity) o;
+    return Objects.equals(id, that.id)
+        && Objects.equals(name, that.name)
+        && Objects.equals(comment, that.comment)
+        && Objects.equals(latestVersion, that.latestVersion)
+        && Objects.equals(properties, that.properties)
+        && Objects.equals(auditInfo, that.auditInfo);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(id, name, comment, latestVersion, properties, 
auditInfo);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private final ModelEntity model;
+
+    private Builder() {
+      model = new ModelEntity();
+    }
+
+    public Builder withId(Long id) {
+      model.id = id;
+      return this;
+    }
+
+    public Builder withName(String name) {
+      model.name = name;
+      return this;
+    }
+
+    public Builder withNamespace(Namespace namespace) {
+      model.namespace = namespace;
+      return this;
+    }
+
+    public Builder withComment(String comment) {
+      model.comment = comment;
+      return this;
+    }
+
+    public Builder withLatestVersion(Integer latestVersion) {
+      model.latestVersion = latestVersion;
+      return this;
+    }
+
+    public Builder withProperties(Map<String, String> properties) {
+      model.properties = properties;
+      return this;
+    }
+
+    public Builder withAuditInfo(AuditInfo auditInfo) {
+      model.auditInfo = auditInfo;
+      return this;
+    }
+
+    public ModelEntity build() {
+      model.validate();
+      return model;
+    }
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java 
b/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java
new file mode 100644
index 000000000..6b9f44a52
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java
@@ -0,0 +1,174 @@
+/*
+ * 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.meta;
+
+import com.google.common.collect.Maps;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import lombok.ToString;
+import org.apache.gravitino.Auditable;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.Field;
+
+@ToString
+public class ModelVersionEntity implements Entity, Auditable {
+
+  public static final Field VERSION =
+      Field.required("version", Integer.class, "The version of the model 
entity.");
+  public static final Field COMMENT =
+      Field.optional("comment", String.class, "The comment or description of 
the model entity.");
+  public static final Field ALIASES =
+      Field.optional("aliases", List.class, "The aliases of the model 
entity.");
+  public static final Field URL =
+      Field.required("uri", String.class, "The URI of the model entity.");
+  public static final Field PROPERTIES =
+      Field.optional("properties", Map.class, "The properties of the model 
entity.");
+  public static final Field AUDIT_INFO =
+      Field.required("audit_info", AuditInfo.class, "The audit details of the 
model entity.");
+
+  private Integer version;
+
+  private String comment;
+
+  private List<String> aliases;
+
+  private String uri;
+
+  private AuditInfo auditInfo;
+
+  private Map<String, String> properties;
+
+  private ModelVersionEntity() {}
+
+  @Override
+  public Map<Field, Object> fields() {
+    Map<Field, Object> fields = Maps.newHashMap();
+    fields.put(VERSION, version);
+    fields.put(COMMENT, comment);
+    fields.put(ALIASES, aliases);
+    fields.put(URL, uri);
+    fields.put(PROPERTIES, properties);
+    fields.put(AUDIT_INFO, auditInfo);
+
+    return Collections.unmodifiableMap(fields);
+  }
+
+  public Integer version() {
+    return version;
+  }
+
+  public String comment() {
+    return comment;
+  }
+
+  public List<String> aliases() {
+    return aliases;
+  }
+
+  public String uri() {
+    return uri;
+  }
+
+  public Map<String, String> properties() {
+    return properties;
+  }
+
+  @Override
+  public AuditInfo auditInfo() {
+    return auditInfo;
+  }
+
+  @Override
+  public EntityType type() {
+    return EntityType.MODEL_VERSION;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+
+    if (!(o instanceof ModelVersionEntity)) {
+      return false;
+    }
+
+    ModelVersionEntity that = (ModelVersionEntity) o;
+    return Objects.equals(version, that.version)
+        && Objects.equals(comment, that.comment)
+        && Objects.equals(aliases, that.aliases)
+        && Objects.equals(uri, that.uri)
+        && Objects.equals(properties, that.properties)
+        && Objects.equals(auditInfo, that.auditInfo);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(version, comment, aliases, uri, properties, auditInfo);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private final ModelVersionEntity model;
+
+    private Builder() {
+      model = new ModelVersionEntity();
+    }
+
+    public Builder withVersion(int version) {
+      model.version = version;
+      return this;
+    }
+
+    public Builder withComment(String comment) {
+      model.comment = comment;
+      return this;
+    }
+
+    public Builder withAliases(List<String> aliases) {
+      model.aliases = aliases;
+      return this;
+    }
+
+    public Builder withUri(String uri) {
+      model.uri = uri;
+      return this;
+    }
+
+    public Builder withProperties(Map<String, String> properties) {
+      model.properties = properties;
+      return this;
+    }
+
+    public Builder withAuditInfo(AuditInfo auditInfo) {
+      model.auditInfo = auditInfo;
+      return this;
+    }
+
+    public ModelVersionEntity build() {
+      model.validate();
+      return model;
+    }
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java 
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
index 1534a7397..f45778e97 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
@@ -296,6 +296,9 @@ public class JDBCBackend implements RelationalBackend {
         return TableColumnMetaService.getInstance()
             .deleteColumnsByLegacyTimeline(legacyTimeline, 
GARBAGE_COLLECTOR_SINGLE_DELETION_LIMIT);
       case AUDIT:
+      case MODEL:
+      case MODEL_VERSION:
+        // TODO (jerryshao): Implement hard delete logic for these entity 
types.
         return 0;
         // TODO: Implement hard delete logic for these entity types.
 
@@ -320,6 +323,8 @@ public class JDBCBackend implements RelationalBackend {
       case AUDIT:
       case ROLE:
       case TAG:
+      case MODEL:
+      case MODEL_VERSION:
         // These entity types have not implemented multi-versions, so we can 
skip.
         return 0;
 
diff --git 
a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java 
b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
index da9f4129a..44b9d30a0 100644
--- a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
@@ -49,6 +49,7 @@ public class MetadataObjectUtil {
           .put(MetadataObject.Type.FILESET, Entity.EntityType.FILESET)
           .put(MetadataObject.Type.COLUMN, Entity.EntityType.COLUMN)
           .put(MetadataObject.Type.ROLE, Entity.EntityType.ROLE)
+          .put(MetadataObject.Type.MODEL, Entity.EntityType.MODEL)
           .build();
 
   private MetadataObjectUtil() {}
@@ -94,6 +95,7 @@ public class MetadataObjectUtil {
       case TOPIC:
       case FILESET:
       case COLUMN:
+      case MODEL:
         String fullName = DOT.join(metalakeName, metadataObject.fullName());
         return NameIdentifier.parse(fullName);
       default:
@@ -158,6 +160,11 @@ public class MetadataObjectUtil {
         check(env.topicDispatcher().topicExists(identifier), 
exceptionToThrowSupplier);
         break;
 
+      case MODEL:
+        NameIdentifierUtil.checkModel(identifier);
+        check(env.modelDispatcher().modelExists(identifier), 
exceptionToThrowSupplier);
+        break;
+
       case ROLE:
         AuthorizationUtils.checkRole(identifier);
         try {
diff --git 
a/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java 
b/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
index 550fef967..a1cb3ead6 100644
--- a/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
@@ -139,6 +139,37 @@ public class NameIdentifierUtil {
     return NameIdentifier.of(metalake, catalog, schema, topic);
   }
 
+  /**
+   * Create the model {@link NameIdentifier} with the given metalake, catalog, 
schema and model
+   * name.
+   *
+   * @param metalake The metalake name
+   * @param catalog The catalog name
+   * @param schema The schema name
+   * @param model The model name
+   * @return The created model {@link NameIdentifier}
+   */
+  public static NameIdentifier ofModel(
+      String metalake, String catalog, String schema, String model) {
+    return NameIdentifier.of(metalake, catalog, schema, model);
+  }
+
+  /**
+   * Create the model version {@link NameIdentifier} with the given metalake, 
catalog, schema, model
+   * and version.
+   *
+   * @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
+   * @return The created model version {@link NameIdentifier}
+   */
+  public static NameIdentifier ofModelVersion(
+      String metalake, String catalog, String schema, String model, int 
version) {
+    return NameIdentifier.of(metalake, catalog, schema, model, 
String.valueOf(version));
+  }
+
   /**
    * Try to get the catalog {@link NameIdentifier} from the given {@link 
NameIdentifier}.
    *
@@ -245,6 +276,28 @@ public class NameIdentifierUtil {
     NamespaceUtil.checkTopic(ident.namespace());
   }
 
+  /**
+   * Check the given {@link NameIdentifier} is a model identifier. Throw an 
{@link
+   * IllegalNameIdentifierException} if it's not.
+   *
+   * @param ident The model {@link NameIdentifier} to check.
+   */
+  public static void checkModel(NameIdentifier ident) {
+    NameIdentifier.check(ident != null, "Model identifier must not be null");
+    NamespaceUtil.checkModel(ident.namespace());
+  }
+
+  /**
+   * Check the given {@link NameIdentifier} is a model version identifier. 
Throw an {@link
+   * IllegalNameIdentifierException} if it's not.
+   *
+   * @param ident The model version {@link NameIdentifier} to check.
+   */
+  public static void checkModelVersion(NameIdentifier ident) {
+    NameIdentifier.check(ident != null, "Model version identifier must not be 
null");
+    NamespaceUtil.checkModelVersion(ident.namespace());
+  }
+
   /**
    * Check the given condition is true. Throw an {@link 
IllegalNamespaceException} if it's not.
    *
@@ -309,6 +362,11 @@ public class NameIdentifierUtil {
         String topicParent = dot.join(ident.namespace().level(1), 
ident.namespace().level(2));
         return MetadataObjects.of(topicParent, ident.name(), 
MetadataObject.Type.TOPIC);
 
+      case MODEL:
+        checkModel(ident);
+        String modelParent = dot.join(ident.namespace().level(1), 
ident.namespace().level(2));
+        return MetadataObjects.of(modelParent, ident.name(), 
MetadataObject.Type.MODEL);
+
       case ROLE:
         AuthorizationUtils.checkRole(ident);
         return MetadataObjects.of(null, ident.name(), 
MetadataObject.Type.ROLE);
diff --git a/core/src/main/java/org/apache/gravitino/utils/NamespaceUtil.java 
b/core/src/main/java/org/apache/gravitino/utils/NamespaceUtil.java
index c24015bb3..03ad8dc2e 100644
--- a/core/src/main/java/org/apache/gravitino/utils/NamespaceUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/NamespaceUtil.java
@@ -107,6 +107,32 @@ public class NamespaceUtil {
     return Namespace.of(metalake, catalog, schema);
   }
 
+  /**
+   * Create a namespace for model.
+   *
+   * @param metalake The metalake name
+   * @param catalog The catalog name
+   * @param schema The schema name
+   * @return A namespace for model
+   */
+  public static Namespace ofModel(String metalake, String catalog, String 
schema) {
+    return Namespace.of(metalake, catalog, schema);
+  }
+
+  /**
+   * Create a namespace for model version.
+   *
+   * @param metalake The metalake name
+   * @param catalog The catalog name
+   * @param schema The schema name
+   * @param model The model name
+   * @return A namespace for model version
+   */
+  public static Namespace ofModelVersion(
+      String metalake, String catalog, String schema, String model) {
+    return Namespace.of(metalake, catalog, schema, model);
+  }
+
   /**
    * Check if the given metalake namespace is legal, throw an {@link 
IllegalNamespaceException} if
    * it's illegal.
@@ -198,6 +224,32 @@ public class NamespaceUtil {
         namespace);
   }
 
+  /**
+   * Check if the given model namespace is legal, throw an {@link 
IllegalNamespaceException} if it's
+   * illegal.
+   *
+   * @param namespace The model namespace
+   */
+  public static void checkModel(Namespace namespace) {
+    check(
+        namespace != null && namespace.length() == 3,
+        "Model namespace must be non-null and have 3 levels, the input 
namespace is %s",
+        namespace);
+  }
+
+  /**
+   * Check if the given model version namespace is legal, throw an {@link 
IllegalNamespaceException}
+   * if it's illegal.
+   *
+   * @param namespace The model version namespace
+   */
+  public static void checkModelVersion(Namespace namespace) {
+    check(
+        namespace != null && namespace.length() == 4,
+        "Model version namespace must be non-null and have 4 levels, the input 
namespace is %s",
+        namespace);
+  }
+
   /**
    * Check the given condition is true. Throw an {@link 
IllegalNamespaceException} if it's not.
    *
diff --git a/core/src/test/java/org/apache/gravitino/meta/TestModelEntity.java 
b/core/src/test/java/org/apache/gravitino/meta/TestModelEntity.java
new file mode 100644
index 000000000..d3b48090b
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/meta/TestModelEntity.java
@@ -0,0 +1,116 @@
+/*
+ * 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.meta;
+
+import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.util.Map;
+import org.apache.gravitino.Namespace;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestModelEntity {
+
+  @Test
+  public void testModelEntityFields() {
+    AuditInfo auditInfo =
+        
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build();
+    Map<String, String> properties = ImmutableMap.of("k1", "v1");
+
+    ModelEntity modelEntity =
+        ModelEntity.builder()
+            .withId(1L)
+            .withName("test")
+            .withComment("test comment")
+            .withLatestVersion(1)
+            .withNamespace(Namespace.of("m1", "c1", "s1"))
+            .withProperties(properties)
+            .withAuditInfo(auditInfo)
+            .build();
+
+    Assertions.assertEquals(1L, modelEntity.id());
+    Assertions.assertEquals("test", modelEntity.name());
+    Assertions.assertEquals("test comment", modelEntity.comment());
+    Assertions.assertEquals(1, modelEntity.latestVersion());
+    Assertions.assertEquals(Namespace.of("m1", "c1", "s1"), 
modelEntity.namespace());
+    Assertions.assertEquals(properties, modelEntity.properties());
+    Assertions.assertEquals(auditInfo, modelEntity.auditInfo());
+
+    ModelEntity modelEntity2 =
+        ModelEntity.builder()
+            .withId(1L)
+            .withName("test")
+            .withLatestVersion(1)
+            .withNamespace(Namespace.of("m1", "c1", "s1"))
+            .withProperties(properties)
+            .withAuditInfo(auditInfo)
+            .build();
+    Assertions.assertNull(modelEntity2.comment());
+
+    ModelEntity modelEntity3 =
+        ModelEntity.builder()
+            .withId(1L)
+            .withName("test")
+            .withComment("test comment")
+            .withLatestVersion(1)
+            .withNamespace(Namespace.of("m1", "c1", "s1"))
+            .withAuditInfo(auditInfo)
+            .build();
+    Assertions.assertNull(modelEntity3.properties());
+  }
+
+  @Test
+  public void testWithoutRequiredFields() {
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
ModelEntity.builder().build());
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelEntity.builder()
+              .withId(1L)
+              .withNamespace(Namespace.of("m1", "c1", "s1"))
+              .withAuditInfo(
+                  
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+              .build();
+        });
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelEntity.builder()
+              .withId(1L)
+              .withName("test")
+              .withNamespace(Namespace.of("m1", "c1", "s1"))
+              .withAuditInfo(
+                  
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+              .build();
+        });
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelEntity.builder()
+              .withId(1L)
+              .withName("test")
+              .withNamespace(Namespace.of("m1", "c1", "s1"))
+              .withLatestVersion(1)
+              .build();
+        });
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/meta/TestModelVersionEntity.java 
b/core/src/test/java/org/apache/gravitino/meta/TestModelVersionEntity.java
new file mode 100644
index 000000000..5b62ea946
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/meta/TestModelVersionEntity.java
@@ -0,0 +1,107 @@
+/*
+ * 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.meta;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestModelVersionEntity {
+
+  @Test
+  public void testModelVersionEntityFields() {
+    AuditInfo auditInfo =
+        
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build();
+    Map<String, String> properties = ImmutableMap.of("k1", "v1");
+    List<String> aliases = Lists.newArrayList("alias1", "alias2");
+
+    ModelVersionEntity modelVersionEntity =
+        ModelVersionEntity.builder()
+            .withVersion(1)
+            .withComment("test comment")
+            .withAliases(aliases)
+            .withProperties(properties)
+            .withUri("test_uri")
+            .withAuditInfo(auditInfo)
+            .build();
+
+    Assertions.assertEquals(1, modelVersionEntity.version());
+    Assertions.assertEquals("test comment", modelVersionEntity.comment());
+    Assertions.assertEquals(aliases, modelVersionEntity.aliases());
+    Assertions.assertEquals(properties, modelVersionEntity.properties());
+    Assertions.assertEquals("test_uri", modelVersionEntity.uri());
+    Assertions.assertEquals(auditInfo, modelVersionEntity.auditInfo());
+
+    ModelVersionEntity modelVersionEntity2 =
+        ModelVersionEntity.builder()
+            .withVersion(1)
+            .withAliases(aliases)
+            .withProperties(properties)
+            .withUri("test_uri")
+            .withAuditInfo(auditInfo)
+            .build();
+    Assertions.assertNull(modelVersionEntity2.comment());
+
+    ModelVersionEntity modelVersionEntity3 =
+        ModelVersionEntity.builder()
+            .withVersion(1)
+            .withComment("test comment")
+            .withAliases(aliases)
+            .withUri("test_uri")
+            .withAuditInfo(auditInfo)
+            .build();
+    Assertions.assertNull(modelVersionEntity3.properties());
+
+    ModelVersionEntity modelVersionEntity4 =
+        ModelVersionEntity.builder()
+            .withVersion(1)
+            .withComment("test comment")
+            .withProperties(properties)
+            .withUri("test_uri")
+            .withAuditInfo(auditInfo)
+            .build();
+    Assertions.assertNull(modelVersionEntity4.aliases());
+  }
+
+  @Test
+  public void testWithoutRequiredFields() {
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> 
ModelVersionEntity.builder().build());
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          ModelVersionEntity.builder()
+              .withVersion(1)
+              .withAuditInfo(
+                  
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+              .build();
+        });
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          
ModelVersionEntity.builder().withVersion(1).withUri("test_uri").build();
+        });
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/utils/TestMetadataObjectUtil.java 
b/core/src/test/java/org/apache/gravitino/utils/TestMetadataObjectUtil.java
index c5a281866..9911ddbfa 100644
--- a/core/src/test/java/org/apache/gravitino/utils/TestMetadataObjectUtil.java
+++ b/core/src/test/java/org/apache/gravitino/utils/TestMetadataObjectUtil.java
@@ -68,6 +68,11 @@ public class TestMetadataObjectUtil {
         Entity.EntityType.COLUMN,
         MetadataObjectUtil.toEntityType(
             MetadataObjects.of("catalog.schema.table", "column", 
MetadataObject.Type.COLUMN)));
+
+    Assertions.assertEquals(
+        Entity.EntityType.MODEL,
+        MetadataObjectUtil.toEntityType(
+            MetadataObjects.of("catalog.schema", "model", 
MetadataObject.Type.MODEL)));
   }
 
   @Test
@@ -113,6 +118,11 @@ public class TestMetadataObjectUtil {
             "metalake",
             MetadataObjects.of("catalog.schema", "fileset", 
MetadataObject.Type.FILESET)));
 
+    Assertions.assertEquals(
+        NameIdentifier.of("metalake", "catalog", "schema", "model"),
+        MetadataObjectUtil.toEntityIdent(
+            "metalake", MetadataObjects.of("catalog.schema", "model", 
MetadataObject.Type.MODEL)));
+
     Assertions.assertEquals(
         NameIdentifier.of("metalake", "catalog", "schema", "table", "column"),
         MetadataObjectUtil.toEntityIdent(
diff --git 
a/core/src/test/java/org/apache/gravitino/utils/TestNameIdentifierUtil.java 
b/core/src/test/java/org/apache/gravitino/utils/TestNameIdentifierUtil.java
index 2eca30351..e5db1eb10 100644
--- a/core/src/test/java/org/apache/gravitino/utils/TestNameIdentifierUtil.java
+++ b/core/src/test/java/org/apache/gravitino/utils/TestNameIdentifierUtil.java
@@ -61,6 +61,21 @@ public class TestNameIdentifierUtil {
     Throwable excep3 =
         assertThrows(IllegalNamespaceException.class, () -> 
NameIdentifierUtil.checkTable(abc));
     assertTrue(excep3.getMessage().contains("Table namespace must be non-null 
and have 3 levels"));
+
+    // test model
+    assertThrows(IllegalNameIdentifierException.class, () -> 
NameIdentifierUtil.checkModel(null));
+    Throwable excep4 =
+        assertThrows(IllegalNamespaceException.class, () -> 
NameIdentifierUtil.checkModel(abc));
+    assertTrue(excep4.getMessage().contains("Model namespace must be non-null 
and have 3 levels"));
+
+    // test model version
+    assertThrows(
+        IllegalNameIdentifierException.class, () -> 
NameIdentifierUtil.checkModelVersion(null));
+    Throwable excep5 =
+        assertThrows(
+            IllegalNamespaceException.class, () -> 
NameIdentifierUtil.checkModelVersion(abcd));
+    assertTrue(
+        excep5.getMessage().contains("Model version namespace must be non-null 
and have 4 levels"));
   }
 
   @Test
@@ -111,6 +126,12 @@ public class TestNameIdentifierUtil {
     assertEquals(
         columnObject, NameIdentifierUtil.toMetadataObject(column, 
Entity.EntityType.COLUMN));
 
+    // test model
+    NameIdentifier model = NameIdentifier.of("metalake1", "catalog1", 
"schema1", "model1");
+    MetadataObject modelObject =
+        MetadataObjects.parse("catalog1.schema1.model1", 
MetadataObject.Type.MODEL);
+    assertEquals(modelObject, NameIdentifierUtil.toMetadataObject(model, 
Entity.EntityType.MODEL));
+
     // test null
     Throwable e1 =
         assertThrows(
@@ -123,5 +144,12 @@ public class TestNameIdentifierUtil {
             IllegalArgumentException.class,
             () -> NameIdentifierUtil.toMetadataObject(fileset, 
Entity.EntityType.TAG));
     assertTrue(e2.getMessage().contains("Entity type TAG is not supported"));
+
+    // test model version
+    Throwable e3 =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> NameIdentifierUtil.toMetadataObject(model, 
Entity.EntityType.MODEL_VERSION));
+    assertTrue(e3.getMessage().contains("Entity type MODEL_VERSION is not 
supported"));
   }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/utils/TestNamespaceUtil.java 
b/core/src/test/java/org/apache/gravitino/utils/TestNamespaceUtil.java
index 4286d3f90..71ba8e4c7 100644
--- a/core/src/test/java/org/apache/gravitino/utils/TestNamespaceUtil.java
+++ b/core/src/test/java/org/apache/gravitino/utils/TestNamespaceUtil.java
@@ -64,5 +64,22 @@ public class TestNamespaceUtil {
             IllegalNamespaceException.class, () -> 
NamespaceUtil.checkTable(abcd));
     Assertions.assertTrue(
         excep3.getMessage().contains("Table namespace must be non-null and 
have 3 levels"));
+
+    // Test model
+    Assertions.assertThrows(IllegalNamespaceException.class, () -> 
NamespaceUtil.checkModel(null));
+    Throwable excep4 =
+        Assertions.assertThrows(
+            IllegalNamespaceException.class, () -> 
NamespaceUtil.checkModel(abcd));
+    Assertions.assertTrue(
+        excep4.getMessage().contains("Model namespace must be non-null and 
have 3 levels"));
+
+    // Test model version
+    Assertions.assertThrows(
+        IllegalNamespaceException.class, () -> 
NamespaceUtil.checkModelVersion(null));
+    Throwable excep5 =
+        Assertions.assertThrows(
+            IllegalNamespaceException.class, () -> 
NamespaceUtil.checkModelVersion(ab));
+    Assertions.assertTrue(
+        excep5.getMessage().contains("Model version namespace must be non-null 
and have 4 levels"));
   }
 }

Reply via email to