roryqi commented on code in PR #13771:
URL: https://github.com/apache/gravitino/pull/13771#discussion_r4248663278


##########
core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java:
##########
@@ -665,7 +677,9 @@ private void importSchema(NameIdentifier identifier) {
                     .build())
             .build();
     try {
-      store.put(schemaEntity, true);
+      // Never overwrite imported schema metadata. In particular, an overwrite 
by stable ID can
+      // silently change catalog ownership and make existing privileges refer 
to the wrong path.
+      store.put(schemaEntity, false);

Review Comment:
   Addressed in 386e00b914. A retained ID from a soft-deleted registration is 
now distinguished with SoftDeletedEntityIdentifierException. We intentionally 
do not restore or reuse that deleted identity; the user-facing error asks the 
operator to remove gravitino.identifier from the external objects and retry so 
they are imported with new IDs. I also added a real Hive catalog 
drop/re-register/load regression test and extended the relational-store 
coverage. No governance reconciliation is performed because relation cleanup 
already happens with entity deletion.



##########
core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java:
##########
@@ -582,11 +592,12 @@ private EntityCombinedTable importTable(NameIdentifier 
identifier) {
             .withAuditInfo(audit)
             .build();
     try {
-      // Overwrite only with the id stored in the catalog: it identifies the 
table, so a row under
-      // the same name with another id is stale and gets replaced. A generated 
id identifies
-      // nothing, so a row that appeared meanwhile, e.g. from a concurrent 
import on another node,
-      // must win. The plain insert then conflicts, and loadTable reloads that 
row.
-      store.put(tableEntity, stringId != null);
+      // Never overwrite imported table metadata. In particular, an overwrite 
by stable ID can
+      // silently change schema or catalog ownership and make existing 
privileges refer to the
+      // wrong path.
+      store.put(tableEntity, false);

Review Comment:
   Addressed in 386e00b914. IcebergCatalogCapability now follows Hive backend 
case-insensitive behavior for schema, table, and column names. I removed the 
expected-failure override, so the inherited test keeps its original success 
assertions. SparkIcebergCatalogHiveBackendIT35.testAlterTableReplaceColumns 
passed locally.



##########
core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java:
##########
@@ -83,6 +83,49 @@ protected OperationDispatcher(
     this.secretManager = secretManager;
   }
 
+  /**
+   * Creates the user-facing error for an external identifier retained by 
deleted metadata.
+   *
+   * <p>Restoring that metadata automatically could also restore governance 
data that the operator
+   * intended to retire. The error instead gives operators an explicit cleanup 
path that imports the
+   * external objects with new identifiers after manual reconciliation.
+   *
+   * @param ident the external entity identifier
+   * @param entityId the stable identifier read from the external system
+   * @param entityType the type of the external entity
+   * @return an exception containing manual reconciliation instructions
+   */
+  protected UnsupportedOperationException 
softDeletedExternalIdentifierException(
+      NameIdentifier ident, long entityId, Entity.EntityType entityType) {
+    String cleanupTarget;
+    switch (entityType) {
+      case SCHEMA:
+        cleanupTarget = "the external schema and all of its tables and views";
+        break;
+      case TABLE:
+        cleanupTarget = "the external table";
+        break;
+      case VIEW:
+        cleanupTarget = "the external view";
+        break;
+      default:
+        cleanupTarget = "the external object";

Review Comment:
   Addressed in 386e00b914. Topic import now uses a create-only store write, 
reloads after a conflict to preserve the HA-race behavior, and reports an 
explicit identifier-cleanup error for a real identity conflict. TOPIC is also 
included in soft-deleted-ID detection, with dispatcher and relational-store 
regression coverage.



##########
core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java:
##########
@@ -177,16 +178,21 @@ public Table loadTable(NameIdentifier ident) throws 
NoSuchTableException {
         entityCombinedTable =
             TreeLockUtils.doWithTreeLock(schemaIdent, LockType.WRITE, () -> 
importTable(ident));
       } catch (EntityAlreadyExistsException e) {
-        // HA race: another Gravitino node concurrently imported this table. 
Reload from the
-        // entity store to pick up the entity stored by the winning node.
-        LOG.info(
-            "Table {} was concurrently imported by another node; reloading 
from store.", ident);
+        // A create-only import can conflict either because another Gravitino 
node won an HA race,
+        // or because the external identifier is already stored under another 
Gravitino path.
+        // Reload to distinguish a successful concurrent import from an 
identity conflict.
+        LOG.info("Table {} import conflicted with existing metadata; reloading 
from store.", ident);
         entityCombinedTable =
             TreeLockUtils.doWithTreeLock(ident, LockType.READ, () -> 
internalLoadTable(ident));
         if (!entityCombinedTable.imported()) {
           throw new UnsupportedOperationException(
-              "Table managed by multiple catalogs. This may cause unexpected 
issues such as privilege conflicts. "
-                  + "To resolve: Remove all catalogs managing this table, then 
recreate one catalog to ensure single-catalog management.");
+              "Table "
+                  + ident
+                  + " cannot be imported because its name or external 
identifier is already "
+                  + "managed under a different Gravitino identifier. Gravitino 
does not "
+                  + "automatically reconcile external table renames or manage 
the same table "
+                  + "through multiple paths because overwriting its stored 
identity can corrupt "
+                  + "privilege relationships. Reconcile the external metadata 
manually.");

Review Comment:
   I rechecked the API contract and kept the existing semantics. The default 
tableExists/schemaExists/viewExists implementations are load plus catch 
NoSuchXException; other load failures are intentionally propagated. An 
identifier conflict means the object cannot be loaded correctly, so returning 
either true or false would hide that failure. Therefore the conflict remains 
explicit in both load and exists paths. The message no longer asks callers to 
reconcile governance data.



##########
catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/integration/test/CatalogHive2IT.java:
##########
@@ -1458,14 +1458,21 @@ public void testOutOfBandRenameKeepsColumnTags() throws 
InterruptedException {
             return null;
           });
 
-      // The column keeps its id, so the tag follows the table to its new name.
+      // Loading the external name now requires manual reconciliation. The 
rejected import must
+      // leave the tag relation attached to the original Gravitino identity.
       NameIdentifier newIdent = NameIdentifier.of(schema, newName);
-      Column column = loadColumn(newIdent, HIVE_COL_NAME1);
-      Assertions.assertArrayEquals(new String[] {tagName}, 
column.supportsTags().listTags());
+      UnsupportedOperationException exception =
+          assertThrows(
+              UnsupportedOperationException.class, () -> loadColumn(newIdent, 
HIVE_COL_NAME1));
+      Assertions.assertTrue(
+          exception
+              .getMessage()
+              .contains("does not automatically reconcile external table 
renames"));
       MetadataObject[] objects = 
metalake.getTag(tagName).associatedObjects().objects();
       Assertions.assertEquals(1, objects.length);
       Assertions.assertEquals(
-          String.join(".", catalogName, schema, newName, 
HIVE_COL_NAME1).toLowerCase(Locale.ROOT),
+          String.join(".", catalogName, schema, ident.name(), HIVE_COL_NAME1)

Review Comment:
   I removed governance reconciliation from the implementation and error text. 
Relations are automatically removed when the stale Gravitino registration is 
deleted, so this change does not add any relation re-pointing logic. The 
out-of-band rename test remains only as a no-silent-overwrite guard: cleanup is 
manual by deleting any stale registration and clearing the external 
gravitino.identifier before importing the object with a new ID.



##########
core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java:
##########
@@ -252,6 +261,30 @@ public void 
testConcurrentImportSchemaReusesExistingEntity() throws IOException
     Schema loadedSchema = Assertions.assertDoesNotThrow(() -> 
dispatcher.loadSchema(schemaIdent));
     Assertions.assertEquals(schemaIdent.name(), loadedSchema.name());
     Assertions.assertEquals("comment", loadedSchema.comment());
+    verify(entityStore).put(any(SchemaEntity.class), eq(false));
+  }
+
+  @Test
+  public void testImportSchemaWithSoftDeletedIdentifierRequiresManualCleanup() 
throws IOException {
+    NameIdentifier schemaIdent =
+        NameIdentifier.of(metalake, catalog, "schema_soft_deleted_identifier");
+    dispatcher.createSchema(schemaIdent, "comment", ImmutableMap.of("k1", 
"v1", "k2", "v2"));
+    SchemaEntity schemaEntity = entityStore.get(schemaIdent, SCHEMA, 
SchemaEntity.class);
+    entityStore.delete(schemaIdent, SCHEMA);
+
+    reset(entityStore);
+    doThrow(new SoftDeletedEntityIdentifierException(SCHEMA, 
schemaEntity.id()))

Review Comment:
   Added in 386e00b914. 
CatalogHive2IT.testCatalogReregistrationRequiresExternalIdentifierCleanup now 
exercises the complete flow with the relational store and Hive backend: create 
an external schema, drop the catalog, re-register it, verify loadSchema reports 
the retained soft-deleted ID, remove the external gravitino.identifier, and 
verify the schema imports successfully with a new ID. The exact Docker-backed 
test passed locally.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to