This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new c7b3f47c01 [Cherry-pick to branch-1.3] [#11262] fix: Clean up orphaned
schema entities after table or view drop (#11362) (#11445)
c7b3f47c01 is described below
commit c7b3f47c01a64ed38afbbbe8443441955bb48727
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Jun 5 13:55:43 2026 +0800
[Cherry-pick to branch-1.3] [#11262] fix: Clean up orphaned schema entities
after table or view drop (#11362) (#11445)
**Cherry-pick Information:**
- Original commit: 35fdb435171782fe03b4ceae54b1612bd88baf50
- Target branch: `branch-1.3`
- Status: ✅ Clean cherry-pick (no conflicts)
Co-authored-by: roryqi <[email protected]>
---
.../gravitino/catalog/OrphanedSchemaCleanup.java | 59 +++++++++
.../catalog/SchemaOperationDispatcher.java | 60 ++-------
.../catalog/TableOperationDispatcher.java | 10 +-
.../gravitino/catalog/ViewOperationDispatcher.java | 3 +
.../gravitino/utils/SchemaEntityCleaner.java | 93 ++++++++++++++
.../catalog/TestTableOperationDispatcher.java | 134 +++++++++++++++++++++
.../catalog/TestViewOperationDispatcher.java | 82 +++++++++++++
.../gravitino/connector/TestCatalogOperations.java | 5 +
.../org/apache/gravitino/iceberg/RESTService.java | 14 ++-
.../dispatcher/IcebergNamespaceHookDispatcher.java | 56 ++-------
.../dispatcher/IcebergOrphanSchemaCleanup.java | 79 ++++++++++++
.../dispatcher/IcebergTableHookDispatcher.java | 8 +-
.../dispatcher/IcebergViewHookDispatcher.java | 9 +-
.../dispatcher/TestIcebergTableHookDispatcher.java | 19 ++-
.../dispatcher/TestIcebergViewHookDispatcher.java | 19 ++-
15 files changed, 545 insertions(+), 105 deletions(-)
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/OrphanedSchemaCleanup.java
b/core/src/main/java/org/apache/gravitino/catalog/OrphanedSchemaCleanup.java
new file mode 100644
index 0000000000..ddcc9a5df1
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/catalog/OrphanedSchemaCleanup.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.catalog;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.utils.SchemaEntityCleaner;
+
+/**
+ * Shared orphaned-schema cleanup for the core operation dispatchers.
+ *
+ * <p>Both the table and view drop paths need to remove Gravitino schema
entities whose backing
+ * catalog schema no longer exists. The existence probe goes through {@link
+ * OperationDispatcher#doWithCatalog} against the {@link
OperationDispatcher#store}, so this helper
+ * lives in the same package to reuse those {@code protected} members and keep
the logic in one
+ * place.
+ */
+final class OrphanedSchemaCleanup {
+
+ private OrphanedSchemaCleanup() {}
+
+ /**
+ * Removes Gravitino schema entities whose backing catalog schema no longer
exists, starting from
+ * {@code schemaIdentifier} and walking toward its outermost ancestor.
+ *
+ * @param dispatcher the dispatcher whose entity store and catalog access
are used for cleanup
+ * @param catalogIdent the identifier of the catalog owning the schema
+ * @param schemaIdentifier the schema identifier to start the orphan check
from
+ */
+ static void cleanUp(
+ OperationDispatcher dispatcher,
+ NameIdentifier catalogIdent,
+ NameIdentifier schemaIdentifier) {
+ SchemaEntityCleaner.deleteOrphanedSchemaEntities(
+ dispatcher.store,
+ schemaIdentifier,
+ true,
+ schemaIdent ->
+ dispatcher.doWithCatalog(
+ catalogIdent,
+ c -> c.doWithSchemaOps(s -> s.schemaExists(schemaIdent)),
+ RuntimeException.class));
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
index 9ab9df5588..eb57d46fe0 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
@@ -23,7 +23,6 @@ import static
org.apache.gravitino.catalog.PropertiesMetadataHelpers.validatePro
import static
org.apache.gravitino.utils.NameIdentifierUtil.getCatalogIdentifier;
import java.time.Instant;
-import java.util.List;
import java.util.Map;
import org.apache.gravitino.EntityAlreadyExistsException;
import org.apache.gravitino.EntityStore;
@@ -44,9 +43,8 @@ import org.apache.gravitino.lock.TreeLockUtils;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.SchemaEntity;
import org.apache.gravitino.storage.IdGenerator;
-import org.apache.gravitino.utils.HierarchicalSchemaUtil;
-import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.PrincipalUtils;
+import org.apache.gravitino.utils.SchemaEntityCleaner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -357,57 +355,19 @@ public class SchemaOperationDispatcher extends
OperationDispatcher implements Sc
throw new RuntimeException(e);
}
- cleanupOrphanedAncestors(catalogIdent, ident);
+ SchemaEntityCleaner.deleteOrphanedSchemaEntities(
+ store,
+ ident,
+ false,
+ schemaIdent ->
+ doWithCatalog(
+ catalogIdent,
+ c -> c.doWithSchemaOps(s -> s.schemaExists(schemaIdent)),
+ RuntimeException.class));
return droppedFromCatalog;
});
}
- /**
- * Reconciles auto-created ancestor entities after a hierarchical schema
leaf is dropped. This
- * mirrors {@code IcebergNamespaceHookDispatcher.dropNamespace}: walk the
ancestors
- * innermost-to-outermost to find the outermost ancestor that no longer
exists in the catalog,
- * stopping at the first ancestor that still exists (its existence implies
all of its outer
- * ancestors exist). A single cascade delete of that outermost orphaned
ancestor removes it and
- * all descendant schema entities (the intermediate ancestors) in one
batched operation, rather
- * than issuing one delete per ancestor. For a flat schema name this is a
no-op.
- *
- * @param catalogIdent the identifier of the catalog the schema belongs to
- * @param ident the identifier of the dropped (leaf) schema
- */
- private void cleanupOrphanedAncestors(NameIdentifier catalogIdent,
NameIdentifier ident) {
- String separator = HierarchicalSchemaUtil.schemaSeparator();
- List<String> ancestorNames =
HierarchicalSchemaUtil.getAncestorNames(ident.name(), separator);
- String metalake = ident.namespace().level(0);
- String catalog = ident.namespace().level(1);
- NameIdentifier outermostOrphan = null;
- for (int i = ancestorNames.size() - 1; i >= 0; i--) {
- NameIdentifier ancestorIdent =
- NameIdentifierUtil.ofSchema(metalake, catalog, ancestorNames.get(i));
- boolean ancestorExistsInCatalog =
- doWithCatalog(
- catalogIdent,
- c -> c.doWithSchemaOps(s -> s.schemaExists(ancestorIdent)),
- RuntimeException.class);
- if (ancestorExistsInCatalog) {
- break;
- }
- outermostOrphan = ancestorIdent;
- }
- if (outermostOrphan == null) {
- return;
- }
- // The leaf was already deleted by the caller; cascade-deleting the
outermost orphaned ancestor
- // cleans up it and every intermediate ancestor (the relational store
matches descendant schemas
- // by name prefix) in a single batched delete.
- try {
- store.delete(outermostOrphan, SCHEMA, true);
- } catch (NoSuchEntityException e) {
- LOG.warn("The orphaned ancestor schema does not exist in the store: {}",
outermostOrphan, e);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
private void importSchema(NameIdentifier identifier) {
EntityCombinedSchema schema = internalLoadSchema(identifier);
if (schema.imported()) {
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
index 34677f3e31..c2646f4b40 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java
@@ -379,7 +379,7 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
// 2. Is found in the catalog but not in the store (not managed by
Gravitino)
// 3. Is found in the catalog and the store (managed by Gravitino)
// 4. Neither found in the catalog nor in the store.
- // In all situations, we try to delete the schema from the store,
but we don't take the
+ // In all situations, we try to delete the table from the store, but
we don't take the
// return value of the store operation into account. We only take
the return value of the
// catalog into account.
try {
@@ -389,6 +389,9 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
} catch (Exception e) {
throw new RuntimeException(e);
}
+ // Run unconditionally: an out-of-band drop may have left orphaned
schema entities. The
+ // cleanup is best-effort and stops as soon as a schema still exists.
+ OrphanedSchemaCleanup.cleanUp(this, catalogIdent, schemaIdentifier);
return droppedFromCatalog;
});
}
@@ -432,7 +435,7 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
// 2. Is found in the catalog but not in the store (not managed by
Gravitino)
// 3. Is found in the catalog and the store (managed by Gravitino)
// 4. Neither found in the catalog nor in the store.
- // In all situations, we try to delete the schema from the store,
but we don't take the
+ // In all situations, we try to delete the table from the store, but
we don't take the
// return value of the store operation into account. We only take
the return value of the
// catalog into account.
try {
@@ -443,6 +446,9 @@ public class TableOperationDispatcher extends
OperationDispatcher implements Tab
} catch (Exception e) {
throw new RuntimeException(e);
}
+ // Run unconditionally: an out-of-band purge may have left orphaned
schema entities. The
+ // cleanup is best-effort and stops as soon as a schema still exists.
+ OrphanedSchemaCleanup.cleanUp(this, catalogIdent, schemaIdentifier);
return droppedFromCatalog;
});
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
index 5b3937ee48..f8960737bf 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
@@ -314,6 +314,9 @@ public class ViewOperationDispatcher extends
OperationDispatcher implements View
} catch (Exception e) {
throw new RuntimeException(e);
}
+ // Run unconditionally: an out-of-band drop may have left orphaned
schema entities. The
+ // cleanup is best-effort and stops as soon as a schema still exists.
+ OrphanedSchemaCleanup.cleanUp(this, catalogIdent, schemaIdentifier);
return droppedFromCatalog;
});
}
diff --git
a/core/src/main/java/org/apache/gravitino/utils/SchemaEntityCleaner.java
b/core/src/main/java/org/apache/gravitino/utils/SchemaEntityCleaner.java
new file mode 100644
index 0000000000..bc56b0f4d9
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/utils/SchemaEntityCleaner.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.utils;
+
+import static org.apache.gravitino.Entity.EntityType.SCHEMA;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Utility for deleting schema entities whose backing catalog schemas no
longer exist. */
+public final class SchemaEntityCleaner {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(SchemaEntityCleaner.class);
+
+ private SchemaEntityCleaner() {}
+
+ /**
+ * Deletes the outermost orphaned schema entity and its descendants from the
store.
+ *
+ * <p>Candidates are checked from the starting schema toward the outermost
ancestor. The first
+ * candidate that still exists in the catalog stops the walk; every
more-specific candidate
+ * checked before that point is stale. A single cascade delete on the
outermost stale schema
+ * removes it and all descendant schema entities.
+ *
+ * <p>This is best-effort: callers invoke it after the primary drop has
already succeeded, so any
+ * failure (a store error or a catalog existence probe failure) is logged
and swallowed rather
+ * than propagated. The orphan, if any, is retried on a subsequent drop.
+ *
+ * @param store the Gravitino entity store; if {@code null}, this method is
a no-op
+ * @param schemaIdent the schema identifier to start checking from
+ * @param includeSelf whether to check {@code schemaIdent} itself before
checking ancestors
+ * @param schemaExists predicate reporting whether a schema still exists in
the underlying catalog
+ */
+ public static void deleteOrphanedSchemaEntities(
+ EntityStore store,
+ NameIdentifier schemaIdent,
+ boolean includeSelf,
+ Predicate<NameIdentifier> schemaExists) {
+ if (store == null) {
+ return;
+ }
+
+ try {
+ String separator = HierarchicalSchemaUtil.schemaSeparator();
+ ArrayList<String> schemaNames =
+ new ArrayList<>(HierarchicalSchemaUtil.allScopes(schemaIdent.name(),
separator));
+ if (!includeSelf && !schemaNames.isEmpty()) {
+ schemaNames.remove(0);
+ }
+
+ NameIdentifier outermostOrphan = null;
+ for (String schemaName : schemaNames) {
+ NameIdentifier candidate = NameIdentifier.of(schemaIdent.namespace(),
schemaName);
+ if (schemaExists.test(candidate)) {
+ break;
+ }
+ outermostOrphan = candidate;
+ }
+
+ if (outermostOrphan == null) {
+ return;
+ }
+
+ store.delete(outermostOrphan, SCHEMA, true);
+ } catch (NoSuchEntityException e) {
+ LOG.debug("The orphaned schema entity was already removed from the
store", e);
+ } catch (Exception e) {
+ // Best-effort: the primary drop already succeeded, so swallow and log
rather than fail it.
+ LOG.warn("Failed to clean up orphaned schema entities starting from {}",
schemaIdent, e);
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
index 10b21dc41d..36c142cc4f 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
@@ -61,6 +61,7 @@ import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.lock.LockManager;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.ColumnEntity;
+import org.apache.gravitino.meta.SchemaEntity;
import org.apache.gravitino.meta.TableEntity;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.Table;
@@ -532,6 +533,127 @@ public class TestTableOperationDispatcher extends
TestOperationDispatcher {
RuntimeException.class, () ->
tableOperationDispatcher.dropTable(tableIdent));
}
+ @Test
+ public void testDropTableCleansUpAutoDroppedParentSchemas() throws Exception
{
+ reset(entityStore);
+ NameIdentifier schemaIdent =
+ NameIdentifier.of(metalake, catalog,
"dropTableParentA:dropTableParentB");
+ NameIdentifier ancestorIdent = NameIdentifier.of(metalake, catalog,
"dropTableParentA");
+ Namespace tableNs =
+ Namespace.of(
+ schemaIdent.namespace().level(0),
schemaIdent.namespace().level(1), schemaIdent.name());
+ NameIdentifier tableIdent = NameIdentifier.of(tableNs,
"table_auto_drop_parent");
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ Column[] columns =
+ new Column[] {
+ TestColumn.builder()
+ .withName("col1")
+ .withPosition(0)
+ .withType(Types.StringType.get())
+ .build()
+ };
+
+ schemaOperationDispatcher.createSchema(schemaIdent, "comment", props);
+ putSchemaEntity(ancestorIdent);
+ tableOperationDispatcher.createTable(tableIdent, columns, "comment",
props, new Transform[0]);
+
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+ Assertions.assertTrue(testCatalogOperations.dropSchema(schemaIdent,
false));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(schemaIdent));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(ancestorIdent));
+
+ Assertions.assertTrue(tableOperationDispatcher.dropTable(tableIdent));
+ Assertions.assertFalse(entityStore.exists(schemaIdent, SCHEMA));
+ Assertions.assertFalse(entityStore.exists(ancestorIdent, SCHEMA));
+ }
+
+ @Test
+ public void testDropMissingTableCleansUpSchemas() throws Exception {
+ reset(entityStore);
+ NameIdentifier schemaIdent =
+ NameIdentifier.of(metalake, catalog, "dropTableGoneA:dropTableGoneB");
+ NameIdentifier ancestorIdent = NameIdentifier.of(metalake, catalog,
"dropTableGoneA");
+ Namespace tableNs =
+ Namespace.of(
+ schemaIdent.namespace().level(0),
schemaIdent.namespace().level(1), schemaIdent.name());
+ NameIdentifier tableIdent = NameIdentifier.of(tableNs,
"table_already_gone");
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ Column[] columns =
+ new Column[] {
+ TestColumn.builder()
+ .withName("col1")
+ .withPosition(0)
+ .withType(Types.StringType.get())
+ .build()
+ };
+
+ schemaOperationDispatcher.createSchema(schemaIdent, "comment", props);
+ putSchemaEntity(ancestorIdent);
+ tableOperationDispatcher.createTable(tableIdent, columns, "comment",
props, new Transform[0]);
+
+ // Simulate an out-of-band drop: the backend already removed the table and
auto-dropped the
+ // now-empty namespaces, so the catalog no longer knows the table
(dropTable returns false),
+ // while Gravitino still holds the orphaned schema entities.
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+ Assertions.assertTrue(testCatalogOperations.dropTable(tableIdent));
+ Assertions.assertTrue(testCatalogOperations.dropSchema(schemaIdent,
false));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(schemaIdent));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(ancestorIdent));
+
+ // dropTable returns false because the table is already gone from the
catalog, but the
+ // orphaned schema entities must still be cleaned up.
+ Assertions.assertFalse(tableOperationDispatcher.dropTable(tableIdent));
+ Assertions.assertFalse(entityStore.exists(schemaIdent, SCHEMA));
+ Assertions.assertFalse(entityStore.exists(ancestorIdent, SCHEMA));
+ }
+
+ @Test
+ public void testPurgeMissingTableCleansUpSchemas() throws Exception {
+ reset(entityStore);
+ NameIdentifier schemaIdent =
+ NameIdentifier.of(metalake, catalog,
"purgeTableGoneA:purgeTableGoneB");
+ NameIdentifier ancestorIdent = NameIdentifier.of(metalake, catalog,
"purgeTableGoneA");
+ Namespace tableNs =
+ Namespace.of(
+ schemaIdent.namespace().level(0),
schemaIdent.namespace().level(1), schemaIdent.name());
+ NameIdentifier tableIdent = NameIdentifier.of(tableNs,
"table_already_gone");
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ Column[] columns =
+ new Column[] {
+ TestColumn.builder()
+ .withName("col1")
+ .withPosition(0)
+ .withType(Types.StringType.get())
+ .build()
+ };
+
+ schemaOperationDispatcher.createSchema(schemaIdent, "comment", props);
+ putSchemaEntity(ancestorIdent);
+ tableOperationDispatcher.createTable(tableIdent, columns, "comment",
props, new Transform[0]);
+
+ // Simulate an out-of-band drop: the backend already removed the table and
auto-dropped the
+ // now-empty namespaces, so the catalog no longer knows the table
(purgeTable returns false),
+ // while Gravitino still holds the orphaned schema entities.
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+ Assertions.assertTrue(testCatalogOperations.purgeTable(tableIdent));
+ Assertions.assertTrue(testCatalogOperations.dropSchema(schemaIdent,
false));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(schemaIdent));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(ancestorIdent));
+
+ // purgeTable returns false because the table is already gone from the
catalog, but the
+ // orphaned schema entities must still be cleaned up. A regression that
re-guards the cleanup
+ // behind the catalog drop result would leave the stale schema entities
behind.
+ Assertions.assertFalse(tableOperationDispatcher.purgeTable(tableIdent));
+ Assertions.assertFalse(entityStore.exists(schemaIdent, SCHEMA));
+ Assertions.assertFalse(entityStore.exists(ancestorIdent, SCHEMA));
+ }
+
@Test
public void testCreateTableNeedImportingSchema() throws IOException {
Namespace tableNs = Namespace.of(metalake, catalog, "schema181");
@@ -1040,6 +1162,18 @@ public class TestTableOperationDispatcher extends
TestOperationDispatcher {
});
}
+ private void putSchemaEntity(NameIdentifier ident) throws IOException {
+ SchemaEntity entity =
+ SchemaEntity.builder()
+ .withId(idGenerator.nextId())
+ .withName(ident.name())
+ .withNamespace(ident.namespace())
+ .withAuditInfo(
+
AuditInfo.builder().withCreator("tester").withCreateTime(Instant.now()).build())
+ .build();
+ entityStore.put(entity, true);
+ }
+
public static TableOperationDispatcher getTableOperationDispatcher() {
return tableOperationDispatcher;
}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
index 5f28bcd20e..7f75fe1984 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.catalog;
import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
+import static org.apache.gravitino.Entity.EntityType.SCHEMA;
import static org.apache.gravitino.Entity.EntityType.VIEW;
import static org.apache.gravitino.StringIdentifier.ID_KEY;
import static org.apache.gravitino.TestBasePropertiesMetadata.COMMENT_KEY;
@@ -53,6 +54,7 @@ import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchViewException;
import org.apache.gravitino.lock.LockManager;
import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.SchemaEntity;
import org.apache.gravitino.meta.ViewEntity;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.Representation;
@@ -499,6 +501,74 @@ public class TestViewOperationDispatcher extends
TestOperationDispatcher {
Assertions.assertFalse(viewOperationDispatcher.dropView(viewIdent));
}
+ @Test
+ public void testDropViewCleansUpAutoDroppedParentSchemas() throws Exception {
+ NameIdentifier schemaIdent =
+ NameIdentifier.of(metalake, catalog,
"dropViewParentA:dropViewParentB");
+ NameIdentifier ancestorIdent = NameIdentifier.of(metalake, catalog,
"dropViewParentA");
+ Namespace viewNs =
+ Namespace.of(
+ schemaIdent.namespace().level(0),
schemaIdent.namespace().level(1), schemaIdent.name());
+ NameIdentifier viewIdent = NameIdentifier.of(viewNs,
"view_auto_drop_parent");
+ Representation[] representations = {
+ SQLRepresentation.builder().withDialect("spark").withSql("SELECT
1").build()
+ };
+
+ schemaOperationDispatcher.createSchema(
+ schemaIdent, "comment", ImmutableMap.of("k1", "v1", "k2", "v2"));
+ putSchemaEntity(ancestorIdent);
+ viewOperationDispatcher.createView(
+ viewIdent, null, new Column[0], representations, null, null,
ImmutableMap.of("k1", "v1"));
+
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+ Assertions.assertTrue(testCatalogOperations.dropSchema(schemaIdent,
false));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(schemaIdent));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(ancestorIdent));
+
+ Assertions.assertTrue(viewOperationDispatcher.dropView(viewIdent));
+ Assertions.assertFalse(entityStore.exists(schemaIdent, SCHEMA));
+ Assertions.assertFalse(entityStore.exists(ancestorIdent, SCHEMA));
+ }
+
+ @Test
+ public void testDropMissingViewCleansUpSchemas() throws Exception {
+ NameIdentifier schemaIdent =
+ NameIdentifier.of(metalake, catalog, "dropViewGoneA:dropViewGoneB");
+ NameIdentifier ancestorIdent = NameIdentifier.of(metalake, catalog,
"dropViewGoneA");
+ Namespace viewNs =
+ Namespace.of(
+ schemaIdent.namespace().level(0),
schemaIdent.namespace().level(1), schemaIdent.name());
+ NameIdentifier viewIdent = NameIdentifier.of(viewNs, "view_already_gone");
+ Representation[] representations = {
+ SQLRepresentation.builder().withDialect("spark").withSql("SELECT
1").build()
+ };
+
+ schemaOperationDispatcher.createSchema(
+ schemaIdent, "comment", ImmutableMap.of("k1", "v1", "k2", "v2"));
+ putSchemaEntity(ancestorIdent);
+ viewOperationDispatcher.createView(
+ viewIdent, null, new Column[0], representations, null, null,
ImmutableMap.of("k1", "v1"));
+
+ // Simulate an out-of-band drop: the backend already removed the view and
auto-dropped the
+ // now-empty namespaces, so the catalog no longer knows the view (dropView
returns false),
+ // while Gravitino still holds the orphaned schema entities.
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+ Assertions.assertTrue(testCatalogOperations.dropView(viewIdent));
+ Assertions.assertTrue(testCatalogOperations.dropSchema(schemaIdent,
false));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(schemaIdent));
+ Assertions.assertFalse(testCatalogOperations.schemaExists(ancestorIdent));
+
+ // dropView returns false because the view is already gone from the
catalog, but the
+ // orphaned schema entities must still be cleaned up.
+ Assertions.assertFalse(viewOperationDispatcher.dropView(viewIdent));
+ Assertions.assertFalse(entityStore.exists(schemaIdent, SCHEMA));
+ Assertions.assertFalse(entityStore.exists(ancestorIdent, SCHEMA));
+ }
+
@Test
public void testListViews() throws IOException {
Namespace viewNs = Namespace.of(metalake, catalog, "schema_list_view");
@@ -607,4 +677,16 @@ public class TestViewOperationDispatcher extends
TestOperationDispatcher {
.orElseThrow(AssertionError::new);
Assertions.assertEquals("SELECT 2", trino.sql());
}
+
+ private void putSchemaEntity(NameIdentifier ident) throws IOException {
+ SchemaEntity entity =
+ SchemaEntity.builder()
+ .withId(idGenerator.nextId())
+ .withName(ident.name())
+ .withNamespace(ident.namespace())
+ .withAuditInfo(
+
AuditInfo.builder().withCreator("tester").withCreateTime(Instant.now()).build())
+ .build();
+ entityStore.put(entity, true);
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
index 27e6780a21..b6324652ca 100644
---
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
+++
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
@@ -305,6 +305,11 @@ public class TestCatalogOperations
}
}
+ @Override
+ public boolean purgeTable(NameIdentifier ident) {
+ return dropTable(ident);
+ }
+
@Override
public View createView(
NameIdentifier ident,
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
index ea2b54fa6f..d536d01cd2 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
@@ -139,13 +139,20 @@ public class RESTService implements
GravitinoAuxiliaryService {
"Async Iceberg table cleanup is only available in auxiliary mode; "
+ "purge requests with async mode will fall back to synchronous
purge.");
}
+
+ // The raw namespace operation executor is shared with the table and view
hook dispatchers so
+ // their orphan-schema cleanup can probe namespace existence without
firing namespace events.
+ IcebergNamespaceOperationDispatcher namespaceOperationDispatcher =
+ new IcebergNamespaceOperationExecutor(icebergCatalogWrapperManager,
cleanupManager);
+
// Table: HookDispatcher -> EventDispatcher -> OperationExecutor
IcebergTableOperationDispatcher icebergTableOperationDispatcher =
new IcebergTableOperationExecutor(icebergCatalogWrapperManager,
cleanupManager);
IcebergTableOperationDispatcher icebergTableEventDispatcher =
new IcebergTableEventDispatcher(icebergTableOperationDispatcher,
eventBus, metalakeName);
if (authorizationContext.isAuthorizationEnabled()) {
- icebergTableEventDispatcher = new
IcebergTableHookDispatcher(icebergTableEventDispatcher);
+ icebergTableEventDispatcher =
+ new IcebergTableHookDispatcher(icebergTableEventDispatcher,
namespaceOperationDispatcher);
}
IcebergTableOperationDispatcher icebergTableDispatcher =
icebergTableEventDispatcher;
@@ -156,13 +163,12 @@ public class RESTService implements
GravitinoAuxiliaryService {
new IcebergViewEventDispatcher(icebergViewOperationDispatcher,
eventBus, metalakeName);
if (authorizationContext.isAuthorizationEnabled()) {
icebergViewEventDispatcher =
- new IcebergViewHookDispatcher(icebergViewEventDispatcher,
metalakeName);
+ new IcebergViewHookDispatcher(
+ icebergViewEventDispatcher, namespaceOperationDispatcher,
metalakeName);
}
IcebergViewOperationDispatcher icebergViewDispatcher =
icebergViewEventDispatcher;
// Namespace: HookDispatcher -> EventDispatcher -> OperationExecutor
- IcebergNamespaceOperationDispatcher namespaceOperationDispatcher =
- new IcebergNamespaceOperationExecutor(icebergCatalogWrapperManager,
cleanupManager);
IcebergNamespaceOperationDispatcher icebergNamespaceEventDispatcher =
new IcebergNamespaceEventDispatcher(namespaceOperationDispatcher,
eventBus, metalakeName);
if (authorizationContext.isAuthorizationEnabled()) {
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java
index e316352339..635f766256 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java
@@ -18,18 +18,13 @@
*/
package org.apache.gravitino.iceberg.service.dispatcher;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.regex.Pattern;
-import org.apache.gravitino.Entity;
-import org.apache.gravitino.EntityStore;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.catalog.SchemaDispatcher;
import org.apache.gravitino.catalog.TableDispatcher;
-import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.iceberg.common.utils.IcebergIdentifierUtils;
import
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
import org.apache.gravitino.listener.api.event.IcebergRequestContext;
@@ -117,7 +112,8 @@ public class IcebergNamespaceHookDispatcher implements
IcebergNamespaceOperation
// probing once we hit one that exists.
List<Namespace> missing = new ArrayList<>();
for (int i = ancestorNames.size() - 1; i >= 0; i--) {
- Namespace ancestor =
Namespace.of(ancestorNames.get(i).split(Pattern.quote(separator)));
+ Namespace ancestor =
+
Namespace.of(HierarchicalSchemaUtil.splitSchemaName(ancestorNames.get(i),
separator));
if (dispatcher.namespaceExists(context, ancestor)) {
break;
}
@@ -147,49 +143,19 @@ public class IcebergNamespaceHookDispatcher implements
IcebergNamespaceOperation
() -> {
dispatcher.dropNamespace(context, namespace);
- // Only the leaf namespace is dropped from the Iceberg catalog
above. getAncestorNames
- // returns ancestors outermost-to-innermost, so we walk it in
reverse (innermost outward),
- // advancing outermostStale past every ancestor whose Iceberg
namespace no longer exists
- // and stopping at the first ancestor that still exists.
outermostStale therefore ends up
- // as the outermost missing ancestor; everything from there down to
the leaf is now stale
- // in Gravitino.
- //
- // An inner Iceberg namespace cannot exist unless all of its outer
ancestors do, so an
- // ancestor missing from the catalog implies none of its descendants
exist there
- // either. A single cascade delete of the outermost empty namespace
therefore removes
- // it and every descendant Gravitino entity (the leaf plus the
intermediate ancestors)
- // in one batched operation, rather than issuing one delete per
target.
+ // Only the leaf namespace is dropped from the Iceberg catalog
above. Dropping it may have
+ // also emptied its ancestors, so clean up every Gravitino schema
entity whose Iceberg
+ // namespace no longer exists. An inner Iceberg namespace cannot
exist unless all of its
+ // outer ancestors do, so the cleaner cascade-deletes the outermost
stale namespace and
+ // every descendant entity in one batched operation.
//
// Ancestor entities are still only cleaned up when the underlying
Iceberg catalog
// removes the empty parents on leaf-drop, which is
catalog-implementation-dependent.
// For catalogs that keep empty parents, operators may need to drop
them manually.
- String separator = HierarchicalSchemaUtil.schemaSeparator();
- Namespace outermostStale = namespace;
- String namespaceName = String.join(separator, namespace.levels());
- List<String> ancestorNames =
- HierarchicalSchemaUtil.getAncestorNames(namespaceName,
separator);
- for (int i = ancestorNames.size() - 1; i >= 0; i--) {
- Namespace ancestor =
Namespace.of(ancestorNames.get(i).split(Pattern.quote(separator)));
- if (dispatcher.namespaceExists(context, ancestor)) {
- break;
- }
- outermostStale = ancestor;
- }
-
- EntityStore store = GravitinoEnv.getInstance().entityStore();
- if (store != null) {
- try {
- store.delete(
- IcebergIdentifierUtils.toGravitinoSchemaIdentifier(
- metalake, catalogName, outermostStale, separator),
- Entity.EntityType.SCHEMA,
- true);
- } catch (NoSuchEntityException ignore) {
- // Already gone.
- } catch (IOException ioe) {
- throw new RuntimeException("io exception when deleting schema
entity", ioe);
- }
- }
+ //
+ // Routed through the same guarded helper as the table and view drop
paths so the cleanup
+ // never surfaces an error after the namespace drop has already
succeeded.
+ IcebergOrphanSchemaCleanup.bestEffortCleanUp(metalake, dispatcher,
context, namespace);
return null;
});
}
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOrphanSchemaCleanup.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOrphanSchemaCleanup.java
new file mode 100644
index 0000000000..75d60b7747
--- /dev/null
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOrphanSchemaCleanup.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.iceberg.service.dispatcher;
+
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.iceberg.common.utils.IcebergIdentifierUtils;
+import org.apache.gravitino.listener.api.event.IcebergRequestContext;
+import org.apache.gravitino.utils.HierarchicalSchemaUtil;
+import org.apache.gravitino.utils.SchemaEntityCleaner;
+import org.apache.iceberg.catalog.Namespace;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Shared best-effort cleanup of Gravitino schema entities that become
orphaned when an Iceberg
+ * table or view drop empties their backing namespaces.
+ */
+final class IcebergOrphanSchemaCleanup {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(IcebergOrphanSchemaCleanup.class);
+
+ private IcebergOrphanSchemaCleanup() {}
+
+ /**
+ * Removes Gravitino schema entities whose backing Iceberg namespace no
longer exists after a
+ * table or view drop.
+ *
+ * <p>Best-effort: the primary drop has already succeeded, so any failure
here is logged and
+ * swallowed rather than propagated to the caller.
+ *
+ * @param metalake the metalake the catalog belongs to
+ * @param namespaceDispatcher dispatcher used to probe whether a namespace
still exists in the
+ * catalog
+ * @param context the Iceberg request context carrying the catalog name
+ * @param namespace the namespace of the dropped table or view
+ */
+ static void bestEffortCleanUp(
+ String metalake,
+ IcebergNamespaceOperationDispatcher namespaceDispatcher,
+ IcebergRequestContext context,
+ Namespace namespace) {
+ try {
+ String separator = HierarchicalSchemaUtil.schemaSeparator();
+ SchemaEntityCleaner.deleteOrphanedSchemaEntities(
+ GravitinoEnv.getInstance().entityStore(),
+ IcebergIdentifierUtils.toGravitinoSchemaIdentifier(
+ metalake, context.catalogName(), namespace, separator),
+ true,
+ schemaIdent ->
+ namespaceDispatcher.namespaceExists(
+ context,
+ Namespace.of(
+
HierarchicalSchemaUtil.splitSchemaName(schemaIdent.name(), separator))));
+ } catch (RuntimeException e) {
+ LOG.warn(
+ "Failed to clean up orphaned Gravitino schema entities after the
Iceberg backend operation "
+ + "succeeded. catalog={}, namespace={}",
+ context.catalogName(),
+ namespace,
+ e);
+ }
+ }
+}
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableHookDispatcher.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableHookDispatcher.java
index d39d93f1ae..bf95e0a5a3 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableHookDispatcher.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableHookDispatcher.java
@@ -53,10 +53,14 @@ public class IcebergTableHookDispatcher implements
IcebergTableOperationDispatch
private static final Logger LOG =
LoggerFactory.getLogger(IcebergTableHookDispatcher.class);
private final IcebergTableOperationDispatcher dispatcher;
+ private final IcebergNamespaceOperationDispatcher namespaceDispatcher;
private String metalake;
- public IcebergTableHookDispatcher(IcebergTableOperationDispatcher
dispatcher) {
+ public IcebergTableHookDispatcher(
+ IcebergTableOperationDispatcher dispatcher,
+ IcebergNamespaceOperationDispatcher namespaceDispatcher) {
this.dispatcher = dispatcher;
+ this.namespaceDispatcher = namespaceDispatcher;
this.metalake = IcebergRESTServerContext.getInstance().metalakeName();
}
@@ -100,6 +104,8 @@ public class IcebergTableHookDispatcher implements
IcebergTableOperationDispatch
// another node may recreate the same table between the drop above and the
// EntityStore delete, leaving a stale Gravitino entity if we blindly
delete.
bestEffortReconcileTableEntity(context, tableIdentifier);
+ IcebergOrphanSchemaCleanup.bestEffortCleanUp(
+ metalake, namespaceDispatcher, context, tableIdentifier.namespace());
}
@Override
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
index dcde32b92c..bce140877b 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
@@ -53,10 +53,15 @@ public class IcebergViewHookDispatcher implements
IcebergViewOperationDispatcher
private static final Logger LOG =
LoggerFactory.getLogger(IcebergViewHookDispatcher.class);
private final IcebergViewOperationDispatcher dispatcher;
+ private final IcebergNamespaceOperationDispatcher namespaceDispatcher;
private final String metalake;
- public IcebergViewHookDispatcher(IcebergViewOperationDispatcher dispatcher,
String metalake) {
+ public IcebergViewHookDispatcher(
+ IcebergViewOperationDispatcher dispatcher,
+ IcebergNamespaceOperationDispatcher namespaceDispatcher,
+ String metalake) {
this.dispatcher = dispatcher;
+ this.namespaceDispatcher = namespaceDispatcher;
this.metalake = metalake;
}
@@ -101,6 +106,8 @@ public class IcebergViewHookDispatcher implements
IcebergViewOperationDispatcher
// another node may recreate the same view between the drop above and the
// EntityStore delete, leaving a stale Gravitino entity if we blindly
delete.
bestEffortReconcileViewEntity(context, viewIdentifier);
+ IcebergOrphanSchemaCleanup.bestEffortCleanUp(
+ metalake, namespaceDispatcher, context, viewIdentifier.namespace());
}
@Override
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
index 40a11ec015..27cf388ffd 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
@@ -74,6 +74,7 @@ public class TestIcebergTableHookDispatcher {
private IcebergTableHookDispatcher hookDispatcher;
private IcebergTableOperationDispatcher mockDispatcher;
+ private IcebergNamespaceOperationDispatcher mockNamespaceDispatcher;
private EntityStore mockEntityStore;
private TableDispatcher mockTableDispatcher;
private TableDispatcher mockInternalTableDispatcher;
@@ -92,6 +93,7 @@ public class TestIcebergTableHookDispatcher {
public void setUp() throws IllegalAccessException {
// Mock the underlying dispatcher
mockDispatcher = mock(IcebergTableOperationDispatcher.class);
+ mockNamespaceDispatcher = mock(IcebergNamespaceOperationDispatcher.class);
// Mock GravitinoEnv components
mockEntityStore = mock(EntityStore.class);
@@ -131,7 +133,7 @@ public class TestIcebergTableHookDispatcher {
IcebergRESTServerContext.create(mockConfigProvider, false, false, true,
null);
// Create hook dispatcher
- hookDispatcher = new IcebergTableHookDispatcher(mockDispatcher);
+ hookDispatcher = new IcebergTableHookDispatcher(mockDispatcher,
mockNamespaceDispatcher);
// Mock request context
mockContext = mock(IcebergRequestContext.class);
@@ -244,6 +246,21 @@ public class TestIcebergTableHookDispatcher {
verify(mockTableDispatcher, never()).loadTable(any());
}
+ @Test
+ public void testDropTableCleansUpOrphanedSchemaEntities() throws IOException
{
+ TableIdentifier tableId = TableIdentifier.of(Namespace.of("parent",
"child"), "test_table");
+ NameIdentifier schemaIdent = NameIdentifier.of(TEST_METALAKE,
TEST_CATALOG, "parent");
+ when(mockNamespaceDispatcher.namespaceExists(mockContext,
Namespace.of("parent", "child")))
+ .thenReturn(false);
+ when(mockNamespaceDispatcher.namespaceExists(mockContext,
Namespace.of("parent")))
+ .thenReturn(false);
+
+ hookDispatcher.dropTable(mockContext, tableId, false);
+
+ verify(mockDispatcher).dropTable(mockContext, tableId, false);
+ verify(mockEntityStore).delete(schemaIdent, Entity.EntityType.SCHEMA,
true);
+ }
+
@Test
public void testDropTableIgnoresNoSuchEntityException() throws IOException {
TableIdentifier tableId = TableIdentifier.of("test_schema", "test_table");
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewHookDispatcher.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewHookDispatcher.java
index 929aeac842..d81cf9fee0 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewHookDispatcher.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewHookDispatcher.java
@@ -72,6 +72,7 @@ public class TestIcebergViewHookDispatcher {
private IcebergViewHookDispatcher hookDispatcher;
private IcebergViewOperationDispatcher mockExecutor;
+ private IcebergNamespaceOperationDispatcher mockNamespaceDispatcher;
private EntityStore mockEntityStore;
private ViewDispatcher mockViewDispatcher;
private ViewDispatcher mockInternalViewDispatcher;
@@ -90,6 +91,7 @@ public class TestIcebergViewHookDispatcher {
@BeforeEach
public void setUp() {
mockExecutor = mock(IcebergViewOperationDispatcher.class);
+ mockNamespaceDispatcher = mock(IcebergNamespaceOperationDispatcher.class);
mockEntityStore = mock(EntityStore.class);
mockViewDispatcher = mock(ViewDispatcher.class);
mockInternalViewDispatcher = mock(ViewDispatcher.class);
@@ -127,7 +129,7 @@ public class TestIcebergViewHookDispatcher {
throw new RuntimeException("Failed to setup test", e);
}
- hookDispatcher = new IcebergViewHookDispatcher(mockExecutor, METALAKE);
+ hookDispatcher = new IcebergViewHookDispatcher(mockExecutor,
mockNamespaceDispatcher, METALAKE);
}
@AfterEach
@@ -309,6 +311,21 @@ public class TestIcebergViewHookDispatcher {
verify(mockViewDispatcher, never()).loadView(any());
}
+ @Test
+ public void testDropViewCleansUpOrphanedSchemaEntities() throws Exception {
+ TableIdentifier viewIdent = TableIdentifier.of(Namespace.of("parent",
"child"), VIEW_NAME);
+ NameIdentifier schemaIdent = NameIdentifier.of(METALAKE, CATALOG,
"parent");
+ when(mockNamespaceDispatcher.namespaceExists(mockContext,
Namespace.of("parent", "child")))
+ .thenReturn(false);
+ when(mockNamespaceDispatcher.namespaceExists(mockContext,
Namespace.of("parent")))
+ .thenReturn(false);
+
+ hookDispatcher.dropView(mockContext, viewIdent);
+
+ verify(mockExecutor, times(1)).dropView(mockContext, viewIdent);
+ verify(mockEntityStore, times(1)).delete(schemaIdent,
Entity.EntityType.SCHEMA, true);
+ }
+
@Test
public void testDropViewHandlesMissingEntity() throws Exception {
TableIdentifier viewIdent = TableIdentifier.of(Namespace.of(SCHEMA_NAME),
VIEW_NAME);