You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by ad...@apache.org on 2026/10/06 15:26:34 UTC

(polaris) branch main updated: NoSQL: honor ALLOW_DROPPING_NON_EMPTY_PASSTHROUGH_FACADE_CATALOG when dropping catalogs (#5705)

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

adutra 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 feba0723a8 NoSQL: honor ALLOW_DROPPING_NON_EMPTY_PASSTHROUGH_FACADE_CATALOG when dropping catalogs (#5705)
feba0723a8 is described below

commit feba0723a8cf31215db30bc92eb2ae1119c18cce
Author: Alexandre Dutra <ad...@apache.org>
AuthorDate: Tue Oct 6 17:26:28 2026 +0200

    NoSQL: honor ALLOW_DROPPING_NON_EMPTY_PASSTHROUGH_FACADE_CATALOG when dropping catalogs (#5705)
---
 CHANGELOG.md                                       |  3 +
 .../nosql/metastore/NoSqlMetaStore.java            |  9 ++-
 .../nosql/metastore/NoSqlMetaStoreManager.java     | 30 ++++++++-
 .../nosql/metastore/mutation/EntityUpdate.java     |  9 ++-
 .../nosql/metastore/mutation/MutationAttempt.java  |  7 +-
 .../nosql/metastore/TestNoSqlMetaStoreManager.java | 77 ++++++++++++++++++++++
 6 files changed, 129 insertions(+), 6 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 81a13264b0..601f715867 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -184,6 +184,9 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
   scheme stripping (e.g. `s3://bucket/path` → `//bucket/path`). `//` and `///` are retained so
   scheme-root ancestors remain visible to the overlap check.
 - Honored pagination for policy API with applicable-policies endpoint excluded.
+- The NoSQL metastore now honors `ALLOW_DROPPING_NON_EMPTY_PASSTHROUGH_FACADE_CATALOG`: dropping a
+  non-empty passthrough-facade (federated) catalog previously always failed with NoSQL persistence,
+  even when the flag was enabled.
 
 ### Commits
 
diff --git a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStore.java b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStore.java
index 90e0c9c3c0..2d73c4f6ae 100644
--- a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStore.java
+++ b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStore.java
@@ -404,7 +404,10 @@ class NoSqlMetaStore extends NonFunctionalBasePersistence {
   }
 
   DropEntityResult dropEntity(
-      PolarisBaseEntity entityToDrop, Map<String, String> cleanupProperties, boolean cleanup) {
+      PolarisBaseEntity entityToDrop,
+      Map<String, String> cleanupProperties,
+      boolean cleanup,
+      boolean dropNonEmptyCatalog) {
     requireNonNull(entityToDrop);
 
     LOGGER.atDebug().addArgument(() -> logEntityInfo(entityToDrop)).log("drop entity: {}");
@@ -412,7 +415,9 @@ class NoSqlMetaStore extends NonFunctionalBasePersistence {
     var results =
         performEntityMutations(
             updateKeyForCatalogAndEntityType(entityToDrop),
-            List.of(new EntityUpdate(EntityUpdate.Operation.DELETE, entityToDrop, cleanup)));
+            List.of(
+                new EntityUpdate(
+                    EntityUpdate.Operation.DELETE, entityToDrop, cleanup, dropNonEmptyCatalog)));
 
     if (cleanup && PolarisEntityType.POLICY == entityToDrop.getType()) {
       cleanup = false;
diff --git a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStoreManager.java b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStoreManager.java
index f2741f0143..27aeba3256 100644
--- a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStoreManager.java
+++ b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStoreManager.java
@@ -35,6 +35,7 @@ import java.util.function.Predicate;
 import java.util.function.Supplier;
 import org.apache.polaris.core.PolarisCallContext;
 import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.EventEntity;
 import org.apache.polaris.core.entity.LocationBasedEntity;
 import org.apache.polaris.core.entity.PolarisBaseEntity;
@@ -174,7 +175,34 @@ record NoSqlMetaStoreManager(
       @NonNull PolarisBaseEntity entityToDrop,
       @Nullable Map<String, String> cleanupProperties,
       boolean cleanup) {
-    return ms(callCtx).dropEntity(entityToDrop, cleanupProperties, cleanup);
+    // For passthrough facade catalogs, all catalog level entities, except catalog roles, are
+    // passthrough entities that are not source-of-truth, and may optionally be dropped with the
+    // catalog.
+    var passthroughFacadeCatalog = false;
+    var dropNonEmptyCatalog = false;
+    if (entityToDrop.getType() == PolarisEntityType.CATALOG) {
+      var catalogEntity = CatalogEntity.of(entityToDrop);
+      passthroughFacadeCatalog = catalogEntity.isPassthroughFacade();
+      dropNonEmptyCatalog =
+          passthroughFacadeCatalog
+              && callCtx
+                  .getRealmConfig()
+                  .getConfig(
+                      FeatureConfiguration.ALLOW_DROPPING_NON_EMPTY_PASSTHROUGH_FACADE_CATALOG,
+                      catalogEntity);
+    }
+    var result =
+        ms(callCtx).dropEntity(entityToDrop, cleanupProperties, cleanup, dropNonEmptyCatalog);
+    if (passthroughFacadeCatalog
+        && !dropNonEmptyCatalog
+        && result.getReturnStatus() == BaseResult.ReturnStatus.NAMESPACE_NOT_EMPTY) {
+      return new DropEntityResult(
+          BaseResult.ReturnStatus.NAMESPACE_NOT_EMPTY,
+          String.format(
+              "Set %s to true to drop non-empty passthrough facade catalogs",
+              FeatureConfiguration.ALLOW_DROPPING_NON_EMPTY_PASSTHROUGH_FACADE_CATALOG.key()));
+    }
+    return result;
   }
 
   @NonNull
diff --git a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/mutation/EntityUpdate.java b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/mutation/EntityUpdate.java
index 2534ee741b..713fe65121 100644
--- a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/mutation/EntityUpdate.java
+++ b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/mutation/EntityUpdate.java
@@ -20,9 +20,14 @@ package org.apache.polaris.persistence.nosql.metastore.mutation;
 
 import org.apache.polaris.core.entity.PolarisBaseEntity;
 
-public record EntityUpdate(Operation operation, PolarisBaseEntity entity, boolean cleanup) {
+/**
+ * @param dropNonEmptyCatalog for {@link Operation#DELETE} of a catalog, whether the catalog may be
+ *     dropped even if it still contains namespaces or tables
+ */
+public record EntityUpdate(
+    Operation operation, PolarisBaseEntity entity, boolean cleanup, boolean dropNonEmptyCatalog) {
   public EntityUpdate(Operation operation, PolarisBaseEntity entity) {
-    this(operation, entity, false);
+    this(operation, entity, false, false);
   }
 
   public enum Operation {
diff --git a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/mutation/MutationAttempt.java b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/mutation/MutationAttempt.java
index 42d2fb5c5e..64abb91d9a 100644
--- a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/mutation/MutationAttempt.java
+++ b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/mutation/MutationAttempt.java
@@ -219,7 +219,12 @@ public record MutationAttempt(
           case CATALOG -> {
             var catalogState = memoizedIndexedAccess.catalogContent(entity.getId());
 
-            if (catalogState.nameIndex().map(idx -> idx.iterator().hasNext()).orElse(false)) {
+            // A non-empty catalog may only be dropped if the caller requested it. Its remaining
+            // namespaces and tables are not dropped here: the catalog's content is no longer
+            // reachable once the catalog is gone, and will be eventually purged by
+            // persistence-maintenance.
+            if (!update.dropNonEmptyCatalog()
+                && catalogState.nameIndex().map(idx -> idx.iterator().hasNext()).orElse(false)) {
               mutationResults.dropResult(NAMESPACE_NOT_EMPTY);
               yield false;
             }
diff --git a/persistence/nosql/persistence/metastore/src/test/java/org/apache/polaris/persistence/nosql/metastore/TestNoSqlMetaStoreManager.java b/persistence/nosql/persistence/metastore/src/test/java/org/apache/polaris/persistence/nosql/metastore/TestNoSqlMetaStoreManager.java
index 6b2baee510..a64287634d 100644
--- a/persistence/nosql/persistence/metastore/src/test/java/org/apache/polaris/persistence/nosql/metastore/TestNoSqlMetaStoreManager.java
+++ b/persistence/nosql/persistence/metastore/src/test/java/org/apache/polaris/persistence/nosql/metastore/TestNoSqlMetaStoreManager.java
@@ -45,8 +45,10 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.IntStream;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.polaris.core.PolarisCallContext;
+import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.config.RealmConfigurationSource;
 import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.NamespaceEntity;
 import org.apache.polaris.core.entity.PolarisBaseEntity;
 import org.apache.polaris.core.entity.PolarisEntity;
@@ -89,6 +91,8 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 @SuppressWarnings("CdiInjectionPointsInspection")
 @EnableWeld
@@ -517,6 +521,79 @@ public class TestNoSqlMetaStoreManager extends BasePolarisMetaStoreManagerTest {
   }
 
   @SuppressWarnings("SameParameterValue")
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void dropNonEmptyPassthroughFacadeCatalog(boolean allowDropping) {
+    RealmConfigurationSource config =
+        (rc, name) ->
+            FeatureConfiguration.ALLOW_DROPPING_NON_EMPTY_PASSTHROUGH_FACADE_CATALOG
+                    .key()
+                    .equals(name)
+                ? allowDropping
+                : configurationSource.getConfigValue(rc, name);
+    var ctx = new PolarisCallContext(realmContext, callContext.getMetaStore(), config);
+
+    var catalog =
+        new PolarisBaseEntity.Builder(
+                new PolarisBaseEntity(
+                    PolarisEntityConstants.getNullId(),
+                    metaStore.generateNewEntityId(ctx).getId(),
+                    PolarisEntityType.CATALOG,
+                    PolarisEntitySubType.NULL_SUBTYPE,
+                    PolarisEntityConstants.getRootEntityId(),
+                    "passthroughFacade" + allowDropping))
+            .internalPropertiesAsMap(
+                Map.of(PolarisEntityConstants.getConnectionConfigInfoPropertyName(), "{}"))
+            .build();
+    catalog = metaStore.createCatalog(ctx, catalog, List.of()).getCatalog();
+    assertThat(CatalogEntity.of(catalog).isPassthroughFacade()).isTrue();
+
+    var ns =
+        createEntity(
+            List.of(catalog),
+            PolarisEntityType.NAMESPACE,
+            PolarisEntitySubType.NULL_SUBTYPE,
+            "ns",
+            Map.of());
+    assertThat(ns).extracting(EntityResult::isSuccess, BOOLEAN).isTrue();
+
+    var dropResult = metaStore.dropEntityIfExists(ctx, null, catalog, Map.of(), false);
+    if (allowDropping) {
+      assertThat(dropResult.isSuccess()).isTrue();
+      assertThat(
+              metaStore.readEntityByName(
+                  ctx, null, CATALOG, NULL_SUBTYPE, "passthroughFacade" + allowDropping))
+          .extracting(EntityResult::isSuccess, BOOLEAN)
+          .isFalse();
+
+      // The content of the dropped catalog must not leak into a new catalog with the same name.
+      var newCatalog =
+          new PolarisBaseEntity(
+              PolarisEntityConstants.getNullId(),
+              metaStore.generateNewEntityId(ctx).getId(),
+              PolarisEntityType.CATALOG,
+              PolarisEntitySubType.NULL_SUBTYPE,
+              PolarisEntityConstants.getRootEntityId(),
+              "passthroughFacade" + allowDropping);
+      newCatalog = metaStore.createCatalog(ctx, newCatalog, List.of()).getCatalog();
+      assertThat(
+              metaStore
+                  .listFullEntities(
+                      ctx,
+                      List.<PolarisEntityCore>of(newCatalog),
+                      PolarisEntityType.NAMESPACE,
+                      PolarisEntitySubType.ANY_SUBTYPE,
+                      PageToken.readEverything())
+                  .items())
+          .isEmpty();
+    } else {
+      assertThat(dropResult.getReturnStatus())
+          .isEqualTo(BaseResult.ReturnStatus.NAMESPACE_NOT_EMPTY);
+      assertThat(dropResult.getExtraInformation())
+          .contains(FeatureConfiguration.ALLOW_DROPPING_NON_EMPTY_PASSTHROUGH_FACADE_CATALOG.key());
+    }
+  }
+
   EntityResult createEntity(
       List<PolarisBaseEntity> catalogPath,
       PolarisEntityType entityType,