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

yuqi4733 pushed a commit to branch branch-1.0
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.0 by this push:
     new a59e1cffba [#8639]feat(core): Add the job template alteration in the 
core part (#8775)
a59e1cffba is described below

commit a59e1cffba34841eb9077a35a2a42b46d11f2289
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 10 09:58:04 2025 +0800

    [#8639]feat(core): Add the job template alteration in the core part (#8775)
    
    ### What changes were proposed in this pull request?
    
    Add the core part for job template alteration.
    
    ### Why are the changes needed?
    
    This is a part of work for job template alteration.
    
    Fix: #8639
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    New UTs to test code.
    
    ---------
    
    Co-authored-by: Jerry Shao <[email protected]>
    Co-authored-by: Jerry Shao <[email protected]>
---
 .../apache/gravitino/job/JobTemplateChange.java    |  39 ----
 .../gravitino/api/job/job_template_change.py       |  49 ++---
 .../java/org/apache/gravitino/job/JobManager.java  | 169 ++++++++++++++++
 .../gravitino/job/JobOperationDispatcher.java      |  14 ++
 .../gravitino/storage/relational/JDBCBackend.java  |   2 +
 .../relational/mapper/JobTemplateMetaMapper.java   |   5 +
 .../mapper/JobTemplateMetaSQLProviderFactory.java  |   6 +
 .../base/JobTemplateMetaBaseSQLProvider.java       |  21 ++
 .../storage/relational/po/JobTemplatePO.java       |  25 +++
 .../relational/service/JobTemplateMetaService.java |  83 ++++++--
 .../org/apache/gravitino/job/TestJobManager.java   | 215 +++++++++++++++++++++
 .../service/TestJobTemplateMetaService.java        |  69 +++++++
 12 files changed, 614 insertions(+), 83 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/job/JobTemplateChange.java 
b/api/src/main/java/org/apache/gravitino/job/JobTemplateChange.java
index 88d9f58d0f..da9078302e 100644
--- a/api/src/main/java/org/apache/gravitino/job/JobTemplateChange.java
+++ b/api/src/main/java/org/apache/gravitino/job/JobTemplateChange.java
@@ -18,14 +18,9 @@
  */
 package org.apache.gravitino.job;
 
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import org.apache.commons.lang3.StringUtils;
 
 /**
  * The interface for job template changes. A job template change is an 
operation that modifies a job
@@ -378,18 +373,6 @@ public interface JobTemplateChange {
         this.newCustomFields = newCustomFields;
         return self();
       }
-
-      /** Validates the builder fields before building the TemplateUpdate 
instance. */
-      protected void validate() {
-        Preconditions.checkArgument(
-            StringUtils.isNoneBlank(newExecutable), "Executable cannot be null 
or blank");
-        this.newArguments =
-            newArguments == null ? Collections.emptyList() : 
ImmutableList.copyOf(newArguments);
-        this.newEnvironments =
-            newEnvironments == null ? Collections.emptyMap() : 
ImmutableMap.copyOf(newEnvironments);
-        this.newCustomFields =
-            newCustomFields == null ? Collections.emptyMap() : 
ImmutableMap.copyOf(newCustomFields);
-      }
     }
   }
 
@@ -470,7 +453,6 @@ public interface JobTemplateChange {
        */
       @Override
       public ShellTemplateUpdate build() {
-        validate();
         return new ShellTemplateUpdate(this);
       }
 
@@ -483,14 +465,6 @@ public interface JobTemplateChange {
       protected Builder self() {
         return this;
       }
-
-      /** Validates the builder fields before building the ShellTemplateUpdate 
instance. */
-      @Override
-      protected void validate() {
-        super.validate();
-        this.newScripts =
-            newScripts == null ? Collections.emptyList() : 
ImmutableList.copyOf(newScripts);
-      }
     }
   }
 
@@ -671,7 +645,6 @@ public interface JobTemplateChange {
        */
       @Override
       public SparkTemplateUpdate build() {
-        validate();
         return new SparkTemplateUpdate(this);
       }
 
@@ -684,18 +657,6 @@ public interface JobTemplateChange {
       protected Builder self() {
         return this;
       }
-
-      /** Validates the builder fields before building the SparkTemplateUpdate 
instance. */
-      @Override
-      protected void validate() {
-        super.validate();
-        this.newJars = newJars == null ? Collections.emptyList() : 
ImmutableList.copyOf(newJars);
-        this.newFiles = newFiles == null ? Collections.emptyList() : 
ImmutableList.copyOf(newFiles);
-        this.newArchives =
-            newArchives == null ? Collections.emptyList() : 
ImmutableList.copyOf(newArchives);
-        this.newConfigs =
-            newConfigs == null ? Collections.emptyMap() : 
ImmutableMap.copyOf(newConfigs);
-      }
     }
   }
 }
diff --git a/clients/client-python/gravitino/api/job/job_template_change.py 
b/clients/client-python/gravitino/api/job/job_template_change.py
index 1c3c1c50db..cef6a6f2e7 100644
--- a/clients/client-python/gravitino/api/job/job_template_change.py
+++ b/clients/client-python/gravitino/api/job/job_template_change.py
@@ -16,7 +16,6 @@
 # under the License.
 from abc import ABC
 from typing import List, Dict, Optional, Any
-from copy import deepcopy
 
 
 class JobTemplateChange:
@@ -184,7 +183,7 @@ class TemplateUpdate(ABC):
 
     def __init__(
         self,
-        new_executable: str,
+        new_executable: Optional[str] = None,
         new_arguments: Optional[List[str]] = None,
         new_environments: Optional[Dict[str, str]] = None,
         new_custom_fields: Optional[Dict[str, str]] = None,
@@ -198,20 +197,12 @@ class TemplateUpdate(ABC):
             new_environments: The new environments of the job template.
             new_custom_fields: The new custom fields of the job template.
         """
-        if not new_executable or not new_executable.strip():
-            raise ValueError("Executable cannot be null or blank")
         self._new_executable = new_executable
-        self._new_arguments = (
-            deepcopy(new_arguments) if new_arguments is not None else []
-        )
-        self._new_environments = (
-            deepcopy(new_environments) if new_environments is not None else {}
-        )
-        self._new_custom_fields = (
-            deepcopy(new_custom_fields) if new_custom_fields is not None else 
{}
-        )
+        self._new_arguments = new_arguments
+        self._new_environments = new_environments
+        self._new_custom_fields = new_custom_fields
 
-    def get_new_executable(self) -> str:
+    def get_new_executable(self) -> Optional[str]:
         """
         Get the new executable of the job template.
 
@@ -220,7 +211,7 @@ class TemplateUpdate(ABC):
         """
         return self._new_executable
 
-    def get_new_arguments(self) -> List[str]:
+    def get_new_arguments(self) -> Optional[List[str]]:
         """
         Get the new arguments of the job template.
 
@@ -229,7 +220,7 @@ class TemplateUpdate(ABC):
         """
         return self._new_arguments
 
-    def get_new_environments(self) -> Dict[str, str]:
+    def get_new_environments(self) -> Optional[Dict[str, str]]:
         """
         Get the new environments of the job template.
 
@@ -238,7 +229,7 @@ class TemplateUpdate(ABC):
         """
         return self._new_environments
 
-    def get_new_custom_fields(self) -> Dict[str, str]:
+    def get_new_custom_fields(self) -> Optional[Dict[str, str]]:
         """
         Get the new custom fields of the job template.
 
@@ -275,7 +266,7 @@ class ShellTemplateUpdate(TemplateUpdate):
 
     def __init__(
         self,
-        new_executable: str,
+        new_executable: Optional[str] = None,
         new_arguments: Optional[List[str]] = None,
         new_environments: Optional[Dict[str, str]] = None,
         new_custom_fields: Optional[Dict[str, str]] = None,
@@ -294,9 +285,9 @@ class ShellTemplateUpdate(TemplateUpdate):
         super().__init__(
             new_executable, new_arguments, new_environments, new_custom_fields
         )
-        self._new_scripts = deepcopy(new_scripts) if new_scripts is not None 
else []
+        self._new_scripts = new_scripts
 
-    def get_new_scripts(self) -> List[str]:
+    def get_new_scripts(self) -> Optional[List[str]]:
         """
         Get the new scripts of the shell job template.
 
@@ -323,7 +314,7 @@ class SparkTemplateUpdate(TemplateUpdate):
 
     def __init__(
         self,
-        new_executable: str,
+        new_executable: Optional[str] = None,
         new_arguments: Optional[List[str]] = None,
         new_environments: Optional[Dict[str, str]] = None,
         new_custom_fields: Optional[Dict[str, str]] = None,
@@ -351,10 +342,10 @@ class SparkTemplateUpdate(TemplateUpdate):
             new_executable, new_arguments, new_environments, new_custom_fields
         )
         self._new_class_name = new_class_name
-        self._new_jars = deepcopy(new_jars) if new_jars is not None else []
-        self._new_files = deepcopy(new_files) if new_files is not None else []
-        self._new_archives = deepcopy(new_archives) if new_archives is not 
None else []
-        self._new_configs = deepcopy(new_configs) if new_configs is not None 
else {}
+        self._new_jars = new_jars
+        self._new_files = new_files
+        self._new_archives = new_archives
+        self._new_configs = new_configs
 
     def get_new_class_name(self) -> Optional[str]:
         """
@@ -365,7 +356,7 @@ class SparkTemplateUpdate(TemplateUpdate):
         """
         return self._new_class_name
 
-    def get_new_jars(self) -> List[str]:
+    def get_new_jars(self) -> Optional[List[str]]:
         """
         Get the new jars of the spark job template.
 
@@ -374,7 +365,7 @@ class SparkTemplateUpdate(TemplateUpdate):
         """
         return self._new_jars
 
-    def get_new_files(self) -> List[str]:
+    def get_new_files(self) -> Optional[List[str]]:
         """
         Get the new files of the spark job template.
 
@@ -383,7 +374,7 @@ class SparkTemplateUpdate(TemplateUpdate):
         """
         return self._new_files
 
-    def get_new_archives(self) -> List[str]:
+    def get_new_archives(self) -> Optional[List[str]]:
         """
         Get the new archives of the spark job template.
 
@@ -392,7 +383,7 @@ class SparkTemplateUpdate(TemplateUpdate):
         """
         return self._new_archives
 
-    def get_new_configs(self) -> Dict[str, str]:
+    def get_new_configs(self) -> Optional[Dict[str, str]]:
         """
         Get the new configs of the spark job template.
 
diff --git a/core/src/main/java/org/apache/gravitino/job/JobManager.java 
b/core/src/main/java/org/apache/gravitino/job/JobManager.java
index af02d1f655..61033b763d 100644
--- a/core/src/main/java/org/apache/gravitino/job/JobManager.java
+++ b/core/src/main/java/org/apache/gravitino/job/JobManager.java
@@ -29,6 +29,7 @@ import java.io.IOException;
 import java.net.URI;
 import java.nio.file.Files;
 import java.time.Instant;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -300,6 +301,53 @@ public class JobManager implements JobOperationDispatcher {
         });
   }
 
+  @Override
+  public JobTemplateEntity alterJobTemplate(
+      String metalake, String jobTemplateName, JobTemplateChange... changes)
+      throws NoSuchJobTemplateException, IllegalArgumentException {
+    checkMetalake(NameIdentifierUtil.ofMetalake(metalake), entityStore);
+
+    Optional<String> newName =
+        Arrays.stream(changes)
+            .filter(c -> c instanceof JobTemplateChange.RenameJobTemplate)
+            .map(c -> ((JobTemplateChange.RenameJobTemplate) c).getNewName())
+            .reduce((first, second) -> second);
+
+    NameIdentifier jobTemplateIdent = 
NameIdentifierUtil.ofJobTemplate(metalake, jobTemplateName);
+    return TreeLockUtils.doWithTreeLock(
+        jobTemplateIdent,
+        LockType.READ, // Use READ lock because the update method in 
JobTemplateMetaService will
+        // handle the update transactionally and update with a new version 
number. So we don't
+        // have to use a WRITE lock here.
+        () -> {
+          try {
+            return entityStore.update(
+                jobTemplateIdent,
+                JobTemplateEntity.class,
+                Entity.EntityType.JOB_TEMPLATE,
+                jobTemplateEntity ->
+                    updateJobTemplateEntity(jobTemplateIdent, 
jobTemplateEntity, changes));
+          } catch (NoSuchEntityException e) {
+            throw new NoSuchJobTemplateException(
+                "Job template with name %s under metalake %s does not exist, 
this could be due to"
+                    + " the job template not existing or updated concurrently. 
For the latter case"
+                    + " please retry the operation.",
+                jobTemplateName, metalake);
+          } catch (IOException ioe) {
+            throw new RuntimeException(ioe);
+          } catch (EntityAlreadyExistsException e) {
+            // If the EntityAlreadyExistsException is thrown, it means the new 
name already exists.
+            // So there should be a rename change, and the new name should be 
present.
+            throw new RuntimeException(
+                String.format(
+                    "Failed to rename job template from %s to %s under 
metalake %s, the new name "
+                        + "already exists",
+                    jobTemplateName, newName, metalake),
+                e);
+          }
+        });
+  }
+
   @Override
   public List<JobEntity> listJobs(String metalake, Optional<String> 
jobTemplateName)
       throws NoSuchJobTemplateException {
@@ -764,4 +812,125 @@ public class JobManager implements JobOperationDispatcher 
{
       throw new RuntimeException("Failed to list in-use metalakes", e);
     }
   }
+
+  @VisibleForTesting
+  JobTemplateEntity updateJobTemplateEntity(
+      NameIdentifier jobTemplateIdent,
+      JobTemplateEntity jobTemplateEntity,
+      JobTemplateChange... changes) {
+    String newName = jobTemplateEntity.name();
+    String newComment = jobTemplateEntity.comment();
+    JobTemplateEntity.Builder newTemplateBuilder = JobTemplateEntity.builder();
+    JobTemplateEntity.TemplateContent.TemplateContentBuilder 
newTemplateContentBuilder =
+        JobTemplateEntity.TemplateContent.builder()
+            .withJobType(jobTemplateEntity.templateContent().jobType())
+            .withExecutable(jobTemplateEntity.templateContent().executable())
+            .withArguments(jobTemplateEntity.templateContent().arguments())
+            
.withEnvironments(jobTemplateEntity.templateContent().environments())
+            
.withCustomFields(jobTemplateEntity.templateContent().customFields())
+            .withScripts(jobTemplateEntity.templateContent().scripts())
+            .withClassName(jobTemplateEntity.templateContent().className())
+            .withJars(jobTemplateEntity.templateContent().jars())
+            .withFiles(jobTemplateEntity.templateContent().files())
+            .withArchives(jobTemplateEntity.templateContent().archives())
+            .withConfigs(jobTemplateEntity.templateContent().configs());
+
+    for (JobTemplateChange change : changes) {
+      if (change instanceof JobTemplateChange.RenameJobTemplate) {
+        newName = ((JobTemplateChange.RenameJobTemplate) change).getNewName();
+
+      } else if (change instanceof JobTemplateChange.UpdateJobTemplateComment) 
{
+        newComment = ((JobTemplateChange.UpdateJobTemplateComment) 
change).getNewComment();
+
+      } else if (change instanceof JobTemplateChange.UpdateJobTemplate) {
+        JobTemplateEntity.TemplateContent oldTemplateContent = 
jobTemplateEntity.templateContent();
+        JobTemplateChange.TemplateUpdate templateUpdate =
+            ((JobTemplateChange.UpdateJobTemplate) change).getTemplateUpdate();
+        newTemplateContentBuilder
+            .withJobType(oldTemplateContent.jobType())
+            .withExecutable(
+                updatedValue(
+                    oldTemplateContent.executable(),
+                    Optional.ofNullable(templateUpdate.getNewExecutable())))
+            .withArguments(
+                updatedValue(
+                    oldTemplateContent.arguments(),
+                    Optional.ofNullable(templateUpdate.getNewArguments())))
+            .withEnvironments(
+                updatedValue(
+                    oldTemplateContent.environments(),
+                    Optional.ofNullable(templateUpdate.getNewEnvironments())))
+            .withCustomFields(
+                updatedValue(
+                    oldTemplateContent.customFields(),
+                    Optional.ofNullable(templateUpdate.getNewCustomFields())));
+
+        if (templateUpdate instanceof JobTemplateChange.ShellTemplateUpdate) {
+          Preconditions.checkArgument(
+              jobTemplateEntity.templateContent().jobType() == 
JobTemplate.JobType.SHELL,
+              "Job template %s is not a shell job template, cannot update to 
shell template",
+              jobTemplateIdent.name());
+
+          JobTemplateChange.ShellTemplateUpdate shellUpdate =
+              (JobTemplateChange.ShellTemplateUpdate) templateUpdate;
+          newTemplateContentBuilder.withScripts(
+              updatedValue(
+                  oldTemplateContent.scripts(), 
Optional.ofNullable(shellUpdate.getNewScripts())));
+
+        } else if (templateUpdate instanceof 
JobTemplateChange.SparkTemplateUpdate) {
+          Preconditions.checkArgument(
+              jobTemplateEntity.templateContent().jobType() == 
JobTemplate.JobType.SPARK,
+              "Job template %s is not a spark job template, cannot update to 
spark template",
+              jobTemplateIdent.name());
+
+          JobTemplateChange.SparkTemplateUpdate sparkUpdate =
+              (JobTemplateChange.SparkTemplateUpdate) templateUpdate;
+          newTemplateContentBuilder
+              .withClassName(
+                  updatedValue(
+                      oldTemplateContent.className(),
+                      Optional.ofNullable(sparkUpdate.getNewClassName())))
+              .withJars(
+                  updatedValue(
+                      oldTemplateContent.jars(), 
Optional.ofNullable(sparkUpdate.getNewJars())))
+              .withFiles(
+                  updatedValue(
+                      oldTemplateContent.files(), 
Optional.ofNullable(sparkUpdate.getNewFiles())))
+              .withArchives(
+                  updatedValue(
+                      oldTemplateContent.archives(),
+                      Optional.ofNullable(sparkUpdate.getNewArchives())))
+              .withConfigs(
+                  updatedValue(
+                      oldTemplateContent.configs(),
+                      Optional.ofNullable(sparkUpdate.getNewConfigs())));
+
+        } else {
+          throw new IllegalArgumentException("Unsupported template update: " + 
templateUpdate);
+        }
+
+      } else {
+        throw new IllegalArgumentException("Unsupported job template change: " 
+ change);
+      }
+    }
+
+    return newTemplateBuilder
+        .withId(jobTemplateEntity.id())
+        .withName(newName)
+        .withComment(newComment)
+        .withNamespace(jobTemplateIdent.namespace())
+        .withTemplateContent(newTemplateContentBuilder.build())
+        .withAuditInfo(
+            AuditInfo.builder()
+                .withCreator(jobTemplateEntity.auditInfo().creator())
+                .withCreateTime(jobTemplateEntity.auditInfo().createTime())
+                
.withLastModifier(PrincipalUtils.getCurrentPrincipal().getName())
+                .withLastModifiedTime(Instant.now())
+                .build())
+        .build();
+  }
+
+  private <T> T updatedValue(T currentValue, Optional<T> newValue) {
+    return newValue.orElse(currentValue);
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/job/JobOperationDispatcher.java 
b/core/src/main/java/org/apache/gravitino/job/JobOperationDispatcher.java
index 464b430847..795ae6aeae 100644
--- a/core/src/main/java/org/apache/gravitino/job/JobOperationDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/job/JobOperationDispatcher.java
@@ -73,6 +73,20 @@ public interface JobOperationDispatcher extends Closeable {
    */
   boolean deleteJobTemplate(String metalake, String jobTemplateName) throws 
InUseException;
 
+  /**
+   * Alters a job template by applying the specified changes in the specified 
metalake.
+   *
+   * @param metalake the name of the metalake
+   * @param jobTemplateName the name of the job template to alter
+   * @param changes the changes to apply to the job template
+   * @return the updated job template entity after applying the changes
+   * @throws NoSuchJobTemplateException if no job template with the specified 
name exists
+   * @throws IllegalArgumentException if any of the changes cannot be applied 
to the job template.
+   */
+  JobTemplateEntity alterJobTemplate(
+      String metalake, String jobTemplateName, JobTemplateChange... changes)
+      throws NoSuchJobTemplateException, IllegalArgumentException;
+
   /**
    * List all the jobs. If the jobTemplateName is provided, it will list the 
jobs associated with
    * that job template, if not, it will list all the jobs in the specified 
metalake.
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 3aa7402a18..e44cb9b2ae 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
@@ -238,6 +238,8 @@ public class JDBCBackend implements RelationalBackend {
         return (E) 
ModelVersionMetaService.getInstance().updateModelVersion(ident, updater);
       case POLICY:
         return (E) PolicyMetaService.getInstance().updatePolicy(ident, 
updater);
+      case JOB_TEMPLATE:
+        return (E) 
JobTemplateMetaService.getInstance().updateJobTemplate(ident, updater);
       default:
         throw new UnsupportedEntityTypeException(
             "Unsupported entity type: %s for update operation", entityType);
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaMapper.java
index c902880908..19af2cc44a 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaMapper.java
@@ -65,4 +65,9 @@ public interface JobTemplateMetaMapper {
       method = "deleteJobTemplateMetasByLegacyTimeline")
   Integer deleteJobTemplateMetasByLegacyTimeline(
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit);
+
+  @UpdateProvider(type = JobTemplateMetaSQLProviderFactory.class, method = 
"updateJobTemplateMeta")
+  Integer updateJobTemplateMeta(
+      @Param("newJobTemplateMeta") JobTemplatePO newJobTemplatePO,
+      @Param("oldJobTemplateMeta") JobTemplatePO oldJobTemplatePO);
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaSQLProviderFactory.java
index 64de88dc88..4dc6908e0d 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaSQLProviderFactory.java
@@ -87,4 +87,10 @@ public class JobTemplateMetaSQLProviderFactory {
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) 
{
     return 
getProvider().deleteJobTemplateMetasByLegacyTimeline(legacyTimeline, limit);
   }
+
+  public static String updateJobTemplateMeta(
+      @Param("newJobTemplateMeta") JobTemplatePO newJobTemplatePO,
+      @Param("oldJobTemplateMeta") JobTemplatePO oldJobTemplatePO) {
+    return getProvider().updateJobTemplateMeta(newJobTemplatePO, 
oldJobTemplatePO);
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/JobTemplateMetaBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/JobTemplateMetaBaseSQLProvider.java
index 43111d3efd..3f6a1082b5 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/JobTemplateMetaBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/JobTemplateMetaBaseSQLProvider.java
@@ -120,4 +120,25 @@ public class JobTemplateMetaBaseSQLProvider {
         + JobTemplateMetaMapper.TABLE_NAME
         + " WHERE deleted_at < #{legacyTimeline} AND deleted_at > 0 LIMIT 
#{limit}";
   }
+
+  public String updateJobTemplateMeta(
+      @Param("newJobTemplateMeta") JobTemplatePO newJobTemplatePO,
+      @Param("oldJobTemplateMeta") JobTemplatePO oldJobTemplatePO) {
+    return "UPDATE "
+        + JobTemplateMetaMapper.TABLE_NAME
+        + " SET job_template_name = #{newJobTemplateMeta.jobTemplateName},"
+        + " metalake_id = #{newJobTemplateMeta.metalakeId},"
+        + " job_template_comment = #{newJobTemplateMeta.jobTemplateComment},"
+        + " job_template_content = #{newJobTemplateMeta.jobTemplateContent},"
+        + " audit_info = #{newJobTemplateMeta.auditInfo},"
+        + " current_version = #{newJobTemplateMeta.currentVersion},"
+        + " last_version = #{newJobTemplateMeta.lastVersion},"
+        + " deleted_at = #{newJobTemplateMeta.deletedAt}"
+        + " WHERE job_template_id = #{oldJobTemplateMeta.jobTemplateId}"
+        + " AND job_template_name = #{oldJobTemplateMeta.jobTemplateName}"
+        + " AND metalake_id = #{oldJobTemplateMeta.metalakeId}"
+        + " AND current_version = #{oldJobTemplateMeta.currentVersion}"
+        + " AND last_version = #{oldJobTemplateMeta.lastVersion}"
+        + " AND deleted_at = 0";
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/po/JobTemplatePO.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/JobTemplatePO.java
index 6efd8fb141..f997ad5df9 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/po/JobTemplatePO.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/JobTemplatePO.java
@@ -111,6 +111,31 @@ public class JobTemplatePO {
     }
   }
 
+  public static JobTemplatePO updateJobTemplatePO(
+      JobTemplatePO oldJobTemplatePO,
+      JobTemplateEntity newJobTemplateEntity,
+      JobTemplatePOBuilder builder) {
+    try {
+      Long lastVersion = oldJobTemplatePO.lastVersion() + 1;
+      Long currentVersion = lastVersion;
+
+      return builder
+          .withJobTemplateId(newJobTemplateEntity.id())
+          .withJobTemplateName(newJobTemplateEntity.name())
+          .withJobTemplateComment(newJobTemplateEntity.comment())
+          .withJobTemplateContent(
+              
JsonUtils.anyFieldMapper().writeValueAsString(newJobTemplateEntity.templateContent()))
+          .withAuditInfo(
+              
JsonUtils.anyFieldMapper().writeValueAsString(newJobTemplateEntity.auditInfo()))
+          .withCurrentVersion(currentVersion)
+          .withLastVersion(lastVersion)
+          .withDeletedAt(DEFAULT_DELETED_AT)
+          .build();
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException("Failed to serialize job template entity", e);
+    }
+  }
+
   public static JobTemplateEntity fromJobTemplatePO(
       JobTemplatePO jobTemplatePO, Namespace namespace) {
     try {
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
index a042d8ba70..203773f371 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
@@ -18,12 +18,16 @@
  */
 package org.apache.gravitino.storage.relational.service;
 
+import com.google.common.base.Preconditions;
 import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.apache.gravitino.Entity;
+import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
@@ -59,21 +63,7 @@ public class JobTemplateMetaService {
   }
 
   public JobTemplateEntity getJobTemplateByIdentifier(NameIdentifier 
jobTemplateIdent) {
-    String metalakeName = jobTemplateIdent.namespace().level(0);
-    String jobTemplateName = jobTemplateIdent.name();
-
-    JobTemplatePO jobTemplatePO =
-        SessionUtils.getWithoutCommit(
-            JobTemplateMetaMapper.class,
-            mapper -> 
mapper.selectJobTemplatePOByMetalakeAndName(metalakeName, jobTemplateName));
-
-    if (jobTemplatePO == null) {
-      throw new NoSuchEntityException(
-          NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
-          Entity.EntityType.JOB_TEMPLATE.name().toLowerCase(Locale.ROOT),
-          jobTemplateName);
-    }
-
+    JobTemplatePO jobTemplatePO = getJobTemplatePO(jobTemplateIdent);
     return JobTemplatePO.fromJobTemplatePO(jobTemplatePO, 
jobTemplateIdent.namespace());
   }
 
@@ -130,4 +120,67 @@ public class JobTemplateMetaService {
         JobTemplateMetaMapper.class,
         mapper -> 
mapper.deleteJobTemplateMetasByLegacyTimeline(legacyTimeline, limit));
   }
+
+  public <E extends Entity & HasIdentifier> JobTemplateEntity 
updateJobTemplate(
+      NameIdentifier jobTemplateIdent, Function<E, E> updater) throws 
IOException {
+    JobTemplatePO oldJobTemplatePO = getJobTemplatePO(jobTemplateIdent);
+    JobTemplateEntity oldJobTemplateEntity =
+        JobTemplatePO.fromJobTemplatePO(oldJobTemplatePO, 
jobTemplateIdent.namespace());
+    JobTemplateEntity newJobTemplateEntity =
+        (JobTemplateEntity) updater.apply((E) oldJobTemplateEntity);
+    Preconditions.checkArgument(
+        Objects.equals(oldJobTemplateEntity.id(), newJobTemplateEntity.id()),
+        "The updated job templated id: %s is not equal to the old one: %s, 
which is unexpected",
+        newJobTemplateEntity.id(),
+        oldJobTemplateEntity.id());
+
+    JobTemplatePO.JobTemplatePOBuilder newBuilder =
+        JobTemplatePO.builder().withMetalakeId(oldJobTemplatePO.metalakeId());
+    JobTemplatePO newJobTemplatePO =
+        JobTemplatePO.updateJobTemplatePO(oldJobTemplatePO, 
newJobTemplateEntity, newBuilder);
+
+    Integer result;
+    try {
+      result =
+          SessionUtils.doWithCommitAndFetchResult(
+              JobTemplateMetaMapper.class,
+              mapper -> mapper.updateJobTemplateMeta(newJobTemplatePO, 
oldJobTemplatePO));
+    } catch (RuntimeException e) {
+      ExceptionUtils.checkSQLException(
+          e, Entity.EntityType.JOB_TEMPLATE, oldJobTemplateEntity.name());
+      throw e;
+    }
+
+    if (result == null || result == 0) {
+      throw new NoSuchEntityException(
+          NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+          Entity.EntityType.JOB_TEMPLATE.name().toLowerCase(Locale.ROOT),
+          oldJobTemplateEntity.name());
+    } else if (result > 1) {
+      throw new IOException(
+          String.format(
+              "Failed to update job template: %s, because more than one rows 
are updated: %d",
+              oldJobTemplateEntity.name(), result));
+    } else {
+      return newJobTemplateEntity;
+    }
+  }
+
+  private JobTemplatePO getJobTemplatePO(NameIdentifier jobTemplateIdent) {
+    String metalakeName = jobTemplateIdent.namespace().level(0);
+    String jobTemplateName = jobTemplateIdent.name();
+
+    JobTemplatePO jobTemplatePO =
+        SessionUtils.getWithoutCommit(
+            JobTemplateMetaMapper.class,
+            mapper -> 
mapper.selectJobTemplatePOByMetalakeAndName(metalakeName, jobTemplateName));
+
+    if (jobTemplatePO == null) {
+      throw new NoSuchEntityException(
+          NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+          Entity.EntityType.JOB_TEMPLATE.name().toLowerCase(Locale.ROOT),
+          jobTemplateName);
+    }
+    return jobTemplatePO;
+  }
 }
diff --git a/core/src/test/java/org/apache/gravitino/job/TestJobManager.java 
b/core/src/test/java/org/apache/gravitino/job/TestJobManager.java
index a95cec8466..674e68cf9a 100644
--- a/core/src/test/java/org/apache/gravitino/job/TestJobManager.java
+++ b/core/src/test/java/org/apache/gravitino/job/TestJobManager.java
@@ -595,6 +595,221 @@ public class TestJobManager {
             });
   }
 
+  @Test
+  public void testUpdateShellJobTemplateEntity() {
+    String jobTemplateName = "old_shell_job";
+    String jobTemplateComment = "An old shell job template";
+    JobTemplateEntity oldJobTemplateEntity =
+        newShellJobTemplateEntity(jobTemplateName, jobTemplateComment);
+
+    // Update name and comment
+    String newJobTemplateName = "new_shell_job";
+    String newJobTemplateComment = "A new shell job template";
+    JobTemplateChange rename = JobTemplateChange.rename(newJobTemplateName);
+    JobTemplateChange updateComment = 
JobTemplateChange.updateComment(newJobTemplateComment);
+
+    JobTemplateEntity newJobTemplateEntity =
+        jobManager.updateJobTemplateEntity(
+            oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
rename, updateComment);
+
+    Assertions.assertEquals(oldJobTemplateEntity.id(), 
newJobTemplateEntity.id());
+    Assertions.assertEquals(newJobTemplateName, newJobTemplateEntity.name());
+    Assertions.assertEquals(oldJobTemplateEntity.namespace(), 
newJobTemplateEntity.namespace());
+    Assertions.assertEquals(newJobTemplateComment, 
newJobTemplateEntity.comment());
+    Assertions.assertEquals(
+        oldJobTemplateEntity.templateContent(), 
newJobTemplateEntity.templateContent());
+
+    // Update the executable of the shell job template
+    JobTemplateChange updateShellTemplate =
+        JobTemplateChange.updateTemplate(
+            
JobTemplateChange.ShellTemplateUpdate.builder().withNewExecutable("/bin/ls").build());
+
+    newJobTemplateEntity =
+        jobManager.updateJobTemplateEntity(
+            oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
updateShellTemplate);
+    Assertions.assertEquals(oldJobTemplateEntity.id(), 
newJobTemplateEntity.id());
+    Assertions.assertEquals(oldJobTemplateEntity.name(), 
newJobTemplateEntity.name());
+    Assertions.assertEquals(oldJobTemplateEntity.namespace(), 
newJobTemplateEntity.namespace());
+    Assertions.assertEquals(oldJobTemplateEntity.comment(), 
newJobTemplateEntity.comment());
+    Assertions.assertNotEquals(
+        oldJobTemplateEntity.templateContent(), 
newJobTemplateEntity.templateContent());
+    JobTemplateEntity.TemplateContent oldContent = 
oldJobTemplateEntity.templateContent();
+    JobTemplateEntity.TemplateContent newContent = 
newJobTemplateEntity.templateContent();
+    Assertions.assertEquals(oldContent.jobType(), newContent.jobType());
+    Assertions.assertEquals("/bin/ls", newContent.executable());
+    Assertions.assertEquals(oldContent.arguments(), newContent.arguments());
+    Assertions.assertEquals(oldContent.environments(), 
newContent.environments());
+    Assertions.assertEquals(oldContent.customFields(), 
newContent.customFields());
+
+    // Update the arguments, environments, custom fields of the shell job 
template
+    JobTemplateChange updateShellTemplate2 =
+        JobTemplateChange.updateTemplate(
+            JobTemplateChange.ShellTemplateUpdate.builder()
+                .withNewArguments(ImmutableList.of("arg1", "arg2"))
+                .withNewEnvironments(Collections.singletonMap("env1", 
"value1"))
+                .withNewCustomFields(Collections.singletonMap("field1", 
"value1"))
+                .build());
+    newJobTemplateEntity =
+        jobManager.updateJobTemplateEntity(
+            oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
updateShellTemplate2);
+
+    JobTemplateEntity.TemplateContent newContent2 = 
newJobTemplateEntity.templateContent();
+    Assertions.assertEquals(oldContent.jobType(), newContent2.jobType());
+    Assertions.assertEquals(oldContent.executable(), newContent2.executable());
+    Assertions.assertEquals(ImmutableList.of("arg1", "arg2"), 
newContent2.arguments());
+    Assertions.assertEquals(Collections.singletonMap("env1", "value1"), 
newContent2.environments());
+    Assertions.assertEquals(
+        Collections.singletonMap("field1", "value1"), 
newContent2.customFields());
+    Assertions.assertEquals(oldContent.scripts(), newContent2.scripts());
+
+    // Update the scripts of the shell job template
+    JobTemplateChange updateShellTemplate3 =
+        JobTemplateChange.updateTemplate(
+            JobTemplateChange.ShellTemplateUpdate.builder()
+                .withNewScripts(ImmutableList.of("echo Hello", "echo World"))
+                .build());
+    newJobTemplateEntity =
+        jobManager.updateJobTemplateEntity(
+            oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
updateShellTemplate3);
+
+    JobTemplateEntity.TemplateContent newContent3 = 
newJobTemplateEntity.templateContent();
+    Assertions.assertEquals(oldContent.jobType(), newContent3.jobType());
+    Assertions.assertEquals(oldContent.executable(), newContent3.executable());
+    Assertions.assertEquals(oldContent.arguments(), newContent3.arguments());
+    Assertions.assertEquals(oldContent.environments(), 
newContent3.environments());
+    Assertions.assertEquals(oldContent.customFields(), 
newContent3.customFields());
+    Assertions.assertEquals(ImmutableList.of("echo Hello", "echo World"), 
newContent3.scripts());
+
+    // Update with no changes
+    JobTemplateChange noChange =
+        
JobTemplateChange.updateTemplate(JobTemplateChange.ShellTemplateUpdate.builder().build());
+    newJobTemplateEntity =
+        jobManager.updateJobTemplateEntity(
+            oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
noChange);
+    Assertions.assertEquals(
+        oldJobTemplateEntity.templateContent(), 
newJobTemplateEntity.templateContent());
+
+    // Update job template with SparkJobTemplateChange should throw 
IllegalArgumentException
+    JobTemplateChange invalidChange =
+        
JobTemplateChange.updateTemplate(JobTemplateChange.SparkTemplateUpdate.builder().build());
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            jobManager.updateJobTemplateEntity(
+                oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
invalidChange));
+  }
+
+  @Test
+  public void testUpdateSparkJobTemplateEntity() {
+    String jobTemplateName = "old_spark_job";
+    String jobTemplateComment = "An old spark job template";
+    JobTemplateEntity oldJobTemplateEntity =
+        newSparkJobTemplateEntity(jobTemplateName, jobTemplateComment);
+
+    // Update the executable and class name of the spark job template
+    JobTemplateChange updateSparkTemplate =
+        JobTemplateChange.updateTemplate(
+            JobTemplateChange.SparkTemplateUpdate.builder()
+                .withNewExecutable("file:/new/path/to/spark-examples.jar")
+                .withNewClassName("org.apache.spark.examples.SparkWordCount")
+                .build());
+    JobTemplateEntity newJobTemplateEntity =
+        jobManager.updateJobTemplateEntity(
+            oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
updateSparkTemplate);
+    Assertions.assertEquals(oldJobTemplateEntity.id(), 
newJobTemplateEntity.id());
+    Assertions.assertEquals(oldJobTemplateEntity.name(), 
newJobTemplateEntity.name());
+    Assertions.assertEquals(oldJobTemplateEntity.namespace(), 
newJobTemplateEntity.namespace());
+    Assertions.assertEquals(oldJobTemplateEntity.comment(), 
newJobTemplateEntity.comment());
+    Assertions.assertNotEquals(
+        oldJobTemplateEntity.templateContent(), 
newJobTemplateEntity.templateContent());
+    JobTemplateEntity.TemplateContent oldContent = 
oldJobTemplateEntity.templateContent();
+    JobTemplateEntity.TemplateContent newContent = 
newJobTemplateEntity.templateContent();
+    Assertions.assertEquals(oldContent.jobType(), newContent.jobType());
+    Assertions.assertEquals("file:/new/path/to/spark-examples.jar", 
newContent.executable());
+    Assertions.assertEquals("org.apache.spark.examples.SparkWordCount", 
newContent.className());
+    Assertions.assertEquals(oldContent.arguments(), newContent.arguments());
+    Assertions.assertEquals(oldContent.environments(), 
newContent.environments());
+    Assertions.assertEquals(oldContent.customFields(), 
newContent.customFields());
+    Assertions.assertEquals(oldContent.jars(), newContent.jars());
+    Assertions.assertEquals(oldContent.files(), newContent.files());
+    Assertions.assertEquals(oldContent.archives(), newContent.archives());
+    Assertions.assertEquals(oldContent.configs(), newContent.configs());
+
+    // Update the arguments, environments, custom fields of the spark job 
template
+    JobTemplateChange updateSparkTemplate2 =
+        JobTemplateChange.updateTemplate(
+            JobTemplateChange.SparkTemplateUpdate.builder()
+                .withNewArguments(ImmutableList.of("arg1", "arg2"))
+                .withNewEnvironments(Collections.singletonMap("env1", 
"value1"))
+                .withNewCustomFields(Collections.singletonMap("field1", 
"value1"))
+                .build());
+    newJobTemplateEntity =
+        jobManager.updateJobTemplateEntity(
+            oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
updateSparkTemplate2);
+    JobTemplateEntity.TemplateContent newContent2 = 
newJobTemplateEntity.templateContent();
+    Assertions.assertEquals(oldContent.jobType(), newContent2.jobType());
+    Assertions.assertEquals(oldContent.executable(), newContent2.executable());
+    Assertions.assertEquals(oldContent.className(), newContent2.className());
+    Assertions.assertEquals(ImmutableList.of("arg1", "arg2"), 
newContent2.arguments());
+    Assertions.assertEquals(Collections.singletonMap("env1", "value1"), 
newContent2.environments());
+    Assertions.assertEquals(
+        Collections.singletonMap("field1", "value1"), 
newContent2.customFields());
+    Assertions.assertEquals(oldContent.jars(), newContent2.jars());
+    Assertions.assertEquals(oldContent.files(), newContent2.files());
+    Assertions.assertEquals(oldContent.archives(), newContent2.archives());
+    Assertions.assertEquals(oldContent.configs(), newContent2.configs());
+
+    // Update the jars, files, archives, configs of the spark job template
+    JobTemplateChange updateSparkTemplate3 =
+        JobTemplateChange.updateTemplate(
+            JobTemplateChange.SparkTemplateUpdate.builder()
+                .withNewJars(ImmutableList.of("file:/new/path/to/jar1 ", 
"file:/new/path/to/jar2"))
+                .withNewFiles(
+                    ImmutableList.of("file:/new/path/to/file1", 
"file:/new/path/to/file2"))
+                .withNewArchives(
+                    ImmutableList.of("file:/new/path/to/archive1", 
"file:/new/path/to/archive2"))
+                
.withNewConfigs(Collections.singletonMap("spark.executor.memory", "4g"))
+                .build());
+    newJobTemplateEntity =
+        jobManager.updateJobTemplateEntity(
+            oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
updateSparkTemplate3);
+    JobTemplateEntity.TemplateContent newContent3 = 
newJobTemplateEntity.templateContent();
+    Assertions.assertEquals(oldContent.jobType(), newContent3.jobType());
+    Assertions.assertEquals(oldContent.executable(), newContent3.executable());
+    Assertions.assertEquals(oldContent.className(), newContent3.className());
+    Assertions.assertEquals(oldContent.arguments(), newContent3.arguments());
+    Assertions.assertEquals(oldContent.environments(), 
newContent3.environments());
+    Assertions.assertEquals(oldContent.customFields(), 
newContent3.customFields());
+    Assertions.assertEquals(
+        ImmutableList.of("file:/new/path/to/jar1 ", "file:/new/path/to/jar2"), 
newContent3.jars());
+    Assertions.assertEquals(
+        ImmutableList.of("file:/new/path/to/file1", "file:/new/path/to/file2"),
+        newContent3.files());
+    Assertions.assertEquals(
+        ImmutableList.of("file:/new/path/to/archive1", 
"file:/new/path/to/archive2"),
+        newContent3.archives());
+    Assertions.assertEquals(
+        Collections.singletonMap("spark.executor.memory", "4g"), 
newContent3.configs());
+
+    // Update with no changes
+    JobTemplateChange noChange =
+        
JobTemplateChange.updateTemplate(JobTemplateChange.SparkTemplateUpdate.builder().build());
+    newJobTemplateEntity =
+        jobManager.updateJobTemplateEntity(
+            oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
noChange);
+    Assertions.assertEquals(
+        oldJobTemplateEntity.templateContent(), 
newJobTemplateEntity.templateContent());
+
+    // Update job template with ShellJobTemplateChange should throw 
IllegalArgumentException
+    JobTemplateChange invalidChange =
+        
JobTemplateChange.updateTemplate(JobTemplateChange.ShellTemplateUpdate.builder().build());
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            jobManager.updateJobTemplateEntity(
+                oldJobTemplateEntity.nameIdentifier(), oldJobTemplateEntity, 
invalidChange));
+  }
+
   private static JobTemplateEntity newShellJobTemplateEntity(String name, 
String comment) {
     ShellJobTemplate shellJobTemplate =
         ShellJobTemplate.builder()
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobTemplateMetaService.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobTemplateMetaService.java
index 2eb21c7620..70f1bf0864 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobTemplateMetaService.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobTemplateMetaService.java
@@ -209,6 +209,75 @@ public class TestJobTemplateMetaService extends 
TestJDBCBackend {
     Assertions.assertEquals(0, jobs.size());
   }
 
+  @Test
+  public void testUpdateJobTemplate() throws IOException {
+    BaseMetalake metalake =
+        createBaseMakeLake(RandomIdGenerator.INSTANCE.nextId(), METALAKE_NAME, 
AUDIT_INFO);
+    backend.insert(metalake, false);
+
+    JobTemplateMetaService jobTemplateMetaService = 
JobTemplateMetaService.getInstance();
+
+    JobTemplateEntity jobTemplateEntity =
+        newShellJobTemplateEntity("updatable_template", "An updatable job 
template", METALAKE_NAME);
+    jobTemplateMetaService.insertJobTemplate(jobTemplateEntity, false);
+
+    // Update the job template's comment
+    JobTemplateEntity updatedJobTemplateEntity =
+        JobTemplateEntity.builder()
+            .withId(jobTemplateEntity.id())
+            .withName("updated_template")
+            .withNamespace(jobTemplateEntity.namespace())
+            .withTemplateContent(jobTemplateEntity.templateContent())
+            .withComment("Updated comment for the job template")
+            .withAuditInfo(jobTemplateEntity.auditInfo())
+            .build();
+
+    jobTemplateMetaService.updateJobTemplate(
+        jobTemplateEntity.nameIdentifier(), e -> updatedJobTemplateEntity);
+
+    // Verify the update
+    JobTemplateEntity fetchedJobTemplate =
+        jobTemplateMetaService.getJobTemplateByIdentifier(
+            NameIdentifierUtil.ofJobTemplate(METALAKE_NAME, 
"updated_template"));
+
+    Assertions.assertEquals(updatedJobTemplateEntity, fetchedJobTemplate);
+
+    // Verify that the old name no longer exists
+    Assertions.assertThrows(
+        NoSuchEntityException.class,
+        () ->
+            jobTemplateMetaService.getJobTemplateByIdentifier(
+                NameIdentifierUtil.ofJobTemplate(METALAKE_NAME, 
"updatable_template")));
+
+    // Test update an non-existent job template
+    Assertions.assertThrows(
+        NoSuchEntityException.class,
+        () ->
+            jobTemplateMetaService.updateJobTemplate(
+                NameIdentifierUtil.ofJobTemplate(METALAKE_NAME, 
"non_existent_template"),
+                e -> updatedJobTemplateEntity));
+
+    // Test update to a name that already exists
+    JobTemplateEntity anotherJobTemplateEntity =
+        newSparkJobTemplateEntity("existing_template", "An existing job 
template", METALAKE_NAME);
+    jobTemplateMetaService.insertJobTemplate(anotherJobTemplateEntity, false);
+    JobTemplateEntity duplicateNameJobTemplateEntity =
+        JobTemplateEntity.builder()
+            .withId(updatedJobTemplateEntity.id())
+            .withName("existing_template")
+            .withNamespace(updatedJobTemplateEntity.namespace())
+            .withTemplateContent(updatedJobTemplateEntity.templateContent())
+            .withComment("Trying to update to an existing name")
+            .withAuditInfo(updatedJobTemplateEntity.auditInfo())
+            .build();
+
+    Assertions.assertThrows(
+        EntityAlreadyExistsException.class,
+        () ->
+            jobTemplateMetaService.updateJobTemplate(
+                updatedJobTemplateEntity.nameIdentifier(), e -> 
duplicateNameJobTemplateEntity));
+  }
+
   static JobTemplateEntity newShellJobTemplateEntity(String name, String 
comment, String metalake) {
     ShellJobTemplate shellJobTemplate =
         ShellJobTemplate.builder()

Reply via email to