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


##########
core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java:
##########
@@ -585,6 +600,80 @@ private EntityCombinedTable importTable(NameIdentifier 
identifier) {
         .withHiddenProperties(table.hiddenProperties());
   }
 
+  /**
+   * Tells an external rename apart from a copied id before an import re-binds 
a row.
+   *
+   * <p>An import that finds a {@link StringIdentifier} but no row under this 
name overwrites the
+   * row that owns the id. That is right after an external rename: the old 
name is gone and the row
+   * should follow the table. It is wrong when the id was copied ({@code 
CREATE TABLE t2 LIKE t1}
+   * carries {@code TBLPROPERTIES}, so does a copy tool or a restored backup): 
the source table is
+   * still there, and re-binding would move its row and every attachment keyed 
by that id (owner,
+   * tags, policies, role grants) to the copy. The store cannot tell the two 
apart; only the
+   * external catalog can, so this asks it whether the id's current owner 
still exists.
+   */
+  private NameIdentifier checkImportedIdNotCopied(NameIdentifier identifier, 
long id) {
+    NameIdentifier currentOwner = 
findRegisteredTableById(identifier.namespace(), id);
+    if (currentOwner == null || currentOwner.equals(identifier)) {
+      return currentOwner;
+    }
+    NameIdentifier catalogIdent = getCatalogIdentifier(identifier);
+    boolean distinctOwnerStillExists =
+        doWithCatalog(
+            catalogIdent,
+            c ->
+                c.doWithTableOps(
+                    ops -> {
+                      if (!ops.tableExists(currentOwner)) {

Review Comment:
   [Question] When the alias is accepted this returns `false`, so the import 
proceeds and `store.put(entity, true)` rewrites the row under the newly 
requested spelling. On a backend that resolves aliases without advertising 
case-insensitivity — precisely the case this branch exists for — two clients 
loading `t` and `T` will rename the entity back and forth: each load takes the 
schema write lock, bumps `current_version`, soft-deletes the previous version 
row and rewrites the column rows (TableMetaService.insertTable:118-190). Reads 
become writes, and the version history grows for as long as the two spellings 
keep alternating.
   
   The ping-pong predates this PR — the unconditional re-bind did the same — 
but this branch is where the alias case is now deliberately recognised, which 
makes it the natural place to stop it. When the only difference is case and the 
listing confirms a single object, would it be better to keep the stored name 
and skip the import write entirely?
   
   Verified by: read `checkImportedIdNotCopied` (:601-661) and `importTable` 
(:529-585), then traced the overwrite path in TableMetaService.insertTable 
(:109-190) to confirm what `store.put(.., true)` rewrites. 
`testLoadTableAcceptsCaseAlias` asserts id continuity but not that the stored 
name flipped.



##########
core/src/main/java/org/apache/gravitino/catalog/TableOperationDispatcher.java:
##########
@@ -549,6 +551,7 @@ private EntityCombinedTable importTable(NameIdentifier 
identifier) {
               + "when Table is renamed by external systems not controlled by 
Gravitino. In this "
               + "case, we need to overwrite the stored entity to keep the 
consistency.",
           stringId);

Review Comment:
   [Important] This check only looks inside `identifier.namespace()` 
(`findRegisteredTableById`, line 649), so the cross-schema copy — `CREATE TABLE 
db2.copy LIKE db1.orders`, no less realistic than the same-schema one — returns 
early here and falls through to the store guard instead.
   
   That guard throws `EntityAlreadyExistsException` (OccWriteSupport.java:89), 
`importTable` rethrows it unchanged (line 579), and `loadTable` catches it at 
line 176 as an HA race: it reloads, the copy is still not imported, and the 
caller ends up with
   
   > `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...`
   
   That diagnosis is wrong for a copied identifier, tells the operator to tear 
down catalogs, and never names `gravitino.identifier` or the source table — so 
the user-facing behaviour claimed in the PR description ("fails with an 
explanatory error") does not hold for exactly the case the store layer was 
added for. `SchemaOperationDispatcher.java:277-288` carries the identical 
handler and the same outcome for a cross-catalog schema copy.
   
   Two ways out: widen `findRegisteredTableById` to search the catalog rather 
than the schema so the good message is produced here, or make the store guard 
throw something the handler at line 176 can tell apart from a genuine 
concurrent import. Either way this deserves a dispatcher-level test on the 
cross-schema copy.
   
   Verified by: read `importTable` (:529-585), `checkImportedIdNotCopied` 
(:601-661), `loadTable` (:162-189), `SchemaOperationDispatcher` (:265-290) and 
`OccWriteSupport.checkOverwriteIdNotOwnedByOtherParent` (:85-90) on this 
branch, then traced the cross-schema path by hand; grepped the tests — 
`TestTableMetaService` asserts only the store-level throw, nothing exercises 
the dispatcher for it.



##########
catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java:
##########
@@ -86,7 +86,13 @@ static boolean shouldValidateWarehouseProperty(String 
backend, String warehouse)
 
   @Override
   public Capability newCapability() {
-    return new 
IcebergCatalogCapability(HierarchicalSchemaUtil.schemaSeparator());
+    Map<String, String> properties = entity().getProperties();
+    boolean hiveBackend =
+        properties != null
+            && IcebergCatalogBackend.HIVE
+                .name()
+                
.equalsIgnoreCase(properties.get(IcebergConstants.CATALOG_BACKEND));
+    return new 
IcebergCatalogCapability(HierarchicalSchemaUtil.schemaSeparator(), hiveBackend);

Review Comment:
   [Important] This flips `caseSensitiveOnName` for SCHEMA and TABLE on **every 
existing** Iceberg-on-Hive catalog, not only for the alias case this PR needs. 
`Capability.normalizeName` folds to lowercase (Capability.java:94), 
`TableNormalizeDispatcher.loadTable` applies it on every load 
(TableNormalizeDispatcher.java:61-64), and the javadoc at Capability.java:81-85 
states the normalized name is "persisted as the Gravitino entity name". After 
an upgrade, an entity stored as `MyTable` is therefore looked up as `mytable`:
   
   - If the external properties carry a `gravitino.identifier`, the new alias 
branch re-binds it and the row is renamed — presumably the intent.
   - If they do not (a table created directly in Iceberg and imported earlier 
under a mixed-case name), `internalLoadTable` finds no entity (:693-702) and 
`importTable` takes the `else` branch, minting a fresh id (:557). The old row 
survives under its mixed-case name, and its owner, tags, policies and 
statistics stay keyed to the old id — silently orphaned, with two entity rows 
for one physical table.
   
   That is the same class of damage this PR sets out to prevent, arriving 
through a different door. It is also a user-facing change the PR description 
does not mention, and it is separable from the copied-identifier fix. Could it 
go in its own PR, with a note on what happens to entities already registered 
under mixed-case names?
   
   Verified by: read Capability.java:74-95, 
CapabilityHelpers.applyCaseSensitive:147-179, TableNormalizeDispatcher:60-131, 
and TableOperationDispatcher.internalLoadTable:690-720 / importTable:529-560 on 
this branch. 
`TestIcebergCatalogCapability.testBackendSpecificIdentifierNormalization` 
covers the wiring but starts from an empty store, so this path is untested.



##########
core/src/main/java/org/apache/gravitino/storage/relational/service/OccWriteSupport.java:
##########
@@ -68,6 +68,28 @@ public static <T> T findAndLockForOverwrite(
     return current;
   }
 
+  /**
+   * Refuses an overwrite whose stable ID is already owned by a live row under 
another parent.
+   *
+   * <p>An import trusts the ID it finds in the external object. When that ID 
was copied from
+   * another object (copied table properties, a restored backup), an upsert 
keyed by the primary key
+   * would move the existing row, and every attachment keyed by that ID, to 
the new name and parent.
+   * The lookup must lock the row so the decision holds until the transaction 
ends. A same-parent
+   * match is allowed: that is how an external rename is re-registered.
+   *
+   * @param <T> the persistent object type
+   * @param byIdLockingLookup the locking lookup by stable ID
+   * @param sameParent checks whether the ID owner belongs to the target parent
+   * @throws EntityAlreadyExistsException if the ID belongs to a live row 
under another parent
+   */
+  public static <T> void checkOverwriteIdNotOwnedByOtherParent(
+      Supplier<T> byIdLockingLookup, Predicate<T> sameParent) {
+    T owner = byIdLockingLookup.get();
+    if (owner != null && !sameParent.test(owner)) {
+      throw new EntityAlreadyExistsException("The entity ID already belongs to 
a different parent");

Review Comment:
   [Nit] Two small things on this helper:
   
   - It repeats the second half of `findAndLockForOverwrite` (lines 59-67) 
verbatim, message included, so the two copies can drift apart. 
`findAndLockForOverwrite` could delegate to this method once its name lookup 
misses.
   - The message names neither the entity, nor the id, nor the parent. 
`PolicyMetaService.java:429` manages "The policy ID %s already belongs to a 
different metalake", and this one now surfaces to API users on a plain 
`loadTable`/`loadSchema` (see the comment on 
TableOperationDispatcher.java:553), where it is the only clue to what happened. 
Threading the id and the owning parent into the message would make the 
cross-parent case diagnosable from a log line.
   
   Verified by: read OccWriteSupport.java:42-90 on this branch and grepped 
`already belongs to a different parent` across `core/src/main` — two hits, both 
in this file.



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