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

mchades 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 2631d87c96 [Cherry-pick to branch-1.3] [#9954] fix(lakehouse-generic): 
Enhance purgeTable logic to handle external tables and missing locations. 
(#9967) (#11463)
2631d87c96 is described below

commit 2631d87c968f641568d54a1310e79aa5e6d4b13e
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Jun 5 23:38:22 2026 +0800

    [Cherry-pick to branch-1.3] [#9954] fix(lakehouse-generic): Enhance 
purgeTable logic to handle external tables and missing locations. (#9967) 
(#11463)
    
    **Cherry-pick Information:**
    - Original commit: 1d365c7cd6c48844dcf7c5ceac2f5c794aab2124
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: Qi Yu <[email protected]>
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
 .../lakehouse/lance/LanceTableOperations.java      | 58 +++++++++++-----
 .../test/CatalogGenericCatalogLanceIT.java         | 79 ++++++++++++++++++++++
 .../lance/integration/test/LanceRESTServiceIT.java | 51 +++++++++++++-
 3 files changed, 171 insertions(+), 17 deletions(-)

diff --git 
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
 
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
index 9edec20a23..89c99d5365 100644
--- 
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
+++ 
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
@@ -23,6 +23,7 @@ import static 
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_TABLE
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -203,17 +204,26 @@ public class LanceTableOperations extends 
ManagedTableOperations {
   public boolean purgeTable(NameIdentifier ident) {
     try {
       Table table = loadTable(ident);
-      String location = table.properties().get(Table.PROPERTY_LOCATION);
+      boolean external =
+          Optional.ofNullable(table.properties().get(Table.PROPERTY_EXTERNAL))
+              .map(Boolean::parseBoolean)
+              .orElse(false);
 
+      // NOTE: the two steps below (metadata removal and dataset deletion) are 
NOT atomic.
+      // If the process crashes between them, the entity-store record will be 
gone but the
+      // Lance dataset will still exist on storage (orphaned data), or 
vice-versa. There is
+      // currently no recovery mechanism for this window.
       boolean purged = super.purgeTable(ident);
+      // If the table is a managed table, super.purgeTable will call dropTable 
to remove the
+      // underlying Lance dataset, so we don't need to do anything here.
+      if (!external) {
+        return purged;
+      }
+
       // If the table metadata is purged successfully, we can delete the Lance 
dataset.
       // Otherwise, we should not delete the dataset.
       if (purged) {
-        // Delete the Lance dataset at the location
-        Dataset.drop(
-            location,
-            LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties, 
table.properties()));
-        LOG.info("Deleted Lance dataset at location {}", location);
+        dropLanceDataset(table);
       }
 
       return purged;
@@ -234,22 +244,19 @@ public class LanceTableOperations extends 
ManagedTableOperations {
               .map(Boolean::parseBoolean)
               .orElse(false);
 
+      // NOTE: the two steps below (metadata removal and dataset deletion) are 
NOT atomic.
+      // If the process crashes between them, the entity-store record will be 
gone but the
+      // Lance dataset will still exist on storage (orphaned data), or 
vice-versa. There is
+      // currently no recovery mechanism for this window.
       boolean dropped = super.dropTable(ident);
       if (external) {
         return dropped;
       }
 
-      // If the table metadata is dropped successfully, and the table is not 
external, we can delete
-      // the
-      // Lance dataset. Otherwise, we should not delete the dataset.
+      // If the table metadata is dropped successfully, and the table is not 
external, we can
+      // delete the Lance dataset. Otherwise, we should not delete the dataset.
       if (dropped) {
-        String location = table.properties().get(Table.PROPERTY_LOCATION);
-
-        // Delete the Lance dataset at the location
-        Dataset.drop(
-            location,
-            LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties, 
table.properties()));
-        LOG.info("Deleted Lance dataset at location {}", location);
+        dropLanceDataset(table);
       }
 
       return dropped;
@@ -261,6 +268,25 @@ public class LanceTableOperations extends 
ManagedTableOperations {
     }
   }
 
+  private void dropLanceDataset(Table table) {
+    String location = table.properties().get(Table.PROPERTY_LOCATION);
+    Map<String, String> resolvedStorageOptions =
+        LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties, 
table.properties());
+    try {
+      Dataset.drop(location, resolvedStorageOptions);
+      LOG.info("Deleted Lance dataset at location {}", location);
+    } catch (Exception e) {
+      // Dataset.drop (native) throws IOException with "Not found:" when path 
doesn't exist.
+      if (e instanceof IOException
+          && e.getMessage() != null
+          && e.getMessage().contains("Not found:")) {
+        LOG.warn("Lance dataset at {} was already deleted, skipping.", 
location);
+      } else {
+        throw new RuntimeException("Failed to delete Lance dataset at " + 
location, e);
+      }
+    }
+  }
+
   // Package-private for testing
   Table createTableInternal(
       NameIdentifier ident,
diff --git 
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/integration/test/CatalogGenericCatalogLanceIT.java
 
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/integration/test/CatalogGenericCatalogLanceIT.java
index d7b2b5f218..f2dc40907a 100644
--- 
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/integration/test/CatalogGenericCatalogLanceIT.java
+++ 
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/integration/test/CatalogGenericCatalogLanceIT.java
@@ -153,6 +153,85 @@ public class CatalogGenericCatalogLanceIT extends BaseIT {
     createSchema();
   }
 
+  @Test
+  public void testPurgeTableWhenLocationMissing() throws IOException {
+    Column[] columns = createColumns();
+    String location = tempDirectory + "/" + 
GravitinoITUtils.genRandomName(TABLE_PREFIX) + "/";
+    NameIdentifier identifier =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName(TABLE_PREFIX));
+    Map<String, String> properties = createProperties();
+    properties.put(Table.PROPERTY_TABLE_FORMAT, LANCE_TABLE_FORMAT);
+    properties.put(Table.PROPERTY_EXTERNAL, "true");
+    properties.put(Table.PROPERTY_LOCATION, location);
+
+    catalog
+        .asTableCatalog()
+        .createTable(
+            identifier, columns, TABLE_COMMENT, properties, 
Transforms.EMPTY_TRANSFORM, null, null);
+    Assertions.assertTrue(new File(location).exists());
+
+    // Simulate missing data before purge
+    FileUtils.deleteDirectory(new File(location));
+    Assertions.assertFalse(new File(location).exists());
+
+    Assertions.assertDoesNotThrow(() -> 
catalog.asTableCatalog().purgeTable(identifier));
+    Assertions.assertFalse(catalog.asTableCatalog().tableExists(identifier));
+  }
+
+  @Test
+  public void testPurgeExternalTableWithEmptyDirectory() {
+    Column[] columns = createColumns();
+    String location = tempDirectory + "/" + 
GravitinoITUtils.genRandomName(TABLE_PREFIX) + "/";
+    NameIdentifier identifier =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName(TABLE_PREFIX));
+    Map<String, String> properties = createProperties();
+    properties.put(Table.PROPERTY_TABLE_FORMAT, LANCE_TABLE_FORMAT);
+    properties.put(Table.PROPERTY_EXTERNAL, "true");
+    properties.put(Table.PROPERTY_LOCATION, location);
+
+    catalog
+        .asTableCatalog()
+        .createTable(
+            identifier, columns, TABLE_COMMENT, properties, 
Transforms.EMPTY_TRANSFORM, null, null);
+    // The directory is created but contains no Lance dataset files
+    Assertions.assertTrue(new File(location).exists());
+
+    Assertions.assertDoesNotThrow(() -> 
catalog.asTableCatalog().purgeTable(identifier));
+    Assertions.assertFalse(catalog.asTableCatalog().tableExists(identifier));
+  }
+
+  @Test
+  public void testPurgeManagedTableWithAutoLocation() {
+    Column[] columns = createColumns();
+    NameIdentifier identifier =
+        NameIdentifier.of(schemaName, 
GravitinoITUtils.genRandomName(TABLE_PREFIX));
+    String location = tempDirectory + "/" + 
GravitinoITUtils.genRandomName(TABLE_PREFIX) + "/";
+
+    Map<String, String> properties = createProperties();
+    properties.put(Table.PROPERTY_TABLE_FORMAT, LANCE_TABLE_FORMAT);
+    properties.put(Table.PROPERTY_EXTERNAL, "false");
+    properties.put(Table.PROPERTY_LOCATION, location);
+
+    Table table =
+        catalog
+            .asTableCatalog()
+            .createTable(
+                identifier,
+                columns,
+                TABLE_COMMENT,
+                properties,
+                Transforms.EMPTY_TRANSFORM,
+                null,
+                null);
+    String resolvedLocation = table.properties().get(Table.PROPERTY_LOCATION);
+    Assertions.assertNotNull(resolvedLocation);
+    Assertions.assertTrue(new File(resolvedLocation).exists());
+
+    Assertions.assertDoesNotThrow(() -> 
catalog.asTableCatalog().purgeTable(identifier));
+    Assertions.assertFalse(catalog.asTableCatalog().tableExists(identifier));
+    Assertions.assertFalse(new File(resolvedLocation).exists());
+  }
+
   @Test
   public void testCrateEmptyTable() {
     String emptyTableName = GravitinoITUtils.genRandomName(TABLE_PREFIX);
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
index 40798407e4..061e61250b 100644
--- 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
@@ -882,7 +882,7 @@ public class LanceRESTServiceIT extends BaseIT {
     // Drop the table
     DropTableRequest dropTableRequest = new DropTableRequest();
     dropTableRequest.setId(ids);
-    Assertions.assertThrows(Exception.class, () -> 
ns.dropTable(dropTableRequest));
+    Assertions.assertDoesNotThrow(() -> ns.dropTable(dropTableRequest));
 
     // Describe the dropped table should fail
     DescribeTableRequest describeTableRequest = new DescribeTableRequest();
@@ -1014,6 +1014,55 @@ public class LanceRESTServiceIT extends BaseIT {
     return new TableApi(apiClient);
   }
 
+  @Test
+  void testDropTableWhenLocationMissingOrNotCreated() {
+    catalog = createCatalog(CATALOG_NAME);
+    createSchema();
+
+    // Create table via register with non-existent location
+    List<String> registerIds = List.of(CATALOG_NAME, SCHEMA_NAME, 
"register_missing_location");
+    String missingLocation = tempDir + "/" + "register_missing_location/";
+    RegisterTableRequest registerTableRequest = new RegisterTableRequest();
+    registerTableRequest.setId(registerIds);
+    registerTableRequest.setLocation(missingLocation);
+    registerTableRequest.setMode("create");
+    RegisterTableResponse registerTableResponse =
+        Assertions.assertDoesNotThrow(() -> 
ns.registerTable(registerTableRequest));
+    Assertions.assertEquals(missingLocation, 
registerTableResponse.getLocation());
+    Assertions.assertFalse(new File(missingLocation).exists());
+
+    DropTableRequest dropRegisteredTable = new DropTableRequest();
+    dropRegisteredTable.setId(registerIds);
+    Assertions.assertDoesNotThrow(() -> ns.dropTable(dropRegisteredTable));
+    DescribeTableRequest describeRegistered = new DescribeTableRequest();
+    describeRegistered.setId(registerIds);
+    Assertions.assertThrows(
+        LanceNamespaceException.class, () -> 
ns.describeTable(describeRegistered));
+
+    // Register table with an existing location, then delete the location 
before drop
+    List<String> existingIds = List.of(CATALOG_NAME, SCHEMA_NAME, 
"existing_then_deleted");
+    String existingLocation = tempDir + "/" + "existing_then_deleted/";
+    new File(existingLocation).mkdirs();
+    Assertions.assertTrue(new File(existingLocation).exists());
+
+    RegisterTableRequest existingRegisterRequest = new RegisterTableRequest();
+    existingRegisterRequest.setId(existingIds);
+    existingRegisterRequest.setLocation(existingLocation);
+    existingRegisterRequest.setMode("create");
+    Assertions.assertDoesNotThrow(() -> 
ns.registerTable(existingRegisterRequest));
+
+    FileUtils.deleteQuietly(new File(existingLocation));
+    Assertions.assertFalse(new File(existingLocation).exists());
+
+    DropTableRequest dropExistingTable = new DropTableRequest();
+    dropExistingTable.setId(existingIds);
+    Assertions.assertDoesNotThrow(() -> ns.dropTable(dropExistingTable));
+    DescribeTableRequest describeExisting = new DescribeTableRequest();
+    describeExisting.setId(existingIds);
+    Assertions.assertThrows(
+        LanceNamespaceException.class, () -> 
ns.describeTable(describeExisting));
+  }
+
   private GravitinoMetalake createMetalake(String metalakeName) {
     return client.createMetalake(metalakeName, "metalake for lance rest 
service tests", null);
   }

Reply via email to