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

yufei pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 6a4d2a18c Policy Store: Check whether Policy is in use before dropping 
and support `detach-all` flag (#1467)
6a4d2a18c is described below

commit 6a4d2a18c379df83cea2a8f0588fb145ad262fb7
Author: Honah (Jonas) J. <hon...@apache.org>
AuthorDate: Tue Apr 29 01:29:17 2025 -0500

    Policy Store: Check whether Policy is in use before dropping and support 
`detach-all` flag (#1467)
---
 .../apache/polaris/service/it/env/PolicyApi.java   | 15 +++++-
 .../test/PolarisPolicyServiceIntegrationTest.java  | 16 +++++-
 .../AtomicOperationMetaStoreManager.java           | 36 +++++++++++++-
 .../core/persistence/dao/entity/BaseResult.java    |  3 ++
 .../TransactionalMetaStoreManagerImpl.java         | 37 +++++++++++++-
 .../TreeMapTransactionalPersistenceImpl.java       |  4 +-
 ...PolicyValidator.java => PolicyMappingUtil.java} | 22 ++++----
 .../policy/exceptions/PolicyInUseException.java    | 34 +++++++++++++
 .../BaseMaintenancePolicyValidator.java            | 23 +--------
 .../BasePolarisMetaStoreManagerTest.java           |  5 ++
 .../persistence/PolarisTestMetaStoreManager.java   | 58 ++++++++++++++++++++++
 .../service/quarkus/catalog/PolicyCatalogTest.java | 25 ++++++++++
 .../service/catalog/policy/PolicyCatalog.java      |  9 +++-
 .../service/exception/PolarisExceptionMapper.java  |  3 ++
 14 files changed, 250 insertions(+), 40 deletions(-)

diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java
index 7597aaef3..6061ef8c9 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java
@@ -28,6 +28,7 @@ import java.util.Map;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.rest.RESTUtil;
 import org.apache.polaris.core.policy.PolicyType;
+import org.apache.polaris.core.policy.exceptions.PolicyInUseException;
 import org.apache.polaris.service.types.ApplicablePolicy;
 import org.apache.polaris.service.types.AttachPolicyRequest;
 import org.apache.polaris.service.types.CreatePolicyRequest;
@@ -72,12 +73,24 @@ public class PolicyApi extends RestApi {
   }
 
   public void dropPolicy(String catalog, PolicyIdentifier policyIdentifier) {
+    dropPolicy(catalog, policyIdentifier, null);
+  }
+
+  public void dropPolicy(String catalog, PolicyIdentifier policyIdentifier, 
Boolean detachAll) {
     String ns = RESTUtil.encodeNamespace(policyIdentifier.getNamespace());
+    Map<String, String> queryParams = new HashMap<>();
+    if (detachAll != null) {
+      queryParams.put("detach-all", detachAll.toString());
+    }
     try (Response res =
         request(
                 "polaris/v1/{cat}/namespaces/{ns}/policies/{policy}",
-                Map.of("cat", catalog, "ns", ns, "policy", 
policyIdentifier.getName()))
+                Map.of("cat", catalog, "ns", ns, "policy", 
policyIdentifier.getName()),
+                queryParams)
             .delete()) {
+      if (res.getStatus() == Response.Status.BAD_REQUEST.getStatusCode()) {
+        throw new PolicyInUseException("Policy in use");
+      }
       
Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode());
     }
   }
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
index 055fa411c..2b833ba74 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
@@ -50,6 +50,7 @@ import org.apache.polaris.core.admin.model.StorageConfigInfo;
 import org.apache.polaris.core.catalog.PolarisCatalogHelpers;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.policy.PredefinedPolicyTypes;
+import org.apache.polaris.core.policy.exceptions.PolicyInUseException;
 import org.apache.polaris.service.it.env.ClientCredentials;
 import org.apache.polaris.service.it.env.IcebergHelper;
 import org.apache.polaris.service.it.env.IntegrationTestsHelper;
@@ -268,8 +269,21 @@ public class PolarisPolicyServiceIntegrationTest {
         PredefinedPolicyTypes.DATA_COMPACTION,
         EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT,
         "test policy");
-    policyApi.dropPolicy(currentCatalogName, NS1_P1);
+
+    PolicyAttachmentTarget catalogTarget =
+        
PolicyAttachmentTarget.builder().setType(PolicyAttachmentTarget.TypeEnum.CATALOG).build();
+    policyApi.attachPolicy(currentCatalogName, NS1_P1, catalogTarget, 
Map.of());
+
+    // dropPolicy should fail because the policy is attached to the catalog
+    Assertions.assertThatThrownBy(() -> 
policyApi.dropPolicy(currentCatalogName, NS1_P1))
+        .isInstanceOf(PolicyInUseException.class);
+
+    // with detach-all=true, the policy and the attachment should be dropped
+    policyApi.dropPolicy(currentCatalogName, NS1_P1, true);
     Assertions.assertThat(policyApi.listPolicies(currentCatalogName, 
NS1)).hasSize(0);
+    // The policy mapping record should be dropped
+    Assertions.assertThat(policyApi.getApplicablePolicies(currentCatalogName, 
null, null, null))
+        .hasSize(0);
   }
 
   @Test
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java
index d6cfd8429..2170550dd 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java
@@ -64,6 +64,7 @@ import 
org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult;
 import org.apache.polaris.core.persistence.dao.entity.ScopedCredentialsResult;
 import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
 import org.apache.polaris.core.policy.PolicyEntity;
+import org.apache.polaris.core.policy.PolicyMappingUtil;
 import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.storage.PolarisCredentialProperty;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
@@ -191,6 +192,27 @@ public class AtomicOperationMetaStoreManager extends 
BaseMetaStoreManager {
         ms.loadAllGrantRecordsOnSecurable(callCtx, entity.getCatalogId(), 
entity.getId());
     ms.deleteAllEntityGrantRecords(callCtx, entity, grantsOnGrantee, 
grantsOnSecurable);
 
+    if (entity.getType() == PolarisEntityType.POLICY
+        || PolicyMappingUtil.isValidTargetEntityType(entity.getType(), 
entity.getSubType())) {
+      // Best-effort cleanup - for policy and potential target entities, drop 
all policy mapping
+      // records related
+      // TODO: Support some more formal garbage-collection mechanism similar 
to grant records case
+      // above
+      try {
+        final List<PolarisPolicyMappingRecord> mappingOnPolicy =
+            (entity.getType() == PolarisEntityType.POLICY)
+                ? ms.loadAllTargetsOnPolicy(callCtx, entity.getCatalogId(), 
entity.getId())
+                : List.of();
+        final List<PolarisPolicyMappingRecord> mappingOnTarget =
+            (entity.getType() == PolarisEntityType.POLICY)
+                ? List.of()
+                : ms.loadAllPoliciesOnTarget(callCtx, entity.getCatalogId(), 
entity.getId());
+        ms.deleteAllEntityPolicyMappingRecords(callCtx, entity, 
mappingOnTarget, mappingOnPolicy);
+      } catch (UnsupportedOperationException e) {
+        // Policy mapping persistence not implemented, but we should not block 
dropping entities
+      }
+    }
+
     // Now determine the set of entities on the other side of the grants we 
just removed. Grants
     // from/to these entities has been removed, hence we need to update the 
grant version of
     // each entity. Collect the id of each.
@@ -1177,6 +1199,18 @@ public class AtomicOperationMetaStoreManager extends 
BaseMetaStoreManager {
           callCtx, null, refreshEntityToDrop.getCatalogId(), 
refreshEntityToDrop.getId())) {
         return new 
DropEntityResult(BaseResult.ReturnStatus.NAMESPACE_NOT_EMPTY, null);
       }
+    } else if (refreshEntityToDrop.getType() == PolarisEntityType.POLICY && 
!cleanup) {
+      try {
+        //  need to check if the policy is attached to any entity
+        List<PolarisPolicyMappingRecord> records =
+            ms.loadAllTargetsOnPolicy(
+                callCtx, refreshEntityToDrop.getCatalogId(), 
refreshEntityToDrop.getId());
+        if (!records.isEmpty()) {
+          return new 
DropEntityResult(BaseResult.ReturnStatus.POLICY_HAS_MAPPINGS, null);
+        }
+      } catch (UnsupportedOperationException e) {
+        // Policy mapping persistence not implemented, but we should not block 
dropping entities
+      }
     }
 
     // simply delete that entity. Will be removed from entities_active, added 
to the
@@ -1186,7 +1220,7 @@ public class AtomicOperationMetaStoreManager extends 
BaseMetaStoreManager {
     // if cleanup, schedule a cleanup task for the entity. do this here, so 
that drop and scheduling
     // the cleanup task is transactional. Otherwise, we'll be unable to 
schedule the cleanup task
     // later
-    if (cleanup) {
+    if (cleanup && refreshEntityToDrop.getType() != PolarisEntityType.POLICY) {
       PolarisBaseEntity taskEntity =
           new PolarisEntity.Builder()
               .setId(ms.generateNewId(callCtx))
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java
index a4eee22cc..a8f3d5aef 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java
@@ -117,6 +117,9 @@ public class BaseResult {
 
     // policy mapping of same type already exists
     POLICY_MAPPING_OF_SAME_TYPE_ALREADY_EXISTS(15),
+
+    // policy has mappings and cannot be dropped
+    POLICY_HAS_MAPPINGS(16),
     ;
 
     // code for the enum
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java
index 22120c7a3..9f508eb4e 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java
@@ -65,6 +65,7 @@ import 
org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult;
 import org.apache.polaris.core.persistence.dao.entity.ScopedCredentialsResult;
 import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
 import org.apache.polaris.core.policy.PolicyEntity;
+import org.apache.polaris.core.policy.PolicyMappingUtil;
 import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.storage.PolarisCredentialProperty;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
@@ -193,6 +194,28 @@ public class TransactionalMetaStoreManagerImpl extends 
BaseMetaStoreManager {
       ms.writeEntityInCurrentTxn(callCtx, entityGrantChanged, false, 
originalEntity);
     }
 
+    if (entity.getType() == PolarisEntityType.POLICY
+        || PolicyMappingUtil.isValidTargetEntityType(entity.getType(), 
entity.getSubType())) {
+      // Best-effort cleanup - for policy and potential target entities, drop 
all policy mapping
+      // records related
+      try {
+        final List<PolarisPolicyMappingRecord> mappingOnPolicy =
+            (entity.getType() == PolarisEntityType.POLICY)
+                ? ms.loadAllTargetsOnPolicyInCurrentTxn(
+                    callCtx, entity.getCatalogId(), entity.getId())
+                : List.of();
+        final List<PolarisPolicyMappingRecord> mappingOnTarget =
+            (entity.getType() == PolarisEntityType.POLICY)
+                ? List.of()
+                : ms.loadAllPoliciesOnTargetInCurrentTxn(
+                    callCtx, entity.getCatalogId(), entity.getId());
+        ms.deleteAllEntityPolicyMappingRecordsInCurrentTxn(
+            callCtx, entity, mappingOnTarget, mappingOnPolicy);
+      } catch (UnsupportedOperationException e) {
+        // Policy mapping persistence not implemented, but we should not block 
dropping entities
+      }
+    }
+
     // remove the entity being dropped now
     ms.deleteEntityInCurrentTxn(callCtx, entity);
 
@@ -1359,6 +1382,18 @@ public class TransactionalMetaStoreManagerImpl extends 
BaseMetaStoreManager {
           callCtx, null, refreshEntityToDrop.getCatalogId(), 
refreshEntityToDrop.getId())) {
         return new 
DropEntityResult(BaseResult.ReturnStatus.NAMESPACE_NOT_EMPTY, null);
       }
+    } else if (refreshEntityToDrop.getType() == PolarisEntityType.POLICY && 
!cleanup) {
+      // need to check if the policy is attached to any entity
+      try {
+        List<PolarisPolicyMappingRecord> records =
+            ms.loadAllTargetsOnPolicyInCurrentTxn(
+                callCtx, refreshEntityToDrop.getCatalogId(), 
refreshEntityToDrop.getId());
+        if (!records.isEmpty()) {
+          return new 
DropEntityResult(BaseResult.ReturnStatus.POLICY_HAS_MAPPINGS, null);
+        }
+      } catch (UnsupportedOperationException e) {
+        // Policy mapping persistence not implemented, but we should not block 
dropping entities
+      }
     }
 
     // simply delete that entity. Will be removed from entities_active, added 
to the
@@ -1368,7 +1403,7 @@ public class TransactionalMetaStoreManagerImpl extends 
BaseMetaStoreManager {
     // if cleanup, schedule a cleanup task for the entity. do this here, so 
that drop and scheduling
     // the cleanup task is transactional. Otherwise, we'll be unable to 
schedule the cleanup task
     // later
-    if (cleanup) {
+    if (cleanup && refreshEntityToDrop.getType() != PolarisEntityType.POLICY) {
       PolarisBaseEntity taskEntity =
           new PolarisEntity.Builder()
               .setId(ms.generateNewIdInCurrentTxn(callCtx))
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java
index e9cbd53f3..39ce364d3 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java
@@ -589,9 +589,9 @@ public class TreeMapTransactionalPersistenceImpl extends 
AbstractTransactionalPe
 
     // also delete the other side. We need to delete these mapping one at a 
time versus doing a
     // range delete
-    mappingOnTarget.forEach(record -> 
this.store.getSlicePolicyMappingRecords().delete(record));
-    mappingOnPolicy.forEach(
+    mappingOnTarget.forEach(
         record -> 
this.store.getSlicePolicyMappingRecordsByPolicy().delete(record));
+    mappingOnPolicy.forEach(record -> 
this.store.getSlicePolicyMappingRecords().delete(record));
   }
 
   /** {@inheritDoc} */
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyMappingUtil.java
similarity index 73%
copy from 
polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java
copy to 
polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyMappingUtil.java
index 86308f555..3bf1296c2 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyMappingUtil.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.core.policy.validator.maintenance;
+package org.apache.polaris.core.policy;
 
 import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG;
 import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE;
@@ -25,21 +25,21 @@ import static 
org.apache.polaris.core.entity.PolarisEntityType.TABLE_LIKE;
 import java.util.Set;
 import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.PolarisEntityType;
-import org.apache.polaris.core.policy.validator.InvalidPolicyException;
-import org.apache.polaris.core.policy.validator.PolicyValidator;
 
-public class BaseMaintenancePolicyValidator implements PolicyValidator {
-  public static final BaseMaintenancePolicyValidator INSTANCE =
-      new BaseMaintenancePolicyValidator();
+public class PolicyMappingUtil {
 
   private static final Set<PolarisEntityType> ATTACHABLE_ENTITY_TYPES =
       Set.of(CATALOG, NAMESPACE, TABLE_LIKE);
 
-  @Override
-  public void validate(String content) throws InvalidPolicyException {}
-
-  @Override
-  public boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType 
entitySubType) {
+  /**
+   * Checks if the given entity type and subtype are valid targets for policy 
attachment.
+   *
+   * <p>This method excludes non-attachable entities such as catalog-role and 
task. Each policy type
+   * may impose additional specific rules * to determine whether it can target 
a particular entity
+   * type or subtype.
+   */
+  public static boolean isValidTargetEntityType(
+      PolarisEntityType entityType, PolarisEntitySubType entitySubType) {
     if (entityType == null) {
       return false;
     }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/policy/exceptions/PolicyInUseException.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/exceptions/PolicyInUseException.java
new file mode 100644
index 000000000..14d165497
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/exceptions/PolicyInUseException.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.policy.exceptions;
+
+import com.google.errorprone.annotations.FormatMethod;
+import org.apache.polaris.core.exceptions.PolarisException;
+
+public class PolicyInUseException extends PolarisException {
+  @FormatMethod
+  public PolicyInUseException(String message, Object... args) {
+    super(String.format(message, args));
+  }
+
+  @FormatMethod
+  public PolicyInUseException(Throwable cause, String message, Object... args) 
{
+    super(String.format(message, args), cause);
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java
index 86308f555..f5a93e8ca 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java
@@ -18,13 +18,9 @@
  */
 package org.apache.polaris.core.policy.validator.maintenance;
 
-import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG;
-import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE;
-import static org.apache.polaris.core.entity.PolarisEntityType.TABLE_LIKE;
-
-import java.util.Set;
 import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.PolarisEntityType;
+import org.apache.polaris.core.policy.PolicyMappingUtil;
 import org.apache.polaris.core.policy.validator.InvalidPolicyException;
 import org.apache.polaris.core.policy.validator.PolicyValidator;
 
@@ -32,26 +28,11 @@ public class BaseMaintenancePolicyValidator implements 
PolicyValidator {
   public static final BaseMaintenancePolicyValidator INSTANCE =
       new BaseMaintenancePolicyValidator();
 
-  private static final Set<PolarisEntityType> ATTACHABLE_ENTITY_TYPES =
-      Set.of(CATALOG, NAMESPACE, TABLE_LIKE);
-
   @Override
   public void validate(String content) throws InvalidPolicyException {}
 
   @Override
   public boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType 
entitySubType) {
-    if (entityType == null) {
-      return false;
-    }
-
-    if (!ATTACHABLE_ENTITY_TYPES.contains(entityType)) {
-      return false;
-    }
-
-    if (entityType == TABLE_LIKE && entitySubType != 
PolarisEntitySubType.ICEBERG_TABLE) {
-      return false;
-    }
-
-    return true;
+    return PolicyMappingUtil.isValidTargetEntityType(entityType, 
entitySubType);
   }
 }
diff --git 
a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
 
b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
index d11bed82d..f58313712 100644
--- 
a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
+++ 
b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
@@ -292,6 +292,11 @@ public abstract class BasePolarisMetaStoreManagerTest {
     polarisTestMetaStoreManager.testPolicyMapping();
   }
 
+  @Test
+  protected void testPolicyMappingCleanup() {
+    polarisTestMetaStoreManager.testPolicyMappingCleanup();
+  }
+
   @Test
   protected void testLoadTasks() {
     for (int i = 0; i < 20; i++) {
diff --git 
a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java
 
b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java
index b401d4efb..428ecf00c 100644
--- 
a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java
+++ 
b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java
@@ -835,6 +835,12 @@ public class PolarisTestMetaStoreManager {
       Assertions.assertThat(dropResult.isSuccess()).isFalse();
       Assertions.assertThat(dropResult.failedBecauseNotEmpty()).isTrue();
       Assertions.assertThat(dropResult.isEntityUnDroppable()).isFalse();
+    } else if (entityToDrop.getType() == PolarisEntityType.POLICY) {
+      // When dropping policy with cleanup = true, we do not need cleanup task
+      Assertions.assertThat(dropResult.isSuccess()).isEqualTo(exists);
+      Assertions.assertThat(dropResult.failedBecauseNotEmpty()).isFalse();
+      Assertions.assertThat(dropResult.isEntityUnDroppable()).isFalse();
+      Assertions.assertThat(dropResult.getCleanupTaskId()).isNull();
     } else {
       Assertions.assertThat(dropResult.isSuccess()).isEqualTo(exists);
       Assertions.assertThat(dropResult.failedBecauseNotEmpty()).isFalse();
@@ -2788,4 +2794,56 @@ public class PolarisTestMetaStoreManager {
     detachPolicyFromTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, 
List.of(catalog, N1), N1_P1);
     detachPolicyFromTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, 
List.of(catalog, N5), N5_P3);
   }
+
+  void testPolicyMappingCleanup() {
+    PolarisBaseEntity catalog = this.createTestCatalog("test");
+    Assertions.assertThat(catalog).isNotNull();
+
+    PolarisBaseEntity N1 =
+        this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, 
"N1");
+    PolarisBaseEntity N1_N2 =
+        this.ensureExistsByName(List.of(catalog, N1), 
PolarisEntityType.NAMESPACE, "N2");
+    PolarisBaseEntity N1_N2_T1 =
+        this.ensureExistsByName(
+            List.of(catalog, N1, N1_N2),
+            PolarisEntityType.TABLE_LIKE,
+            PolarisEntitySubType.ANY_SUBTYPE,
+            "T1");
+
+    PolarisBaseEntity N1_N2_T3 =
+        this.createEntity(
+            List.of(catalog, N1, N1_N2),
+            PolarisEntityType.TABLE_LIKE,
+            PolarisEntitySubType.ICEBERG_TABLE,
+            "T3");
+    PolicyEntity N1_P1 =
+        this.createPolicy(List.of(catalog, N1), "P1", 
PredefinedPolicyTypes.DATA_COMPACTION);
+
+    PolicyEntity N1_P2 =
+        this.createPolicy(List.of(catalog, N1), "P2", 
PredefinedPolicyTypes.DATA_COMPACTION);
+
+    attachPolicyToTarget(List.of(catalog, N1, N1_N2), N1_N2_T3, 
List.of(catalog, N1), N1_P1);
+    LoadPolicyMappingsResult loadPolicyMappingsResult =
+        polarisMetaStoreManager.loadPoliciesOnEntity(polarisCallContext, 
N1_N2_T3);
+    Assertions.assertThat(loadPolicyMappingsResult.isSuccess()).isTrue();
+    Assertions.assertThat(loadPolicyMappingsResult.getEntities()).hasSize(1);
+
+    // Drop N1_N2_T1, the corresponding policy mapping should be cleaned-up
+    this.dropEntity(List.of(catalog, N1, N1_N2), N1_N2_T3);
+
+    BasePersistence ms = polarisCallContext.getMetaStore();
+    Assertions.assertThat(
+            ms.loadAllTargetsOnPolicy(polarisCallContext, 
N1_P1.getCatalogId(), N1_P1.getId()))
+        .isEmpty();
+
+    attachPolicyToTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, 
List.of(catalog, N1), N1_P2);
+
+    // Drop N1_P2, the dropEntity helper will have cleanup enabled to detach 
the policy from all
+    // targets
+    this.dropEntity(List.of(catalog, N1), N1_P2);
+    loadPolicyMappingsResult =
+        polarisMetaStoreManager.loadPoliciesOnEntity(polarisCallContext, 
N1_N2_T1);
+    Assertions.assertThat(loadPolicyMappingsResult.isSuccess()).isTrue();
+    Assertions.assertThat(loadPolicyMappingsResult.getEntities()).isEmpty();
+  }
 }
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java
index 57bd52132..a5bf98ba9 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java
@@ -77,6 +77,7 @@ import 
org.apache.polaris.core.persistence.dao.entity.PrincipalSecretsResult;
 import 
org.apache.polaris.core.persistence.transactional.TransactionalPersistence;
 import org.apache.polaris.core.policy.PredefinedPolicyTypes;
 import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException;
+import org.apache.polaris.core.policy.exceptions.PolicyInUseException;
 import 
org.apache.polaris.core.policy.exceptions.PolicyVersionMismatchException;
 import org.apache.polaris.core.policy.validator.InvalidPolicyException;
 import org.apache.polaris.core.secrets.UserSecretsManager;
@@ -506,6 +507,30 @@ public class PolicyCatalogTest {
         .isInstanceOf(NoSuchPolicyException.class);
   }
 
+  @Test
+  public void testDropPolicyInUse() {
+    icebergCatalog.createNamespace(NS);
+    policyCatalog.createPolicy(
+        POLICY1, PredefinedPolicyTypes.DATA_COMPACTION.getName(), "test", 
"{\"enable\": false}");
+    var target = new 
PolicyAttachmentTarget(PolicyAttachmentTarget.TypeEnum.CATALOG, List.of());
+    policyCatalog.attachPolicy(POLICY1, target, null);
+
+    assertThatThrownBy(() -> policyCatalog.dropPolicy(POLICY1, false))
+        .isInstanceOf(PolicyInUseException.class);
+
+    // The policy is still attached to the catalog
+    List<ApplicablePolicy> applicablePolicies =
+        policyCatalog.getApplicablePolicies(null, null, null);
+    assertThat(applicablePolicies.size()).isEqualTo(1);
+
+    // Drop the policy with detach-all flag
+    policyCatalog.dropPolicy(POLICY1, true);
+
+    // The policy should be detached from the catalog and dropped
+    applicablePolicies = policyCatalog.getApplicablePolicies(null, null, null);
+    assertThat(applicablePolicies.size()).isEqualTo(0);
+  }
+
   @Test
   public void testDropPolicyNotExist() {
     icebergCatalog.createNamespace(NS);
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java
 
b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java
index 45e5f17e6..066d513a3 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java
@@ -18,6 +18,7 @@
  */
 package org.apache.polaris.service.catalog.policy;
 
+import static 
org.apache.polaris.core.persistence.dao.entity.BaseResult.ReturnStatus.POLICY_HAS_MAPPINGS;
 import static 
org.apache.polaris.core.persistence.dao.entity.BaseResult.ReturnStatus.POLICY_MAPPING_OF_SAME_TYPE_ALREADY_EXISTS;
 import static 
org.apache.polaris.service.types.PolicyAttachmentTarget.TypeEnum.CATALOG;
 
@@ -54,6 +55,7 @@ import org.apache.polaris.core.policy.PolicyEntity;
 import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException;
 import org.apache.polaris.core.policy.exceptions.PolicyAttachException;
+import org.apache.polaris.core.policy.exceptions.PolicyInUseException;
 import 
org.apache.polaris.core.policy.exceptions.PolicyVersionMismatchException;
 import org.apache.polaris.core.policy.validator.PolicyValidators;
 import org.apache.polaris.service.types.ApplicablePolicy;
@@ -254,7 +256,6 @@ public class PolicyCatalog {
   }
 
   public boolean dropPolicy(PolicyIdentifier policyIdentifier, boolean 
detachAll) {
-    // TODO: Implement detachAll when we support attach/detach policy
     var resolvedPolicyPath = getResolvedPathWrapper(policyIdentifier);
     var catalogPath = resolvedPolicyPath.getRawParentPath();
     var policyEntity = resolvedPolicyPath.getRawLeafEntity();
@@ -265,9 +266,13 @@ public class PolicyCatalog {
             PolarisEntity.toCoreList(catalogPath),
             policyEntity,
             Map.of(),
-            false);
+            detachAll);
 
     if (!result.isSuccess()) {
+      if (result.getReturnStatus() == POLICY_HAS_MAPPINGS) {
+        throw new PolicyInUseException("Policy %s is still attached to 
entities", policyIdentifier);
+      }
+
       throw new IllegalStateException(
           String.format(
               "Failed to drop policy %s error status: %s with extraInfo: %s",
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
 
b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
index 57df2cbc5..7eac23bd5 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
@@ -28,6 +28,7 @@ import org.apache.polaris.core.exceptions.PolarisException;
 import org.apache.polaris.core.persistence.PolicyMappingAlreadyExistsException;
 import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException;
 import org.apache.polaris.core.policy.exceptions.PolicyAttachException;
+import org.apache.polaris.core.policy.exceptions.PolicyInUseException;
 import 
org.apache.polaris.core.policy.exceptions.PolicyVersionMismatchException;
 import org.apache.polaris.core.policy.validator.InvalidPolicyException;
 import org.apache.polaris.service.context.UnresolvableRealmContextException;
@@ -59,6 +60,8 @@ public class PolarisExceptionMapper implements 
ExceptionMapper<PolarisException>
       return Response.Status.CONFLICT;
     } else if (exception instanceof PolicyMappingAlreadyExistsException) {
       return Response.Status.CONFLICT;
+    } else if (exception instanceof PolicyInUseException) {
+      return Response.Status.BAD_REQUEST;
     } else {
       return Response.Status.INTERNAL_SERVER_ERROR;
     }

Reply via email to