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


##########
core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java:
##########
@@ -83,6 +83,90 @@ protected OperationDispatcher(
     this.secretManager = secretManager;
   }
 
+  /**
+   * Creates the user-facing error for an external identifier retained by 
deleted metadata.
+   *
+   * <p>The error gives operators an explicit cleanup path that imports the 
external objects with
+   * new identifiers.
+   *
+   * @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 cleanup 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";
+    }
+
+    return new UnsupportedOperationException(
+        String.format(
+            "%s %s cannot be imported because external identifier %s is 
retained by a "
+                + "soft-deleted Gravitino registration. "
+                + "Gravitino does not automatically restore or reuse deleted 
registrations. "
+                + "Remove `%s` from %s before retrying. The objects will be 
imported with new "
+                + "identifiers.",
+            entityType.name().toLowerCase(),
+            ident,
+            entityId,
+            StringIdentifier.ID_KEY,
+            cleanupTarget));
+  }
+
+  /**
+   * Creates the user-facing error for an external identifier conflict with 
active metadata.
+   *
+   * <p>The error describes the supported recovery choices without implying 
that Gravitino offers
+   * object-level metadata-only unregister operations.
+   *
+   * @param ident the external entity identifier
+   * @param entityType the type of the external entity
+   * @return an exception containing conflict recovery instructions
+   */
+  protected UnsupportedOperationException 
retainedExternalIdentifierConflictException(
+      NameIdentifier ident, Entity.EntityType entityType) {
+    String entityName;
+    switch (entityType) {
+      case SCHEMA:
+        entityName = "schema";
+        break;
+      case TABLE:
+        entityName = "table";
+        break;
+      case VIEW:
+        entityName = "view";
+        break;
+      default:
+        entityName = "object";
+    }
+
+    return new UnsupportedOperationException(
+        String.format(
+            "%s %s cannot be imported because its name or external identifier 
is already managed "

Review Comment:
   [Important] The PR description now contradicts the recovery procedure this 
commit ships.
   
   The message built here tells the operator that Gravitino `does not support 
metadata-only unregister of an individual external` object and that `Recovery 
requires an administrator to force-drop and recreate the containing Gravitino 
catalog registration`, losing every registration, privilege and tag in that 
catalog. All four doc sections added in 4d0b464/782c1e2 say the same (for 
example `docs/apache-hive-catalog.md:86-98`).
   
   The PR's "Does this PR introduce any user-facing change?" section still 
describes the previous, per-object remedy: that the load "asks the operator to 
delete the stale Gravitino registration and remove `gravitino.identifier` from 
the external object before importing it with a new ID". That is exactly the 
operation this message now says is unsupported, and it understates the blast 
radius by a whole catalog. The description is what reviewers and release notes 
read; please bring it in line with the message and the docs - catalog-wide 
force-drop, then clear `gravitino.identifier`, then re-register and reapply 
governance.
   
   Verified by: read the final message text at OperationDispatcher.java:155-167 
on this head, the four doc sections added by 4d0b464 and 782c1e2, and the PR 
body as currently published.



##########
docs/lakehouse-iceberg-catalog.md:
##########
@@ -57,6 +57,26 @@ If you are using the Gravitino with Trino, you can pass the 
Trino Iceberg connec
 
 If you are using the Gravitino with Spark, you can pass the Spark Iceberg 
connector configuration using prefix `spark.bypass.`. For example, using 
`spark.bypass.io-impl` to pass the `io-impl` to the Spark Iceberg connector in 
Spark runtime.
 
+#### Identifier Case Behavior
+
+For `catalog-backend=hive`, schema, table, and view identifiers are 
case-insensitive; you can use any casing.
+
+For `catalog-backend=rest`, always use the same identifier casing that was 
used when the object was first registered. Loading the same object with 
different casing is rejected.

Review Comment:
   [Nit] These two sentences cover `hive` and `rest` but leave out the other 
backends, and the code treats them like `rest`, not like `hive`. 
`IcebergCatalogBackend` also has `JDBC`, `MEMORY` and `CUSTOM` 
(`IcebergCatalogBackend.java:21-27`), and `IcebergCatalog.newCapability` folds 
only when the property equals `hive` (`IcebergCatalog.java:89-93`). The same 
gap is in `docs/lakehouse-paimon-catalog.md:85-87`, where 
`PaimonCatalogBackend` also has `FILESYSTEM` and `JDBC` 
(`PaimonCatalogBackend.java:22-27`).
   
   A reader on `catalog-backend=jdbc` finds no statement at all, even though 
they get the exact-case behaviour and the same rejection on a different casing. 
Suggest phrasing it as `hive` versus "every other backend".
   
   Verified by: read both backend enums and both `newCapability()` 
implementations on this head.



##########
catalogs/catalog-lakehouse-hudi/src/main/java/org/apache/gravitino/catalog/lakehouse/hudi/HudiCatalog.java:
##########
@@ -54,6 +55,11 @@ protected CatalogOperations newOps(Map<String, String> 
config) {
     return new HudiCatalogOperations();
   }
 
+  @Override
+  public Capability newCapability() {

Review Comment:
   [Nit] Two small asymmetries with the Iceberg and Paimon equivalents in the 
same commit:
   
   1. No unit test asserts this wiring. `TestIcebergCatalog:133` and 
`TestPaimonCatalog:133` each gained a 
`newCapability().caseSensitiveOnName(...)` assertion, but `TestHudiCatalog` has 
none, so the only thing proving `HudiCatalog` actually returns 
`HudiCatalogCapability` rather than `Capability.DEFAULT` is the Docker-tagged 
`HudiCatalogHMSIT`. A two-line assertion in `TestHudiCatalog` would cover it in 
the fast suite.
   2. `HudiCatalogCapability` folds unconditionally while the other two key on 
the `catalog-backend` property. That is correct today - `BackendType` has only 
`HMS` (`BackendType.java:21-22`) - but the capability's own javadoc says 
"backed by Hive Metastore", so a future non-HMS backend would silently inherit 
the fold. Keying it on `BackendType` here, as `PaimonCatalog.java:73-76` does, 
would make that impossible to get wrong.
   
   Verified by: read `HudiCatalogPropertiesMetadata`, `BackendType`, all three 
`newCapability()` implementations, and `TestHudiCatalog` on this head.



##########
spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestBackendIT.java:
##########
@@ -33,6 +35,17 @@
 @DisabledIf("org.apache.gravitino.integration.test.util.ITUtils#isEmbedded")
 public abstract class SparkIcebergCatalogRestBackendIT extends 
SparkIcebergCatalogIT {
 
+  /**
+   * Tests replacing columns without assuming the case-sensitivity of the 
catalog behind the REST
+   * service. Gravitino cannot infer that backend capability through the REST 
protocol.
+   */
+  @Override
+  @Test
+  @EnabledIf("supportsReplaceColumns")
+  protected void testAlterTableReplaceColumns() {

Review Comment:
   [Question] Was `SparkIcebergCatalogRestRoutingIT` meant to get this same 
override?
   
   `runAlterTableReplaceColumnsTest(true)` is the new default 
(`SparkCommonIT.java:610-620`), and this is the only subclass that opts out. 
`SparkIcebergCatalogRestRoutingIT extends SparkIcebergCatalogIT` directly 
(`SparkIcebergCatalogRestRoutingIT.java:43`), not this class, so it inherits 
the uppercase statements.
   
   In routing mode Spark speaks the Iceberg REST protocol to Gravitino's 
auxiliary service, and those requests are served by 
`IcebergTableOperationExecutor` through `IcebergCatalogWrapperManager` 
(`IcebergTableOperationExecutor.java:58-62,173-182`) - they never reach 
`TableOperationDispatcher`/`TableNormalizeDispatcher`, so the new 
`IcebergCatalogCapability` fold has no effect on that path. If that test goes 
green it will be because Iceberg's `HiveCatalog` and HMS fold the name 
themselves, not because of this PR; if HMS did not fold, it would fail exactly 
as the REST-backend case does. Either outcome is worth deciding deliberately: 
add the same `runAlterTableReplaceColumnsTest(false)` override here if the REST 
protocol is meant to stay exact-case end to end, or say in the test's javadoc 
why the routed path is expected to tolerate uppercase.
   
   No SparkIT job has run on this head (all queued), so CI has not answered 
this yet.
   
   Verified by: read the class hierarchy of all `SparkCommonIT` subclasses and 
their `supportsReplaceColumns()` (only hive and the three iceberg suites return 
true), and traced the REST-routed load path to `IcebergTableOperationExecutor`.



##########
core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java:
##########
@@ -83,6 +83,90 @@ protected OperationDispatcher(
     this.secretManager = secretManager;
   }
 
+  /**
+   * Creates the user-facing error for an external identifier retained by 
deleted metadata.
+   *
+   * <p>The error gives operators an explicit cleanup path that imports the 
external objects with
+   * new identifiers.
+   *
+   * @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 cleanup 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";
+    }
+
+    return new UnsupportedOperationException(
+        String.format(
+            "%s %s cannot be imported because external identifier %s is 
retained by a "
+                + "soft-deleted Gravitino registration. "
+                + "Gravitino does not automatically restore or reuse deleted 
registrations. "
+                + "Remove `%s` from %s before retrying. The objects will be 
imported with new "
+                + "identifiers.",
+            entityType.name().toLowerCase(),
+            ident,
+            entityId,
+            StringIdentifier.ID_KEY,
+            cleanupTarget));
+  }
+
+  /**
+   * Creates the user-facing error for an external identifier conflict with 
active metadata.
+   *
+   * <p>The error describes the supported recovery choices without implying 
that Gravitino offers
+   * object-level metadata-only unregister operations.
+   *
+   * @param ident the external entity identifier
+   * @param entityType the type of the external entity
+   * @return an exception containing conflict recovery instructions
+   */
+  protected UnsupportedOperationException 
retainedExternalIdentifierConflictException(
+      NameIdentifier ident, Entity.EntityType entityType) {
+    String entityName;
+    switch (entityType) {
+      case SCHEMA:
+        entityName = "schema";
+        break;
+      case TABLE:
+        entityName = "table";
+        break;
+      case VIEW:
+        entityName = "view";
+        break;
+      default:
+        entityName = "object";
+    }
+
+    return new UnsupportedOperationException(
+        String.format(
+            "%s %s cannot be imported because its name or external identifier 
is already managed "
+                + "under a different Gravitino identifier. Gravitino does not 
automatically "
+                + "reconcile external %s renames or stale registrations, and 
it does not support "
+                + "metadata-only unregister of an individual external %s. 
Recovery requires an "
+                + "administrator to force-drop and recreate the containing 
Gravitino catalog "
+                + "registration. This catalog-wide reset removes every stored 
registration and "
+                + "its privileges, tags, and other relationships in that 
Gravitino catalog. "
+                + "Follow the catalog-specific recovery guidance to confirm 
that external objects "
+                + "are retained, and do not remove `%s` until the conflicting 
catalog registration "

Review Comment:
   [Nit] Three small things in this message, now that it is the single 
user-facing text for all three entity types:
   
   1. It never states the step the operator must take - it only says when *not* 
to take it (`do not remove ... until the conflicting catalog registration has 
been dropped`). The docs do say to remove `gravitino.identifier` after the 
drop; the message should too, or point at the doc page.
   2. `the containing Gravitino catalog registration` (line 161) and `the 
conflicting catalog registration` (line 165) read as two different catalogs. In 
the shared-metastore case they genuinely are: the stale registration lives in 
another catalog, and force-dropping the catalog you are loading through fixes 
nothing. Naming which one would save an operator a wrong drop.
   3. `%s` at the start resolves to the lowercase entity name, so the sentence 
now begins `schema metalake.cat.sch cannot be imported` where it previously 
began `Schema ...`.
   
   Verified by: read the format string and its five arguments at 
OperationDispatcher.java:155-167, and compared with the procedure in 
docs/apache-hive-catalog.md:86-98.



-- 
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