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

jerryshao 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 f47d6d1155 [#12776] feat(core): Add OCC for policy metadata (#12782)
f47d6d1155 is described below

commit f47d6d1155db57a5b2bb70075da305d789d4b063
Author: Qi Yu <[email protected]>
AuthorDate: Tue Sep 8 16:04:23 2026 +0800

    [#12776] feat(core): Add OCC for policy metadata (#12782)
    
    ### What changes were proposed in this pull request?
    
    - Apply stable-ID and expected-version CAS to policy alter, overwrite,
    and delete operations.
    - Create a complete immutable policy snapshot for every new version.
    - Preserve monotonic policy version history during overwrite.
    - Lock parent metalakes and policy roots during relationship changes.
    - Atomically clean policy versions and dependent relationships during
    deletion.
    - Reuse the common OCC support introduced by #12639.
    
    ### Why are the changes needed?
    
    Concurrent policy writes could otherwise cause lost updates, incomplete
    snapshots, orphan versions, or partial relationship cleanup.
    
    Fix: #12776
    
    ### Does this PR introduce _any_ user-facing change?
    
    No API or configuration change. Concurrent stale writes now fail with an
    optimistic lock exception, and policy overwrite retains version history.
    
    ### How was this patch tested?
    
    - `./gradlew :core:spotlessApply`
    - `env dockerTest=false ./gradlew :core:test --tests
    org.apache.gravitino.storage.relational.service.TestPolicyMetaService
    --tests
    org.apache.gravitino.storage.relational.service.TestPolicyTagRelService
    --tests
    org.apache.gravitino.storage.relational.service.TestTagMetaService
    -PskipDockerTests=true`
    
    New tests: a metadata-only alter writing a complete snapshot, an
    overwrite advancing the version while keeping history, an alter conflict
    leaving no orphan version, a stale delete rolling back the relationship
    cleanup, parent-metalake fencing on create, an overwrite landing on a
    name held by another row, and the full cascade cleanup a delete
    performs.
    
    Only H2 runs locally; MySQL and PostgreSQL coverage comes from CI.
---
 .../relational/mapper/PolicyMetaMapper.java        |  96 +++-
 .../mapper/PolicyMetaSQLProviderFactory.java       |  28 +-
 .../mapper/PolicyMetadataObjectRelMapper.java      |  11 +-
 .../PolicyMetadataObjectRelSQLProviderFactory.java |   8 +-
 .../relational/mapper/PolicyTagRelMapper.java      |   9 +
 .../mapper/PolicyTagRelSQLProviderFactory.java     |   5 +
 .../relational/mapper/PolicyVersionMapper.java     |  17 +-
 .../mapper/PolicyVersionSQLProviderFactory.java    |  11 +-
 .../provider/base/PolicyMetaBaseSQLProvider.java   |  92 ++--
 .../PolicyMetadataObjectRelBaseSQLProvider.java    |  16 +-
 .../provider/base/PolicyTagRelBaseSQLProvider.java |   9 +
 .../base/PolicyVersionBaseSQLProvider.java         |  41 +-
 .../postgresql/PolicyMetaPostgreSQLProvider.java   |  33 +-
 .../PolicyMetadataObjectRelPostgreSQLProvider.java |  12 +-
 .../PolicyVersionPostgreSQLProvider.java           |  38 +-
 .../relational/service/OccWriteSupport.java        |  14 +
 .../relational/service/PolicyMetaService.java      | 587 +++++++++++++++------
 .../relational/service/PolicyTagRelService.java    |  14 +-
 .../storage/relational/utils/POConverters.java     | 129 +++--
 .../relational/service/TestOccWriteSupport.java    |  17 +
 .../relational/service/TestPolicyMetaService.java  | 559 +++++++++++++++++++-
 21 files changed, 1311 insertions(+), 435 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetaMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetaMapper.java
index 9862c5f151..eca19cb7fb 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetaMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetaMapper.java
@@ -25,12 +25,25 @@ import org.apache.ibatis.annotations.InsertProvider;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Result;
 import org.apache.ibatis.annotations.Results;
+import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.SelectProvider;
 import org.apache.ibatis.annotations.UpdateProvider;
 
 public interface PolicyMetaMapper {
   String POLICY_META_TABLE_NAME = "policy_meta";
 
+  /**
+   * Checks whether a soft-deleted policy still owns the requested primary key.
+   *
+   * @param policyId the policy ID
+   * @return one if a deleted row reserves the ID, otherwise zero
+   */
+  @Select(
+      "SELECT COUNT(*) FROM "
+          + POLICY_META_TABLE_NAME
+          + " WHERE policy_id = #{policyId} AND deleted_at > 0")
+  int countDeletedPolicyMetasById(@Param("policyId") Long policyId);
+
   @Results({
     @Result(property = "policyId", column = "policy_id"),
     @Result(property = "policyName", column = "policy_name"),
@@ -76,11 +89,6 @@ public interface PolicyMetaMapper {
   List<PolicyPO> listPolicyPOsByMetalakeAndPolicyNames(
       @Param("metalakeName") String metalakeName, @Param("policyNames") 
List<String> policyNames);
 
-  @InsertProvider(
-      type = PolicyMetaSQLProviderFactory.class,
-      method = "insertPolicyMetaOnDuplicateKeyUpdate")
-  void insertPolicyMetaOnDuplicateKeyUpdate(@Param("policyMeta") PolicyPO 
policyPO);
-
   @InsertProvider(type = PolicyMetaSQLProviderFactory.class, method = 
"insertPolicyMeta")
   void insertPolicyMeta(@Param("policyMeta") PolicyPO policyPO);
 
@@ -113,11 +121,18 @@ public interface PolicyMetaMapper {
       @Param("newPolicyMeta") PolicyPO newPolicyMeta,
       @Param("oldPolicyMeta") PolicyPO oldPolicyMeta);
 
+  /**
+   * Soft-deletes an active policy when its OCC version still matches.
+   *
+   * @param policyId The policy ID.
+   * @param currentVersion The version observed by the caller.
+   * @return The number of affected rows.
+   */
   @UpdateProvider(
       type = PolicyMetaSQLProviderFactory.class,
-      method = "softDeletePolicyByMetalakeAndPolicyName")
-  Integer softDeletePolicyByMetalakeAndPolicyName(
-      @Param("metalakeName") String metalakeName, @Param("policyName") String 
policyName);
+      method = "softDeletePolicyByIdAndVersion")
+  Integer softDeletePolicyByIdAndVersion(
+      @Param("policyId") Long policyId, @Param("currentVersion") Long 
currentVersion);
 
   @UpdateProvider(
       type = PolicyMetaSQLProviderFactory.class,
@@ -146,6 +161,29 @@ public interface PolicyMetaMapper {
   PolicyPO selectPolicyMetaByMetalakeIdAndName(
       @Param("metalakeId") long metalakeId, @Param("policyName") String 
policyName);
 
+  /**
+   * Selects and exclusively locks an active policy by its natural key.
+   *
+   * @param metalakeId The metalake ID.
+   * @param policyName The policy name.
+   * @return The locked policy, or null if the natural key is not active.
+   */
+  @Results({
+    @Result(property = "policyId", column = "policy_id"),
+    @Result(property = "policyName", column = "policy_name"),
+    @Result(property = "policyType", column = "policy_type"),
+    @Result(property = "metalakeId", column = "metalake_id"),
+    @Result(property = "auditInfo", column = "audit_info"),
+    @Result(property = "currentVersion", column = "current_version"),
+    @Result(property = "lastVersion", column = "last_version"),
+    @Result(property = "deletedAt", column = "deleted_at")
+  })
+  @SelectProvider(
+      type = PolicyMetaSQLProviderFactory.class,
+      method = "selectPolicyMetaByMetalakeIdAndNameForUpdate")
+  PolicyPO selectPolicyMetaByMetalakeIdAndNameForUpdate(
+      @Param("metalakeId") long metalakeId, @Param("policyName") String 
policyName);
+
   @Results({
     @Result(property = "policyId", column = "policy_id"),
     @Result(property = "policyName", column = "policy_name"),
@@ -159,6 +197,27 @@ public interface PolicyMetaMapper {
   @SelectProvider(type = PolicyMetaSQLProviderFactory.class, method = 
"selectPolicyByPolicyId")
   PolicyPO selectPolicyByPolicyId(@Param("policyId") Long policyId);
 
+  /**
+   * Selects and exclusively locks an active policy by ID.
+   *
+   * @param policyId The policy ID.
+   * @return The locked policy, or null if it is not active.
+   */
+  @Results({
+    @Result(property = "policyId", column = "policy_id"),
+    @Result(property = "policyName", column = "policy_name"),
+    @Result(property = "policyType", column = "policy_type"),
+    @Result(property = "metalakeId", column = "metalake_id"),
+    @Result(property = "auditInfo", column = "audit_info"),
+    @Result(property = "currentVersion", column = "current_version"),
+    @Result(property = "lastVersion", column = "last_version"),
+    @Result(property = "deletedAt", column = "deleted_at")
+  })
+  @SelectProvider(
+      type = PolicyMetaSQLProviderFactory.class,
+      method = "selectPolicyByPolicyIdForUpdate")
+  PolicyPO selectPolicyByPolicyIdForUpdate(@Param("policyId") Long policyId);
+
   @Results({
     @Result(property = "policyId", column = "policy_id"),
     @Result(property = "policyName", column = "policy_name"),
@@ -172,6 +231,27 @@ public interface PolicyMetaMapper {
   @SelectProvider(type = PolicyMetaSQLProviderFactory.class, method = 
"listPolicyPOsByPolicyIds")
   List<PolicyPO> listPolicyPOsByPolicyIds(@Param("policyIds") List<Long> 
policyIds);
 
+  /**
+   * Selects and exclusively locks the active policies with the given IDs, in 
ascending ID order.
+   *
+   * @param policyIds The policy IDs to lock.
+   * @return The locked policies. Policies that are not active are absent from 
the result.
+   */
+  @Results({
+    @Result(property = "policyId", column = "policy_id"),
+    @Result(property = "policyName", column = "policy_name"),
+    @Result(property = "policyType", column = "policy_type"),
+    @Result(property = "metalakeId", column = "metalake_id"),
+    @Result(property = "auditInfo", column = "audit_info"),
+    @Result(property = "currentVersion", column = "current_version"),
+    @Result(property = "lastVersion", column = "last_version"),
+    @Result(property = "deletedAt", column = "deleted_at")
+  })
+  @SelectProvider(
+      type = PolicyMetaSQLProviderFactory.class,
+      method = "listPolicyPOsByPolicyIdsForUpdate")
+  List<PolicyPO> listPolicyPOsByPolicyIdsForUpdate(@Param("policyIds") 
List<Long> policyIds);
+
   @Results({
     @Result(property = "policyId", column = "policy_id"),
     @Result(property = "policyName", column = "policy_name"),
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetaSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetaSQLProviderFactory.java
index 4f7b1d1177..807badd1fb 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetaSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetaSQLProviderFactory.java
@@ -57,11 +57,6 @@ public class PolicyMetaSQLProviderFactory {
     return getProvider().listPolicyPOsByMetalakeAndPolicyNames(metalakeName, 
policyNames);
   }
 
-  public static String insertPolicyMetaOnDuplicateKeyUpdate(
-      @Param("policyMeta") PolicyPO policyPO) {
-    return getProvider().insertPolicyMetaOnDuplicateKeyUpdate(policyPO);
-  }
-
   public static String insertPolicyMeta(@Param("policyMeta") PolicyPO 
policyPO) {
     return getProvider().insertPolicyMeta(policyPO);
   }
@@ -77,9 +72,10 @@ public class PolicyMetaSQLProviderFactory {
     return getProvider().updatePolicyMeta(newPolicyMeta, oldPolicyMeta);
   }
 
-  public static String softDeletePolicyByMetalakeAndPolicyName(
-      @Param("metalakeName") String metalakeName, @Param("policyName") String 
policyName) {
-    return getProvider().softDeletePolicyByMetalakeAndPolicyName(metalakeName, 
policyName);
+  /** Delegates a version-checked policy soft delete. */
+  public static String softDeletePolicyByIdAndVersion(
+      @Param("policyId") Long policyId, @Param("currentVersion") Long 
currentVersion) {
+    return getProvider().softDeletePolicyByIdAndVersion(policyId, 
currentVersion);
   }
 
   public static String deletePolicyMetasByLegacyTimeline(
@@ -96,10 +92,26 @@ public class PolicyMetaSQLProviderFactory {
     return getProvider().selectPolicyMetaByMetalakeIdAndName(metalakeId, 
policyName);
   }
 
+  /** Delegates an exclusive-lock policy query by natural key. */
+  public static String selectPolicyMetaByMetalakeIdAndNameForUpdate(
+      @Param("metalakeId") Long metalakeId, @Param("policyName") String 
policyName) {
+    return 
getProvider().selectPolicyMetaByMetalakeIdAndNameForUpdate(metalakeId, 
policyName);
+  }
+
   public static String selectPolicyByPolicyId(@Param("policyId") Long 
policyId) {
     return getProvider().selectPolicyByPolicyId(policyId);
   }
 
+  /** Delegates an exclusive-lock policy query. */
+  public static String selectPolicyByPolicyIdForUpdate(@Param("policyId") Long 
policyId) {
+    return getProvider().selectPolicyByPolicyIdForUpdate(policyId);
+  }
+
+  /** Delegates a locking read of several policies. */
+  public static String listPolicyPOsByPolicyIdsForUpdate(@Param("policyIds") 
List<Long> policyIds) {
+    return getProvider().listPolicyPOsByPolicyIdsForUpdate(policyIds);
+  }
+
   public static String listPolicyPOsByPolicyIds(@Param("policyIds") List<Long> 
policyIds) {
     return getProvider().listPolicyPOsByPolicyIds(policyIds);
   }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetadataObjectRelMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetadataObjectRelMapper.java
index 31226a0eeb..b3ddb04368 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetadataObjectRelMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetadataObjectRelMapper.java
@@ -103,11 +103,16 @@ public interface PolicyMetadataObjectRelMapper {
       @Param("metadataObjectType") String metadataObjectType,
       @Param("policyIds") List<Long> policyIds);
 
+  /**
+   * Soft-deletes all active metadata-object relations for a policy.
+   *
+   * @param policyId The policy ID.
+   * @return The number of affected rows.
+   */
   @UpdateProvider(
       type = PolicyMetadataObjectRelSQLProviderFactory.class,
-      method = "softDeletePolicyMetadataObjectRelsByMetalakeAndPolicyName")
-  Integer softDeletePolicyMetadataObjectRelsByMetalakeAndPolicyName(
-      @Param("metalakeName") String metalakeName, @Param("policyName") String 
policyName);
+      method = "softDeletePolicyMetadataObjectRelsByPolicyId")
+  Integer softDeletePolicyMetadataObjectRelsByPolicyId(@Param("policyId") Long 
policyId);
 
   @UpdateProvider(
       type = PolicyMetadataObjectRelSQLProviderFactory.class,
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetadataObjectRelSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetadataObjectRelSQLProviderFactory.java
index 8b38bfb3e0..e968bfcc44 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetadataObjectRelSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyMetadataObjectRelSQLProviderFactory.java
@@ -89,10 +89,10 @@ public class PolicyMetadataObjectRelSQLProviderFactory {
             metadataObjectId, metadataObjectType, policyIds);
   }
 
-  public static String 
softDeletePolicyMetadataObjectRelsByMetalakeAndPolicyName(
-      @Param("metalakeName") String metalakeName, @Param("policyName") String 
policyName) {
-    return getProvider()
-        
.softDeletePolicyMetadataObjectRelsByMetalakeAndPolicyName(metalakeName, 
policyName);
+  /** Delegates cleanup of metadata-object relations by policy ID. */
+  public static String softDeletePolicyMetadataObjectRelsByPolicyId(
+      @Param("policyId") Long policyId) {
+    return 
getProvider().softDeletePolicyMetadataObjectRelsByPolicyId(policyId);
   }
 
   public static String softDeletePolicyMetadataObjectRelsByMetalakeId(
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelMapper.java
index 35a97b04a8..f1b61a0c43 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelMapper.java
@@ -91,6 +91,15 @@ public interface PolicyTagRelMapper {
   @UpdateProvider(type = PolicyTagRelSQLProviderFactory.class, method = 
"softDeleteByMetalakeId")
   int softDeleteByMetalakeId(@Param("metalakeId") Long metalakeId);
 
+  /**
+   * Soft-deletes every active tag relation for a policy.
+   *
+   * @param policyId The policy ID.
+   * @return The number of affected rows.
+   */
+  @UpdateProvider(type = PolicyTagRelSQLProviderFactory.class, method = 
"softDeleteByPolicyId")
+  int softDeleteByPolicyId(@Param("policyId") Long policyId);
+
   /**
    * Soft-deletes every active policy relation for a tag.
    *
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelSQLProviderFactory.java
index 5a82d68d77..83667a9c7a 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelSQLProviderFactory.java
@@ -82,6 +82,11 @@ public class PolicyTagRelSQLProviderFactory {
     return getProvider().softDeleteByMetalakeId(metalakeId);
   }
 
+  /** Delegates policy deletion cleanup. */
+  public static String softDeleteByPolicyId(@Param("policyId") Long policyId) {
+    return getProvider().softDeleteByPolicyId(policyId);
+  }
+
   /** Delegates tag deletion cleanup. */
   public static String softDeleteByTagId(@Param("tagId") Long tagId) {
     return getProvider().softDeleteByTagId(tagId);
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyVersionMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyVersionMapper.java
index 9bfbc62ea7..c926d60bac 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyVersionMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyVersionMapper.java
@@ -29,20 +29,19 @@ import org.apache.ibatis.annotations.UpdateProvider;
 public interface PolicyVersionMapper {
   String POLICY_VERSION_TABLE_NAME = "policy_version_info";
 
-  @InsertProvider(
-      type = PolicyVersionSQLProviderFactory.class,
-      method = "insertPolicyVersionOnDuplicateKeyUpdate")
-  void insertPolicyVersionOnDuplicateKeyUpdate(
-      @Param("policyVersion") PolicyVersionPO policyVersionPO);
-
   @InsertProvider(type = PolicyVersionSQLProviderFactory.class, method = 
"insertPolicyVersion")
   void insertPolicyVersion(@Param("policyVersion") PolicyVersionPO 
policyVersionPO);
 
+  /**
+   * Soft-deletes all active content snapshots for a policy.
+   *
+   * @param policyId The policy ID.
+   * @return The number of affected rows.
+   */
   @UpdateProvider(
       type = PolicyVersionSQLProviderFactory.class,
-      method = "softDeletePolicyVersionByMetalakeAndPolicyName")
-  Integer softDeletePolicyVersionByMetalakeAndPolicyName(
-      @Param("metalakeName") String metalakeName, @Param("policyName") String 
policyName);
+      method = "softDeletePolicyVersionsByPolicyId")
+  Integer softDeletePolicyVersionsByPolicyId(@Param("policyId") Long policyId);
 
   @UpdateProvider(
       type = PolicyVersionSQLProviderFactory.class,
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyVersionSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyVersionSQLProviderFactory.java
index 9fbe4ddc79..3e71ebf771 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyVersionSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyVersionSQLProviderFactory.java
@@ -47,19 +47,14 @@ public class PolicyVersionSQLProviderFactory {
     return POLICY_VERSION_SQL_PROVIDER_MAP.get(jdbcBackendType);
   }
 
-  public static String insertPolicyVersionOnDuplicateKeyUpdate(
-      @Param("policyVersion") PolicyVersionPO policyVersionPO) {
-    return 
getProvider().insertPolicyVersionOnDuplicateKeyUpdate(policyVersionPO);
-  }
-
   public static String insertPolicyVersion(
       @Param("policyVersion") PolicyVersionPO policyVersionPO) {
     return getProvider().insertPolicyVersion(policyVersionPO);
   }
 
-  public static String softDeletePolicyVersionByMetalakeAndPolicyName(
-      @Param("metalakeName") String metalakeName, @Param("policyName") String 
policyName) {
-    return 
getProvider().softDeletePolicyVersionByMetalakeAndPolicyName(metalakeName, 
policyName);
+  /** Delegates cleanup of policy snapshots by policy ID. */
+  public static String softDeletePolicyVersionsByPolicyId(@Param("policyId") 
Long policyId) {
+    return getProvider().softDeletePolicyVersionsByPolicyId(policyId);
   }
 
   public static String deletePolicyVersionsByLegacyTimeline(
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyMetaBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyMetaBaseSQLProvider.java
index 909b9fe246..730ce9bed0 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyMetaBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyMetaBaseSQLProvider.java
@@ -74,24 +74,6 @@ public class PolicyMetaBaseSQLProvider {
         + "</script>";
   }
 
-  public String insertPolicyMetaOnDuplicateKeyUpdate(@Param("policyMeta") 
PolicyPO policyPO) {
-    return "INSERT INTO "
-        + POLICY_META_TABLE_NAME
-        + " (policy_id, policy_name, policy_type, metalake_id,"
-        + " audit_info, current_version, last_version, deleted_at)"
-        + " VALUES (#{policyMeta.policyId}, #{policyMeta.policyName}, 
#{policyMeta.policyType},"
-        + " #{policyMeta.metalakeId}, #{policyMeta.auditInfo}, 
#{policyMeta.currentVersion},"
-        + " #{policyMeta.lastVersion}, #{policyMeta.deletedAt})"
-        + " ON DUPLICATE KEY UPDATE"
-        + " policy_name = #{policyMeta.policyName},"
-        + " policy_type = #{policyMeta.policyType},"
-        + " metalake_id = #{policyMeta.metalakeId},"
-        + " audit_info = #{policyMeta.auditInfo},"
-        + " current_version = #{policyMeta.currentVersion},"
-        + " last_version = #{policyMeta.lastVersion},"
-        + " deleted_at = #{policyMeta.deletedAt}";
-  }
-
   public String insertPolicyMeta(@Param("policyMeta") PolicyPO policyPO) {
     return "INSERT INTO "
         + POLICY_META_TABLE_NAME
@@ -136,26 +118,19 @@ public class PolicyMetaBaseSQLProvider {
         + " last_version = #{newPolicyMeta.lastVersion},"
         + " deleted_at = #{newPolicyMeta.deletedAt}"
         + " WHERE policy_id = #{oldPolicyMeta.policyId}"
-        + " AND policy_name = #{oldPolicyMeta.policyName}"
-        + " AND policy_type = #{oldPolicyMeta.policyType}"
-        + " AND metalake_id = #{oldPolicyMeta.metalakeId}"
-        + " AND audit_info = #{oldPolicyMeta.auditInfo}"
         + " AND current_version = #{oldPolicyMeta.currentVersion}"
-        + " AND last_version = #{oldPolicyMeta.lastVersion}"
         + " AND deleted_at = 0";
   }
 
-  public String softDeletePolicyByMetalakeAndPolicyName(
-      @Param("metalakeName") String metalakeName, @Param("policyName") String 
policyName) {
+  /** Returns SQL that soft-deletes a policy using its stable ID and observed 
OCC version. */
+  public String softDeletePolicyByIdAndVersion(
+      @Param("policyId") Long policyId, @Param("currentVersion") Long 
currentVersion) {
     return "UPDATE "
         + POLICY_META_TABLE_NAME
-        + " pm SET pm.deleted_at = "
+        + " SET deleted_at = "
         + DatabaseTimeSQL.MYSQL
-        + " WHERE pm.metalake_id IN ("
-        + " SELECT mm.metalake_id FROM "
-        + MetalakeMetaMapper.TABLE_NAME
-        + " mm WHERE mm.metalake_name = #{metalakeName} AND mm.deleted_at = 0)"
-        + " AND pm.policy_name = #{policyName} AND pm.deleted_at = 0";
+        + " WHERE policy_id = #{policyId} AND current_version = 
#{currentVersion}"
+        + " AND deleted_at = 0";
   }
 
   public String deletePolicyMetasByLegacyTimeline(
@@ -185,21 +160,23 @@ public class PolicyMetaBaseSQLProvider {
         + " AND pm.deleted_at = 0 ";
   }
 
-  public String listPolicyPOsByPolicyIds(@Param("policyIds") List<Long> 
policyIds) {
+  /** Returns SQL that selects and exclusively locks an active policy by ID. */
+  public String selectPolicyByPolicyIdForUpdate(@Param("policyId") Long 
policyId) {
+    return selectPolicyByPolicyId(policyId) + " FOR UPDATE";
+  }
+
+  /**
+   * Returns SQL that selects and exclusively locks several active policies, 
ordered by policy ID so
+   * that concurrent callers take the row locks in the same order.
+   */
+  public String listPolicyPOsByPolicyIdsForUpdate(@Param("policyIds") 
List<Long> policyIds) {
     return "<script>"
-        + "SELECT pm.policy_id, pm.policy_name, pm.policy_type, 
pm.metalake_id,"
-        + " pm.audit_info, pm.current_version, pm.last_version,"
-        + " pm.deleted_at"
-        + " FROM "
-        + POLICY_META_TABLE_NAME
-        + " pm"
-        + " WHERE pm.deleted_at = 0"
-        + " AND pm.policy_id IN ("
-        + "<foreach collection='policyIds' item='policyId' separator=','>"
-        + "#{policyId}"
-        + "</foreach>"
-        + ")"
-        + "</script>";
+        + selectPolicyPOsByPolicyIdsBody()
+        + " ORDER BY pm.policy_id FOR UPDATE</script>";
+  }
+
+  public String listPolicyPOsByPolicyIds(@Param("policyIds") List<Long> 
policyIds) {
+    return "<script>" + selectPolicyPOsByPolicyIdsBody() + "</script>";
   }
 
   public String selectPolicyMetaByMetalakeIdAndName(
@@ -216,6 +193,12 @@ public class PolicyMetaBaseSQLProvider {
         + " AND pm.deleted_at = 0 ";
   }
 
+  /** Returns SQL that selects and exclusively locks an active policy by its 
natural key. */
+  public String selectPolicyMetaByMetalakeIdAndNameForUpdate(
+      @Param("metalakeId") Long metalakeId, @Param("policyName") String 
policyName) {
+    return selectPolicyMetaByMetalakeIdAndName(metalakeId, policyName) + " FOR 
UPDATE";
+  }
+
   public String batchSelectPolicyByIdentifier(
       @Param("metalakeName") String metalakeName, @Param("policyNames") 
List<String> policyNames) {
     return "<script>"
@@ -241,4 +224,23 @@ public class PolicyMetaBaseSQLProvider {
         + " AND pm.deleted_at = 0 AND pv.deleted_at = 0 AND mm.deleted_at = 0"
         + "</script>";
   }
+
+  /**
+   * Returns the shared body of the by-ID list queries, without the enclosing 
{@code <script>} tag,
+   * so the plain and the locking variant cannot drift apart when the selected 
columns change.
+   */
+  private String selectPolicyPOsByPolicyIdsBody() {
+    return "SELECT pm.policy_id, pm.policy_name, pm.policy_type, 
pm.metalake_id,"
+        + " pm.audit_info, pm.current_version, pm.last_version,"
+        + " pm.deleted_at"
+        + " FROM "
+        + POLICY_META_TABLE_NAME
+        + " pm"
+        + " WHERE pm.deleted_at = 0"
+        + " AND pm.policy_id IN ("
+        + "<foreach collection='policyIds' item='policyId' separator=','>"
+        + "#{policyId}"
+        + "</foreach>"
+        + ")";
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyMetadataObjectRelBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyMetadataObjectRelBaseSQLProvider.java
index dbd444b0c3..e239139d5a 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyMetadataObjectRelBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyMetadataObjectRelBaseSQLProvider.java
@@ -134,19 +134,13 @@ public class PolicyMetadataObjectRelBaseSQLProvider {
         + "</script>";
   }
 
-  public String softDeletePolicyMetadataObjectRelsByMetalakeAndPolicyName(
-      @Param("metalakeName") String metalakeName, @Param("policyName") String 
policyName) {
+  /** Returns SQL that soft-deletes every active metadata-object relation for 
a policy ID. */
+  public String 
softDeletePolicyMetadataObjectRelsByPolicyId(@Param("policyId") Long policyId) {
     return "UPDATE "
         + 
PolicyMetadataObjectRelMapper.POLICY_METADATA_OBJECT_RELATION_TABLE_NAME
-        + " pe JOIN "
-        + PolicyMetaMapper.POLICY_META_TABLE_NAME
-        + " pm ON pe.policy_id = pm.policy_id JOIN "
-        + MetalakeMetaMapper.TABLE_NAME
-        + " mm ON pm.metalake_id = mm.metalake_id"
-        + " SET pe.deleted_at = "
-        + DatabaseTimeSQL.MYSQL
-        + " WHERE mm.metalake_name = #{metalakeName} AND pm.policy_name = 
#{policyName}"
-        + " AND pe.deleted_at = 0 AND pm.deleted_at = 0 AND mm.deleted_at = 0";
+        + " SET deleted_at = (UNIX_TIMESTAMP() * 1000.0)"
+        + " + EXTRACT(MICROSECOND FROM CURRENT_TIMESTAMP(3)) / 1000"
+        + " WHERE policy_id = #{policyId} AND deleted_at = 0";
   }
 
   public String softDeletePolicyMetadataObjectRelsByMetalakeId(
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyTagRelBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyTagRelBaseSQLProvider.java
index 826e37ba88..b193b1e01b 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyTagRelBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyTagRelBaseSQLProvider.java
@@ -90,6 +90,15 @@ public class PolicyTagRelBaseSQLProvider {
         + " AND deleted_at = 0";
   }
 
+  /** Returns SQL for soft-deleting tag relations when a policy is deleted. */
+  public String softDeleteByPolicyId(@Param("policyId") Long policyId) {
+    return "UPDATE "
+        + POLICY_TAG_RELATION_TABLE_NAME
+        + " SET deleted_at = "
+        + deletedAtNowExpression()
+        + " WHERE policy_id = #{policyId} AND deleted_at = 0";
+  }
+
   /** Returns SQL for soft-deleting policy relations when a tag is deleted. */
   public String softDeleteByTagId(@Param("tagId") Long tagId) {
     return "UPDATE "
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyVersionBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyVersionBaseSQLProvider.java
index 7912253daa..3db120be11 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyVersionBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyVersionBaseSQLProvider.java
@@ -18,33 +18,14 @@
  */
 package org.apache.gravitino.storage.relational.mapper.provider.base;
 
-import static 
org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper.POLICY_META_TABLE_NAME;
 import static 
org.apache.gravitino.storage.relational.mapper.PolicyVersionMapper.POLICY_VERSION_TABLE_NAME;
 
-import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.provider.DatabaseTimeSQL;
 import org.apache.gravitino.storage.relational.po.PolicyVersionPO;
 import org.apache.ibatis.annotations.Param;
 
 public class PolicyVersionBaseSQLProvider {
 
-  public String insertPolicyVersionOnDuplicateKeyUpdate(
-      @Param("policyVersion") PolicyVersionPO policyVersion) {
-    return "INSERT INTO "
-        + POLICY_VERSION_TABLE_NAME
-        + " (metalake_id, policy_id, version, policy_comment, enabled, 
content, deleted_at)"
-        + " VALUES (#{policyVersion.metalakeId}, #{policyVersion.policyId}, 
#{policyVersion.version}, #{policyVersion.policyComment},"
-        + " #{policyVersion.enabled}, #{policyVersion.content}, 
#{policyVersion.deletedAt})"
-        + " ON DUPLICATE KEY UPDATE"
-        + " metalake_id = #{policyVersion.metalakeId},"
-        + " policy_id = #{policyVersion.policyId},"
-        + " version = #{policyVersion.version},"
-        + " policy_comment = #{policyVersion.policyComment},"
-        + " enabled = #{policyVersion.enabled},"
-        + " content = #{policyVersion.content},"
-        + " deleted_at = #{policyVersion.deletedAt}";
-  }
-
   public String insertPolicyVersion(@Param("policyVersion") PolicyVersionPO 
policyVersion) {
     return "INSERT INTO "
         + POLICY_VERSION_TABLE_NAME
@@ -54,25 +35,13 @@ public class PolicyVersionBaseSQLProvider {
         + " #{policyVersion.deletedAt})";
   }
 
-  public String softDeletePolicyVersionByMetalakeAndPolicyName(
-      @Param("metalakeName") String metalakeName, @Param("policyName") String 
policyName) {
+  /** Returns SQL that soft-deletes every active content snapshot for a policy 
ID. */
+  public String softDeletePolicyVersionsByPolicyId(@Param("policyId") Long 
policyId) {
     return "UPDATE "
         + POLICY_VERSION_TABLE_NAME
-        + " pv SET pv.deleted_at = "
-        + DatabaseTimeSQL.MYSQL
-        + " WHERE pv.metalake_id IN ("
-        + " SELECT mm.metalake_id FROM "
-        + MetalakeMetaMapper.TABLE_NAME
-        + " mm WHERE mm.metalake_name = #{metalakeName} AND mm.deleted_at = 0)"
-        + " AND pv.policy_id IN ("
-        + " SELECT pm.policy_id FROM "
-        + POLICY_META_TABLE_NAME
-        + " pm WHERE pm.policy_name = #{policyName} AND pm.deleted_at = 0"
-        + " AND pm.metalake_id IN ("
-        + " SELECT mm.metalake_id FROM "
-        + MetalakeMetaMapper.TABLE_NAME
-        + " mm WHERE mm.metalake_name = #{metalakeName} AND mm.deleted_at = 
0))"
-        + " AND pv.deleted_at = 0";
+        + " SET deleted_at = (UNIX_TIMESTAMP() * 1000.0)"
+        + " + EXTRACT(MICROSECOND FROM CURRENT_TIMESTAMP(3)) / 1000"
+        + " WHERE policy_id = #{policyId} AND deleted_at = 0";
   }
 
   public String deletePolicyVersionsByLegacyTimeline(
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyMetaPostgreSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyMetaPostgreSQLProvider.java
index 1302ca9774..e2c5770da2 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyMetaPostgreSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyMetaPostgreSQLProvider.java
@@ -22,19 +22,17 @@ import static 
org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper.PO
 
 import org.apache.gravitino.storage.relational.mapper.provider.DatabaseTimeSQL;
 import 
org.apache.gravitino.storage.relational.mapper.provider.base.PolicyMetaBaseSQLProvider;
-import org.apache.gravitino.storage.relational.po.PolicyPO;
 
 public class PolicyMetaPostgreSQLProvider extends PolicyMetaBaseSQLProvider {
 
   @Override
-  public String softDeletePolicyByMetalakeAndPolicyName(String metalakeName, 
String policyName) {
+  public String softDeletePolicyByIdAndVersion(Long policyId, Long 
currentVersion) {
     return "UPDATE "
         + POLICY_META_TABLE_NAME
         + " SET deleted_at = "
         + DatabaseTimeSQL.POSTGRESQL
-        + " WHERE metalake_id = (SELECT metalake_id FROM "
-        + " metalake_meta mm WHERE mm.metalake_name = #{metalakeName} AND 
mm.deleted_at = 0)"
-        + " AND policy_name = #{policyName} AND deleted_at = 0";
+        + " WHERE policy_id = #{policyId} AND current_version = 
#{currentVersion}"
+        + " AND deleted_at = 0";
   }
 
   @Override
@@ -54,29 +52,4 @@ public class PolicyMetaPostgreSQLProvider extends 
PolicyMetaBaseSQLProvider {
         + POLICY_META_TABLE_NAME
         + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT 
#{limit})";
   }
-
-  @Override
-  public String insertPolicyMetaOnDuplicateKeyUpdate(PolicyPO policyPO) {
-    return "INSERT INTO "
-        + POLICY_META_TABLE_NAME
-        + " (policy_id, policy_name, policy_type, metalake_id,"
-        + " audit_info, current_version, last_version, deleted_at)"
-        + " VALUES ("
-        + " #{policyMeta.policyId},"
-        + " #{policyMeta.policyName},"
-        + " #{policyMeta.policyType},"
-        + " #{policyMeta.metalakeId},"
-        + " #{policyMeta.auditInfo},"
-        + " #{policyMeta.currentVersion},"
-        + " #{policyMeta.lastVersion},"
-        + " #{policyMeta.deletedAt})"
-        + " ON CONFLICT (policy_id) DO UPDATE SET"
-        + " policy_name = #{policyMeta.policyName},"
-        + " policy_type = #{policyMeta.policyType},"
-        + " metalake_id = #{policyMeta.metalakeId},"
-        + " audit_info = #{policyMeta.auditInfo},"
-        + " current_version = #{policyMeta.currentVersion},"
-        + " last_version = #{policyMeta.lastVersion},"
-        + " deleted_at = #{policyMeta.deletedAt}";
-  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyMetadataObjectRelPostgreSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyMetadataObjectRelPostgreSQLProvider.java
index 0978873a1e..e2ae7b9345 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyMetadataObjectRelPostgreSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyMetadataObjectRelPostgreSQLProvider.java
@@ -41,18 +41,12 @@ public class PolicyMetadataObjectRelPostgreSQLProvider
   private static final String DELETED_AT_NOW_EXPRESSION = " " + 
DatabaseTimeSQL.POSTGRESQL;
 
   @Override
-  public String softDeletePolicyMetadataObjectRelsByMetalakeAndPolicyName(
-      String metalakeName, String policyName) {
+  public String softDeletePolicyMetadataObjectRelsByPolicyId(Long policyId) {
     return "UPDATE "
         + POLICY_METADATA_OBJECT_RELATION_TABLE_NAME
-        + " te SET deleted_at ="
+        + " SET deleted_at ="
         + DELETED_AT_NOW_EXPRESSION
-        + " WHERE te.policy_id IN (SELECT tm.policy_id FROM "
-        + PolicyMetaMapper.POLICY_META_TABLE_NAME
-        + " tm WHERE tm.metalake_id IN (SELECT mm.metalake_id FROM "
-        + MetalakeMetaMapper.TABLE_NAME
-        + " mm WHERE mm.metalake_name = #{metalakeName} AND mm.deleted_at = 0)"
-        + " AND tm.policy_name = #{policyName} AND tm.deleted_at = 0) AND 
te.deleted_at = 0";
+        + " WHERE policy_id = #{policyId} AND deleted_at = 0";
   }
 
   @Override
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyVersionPostgreSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyVersionPostgreSQLProvider.java
index e0c7b9f0a1..7042f1f1e5 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyVersionPostgreSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyVersionPostgreSQLProvider.java
@@ -18,29 +18,18 @@
  */
 package org.apache.gravitino.storage.relational.mapper.provider.postgresql;
 
-import static 
org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper.POLICY_META_TABLE_NAME;
 import static 
org.apache.gravitino.storage.relational.mapper.PolicyVersionMapper.POLICY_VERSION_TABLE_NAME;
 
-import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.provider.DatabaseTimeSQL;
 import 
org.apache.gravitino.storage.relational.mapper.provider.base.PolicyVersionBaseSQLProvider;
-import org.apache.gravitino.storage.relational.po.PolicyVersionPO;
 
 public class PolicyVersionPostgreSQLProvider extends 
PolicyVersionBaseSQLProvider {
   @Override
-  public String softDeletePolicyVersionByMetalakeAndPolicyName(
-      String metalakeName, String policyName) {
+  public String softDeletePolicyVersionsByPolicyId(Long policyId) {
     return "UPDATE "
         + POLICY_VERSION_TABLE_NAME
-        + " SET deleted_at = "
-        + DatabaseTimeSQL.POSTGRESQL
-        + " WHERE metalake_id = (SELECT metalake_id FROM "
-        + MetalakeMetaMapper.TABLE_NAME
-        + " mm WHERE mm.metalake_name = #{metalakeName} AND mm.deleted_at = 0)"
-        + " AND policy_id = (SELECT policy_id FROM "
-        + POLICY_META_TABLE_NAME
-        + " pm WHERE pm.policy_name = #{policyName} AND pm.deleted_at = 0)"
-        + " AND deleted_at = 0";
+        + " SET deleted_at = CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 
AS BIGINT)"
+        + " WHERE policy_id = #{policyId} AND deleted_at = 0";
   }
 
   @Override
@@ -73,25 +62,4 @@ public class PolicyVersionPostgreSQLProvider extends 
PolicyVersionBaseSQLProvide
         + DatabaseTimeSQL.POSTGRESQL
         + " WHERE metalake_id = #{metalakeId} AND deleted_at = 0";
   }
-
-  @Override
-  public String insertPolicyVersionOnDuplicateKeyUpdate(PolicyVersionPO 
policyVersion) {
-    return "INSERT INTO "
-        + POLICY_VERSION_TABLE_NAME
-        + " (metalake_id, policy_id, version, policy_comment, enabled,"
-        + " content, deleted_at)"
-        + " VALUES ("
-        + " #{policyVersion.metalakeId},"
-        + " #{policyVersion.policyId},"
-        + " #{policyVersion.version},"
-        + " #{policyVersion.policyComment},"
-        + " #{policyVersion.enabled},"
-        + " #{policyVersion.content},"
-        + " #{policyVersion.deletedAt})"
-        + " ON CONFLICT (policy_id, version, deleted_at) DO UPDATE SET"
-        + " policy_comment = #{policyVersion.policyComment},"
-        + " enabled = #{policyVersion.enabled},"
-        + " content = #{policyVersion.content},"
-        + " deleted_at = #{policyVersion.deletedAt}";
-  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/OccWriteSupport.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/OccWriteSupport.java
index f9797e0646..01842bfe19 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/OccWriteSupport.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/OccWriteSupport.java
@@ -115,6 +115,20 @@ public class OccWriteSupport {
     }
   }
 
+  /**
+   * Executes a single-row compare-and-set update for an entity guarded by 
version.
+   *
+   * @param updateOps an operation supplying the number of rows affected by 
the update
+   * @param onMissSupplier a supplier providing the RuntimeException when zero 
rows are updated
+   */
+  public static void updateWithVersion(
+      IntSupplier updateOps, Supplier<RuntimeException> onMissSupplier) {
+    int updated = updateOps.getAsInt();
+    if (updated == 0) {
+      throw onMissSupplier.get();
+    }
+  }
+
   /**
    * Executes a batch soft delete of child entities guarded by their 
individual versions.
    *
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java
index 0c623f96c0..71ed987f9c 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyMetaService.java
@@ -20,11 +20,15 @@ package org.apache.gravitino.storage.relational.service;
 
 import static 
org.apache.gravitino.metrics.source.MetricsSource.GRAVITINO_RELATIONAL_STORE_METRIC_NAME;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -38,9 +42,15 @@ import org.apache.gravitino.exceptions.NoSuchEntityException;
 import org.apache.gravitino.meta.GenericEntity;
 import org.apache.gravitino.meta.PolicyEntity;
 import org.apache.gravitino.metrics.Monitored;
+import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.OwnerMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper;
 import 
org.apache.gravitino.storage.relational.mapper.PolicyMetadataObjectRelMapper;
+import org.apache.gravitino.storage.relational.mapper.PolicyTagRelMapper;
 import org.apache.gravitino.storage.relational.mapper.PolicyVersionMapper;
+import org.apache.gravitino.storage.relational.mapper.SecurableObjectMapper;
+import 
org.apache.gravitino.storage.relational.mapper.TagMetadataObjectRelMapper;
+import org.apache.gravitino.storage.relational.po.MetalakePO;
 import org.apache.gravitino.storage.relational.po.PolicyMaxVersionPO;
 import org.apache.gravitino.storage.relational.po.PolicyMetadataObjectRelPO;
 import org.apache.gravitino.storage.relational.po.PolicyPO;
@@ -93,36 +103,35 @@ public class PolicyMetaService {
     String metalakeName = ns.level(0);
 
     try {
-      Long metalakeId =
-          EntityIdService.getEntityId(NameIdentifier.of(metalakeName), 
Entity.EntityType.METALAKE);
+      MetalakePO metalakePO =
+          SessionUtils.getWithoutCommit(
+              MetalakeMetaMapper.class, mapper -> 
mapper.selectMetalakeMetaByName(metalakeName));
+      if (metalakePO == null) {
+        throw new NoSuchEntityException(
+            NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+            Entity.EntityType.METALAKE.name().toLowerCase(),
+            metalakeName);
+      }
 
-      PolicyPO.Builder builder = PolicyPO.builder().withMetalakeId(metalakeId);
+      PolicyPO.Builder builder = 
PolicyPO.builder().withMetalakeId(metalakePO.getMetalakeId());
       PolicyPO policyPO = 
POConverters.initializePolicyPOWithVersion(policyEntity, builder);
 
-      // insert both policy meta table and policy version table
       SessionUtils.doMultipleWithCommit(
-          () ->
-              SessionUtils.doWithoutCommit(
-                  PolicyMetaMapper.class,
-                  mapper -> {
-                    if (overwritten) {
-                      mapper.insertPolicyMetaOnDuplicateKeyUpdate(policyPO);
-                    } else {
-                      mapper.insertPolicyMeta(policyPO);
-                    }
-                  }),
-          () ->
-              SessionUtils.doWithoutCommit(
-                  PolicyVersionMapper.class,
-                  mapper -> {
-                    if (overwritten) {
-                      
mapper.insertPolicyVersionOnDuplicateKeyUpdate(policyPO.getPolicyVersionPO());
-                    } else {
-                      
mapper.insertPolicyVersion(policyPO.getPolicyVersionPO());
-                    }
-                  }));
+          () -> lockMetalakeForPolicyCreate(metalakePO),
+          () -> insertPolicyWithoutCommit(policyEntity, policyPO, 
overwritten));
     } catch (RuntimeException e) {
-      ExceptionUtils.checkSQLException(e, Entity.EntityType.POLICY, 
policyEntity.toString());
+      try {
+        ExceptionUtils.checkSQLException(
+            e, Entity.EntityType.POLICY, 
policyEntity.nameIdentifier().toString());
+      } catch (EntityAlreadyExistsException duplicate) {
+        if (overwritten) {
+          // A missing-row locking read does not fence a concurrent insert at 
READ_COMMITTED.
+          // Propagate the conflict so the whole transaction is rolled back 
before retrying.
+          throw ExceptionUtils.concurrentModification(
+              Entity.EntityType.POLICY, policyEntity.nameIdentifier());
+        }
+        throw duplicate;
+      }
       throw e;
     }
   }
@@ -143,69 +152,87 @@ public class PolicyMetaService {
         updatedPolicyEntity.id(),
         oldPolicyEntity.id());
 
-    Integer updateResult;
     try {
-      boolean checkNeedUpdateVersion =
-          POConverters.checkPolicyVersionNeedUpdate(
-              oldPolicyPO.getPolicyVersionPO(), updatedPolicyEntity);
       PolicyPO newPolicyPO =
-          POConverters.updatePolicyPOWithVersion(
-              oldPolicyPO, updatedPolicyEntity, checkNeedUpdateVersion);
-      if (checkNeedUpdateVersion) {
-        SessionUtils.doMultipleWithCommit(
-            () ->
-                SessionUtils.doWithoutCommit(
-                    PolicyVersionMapper.class,
-                    mapper -> 
mapper.insertPolicyVersion(newPolicyPO.getPolicyVersionPO())),
-            () ->
-                SessionUtils.doWithoutCommit(
-                    PolicyMetaMapper.class,
-                    mapper -> mapper.updatePolicyMeta(newPolicyPO, 
oldPolicyPO)));
-        // we set the updateResult to 1 to indicate that the update is 
successful
-        updateResult = 1;
-      } else {
-        updateResult =
-            SessionUtils.doWithCommitAndFetchResult(
-                PolicyMetaMapper.class,
-                mapper -> mapper.updatePolicyMeta(newPolicyPO, oldPolicyPO));
-      }
+          POConverters.updatePolicyPOWithVersion(oldPolicyPO, 
updatedPolicyEntity);
+      SessionUtils.doMultipleWithCommit(
+          () -> updatePolicyRootWithVersion(ident, oldPolicyPO, newPolicyPO),
+          () ->
+              SessionUtils.doWithoutCommit(
+                  PolicyVersionMapper.class,
+                  mapper -> 
mapper.insertPolicyVersion(newPolicyPO.getPolicyVersionPO())));
     } catch (RuntimeException re) {
       ExceptionUtils.checkSQLException(
           re, Entity.EntityType.POLICY, 
updatedPolicyEntity.nameIdentifier().toString());
       throw re;
     }
 
-    if (updateResult > 0) {
-      return updatedPolicyEntity;
-    } else {
-      throw new IOException("Failed to update the entity: " + 
updatedPolicyEntity);
-    }
+    return updatedPolicyEntity;
   }
 
   @Monitored(
       metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
       baseMetricName = "deletePolicy")
   public boolean deletePolicy(NameIdentifier ident) {
-    String metalakeName = ident.namespace().level(0);
-    int[] policyMetaDeletedCount = new int[] {0};
-    int[] policyVersionDeletedCount = new int[] {0};
+    PolicyPO policyPO;
+    try {
+      policyPO = getPolicyPOByMetalakeAndName(ident.namespace().level(0), 
ident.name());
+    } catch (NoSuchEntityException e) {
+      return false;
+    }
+    return deletePolicy(ident, policyPO);
+  }
 
-    // We should delete meta and version info
-    SessionUtils.doMultipleWithCommit(
-        () ->
-            policyMetaDeletedCount[0] =
-                SessionUtils.getWithoutCommit(
-                    PolicyMetaMapper.class,
-                    mapper ->
-                        
mapper.softDeletePolicyByMetalakeAndPolicyName(metalakeName, ident.name())),
-        () ->
-            policyVersionDeletedCount[0] =
-                SessionUtils.getWithoutCommit(
-                    PolicyVersionMapper.class,
-                    mapper ->
-                        mapper.softDeletePolicyVersionByMetalakeAndPolicyName(
-                            metalakeName, ident.name())));
-    return policyMetaDeletedCount[0] + policyVersionDeletedCount[0] > 0;
+  /**
+   * Deletes the policy the caller observed. The delete is a compare-and-set 
on the observed
+   * version, so a policy that changed since it was read is rejected instead 
of being removed, and
+   * the version snapshots and dependent relations are cleaned up in the same 
transaction as the
+   * policy row itself. If another transaction deletes or renames the observed 
policy first, this
+   * method returns {@code false}, preserving the idempotent delete contract.
+   *
+   * <p>The observed row is a parameter so that a test can hand in a stale 
one; production callers
+   * use {@link #deletePolicy(NameIdentifier)}, which reads it first.
+   */
+  @VisibleForTesting
+  boolean deletePolicy(NameIdentifier ident, PolicyPO policyPO) {
+    long policyId = policyPO.getPolicyId();
+
+    try {
+      SessionUtils.doMultipleWithCommit(
+          () -> deletePolicyWithVersion(ident, policyPO),
+          () ->
+              SessionUtils.doWithoutCommit(
+                  PolicyVersionMapper.class,
+                  mapper -> 
mapper.softDeletePolicyVersionsByPolicyId(policyId)),
+          () ->
+              SessionUtils.doWithoutCommit(
+                  PolicyMetadataObjectRelMapper.class,
+                  mapper -> 
mapper.softDeletePolicyMetadataObjectRelsByPolicyId(policyId)),
+          () ->
+              SessionUtils.doWithoutCommit(
+                  PolicyTagRelMapper.class, mapper -> 
mapper.softDeleteByPolicyId(policyId)),
+          () ->
+              SessionUtils.doWithoutCommit(
+                  TagMetadataObjectRelMapper.class,
+                  mapper ->
+                      mapper.softDeleteTagMetadataObjectRelsByMetadataObject(
+                          policyId, MetadataObject.Type.POLICY.name())),
+          () ->
+              SessionUtils.doWithoutCommit(
+                  OwnerMetaMapper.class,
+                  mapper ->
+                      mapper.softDeleteOwnerRelByMetadataObjectIdAndType(
+                          policyId, MetadataObject.Type.POLICY.name())),
+          () ->
+              SessionUtils.doWithoutCommit(
+                  SecurableObjectMapper.class,
+                  mapper ->
+                      mapper.softDeleteObjectRelsByMetadataObject(
+                          policyId, MetadataObject.Type.POLICY.name())));
+      return true;
+    } catch (NoSuchEntityException e) {
+      return false;
+    }
   }
 
   @Monitored(
@@ -313,75 +340,16 @@ public class PolicyMetaService {
       NameIdentifier[] policiesToAdd,
       NameIdentifier[] policiesToRemove)
       throws NoSuchEntityException, EntityAlreadyExistsException, IOException {
-    MetadataObject metadataObject = 
NameIdentifierUtil.toMetadataObject(objectIdent, objectType);
-    String metalake = objectIdent.namespace().level(0);
-
     try {
-      Long metadataObjectId = EntityIdService.getEntityId(objectIdent, 
objectType);
-
-      // Fetch all the policies need to associate with the metadata object.
-      List<String> policyNamesToAdd =
-          
Arrays.stream(policiesToAdd).map(NameIdentifier::name).collect(Collectors.toList());
-      List<PolicyPO> policyPOsToAdd =
-          policyNamesToAdd.isEmpty()
-              ? Collections.emptyList()
-              : getPolicyPOsByMetalakeAndNames(metalake, policyNamesToAdd);
-
-      // Fetch all the policies need to remove from the metadata object.
-      List<String> policyNamesToRemove =
-          
Arrays.stream(policiesToRemove).map(NameIdentifier::name).collect(Collectors.toList());
-      List<PolicyPO> policyPOsToRemove =
-          policyNamesToRemove.isEmpty()
-              ? Collections.emptyList()
-              : getPolicyPOsByMetalakeAndNames(metalake, policyNamesToRemove);
-
-      SessionUtils.doMultipleWithCommit(
-          () -> {
-            // Insert the policy metadata object relations.
-            if (policyPOsToAdd.isEmpty()) {
-              return;
-            }
-
-            List<PolicyMetadataObjectRelPO> policyRelsToAdd =
-                policyPOsToAdd.stream()
-                    .map(
-                        policyPO ->
-                            
POConverters.initializePolicyMetadataObjectRelPOWithVersion(
-                                policyPO.getPolicyId(),
-                                metadataObjectId,
-                                metadataObject.type().toString()))
-                    .collect(Collectors.toList());
-            SessionUtils.doWithoutCommit(
-                PolicyMetadataObjectRelMapper.class,
-                mapper -> 
mapper.batchInsertPolicyMetadataObjectRels(policyRelsToAdd));
-          },
-          () -> {
-            // Remove the policy metadata object relations.
-            if (policyPOsToRemove.isEmpty()) {
-              return;
-            }
-
-            List<Long> policyIdsToRemove =
-                
policyPOsToRemove.stream().map(PolicyPO::getPolicyId).collect(Collectors.toList());
-            SessionUtils.doWithoutCommit(
-                PolicyMetadataObjectRelMapper.class,
-                mapper ->
-                    
mapper.batchDeletePolicyMetadataObjectRelsByPolicyIdsAndMetadataObject(
-                        metadataObjectId, metadataObject.type().toString(), 
policyIdsToRemove));
-          });
-
-      // Fetch all the policies associated with the metadata object after the 
operation.
-      List<PolicyPO> policyPOs =
-          SessionUtils.getWithoutCommit(
-              PolicyMetadataObjectRelMapper.class,
-              mapper ->
-                  mapper.listPolicyPOsByMetadataObjectIdAndType(
-                      metadataObjectId, metadataObject.type().toString()));
-
-      return policyPOs.stream()
-          .map(policyPO -> POConverters.fromPolicyPO(policyPO, 
NamespaceUtil.ofPolicy(metalake)))
-          .collect(Collectors.toList());
-
+      // One transaction for the whole association change: the policy rows 
stay locked from the
+      // moment they are read until the relation rows are rewritten and read 
back, so a conflict
+      // rolls the whole change back instead of leaving a half-applied 
association set behind. The
+      // mapper handed to the callback is unused; the call only opens and 
closes the transaction.
+      return SessionUtils.doWithCommitAndFetchResult(
+          PolicyMetaMapper.class,
+          ignored ->
+              associatePoliciesWithMetadataObjectWithoutCommit(
+                  objectIdent, objectType, policiesToAdd, policiesToRemove));
     } catch (RuntimeException e) {
       ExceptionUtils.checkSQLException(e, Entity.EntityType.POLICY, 
objectIdent.toString());
       throw e;
@@ -440,28 +408,6 @@ public class PolicyMetaService {
     return totalDeletedCount;
   }
 
-  private PolicyPO getPolicyPOByMetalakeAndName(String metalakeName, String 
policyName) {
-    PolicyPO policyPO =
-        SessionUtils.getWithoutCommit(
-            PolicyMetaMapper.class,
-            mapper -> mapper.selectPolicyMetaByMetalakeAndName(metalakeName, 
policyName));
-
-    if (policyPO == null) {
-      throw new NoSuchEntityException(
-          NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
-          Entity.EntityType.POLICY.name().toLowerCase(),
-          policyName);
-    }
-    return policyPO;
-  }
-
-  private List<PolicyPO> getPolicyPOsByMetalakeAndNames(
-      String metalakeName, List<String> policyNames) {
-    return SessionUtils.getWithoutCommit(
-        PolicyMetaMapper.class,
-        mapper -> mapper.listPolicyPOsByMetalakeAndPolicyNames(metalakeName, 
policyNames));
-  }
-
   /**
    * Get policy id by policy name
    *
@@ -500,4 +446,319 @@ public class PolicyMetaService {
           return POConverters.fromPolicyPOs(policyPOs, firstIdent.namespace());
         });
   }
+
+  private List<PolicyEntity> associatePoliciesWithMetadataObjectWithoutCommit(
+      NameIdentifier objectIdent,
+      Entity.EntityType objectType,
+      NameIdentifier[] policiesToAdd,
+      NameIdentifier[] policiesToRemove) {
+    MetadataObject metadataObject = 
NameIdentifierUtil.toMetadataObject(objectIdent, objectType);
+    String metalake = objectIdent.namespace().level(0);
+
+    Long metadataObjectId = EntityIdService.getEntityId(objectIdent, 
objectType);
+
+    // Fetch all the policies need to associate with the metadata object.
+    List<String> policyNamesToAdd =
+        
Arrays.stream(policiesToAdd).map(NameIdentifier::name).collect(Collectors.toList());
+    List<PolicyPO> policyPOsToAdd =
+        policyNamesToAdd.isEmpty()
+            ? Collections.emptyList()
+            : getPolicyPOsByMetalakeAndNames(metalake, policyNamesToAdd);
+
+    // Fetch all the policies need to remove from the metadata object.
+    List<String> policyNamesToRemove =
+        
Arrays.stream(policiesToRemove).map(NameIdentifier::name).collect(Collectors.toList());
+    List<PolicyPO> policyPOsToRemove =
+        policyNamesToRemove.isEmpty()
+            ? Collections.emptyList()
+            : getPolicyPOsByMetalakeAndNames(metalake, policyNamesToRemove);
+    Map<Long, PolicyPO> lockedPolicyPOs =
+        lockPoliciesForAssociation(policyPOsToAdd, policyPOsToRemove);
+    policyPOsToAdd = currentPolicyPOs(policyPOsToAdd, lockedPolicyPOs);
+    policyPOsToRemove = currentPolicyPOs(policyPOsToRemove, lockedPolicyPOs);
+
+    if (!policyPOsToAdd.isEmpty()) {
+      List<PolicyMetadataObjectRelPO> policyRelsToAdd =
+          policyPOsToAdd.stream()
+              .map(
+                  policyPO ->
+                      
POConverters.initializePolicyMetadataObjectRelPOWithVersion(
+                          policyPO.getPolicyId(),
+                          metadataObjectId,
+                          metadataObject.type().toString()))
+              .collect(Collectors.toList());
+      SessionUtils.doWithoutCommit(
+          PolicyMetadataObjectRelMapper.class,
+          mapper -> 
mapper.batchInsertPolicyMetadataObjectRels(policyRelsToAdd));
+    }
+    if (!policyPOsToRemove.isEmpty()) {
+      List<Long> policyIdsToRemove =
+          
policyPOsToRemove.stream().map(PolicyPO::getPolicyId).collect(Collectors.toList());
+      SessionUtils.doWithoutCommit(
+          PolicyMetadataObjectRelMapper.class,
+          mapper ->
+              
mapper.batchDeletePolicyMetadataObjectRelsByPolicyIdsAndMetadataObject(
+                  metadataObjectId, metadataObject.type().toString(), 
policyIdsToRemove));
+    }
+
+    // Fetch all the policies associated with the metadata object after the 
operation.
+    List<PolicyPO> policyPOs =
+        SessionUtils.getWithoutCommit(
+            PolicyMetadataObjectRelMapper.class,
+            mapper ->
+                mapper.listPolicyPOsByMetadataObjectIdAndType(
+                    metadataObjectId, metadataObject.type().toString()));
+
+    return policyPOs.stream()
+        .map(policyPO -> POConverters.fromPolicyPO(policyPO, 
NamespaceUtil.ofPolicy(metalake)))
+        .collect(Collectors.toList());
+  }
+
+  /**
+   * Holds the parent metalake row for the rest of the transaction, so a 
policy cannot be created
+   * under a metalake that is going away.
+   *
+   * <p>The lock is shared, not exclusive: many policies can be created under 
the same metalake at
+   * the same time, while dropping the metalake takes an exclusive lock on 
this row, so a drop and a
+   * create cannot overlap.
+   *
+   * <p>The name is compared again because the ID alone cannot tell a rename 
apart: the caller
+   * looked the metalake up by name, so a renamed row means the name in the 
request no longer
+   * exists. The metalake version is deliberately not compared, matching 
{@code CatalogMetaService}:
+   * holding the row is what makes the create safe, and an unrelated metalake 
edit that commits in
+   * between would otherwise reject the create for no reason.
+   */
+  private void lockMetalakeForPolicyCreate(MetalakePO observedMetalakePO) {
+    OccWriteSupport.lockParentForChildWrite(
+        observedMetalakePO.getMetalakeName(),
+        Entity.EntityType.METALAKE,
+        () ->
+            SessionUtils.getWithoutCommit(
+                MetalakeMetaMapper.class,
+                mapper ->
+                    
mapper.selectMetalakeMetaByIdForShare(observedMetalakePO.getMetalakeId())),
+        null,
+        current -> Objects.equals(current.getMetalakeName(), 
observedMetalakePO.getMetalakeName()));
+  }
+
+  /**
+   * Writes the policy row and its content snapshot.
+   *
+   * <p>An overwrite is not an upsert any more: the existing row is located 
and locked first, and
+   * the replacement is written as the next version of that row, so the 
snapshot history survives
+   * the overwrite instead of being reset. When no row is there to replace, 
the overwrite inserts
+   * like a plain create. If another create wins the unique key after the 
locking lookup misses, the
+   * caller receives a retryable optimistic-lock failure after the transaction 
is rolled back.
+   *
+   * <p>An overwrite no longer revives a soft-deleted row that happens to 
carry the same policy ID.
+   * Such a row keeps the primary key, so the insert is rejected as an 
already-existing policy,
+   * which is the honest answer: the snapshots and relations of the deleted 
policy are gone with it.
+   */
+  private void insertPolicyWithoutCommit(
+      PolicyEntity policyEntity, PolicyPO initializedPolicyPO, boolean 
overwritten) {
+    if (!overwritten) {
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO existingPolicyPO = 
findAndLockPolicyForOverwrite(initializedPolicyPO);
+    if (existingPolicyPO == null) {
+      if (SessionUtils.getWithoutCommit(
+              PolicyMetaMapper.class,
+              mapper -> 
mapper.countDeletedPolicyMetasById(initializedPolicyPO.getPolicyId()))
+          > 0) {
+        throw new EntityAlreadyExistsException(
+            "The policy ID %s is reserved by a deleted policy; use a new ID",
+            initializedPolicyPO.getPolicyId());
+      }
+      insertNewPolicyWithoutCommit(initializedPolicyPO);
+      return;
+    }
+
+    PolicyPO replacementPolicyPO =
+        POConverters.updatePolicyPOWithVersion(existingPolicyPO, 
initializedPolicyPO);
+    NameIdentifier observedIdentifier =
+        NameIdentifier.of(policyEntity.namespace(), 
existingPolicyPO.getPolicyName());
+    updatePolicyRootWithVersion(observedIdentifier, existingPolicyPO, 
replacementPolicyPO);
+    SessionUtils.doWithoutCommit(
+        PolicyVersionMapper.class,
+        mapper -> 
mapper.insertPolicyVersion(replacementPolicyPO.getPolicyVersionPO()));
+  }
+
+  private void insertNewPolicyWithoutCommit(PolicyPO policyPO) {
+    SessionUtils.doWithoutCommit(
+        PolicyMetaMapper.class, mapper -> mapper.insertPolicyMeta(policyPO));
+    SessionUtils.doWithoutCommit(
+        PolicyVersionMapper.class,
+        mapper -> mapper.insertPolicyVersion(policyPO.getPolicyVersionPO()));
+  }
+
+  /**
+   * Resolves and exclusively locks the row an overwrite replaces, or returns 
null when the name and
+   * the ID are both free.
+   *
+   * <p>The name is the primary key of the search: an overwrite claims the row 
that currently holds
+   * the target name, and the caller-supplied policy ID is dropped in that 
case so the row keeps the
+   * stable ID its version snapshots and relation rows point at. A caller that 
supplies the ID of
+   * one policy together with the name of another therefore replaces the 
content of the policy that
+   * holds the name, not the one the ID identifies. This matches {@code
+   * TagMetaService.findAndLockTagForOverwrite} and is only reachable through 
tests today: {@link
+   * org.apache.gravitino.policy.PolicyManager} always writes with {@code 
overwritten = false}.
+   *
+   * <p>The lookup by ID is the fallback for a rename-by-overwrite, where the 
new name is free and
+   * the row is found by its stable ID.
+   */
+  private PolicyPO findAndLockPolicyForOverwrite(PolicyPO initializedPolicyPO) 
{
+    PolicyPO sameNamePolicyPO =
+        SessionUtils.getWithoutCommit(
+            PolicyMetaMapper.class,
+            mapper ->
+                mapper.selectPolicyMetaByMetalakeIdAndNameForUpdate(
+                    initializedPolicyPO.getMetalakeId(), 
initializedPolicyPO.getPolicyName()));
+    if (sameNamePolicyPO != null) {
+      return sameNamePolicyPO;
+    }
+
+    PolicyPO sameIdPolicyPO =
+        SessionUtils.getWithoutCommit(
+            PolicyMetaMapper.class,
+            mapper -> 
mapper.selectPolicyByPolicyIdForUpdate(initializedPolicyPO.getPolicyId()));
+    if (sameIdPolicyPO != null
+        && !Objects.equals(sameIdPolicyPO.getMetalakeId(), 
initializedPolicyPO.getMetalakeId())) {
+      throw new EntityAlreadyExistsException(
+          "The policy ID %s already belongs to a different metalake",
+          initializedPolicyPO.getPolicyId());
+    }
+    return sameIdPolicyPO;
+  }
+
+  /**
+   * Advances the policy row to the next version, keyed on the version the 
caller observed. A row
+   * that moved on, was renamed away, or was deleted in between matches 
nothing, and the failure is
+   * classified as a conflict or as a missing policy by {@link 
#policyWriteFailure}.
+   */
+  private void updatePolicyRootWithVersion(
+      NameIdentifier identifier, PolicyPO oldPolicyPO, PolicyPO newPolicyPO) {
+    OccWriteSupport.updateWithVersion(
+        () -> {
+          Integer updated =
+              SessionUtils.getWithoutCommit(
+                  PolicyMetaMapper.class,
+                  mapper -> mapper.updatePolicyMeta(newPolicyPO, oldPolicyPO));
+          return updated == null ? 0 : updated;
+        },
+        () -> policyWriteFailure(identifier, oldPolicyPO));
+  }
+
+  private void deletePolicyWithVersion(NameIdentifier identifier, PolicyPO 
observedPolicyPO) {
+    OccWriteSupport.deleteWithVersion(
+        () ->
+            SessionUtils.getWithoutCommit(
+                PolicyMetaMapper.class,
+                mapper ->
+                    mapper.softDeletePolicyByIdAndVersion(
+                        observedPolicyPO.getPolicyId(), 
observedPolicyPO.getCurrentVersion())),
+        () -> policyWriteFailure(identifier, observedPolicyPO));
+  }
+
+  private RuntimeException policyWriteFailure(
+      NameIdentifier identifier, PolicyPO observedPolicyPO) {
+    return OccWriteSupport.writeFailure(
+        identifier,
+        Entity.EntityType.POLICY,
+        () ->
+            SessionUtils.getWithoutCommit(
+                PolicyMetaMapper.class,
+                mapper -> 
mapper.selectPolicyByPolicyIdForUpdate(observedPolicyPO.getPolicyId())),
+        null,
+        current ->
+            Objects.equals(current.getPolicyName(), 
observedPolicyPO.getPolicyName())
+                && Objects.equals(current.getMetalakeId(), 
observedPolicyPO.getMetalakeId()));
+  }
+
+  /**
+   * Locks every policy taking part in an association change and returns the 
rows as they are now,
+   * keyed by policy ID, so the association cannot be written against a policy 
that is being renamed
+   * or dropped.
+   *
+   * <p>The rows are locked in policy-ID order. Two association changes that 
touch the same policies
+   * therefore take the locks in the same order and queue up instead of 
deadlocking.
+   */
+  private Map<Long, PolicyPO> lockPoliciesForAssociation(
+      List<PolicyPO> policyPOsToAdd, List<PolicyPO> policyPOsToRemove) {
+    List<PolicyPO> observedPolicyPOs = new ArrayList<>(policyPOsToAdd);
+    observedPolicyPOs.addAll(policyPOsToRemove);
+    return lockPolicies(observedPolicyPOs);
+  }
+
+  /**
+   * Locks the given policy rows and returns them as they are now, keyed by 
policy ID.
+   *
+   * <p>The rows are locked by one statement that orders them by policy ID, so 
callers that touch
+   * overlapping policies take the row locks in the same order and queue up 
instead of deadlocking,
+   * and a change touching many policies still costs a single round trip.
+   *
+   * <p>A row that is gone, or whose name or metalake no longer matches what 
the caller resolved by
+   * name, is reported as missing: the caller asked for a policy name, and 
that name no longer
+   * points at this row.
+   */
+  static Map<Long, PolicyPO> lockPolicies(List<PolicyPO> observedPolicyPOs) {
+    Map<Long, PolicyPO> observedById = new LinkedHashMap<>();
+    observedPolicyPOs.forEach(policyPO -> 
observedById.put(policyPO.getPolicyId(), policyPO));
+    if (observedById.isEmpty()) {
+      return new LinkedHashMap<>();
+    }
+
+    List<PolicyPO> lockedRows =
+        SessionUtils.getWithoutCommit(
+            PolicyMetaMapper.class,
+            mapper ->
+                mapper.listPolicyPOsByPolicyIdsForUpdate(new 
ArrayList<>(observedById.keySet())));
+    Map<Long, PolicyPO> lockedById = new LinkedHashMap<>();
+    lockedRows.forEach(policyPO -> lockedById.put(policyPO.getPolicyId(), 
policyPO));
+
+    Map<Long, PolicyPO> lockedPolicyPOs = new LinkedHashMap<>();
+    for (PolicyPO observedPolicyPO : observedById.values()) {
+      PolicyPO lockedPolicyPO =
+          OccWriteSupport.lockParentForChildWrite(
+              observedPolicyPO.getPolicyName(),
+              Entity.EntityType.POLICY,
+              () -> lockedById.get(observedPolicyPO.getPolicyId()),
+              null,
+              current ->
+                  Objects.equals(current.getPolicyName(), 
observedPolicyPO.getPolicyName())
+                      && Objects.equals(current.getMetalakeId(), 
observedPolicyPO.getMetalakeId()));
+      lockedPolicyPOs.put(lockedPolicyPO.getPolicyId(), lockedPolicyPO);
+    }
+    return lockedPolicyPOs;
+  }
+
+  private static List<PolicyPO> currentPolicyPOs(
+      List<PolicyPO> observedPolicyPOs, Map<Long, PolicyPO> lockedPolicyPOs) {
+    return observedPolicyPOs.stream()
+        .map(policyPO -> lockedPolicyPOs.get(policyPO.getPolicyId()))
+        .collect(Collectors.toList());
+  }
+
+  private PolicyPO getPolicyPOByMetalakeAndName(String metalakeName, String 
policyName) {
+    PolicyPO policyPO =
+        SessionUtils.getWithoutCommit(
+            PolicyMetaMapper.class,
+            mapper -> mapper.selectPolicyMetaByMetalakeAndName(metalakeName, 
policyName));
+
+    if (policyPO == null) {
+      throw new NoSuchEntityException(
+          NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+          Entity.EntityType.POLICY.name().toLowerCase(),
+          policyName);
+    }
+    return policyPO;
+  }
+
+  private List<PolicyPO> getPolicyPOsByMetalakeAndNames(
+      String metalakeName, List<String> policyNames) {
+    return SessionUtils.getWithoutCommit(
+        PolicyMetaMapper.class,
+        mapper -> mapper.listPolicyPOsByMetalakeAndPolicyNames(metalakeName, 
policyNames));
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyTagRelService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyTagRelService.java
index 49ae66a8d9..886fd565e6 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyTagRelService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyTagRelService.java
@@ -25,6 +25,7 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -359,7 +360,9 @@ public class PolicyTagRelService {
         .map(target -> target.nameIdentifier().name())
         .forEach(policyNames::add);
     if (policyNames.isEmpty()) {
-      return Collections.emptyMap();
+      // A mutable map, like the one built below: returning 
Collections.emptyMap() here would mix
+      // mutable and immutable return values, which Error Prone rejects.
+      return new LinkedHashMap<>();
     }
 
     List<PolicyPO> policies =
@@ -368,8 +371,13 @@ public class PolicyTagRelService {
             mapper ->
                 mapper.listPolicyPOsByMetalakeAndPolicyNames(
                     metalake, new ArrayList<>(policyNames)));
-    Map<String, Long> policyIds =
-        policies.stream().collect(Collectors.toMap(PolicyPO::getPolicyName, 
PolicyPO::getPolicyId));
+    // Lock the policy rows in policy-ID order so that two relation changes 
touching the same
+    // policies queue up instead of deadlocking. The tag row is locked before 
this, so every path
+    // through this service takes its locks in the same tag-then-policy order.
+    Map<String, Long> policyIds = new LinkedHashMap<>();
+    PolicyMetaService.lockPolicies(policies)
+        .values()
+        .forEach(policy -> policyIds.put(policy.getPolicyName(), 
policy.getPolicyId()));
 
     for (String policyName : policyNames) {
       if (!policyIds.containsKey(policyName)) {
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
index 07f98776c8..9f87f4c0fb 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
@@ -64,7 +64,6 @@ import org.apache.gravitino.meta.TagEntity;
 import org.apache.gravitino.meta.TopicEntity;
 import org.apache.gravitino.meta.UserEntity;
 import org.apache.gravitino.policy.Policy;
-import org.apache.gravitino.policy.PolicyContent;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
 import org.apache.gravitino.rel.expressions.Expression;
@@ -761,64 +760,56 @@ public class POConverters {
     }
   }
 
-  public static boolean checkPolicyVersionNeedUpdate(
-      PolicyVersionPO oldPolicyVersionPO, PolicyEntity newPolicy) {
-    if (!StringUtils.equals(oldPolicyVersionPO.getPolicyComment(), 
newPolicy.comment())
-        || oldPolicyVersionPO.isEnabled() != newPolicy.enabled()) {
-      return true;
-    }
-
+  /**
+   * Builds the next complete policy metadata and content snapshot.
+   *
+   * <p>The row keeps the ID it already has: {@code oldPolicyPO} is the row 
being replaced, and its
+   * ID is what the version snapshots and every relation row point at. An 
alter cannot change the
+   * ID, because {@code PolicyMetaService.updatePolicy} rejects an updater 
that returns a different
+   * one; an overwrite of a name held by another row deliberately updates that 
row rather than
+   * inserting a second one under the same name, so the ID the caller supplied 
is dropped.
+   *
+   * @param oldPolicyPO The policy row observed by the caller.
+   * @param newPolicy The policy values to persist.
+   * @return The policy row and version snapshot at the next monotonic version.
+   */
+  public static PolicyPO updatePolicyPOWithVersion(PolicyPO oldPolicyPO, 
PolicyEntity newPolicy) {
     try {
-      PolicyContent oldContent =
-          JsonUtils.anyFieldMapper()
-              .readValue(oldPolicyVersionPO.getContent(), 
newPolicy.policyType().contentClass());
-      if (oldContent == null) {
-        return newPolicy.content() != null;
-      }
-      return !oldContent.equals(newPolicy.content());
+      return buildNextPolicyPOVersion(
+          oldPolicyPO,
+          newPolicy.name(),
+          newPolicy.policyType().policyType(),
+          JsonUtils.anyFieldMapper().writeValueAsString(newPolicy.auditInfo()),
+          newPolicy.comment(),
+          newPolicy.enabled(),
+          JsonUtils.anyFieldMapper().writeValueAsString(newPolicy.content()));
     } catch (JsonProcessingException e) {
-      throw new RuntimeException("Failed to deserialize json object:", e);
+      throw new RuntimeException("Failed to serialize json object:", e);
     }
   }
 
+  /**
+   * Builds the next policy version from values that were serialized before 
acquiring a row lock.
+   *
+   * <p>This overload is used by overwrite: the initialized replacement 
already contains the
+   * serialized audit and content values, so advancing the locked row does not 
repeat CPU-bound JSON
+   * serialization while other writers wait for the lock.
+   *
+   * @param oldPolicyPO The locked policy row being replaced.
+   * @param replacementPolicyPO The initialized replacement values.
+   * @return The policy row and version snapshot at the next monotonic version.
+   */
   public static PolicyPO updatePolicyPOWithVersion(
-      PolicyPO oldPolicyPO, PolicyEntity newPolicy, boolean needUpdateVersion) 
{
-    try {
-      Long lastVersion = oldPolicyPO.getLastVersion();
-      Long currentVersion;
-      PolicyVersionPO newPolicyVersionPO;
-      // Will set the version to the last version + 1
-      if (needUpdateVersion) {
-        lastVersion++;
-        currentVersion = lastVersion;
-        newPolicyVersionPO =
-            PolicyVersionPO.builder()
-                .withMetalakeId(oldPolicyPO.getMetalakeId())
-                .withPolicyId(newPolicy.id())
-                .withVersion(currentVersion)
-                .withPolicyComment(newPolicy.comment())
-                .withEnabled(newPolicy.enabled())
-                
.withContent(JsonUtils.anyFieldMapper().writeValueAsString(newPolicy.content()))
-                .withDeletedAt(DEFAULT_DELETED_AT)
-                .build();
-      } else {
-        currentVersion = oldPolicyPO.getCurrentVersion();
-        newPolicyVersionPO = oldPolicyPO.getPolicyVersionPO();
-      }
-      return PolicyPO.builder()
-          .withPolicyId(newPolicy.id())
-          .withPolicyName(newPolicy.name())
-          .withPolicyType(newPolicy.policyType().policyType())
-          .withMetalakeId(oldPolicyPO.getMetalakeId())
-          
.withAuditInfo(JsonUtils.anyFieldMapper().writeValueAsString(newPolicy.auditInfo()))
-          .withCurrentVersion(currentVersion)
-          .withLastVersion(lastVersion)
-          .withDeletedAt(DEFAULT_DELETED_AT)
-          .withPolicyVersionPO(newPolicyVersionPO)
-          .build();
-    } catch (JsonProcessingException e) {
-      throw new RuntimeException("Failed to serialize json object:", e);
-    }
+      PolicyPO oldPolicyPO, PolicyPO replacementPolicyPO) {
+    PolicyVersionPO replacementVersionPO = 
replacementPolicyPO.getPolicyVersionPO();
+    return buildNextPolicyPOVersion(
+        oldPolicyPO,
+        replacementPolicyPO.getPolicyName(),
+        replacementPolicyPO.getPolicyType(),
+        replacementPolicyPO.getAuditInfo(),
+        replacementVersionPO.getPolicyComment(),
+        replacementVersionPO.isEnabled(),
+        replacementVersionPO.getContent());
   }
 
   /**
@@ -1819,6 +1810,38 @@ public class POConverters {
         .collect(Collectors.toList());
   }
 
+  private static PolicyPO buildNextPolicyPOVersion(
+      PolicyPO oldPolicyPO,
+      String policyName,
+      String policyType,
+      String auditInfo,
+      String policyComment,
+      boolean enabled,
+      String content) {
+    Long nextVersion = Math.max(oldPolicyPO.getCurrentVersion(), 
oldPolicyPO.getLastVersion()) + 1;
+    PolicyVersionPO newPolicyVersionPO =
+        PolicyVersionPO.builder()
+            .withMetalakeId(oldPolicyPO.getMetalakeId())
+            .withPolicyId(oldPolicyPO.getPolicyId())
+            .withVersion(nextVersion)
+            .withPolicyComment(policyComment)
+            .withEnabled(enabled)
+            .withContent(content)
+            .withDeletedAt(DEFAULT_DELETED_AT)
+            .build();
+    return PolicyPO.builder()
+        .withPolicyId(oldPolicyPO.getPolicyId())
+        .withPolicyName(policyName)
+        .withPolicyType(policyType)
+        .withMetalakeId(oldPolicyPO.getMetalakeId())
+        .withAuditInfo(auditInfo)
+        .withCurrentVersion(nextVersion)
+        .withLastVersion(nextVersion)
+        .withDeletedAt(DEFAULT_DELETED_AT)
+        .withPolicyVersionPO(newPolicyVersionPO)
+        .build();
+  }
+
   private static ModelVersionAliasRelPO createAliasRelPO(Long modelId, int 
version, String alias) {
     return ModelVersionAliasRelPO.builder()
         .withModelVersion(version)
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestOccWriteSupport.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestOccWriteSupport.java
index e4432034b8..8e1ecbf01b 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestOccWriteSupport.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestOccWriteSupport.java
@@ -119,6 +119,23 @@ public class TestOccWriteSupport {
                         NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE, 
"metalake", "test")));
   }
 
+  @Test
+  void testUpdateWithVersionSuccess() {
+    assertDoesNotThrow(
+        () ->
+            OccWriteSupport.updateWithVersion(
+                () -> 1, () -> new RuntimeException("Should not be thrown")));
+  }
+
+  @Test
+  void testUpdateWithVersionThrowsOnMiss() {
+    assertThrows(
+        OptimisticLockException.class,
+        () ->
+            OccWriteSupport.updateWithVersion(
+                () -> 0, () -> new OptimisticLockException("test conflict")));
+  }
+
   @Test
   void testDeleteChildrenWithVersionsEmptyOrNull() {
     NameIdentifier parentIdent = NameIdentifier.of("parent");
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestPolicyMetaService.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestPolicyMetaService.java
index 5dd6056e41..ec4d530da3 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestPolicyMetaService.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestPolicyMetaService.java
@@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import java.io.IOException;
 import java.sql.Connection;
 import java.sql.ResultSet;
@@ -34,28 +35,50 @@ import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.EntityAlreadyExistsException;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
+import org.apache.gravitino.RelationEdgeTarget;
+import org.apache.gravitino.RelationUpdate;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.authorization.SecurableObjects;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.exceptions.OptimisticLockException;
+import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.BaseMetalake;
 import org.apache.gravitino.meta.CatalogEntity;
 import org.apache.gravitino.meta.FilesetEntity;
 import org.apache.gravitino.meta.GenericEntity;
 import org.apache.gravitino.meta.ModelEntity;
 import org.apache.gravitino.meta.PolicyEntity;
+import org.apache.gravitino.meta.RoleEntity;
 import org.apache.gravitino.meta.SchemaEntity;
 import org.apache.gravitino.meta.TableEntity;
+import org.apache.gravitino.meta.TagEntity;
 import org.apache.gravitino.meta.TopicEntity;
+import org.apache.gravitino.meta.UserEntity;
 import org.apache.gravitino.policy.Policy;
 import org.apache.gravitino.policy.PolicyContent;
 import org.apache.gravitino.policy.PolicyContents;
 import org.apache.gravitino.storage.RandomIdGenerator;
 import org.apache.gravitino.storage.relational.TestJDBCBackend;
+import org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.PolicyVersionMapper;
+import org.apache.gravitino.storage.relational.po.PolicyPO;
 import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+import org.apache.gravitino.storage.relational.utils.POConverters;
+import org.apache.gravitino.storage.relational.utils.SessionUtils;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.apache.gravitino.utils.NamespaceUtil;
 import org.apache.ibatis.session.SqlSession;
@@ -417,6 +440,484 @@ public class TestPolicyMetaService extends 
TestJDBCBackend {
     assertEquals(policyEntity2, loadedPolicyEntity1);
   }
 
+  @TestTemplate
+  public void testMetadataOnlyPolicyAlterCreatesCompleteSnapshot() throws 
IOException {
+    createAndInsertMakeLake(METALAKE_NAME);
+    PolicyMetaService policyMetaService = PolicyMetaService.getInstance();
+    PolicyEntity policy =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            "policy_metadata_occ",
+            AUDIT_INFO);
+    policyMetaService.insertPolicy(policy, false);
+    PolicyPO initialPO = getPolicyPO(policy.nameIdentifier());
+
+    AuditInfo updatedAudit =
+        
AuditInfo.builder().withCreator("updated-creator").withCreateTime(Instant.now()).build();
+    PolicyEntity metadataOnlyUpdate =
+        copyPolicy(policy, policy.name(), policy.comment(), updatedAudit);
+    policyMetaService.updatePolicy(policy.nameIdentifier(), ignored -> 
metadataOnlyUpdate);
+
+    PolicyPO updatedPO = getPolicyPO(policy.nameIdentifier());
+    assertEquals(initialPO.getCurrentVersion() + 1, 
updatedPO.getCurrentVersion().longValue());
+    assertEquals(updatedPO.getCurrentVersion(), updatedPO.getLastVersion());
+    assertEquals(updatedPO.getCurrentVersion(), 
updatedPO.getPolicyVersionPO().getVersion());
+    assertEquals(policy.comment(), 
updatedPO.getPolicyVersionPO().getPolicyComment());
+    assertEquals(policy.enabled(), updatedPO.getPolicyVersionPO().isEnabled());
+    assertEquals(
+        initialPO.getPolicyVersionPO().getContent(), 
updatedPO.getPolicyVersionPO().getContent());
+    assertEquals(2, listPolicyVersions(policy.id()).size());
+  }
+
+  @TestTemplate
+  public void testPolicyOverwriteAdvancesVersionAndRetainsHistory() throws 
IOException {
+    createAndInsertMakeLake(METALAKE_NAME);
+    PolicyMetaService policyMetaService = PolicyMetaService.getInstance();
+    PolicyEntity policy =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            "policy_overwrite_occ",
+            AUDIT_INFO);
+    policyMetaService.insertPolicy(policy, false);
+    PolicyPO initialPO = getPolicyPO(policy.nameIdentifier());
+
+    PolicyEntity replacement = copyPolicy(policy, 
"policy_overwrite_occ_renamed", "replacement");
+    policyMetaService.insertPolicy(replacement, true);
+
+    PolicyPO overwrittenPO = getPolicyPO(replacement.nameIdentifier());
+    assertEquals(initialPO.getCurrentVersion() + 1, 
overwrittenPO.getCurrentVersion().longValue());
+    assertEquals(overwrittenPO.getCurrentVersion(), 
overwrittenPO.getLastVersion());
+    assertEquals(2, listPolicyVersions(policy.id()).size());
+    assertEquals(
+        replacement, 
policyMetaService.getPolicyByIdentifier(replacement.nameIdentifier()));
+  }
+
+  @TestTemplate
+  public void 
testPolicyAlterReportsOptimisticLockConflictWithoutOrphanVersion()
+      throws IOException {
+    createAndInsertMakeLake(METALAKE_NAME);
+    PolicyMetaService policyMetaService = PolicyMetaService.getInstance();
+    PolicyEntity policy =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            "policy_alter_occ",
+            AUDIT_INFO);
+    policyMetaService.insertPolicy(policy, false);
+
+    assertThrows(
+        OptimisticLockException.class,
+        () ->
+            policyMetaService.updatePolicy(
+                policy.nameIdentifier(),
+                entity -> {
+                  PolicyEntity current = (PolicyEntity) entity;
+                  PolicyPO currentPO = getPolicyPO(current.nameIdentifier());
+                  PolicyEntity competing = copyPolicy(current, current.name(), 
"competing");
+                  PolicyPO competingPO =
+                      POConverters.updatePolicyPOWithVersion(currentPO, 
competing);
+                  SessionUtils.doMultipleWithCommit(
+                      () ->
+                          assertEquals(
+                              Integer.valueOf(1),
+                              SessionUtils.getWithoutCommit(
+                                  PolicyMetaMapper.class,
+                                  mapper -> 
mapper.updatePolicyMeta(competingPO, currentPO))),
+                      () ->
+                          SessionUtils.doWithoutCommit(
+                              PolicyVersionMapper.class,
+                              mapper ->
+                                  
mapper.insertPolicyVersion(competingPO.getPolicyVersionPO())));
+                  return copyPolicy(current, current.name(), "requested");
+                }));
+
+    assertEquals(2, listPolicyVersions(policy.id()).size());
+    assertEquals(
+        "competing", 
policyMetaService.getPolicyByIdentifier(policy.nameIdentifier()).comment());
+  }
+
+  @TestTemplate
+  public void testStalePolicyDeleteRollsBackRelationshipCleanup() throws 
IOException {
+    createAndInsertMakeLake(METALAKE_NAME);
+    CatalogEntity catalog = createAndInsertCatalog(METALAKE_NAME, 
"catalog_policy_delete_occ");
+    PolicyMetaService policyMetaService = PolicyMetaService.getInstance();
+    PolicyEntity policy =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            "policy_delete_occ",
+            AUDIT_INFO);
+    policyMetaService.insertPolicy(policy, false);
+    policyMetaService.associatePoliciesWithMetadataObject(
+        catalog.nameIdentifier(),
+        catalog.type(),
+        new NameIdentifier[] {policy.nameIdentifier()},
+        new NameIdentifier[0]);
+    PolicyPO stalePO = getPolicyPO(policy.nameIdentifier());
+    policyMetaService.updatePolicy(
+        policy.nameIdentifier(),
+        entity -> copyPolicy((PolicyEntity) entity, ((PolicyEntity) 
entity).name(), "updated"));
+
+    assertThrows(
+        OptimisticLockException.class,
+        () -> policyMetaService.deletePolicy(policy.nameIdentifier(), 
stalePO));
+    assertEquals(1, countActivePolicyRel(policy.id()));
+    assertTrue(backend.exists(policy.nameIdentifier(), 
Entity.EntityType.POLICY));
+    assertEquals(
+        2,
+        listPolicyVersions(policy.id()).values().stream().filter(v -> 
v.longValue() == 0L).count());
+
+    assertTrue(policyMetaService.deletePolicy(policy.nameIdentifier()));
+    assertEquals(0, countActivePolicyRel(policy.id()));
+    assertEquals(
+        0,
+        listPolicyVersions(policy.id()).values().stream().filter(v -> 
v.longValue() == 0L).count());
+  }
+
+  @TestTemplate
+  public void testPolicyCreateIsFencedByParentMetalake() {
+    PolicyMetaService policyMetaService = PolicyMetaService.getInstance();
+    PolicyEntity policy =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy("metalake_that_does_not_exist"),
+            "policy_without_metalake",
+            AUDIT_INFO);
+
+    assertThrows(NoSuchEntityException.class, () -> 
policyMetaService.insertPolicy(policy, false));
+    assertThrows(NoSuchEntityException.class, () -> 
policyMetaService.insertPolicy(policy, true));
+  }
+
+  @TestTemplate
+  public void testPolicyOverwriteReplacesTheRowHoldingTheName() throws 
IOException {
+    createAndInsertMakeLake(METALAKE_NAME);
+    PolicyMetaService policyMetaService = PolicyMetaService.getInstance();
+    PolicyEntity policy =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            "policy_overwrite_by_name",
+            AUDIT_INFO);
+    policyMetaService.insertPolicy(policy, false);
+    PolicyPO initialPO = getPolicyPO(policy.nameIdentifier());
+
+    // A different ID for a name that is already taken replaces the row that 
holds the name,
+    // instead of inserting a second row for it.
+    PolicyEntity sameNameOtherId =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            "policy_overwrite_by_name",
+            AUDIT_INFO);
+    policyMetaService.insertPolicy(sameNameOtherId, true);
+
+    PolicyPO overwrittenPO = getPolicyPO(policy.nameIdentifier());
+    assertEquals(policy.id(), overwrittenPO.getPolicyId().longValue());
+    assertEquals(initialPO.getCurrentVersion() + 1, 
overwrittenPO.getCurrentVersion().longValue());
+    assertEquals(2, listPolicyVersions(policy.id()).size());
+  }
+
+  /** A deleted primary key must not be classified as a retryable overwrite 
race. */
+  @TestTemplate
+  public void testOverwriteRejectsDeletedPolicyId() throws IOException {
+    createAndInsertMakeLake(METALAKE_NAME);
+    PolicyMetaService service = PolicyMetaService.getInstance();
+    Namespace ns = NamespaceUtil.ofPolicy(METALAKE_NAME);
+    PolicyEntity policy =
+        createPolicy(RandomIdGenerator.INSTANCE.nextId(), ns, 
"deleted_policy_id", AUDIT_INFO);
+    service.insertPolicy(policy, false);
+    assertTrue(service.deletePolicy(policy.nameIdentifier()));
+    EntityAlreadyExistsException failure =
+        assertThrows(EntityAlreadyExistsException.class, () -> 
service.insertPolicy(policy, true));
+    assertTrue(failure.getMessage().contains("use a new ID"));
+    assertThrows(
+        NoSuchEntityException.class, () -> 
service.getPolicyByIdentifier(policy.nameIdentifier()));
+    listPolicyVersions(policy.id()).values().forEach(deletedAt -> 
assertTrue(deletedAt > 0));
+
+    PolicyEntity replacement =
+        createPolicy(RandomIdGenerator.INSTANCE.nextId(), ns, policy.name(), 
AUDIT_INFO);
+    service.insertPolicy(replacement, true);
+    assertEquals(replacement.id(), 
service.getPolicyByIdentifier(policy.nameIdentifier()).id());
+  }
+
+  /** An overwrite cannot adopt a stable ID from a metalake it has not locked. 
*/
+  @TestTemplate
+  public void testOverwriteRejectsPolicyIdInAnotherMetalake() throws 
IOException {
+    createAndInsertMakeLake(METALAKE_NAME);
+    createAndInsertMakeLake("foreign_policy_metalake");
+    PolicyMetaService service = PolicyMetaService.getInstance();
+    PolicyEntity foreign =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy("foreign_policy_metalake"),
+            "foreign_policy",
+            AUDIT_INFO);
+    service.insertPolicy(foreign, false);
+    PolicyEntity incoming =
+        createPolicy(
+            foreign.id(), NamespaceUtil.ofPolicy(METALAKE_NAME), 
foreign.name(), AUDIT_INFO);
+    assertThrows(EntityAlreadyExistsException.class, () -> 
service.insertPolicy(incoming, true));
+    assertEquals(foreign.id(), 
service.getPolicyByIdentifier(foreign.nameIdentifier()).id());
+    assertThrows(
+        NoSuchEntityException.class,
+        () -> service.getPolicyByIdentifier(incoming.nameIdentifier()));
+  }
+
+  /** Two first-time overwrites must serialize or expose a retryable insert 
conflict. */
+  @TestTemplate
+  public void testConcurrentOverwriteOfMissingPolicy() throws Exception {
+    createAndInsertMakeLake(METALAKE_NAME);
+    PolicyMetaService service = PolicyMetaService.getInstance();
+    Namespace ns = NamespaceUtil.ofPolicy(METALAKE_NAME);
+    PolicyEntity first =
+        createPolicy(RandomIdGenerator.INSTANCE.nextId(), ns, 
"first_overwrite", AUDIT_INFO);
+    PolicyEntity second =
+        copyPolicy(
+            createPolicy(RandomIdGenerator.INSTANCE.nextId(), ns, 
first.name(), AUDIT_INFO),
+            first.name(),
+            "second overwrite");
+    CountDownLatch firstWritten = new CountDownLatch(1);
+    CountDownLatch allowCommit = new CountDownLatch(1);
+    CountDownLatch secondStarted = new CountDownLatch(1);
+    ExecutorService executor = Executors.newFixedThreadPool(2);
+    Future<Throwable> firstResult =
+        executor.submit(
+            () -> {
+              SessionUtils.beginTransaction();
+              try {
+                service.insertPolicy(first, true);
+                firstWritten.countDown();
+                await(allowCommit);
+                SessionUtils.commitTransaction();
+                return null;
+              } catch (Throwable failure) {
+                SessionUtils.rollbackTransaction();
+                return failure;
+              }
+            });
+    try {
+      assertTrue(firstWritten.await(30, TimeUnit.SECONDS));
+      Future<Throwable> secondResult =
+          executor.submit(
+              () -> {
+                secondStarted.countDown();
+                try {
+                  service.insertPolicy(second, true);
+                  return null;
+                } catch (Throwable failure) {
+                  return failure;
+                }
+              });
+      assertTrue(secondStarted.await(30, TimeUnit.SECONDS));
+      assertThrows(TimeoutException.class, () -> secondResult.get(500, 
TimeUnit.MILLISECONDS));
+      allowCommit.countDown();
+      Assertions.assertNull(firstResult.get(30, TimeUnit.SECONDS));
+      Throwable failure = secondResult.get(30, TimeUnit.SECONDS);
+      if (failure != null) {
+        Assertions.assertInstanceOf(OptimisticLockException.class, failure);
+        assertEquals(1, listPolicyVersions(first.id()).size());
+        assertTrue(listPolicyVersions(second.id()).isEmpty());
+        service.insertPolicy(second, true);
+      }
+      PolicyEntity stored = 
service.getPolicyByIdentifier(first.nameIdentifier());
+      assertEquals(first.id(), stored.id());
+      assertEquals("second overwrite", stored.comment());
+      assertEquals(2L, 
getPolicyPO(first.nameIdentifier()).getCurrentVersion());
+      assertEquals(2, listPolicyVersions(first.id()).size());
+      assertTrue(listPolicyVersions(second.id()).isEmpty());
+    } finally {
+      allowCommit.countDown();
+      executor.shutdownNow();
+    }
+  }
+
+  @TestTemplate
+  public void testPolicyOverwriteByNameDoesNotRevertConcurrentRename() throws 
Exception {
+    createAndInsertMakeLake(METALAKE_NAME);
+    PolicyMetaService policyMetaService = PolicyMetaService.getInstance();
+    PolicyEntity original =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            "policy_overwrite_rename_race",
+            AUDIT_INFO);
+    policyMetaService.insertPolicy(original, false);
+    PolicyPO observedPO = getPolicyPO(original.nameIdentifier());
+
+    PolicyEntity renamed = copyPolicy(original, 
"policy_overwrite_rename_winner", "rename winner");
+    PolicyPO renamedPO = POConverters.updatePolicyPOWithVersion(observedPO, 
renamed);
+    PolicyEntity replacement =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            original.name(),
+            AUDIT_INFO);
+
+    CountDownLatch renameWritten = new CountDownLatch(1);
+    CountDownLatch allowRenameCommit = new CountDownLatch(1);
+    CountDownLatch overwriteStarted = new CountDownLatch(1);
+    ExecutorService executor = Executors.newFixedThreadPool(2);
+    Future<Throwable> renameResult =
+        executor.submit(
+            () -> {
+              try {
+                SessionUtils.doMultipleWithCommit(
+                    () ->
+                        assertEquals(
+                            Integer.valueOf(1),
+                            SessionUtils.getWithoutCommit(
+                                PolicyMetaMapper.class,
+                                mapper -> mapper.updatePolicyMeta(renamedPO, 
observedPO))),
+                    () ->
+                        SessionUtils.doWithoutCommit(
+                            PolicyVersionMapper.class,
+                            mapper -> 
mapper.insertPolicyVersion(renamedPO.getPolicyVersionPO())),
+                    () -> {
+                      renameWritten.countDown();
+                      await(allowRenameCommit);
+                    });
+                return null;
+              } catch (Throwable throwable) {
+                return throwable;
+              }
+            });
+
+    try {
+      assertTrue(renameWritten.await(30, TimeUnit.SECONDS));
+      Future<Throwable> overwriteResult =
+          executor.submit(
+              () -> {
+                overwriteStarted.countDown();
+                try {
+                  policyMetaService.insertPolicy(replacement, true);
+                  return null;
+                } catch (Throwable throwable) {
+                  return throwable;
+                }
+              });
+      assertTrue(overwriteStarted.await(30, TimeUnit.SECONDS));
+      assertThrows(TimeoutException.class, () -> overwriteResult.get(500, 
TimeUnit.MILLISECONDS));
+
+      allowRenameCommit.countDown();
+      Assertions.assertNull(renameResult.get(30, TimeUnit.SECONDS));
+      Assertions.assertNull(overwriteResult.get(30, TimeUnit.SECONDS));
+    } finally {
+      allowRenameCommit.countDown();
+      executor.shutdownNow();
+    }
+
+    assertEquals(
+        original.id(), 
policyMetaService.getPolicyByIdentifier(renamed.nameIdentifier()).id());
+    assertEquals(
+        replacement.id(), 
policyMetaService.getPolicyByIdentifier(original.nameIdentifier()).id());
+  }
+
+  @TestTemplate
+  public void testPolicyDeleteReturnsFalseWhenConcurrentDeleteWins() throws 
IOException {
+    createAndInsertMakeLake(METALAKE_NAME);
+    PolicyMetaService policyMetaService = PolicyMetaService.getInstance();
+    PolicyEntity policy =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            "policy_concurrent_delete",
+            AUDIT_INFO);
+    policyMetaService.insertPolicy(policy, false);
+    PolicyPO observedPO = getPolicyPO(policy.nameIdentifier());
+
+    assertTrue(policyMetaService.deletePolicy(policy.nameIdentifier()));
+    assertFalse(policyMetaService.deletePolicy(policy.nameIdentifier(), 
observedPO));
+  }
+
+  @TestTemplate
+  public void testDeletePolicyCleansEveryDependentRelation() throws 
IOException {
+    createAndInsertMakeLake(METALAKE_NAME);
+    CatalogEntity catalog = createAndInsertCatalog(METALAKE_NAME, 
"catalog_policy_cascade");
+    PolicyMetaService policyMetaService = PolicyMetaService.getInstance();
+    PolicyEntity policy =
+        createPolicy(
+            RandomIdGenerator.INSTANCE.nextId(),
+            NamespaceUtil.ofPolicy(METALAKE_NAME),
+            "policy_cascade_occ",
+            AUDIT_INFO);
+    policyMetaService.insertPolicy(policy, false);
+    policyMetaService.associatePoliciesWithMetadataObject(
+        catalog.nameIdentifier(),
+        catalog.type(),
+        new NameIdentifier[] {policy.nameIdentifier()},
+        new NameIdentifier[0]);
+
+    TagEntity tag = createAndInsertTagEntity("tag_policy_cascade", "tag 
comment", METALAKE_NAME);
+    backend.updateEntityRelations(
+        RelationUpdate.of(
+            SupportsRelationOperations.Type.POLICY_TAG_REL,
+            tag.nameIdentifier(),
+            Entity.EntityType.TAG,
+            new RelationEdgeTarget[] {
+              RelationEdgeTarget.of(
+                  policy.nameIdentifier(),
+                  Entity.EntityType.POLICY,
+                  "{\"type\":\"TAG_VALUE\",\"value\":\"finance\"}")
+            },
+            new RelationEdgeTarget[0]));
+    TagMetaService.getInstance()
+        .associateTagsWithMetadataObject(
+            policy.nameIdentifier(),
+            Entity.EntityType.POLICY,
+            new NameIdentifier[] {tag.nameIdentifier()},
+            new NameIdentifier[0]);
+
+    UserEntity user =
+        createUserEntity(
+            RandomIdGenerator.INSTANCE.nextId(),
+            AuthorizationUtils.ofUserNamespace(METALAKE_NAME),
+            "user_policy_cascade",
+            AUDIT_INFO);
+    backend.insert(user, false);
+    OwnerMetaService.getInstance()
+        .setOwner(
+            policy.nameIdentifier(), Entity.EntityType.POLICY, 
user.nameIdentifier(), user.type());
+
+    RoleEntity role =
+        createRoleEntity(
+            RandomIdGenerator.INSTANCE.nextId(),
+            AuthorizationUtils.ofRoleNamespace(METALAKE_NAME),
+            "role_policy_cascade",
+            AUDIT_INFO,
+            Lists.newArrayList(
+                SecurableObjects.ofPolicy(
+                    policy.name(), 
Lists.newArrayList(Privileges.ApplyPolicy.allow()))),
+            null);
+    backend.insert(role, false);
+
+    String policyAsMetadataObject =
+        String.format("metadata_object_id = %d AND metadata_object_type = 
'POLICY'", policy.id());
+    assertEquals(1, countActivePolicyRel(policy.id()));
+    assertEquals(1, countActiveRows("policy_tag_relation_meta", "policy_id = " 
+ policy.id()));
+    assertEquals(1, countActiveRows("tag_relation_meta", 
policyAsMetadataObject));
+    assertEquals(1, countActiveRows("owner_meta", policyAsMetadataObject));
+    assertEquals(
+        1,
+        countActiveRows(
+            "role_meta_securable_object",
+            String.format("metadata_object_id = %d AND type = 'POLICY'", 
policy.id())));
+
+    assertTrue(policyMetaService.deletePolicy(policy.nameIdentifier()));
+
+    assertEquals(0, countActivePolicyRel(policy.id()));
+    assertEquals(0, countActiveRows("policy_tag_relation_meta", "policy_id = " 
+ policy.id()));
+    assertEquals(0, countActiveRows("tag_relation_meta", 
policyAsMetadataObject));
+    assertEquals(0, countActiveRows("owner_meta", policyAsMetadataObject));
+    assertEquals(
+        0,
+        countActiveRows(
+            "role_meta_securable_object",
+            String.format("metadata_object_id = %d AND type = 'POLICY'", 
policy.id())));
+    assertEquals(0, listPolicyVersions(policy.id()).values().stream().filter(v 
-> v == 0L).count());
+  }
+
   @TestTemplate
   public void testDeletePolicy() throws IOException {
     createAndInsertMakeLake(METALAKE_NAME);
@@ -1040,26 +1541,28 @@ public class TestPolicyMetaService extends 
TestJDBCBackend {
     return new EntitiesToTest(catalog, schema, table, topic, fileset, model);
   }
 
-  private Integer countActivePolicyRel(Long policyId) {
+  private int countActiveRows(String table, String whereClause) {
     try (SqlSession sqlSession =
             
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
         Connection connection = sqlSession.getConnection();
-        Statement statement1 = connection.createStatement();
-        ResultSet rs1 =
-            statement1.executeQuery(
+        Statement statement = connection.createStatement();
+        ResultSet rs =
+            statement.executeQuery(
                 String.format(
-                    "SELECT count(*) FROM policy_relation_meta WHERE policy_id 
= %d AND deleted_at = 0",
-                    policyId))) {
-      if (rs1.next()) {
-        return rs1.getInt(1);
-      } else {
-        throw new RuntimeException("Doesn't contain data");
+                    "SELECT count(*) FROM %s WHERE %s AND deleted_at = 0", 
table, whereClause))) {
+      if (rs.next()) {
+        return rs.getInt(1);
       }
+      throw new RuntimeException("Doesn't contain data");
     } catch (SQLException se) {
       throw new RuntimeException("SQL execution failed", se);
     }
   }
 
+  private Integer countActivePolicyRel(Long policyId) {
+    return countActiveRows("policy_relation_meta", "policy_id = " + policyId);
+  }
+
   private Integer countAllPolicyRel(Long policyId) {
     try (SqlSession sqlSession =
             
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
@@ -1078,4 +1581,40 @@ public class TestPolicyMetaService extends 
TestJDBCBackend {
       throw new RuntimeException("SQL execution failed", se);
     }
   }
+
+  private PolicyPO getPolicyPO(NameIdentifier identifier) {
+    return SessionUtils.getWithoutCommit(
+        PolicyMetaMapper.class,
+        mapper ->
+            mapper.selectPolicyMetaByMetalakeAndName(
+                identifier.namespace().level(0), identifier.name()));
+  }
+
+  /** Copies the policy under a new name and comment, keeping the audit info 
tests create with. */
+  private PolicyEntity copyPolicy(PolicyEntity policy, String name, String 
comment) {
+    return copyPolicy(policy, name, comment, AUDIT_INFO);
+  }
+
+  private PolicyEntity copyPolicy(
+      PolicyEntity policy, String name, String comment, AuditInfo auditInfo) {
+    return PolicyEntity.builder()
+        .withId(policy.id())
+        .withName(name)
+        .withNamespace(policy.namespace())
+        .withPolicyType(policy.policyType())
+        .withComment(comment)
+        .withEnabled(policy.enabled())
+        .withContent(policy.content())
+        .withAuditInfo(auditInfo)
+        .build();
+  }
+
+  private void await(CountDownLatch latch) {
+    try {
+      assertTrue(latch.await(30, TimeUnit.SECONDS));
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException(e);
+    }
+  }
 }

Reply via email to