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

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 9e0ff42f27 [#9088] feat(lance-rest): Support table exists and table 
drop APIs (#9201)
9e0ff42f27 is described below

commit 9e0ff42f27c12b0fceca1f1f4f2e3188dcd8c98f
Author: Mini Yu <[email protected]>
AuthorDate: Wed Nov 26 19:26:53 2025 +0800

    [#9088] feat(lance-rest): Support table exists and table drop APIs (#9201)
    
    ### What changes were proposed in this pull request?
    
    Add tableExists and tableDrop APIs in lance rest
    
    ### Why are the changes needed?
    
    It's a need.
    
    Fix: #9088
    
    ### Does this PR introduce _any_ user-facing change?
    
    N/A.
    
    ### How was this patch tested?
    
    UTs and ITs
---
 .../lance/common/ops/LanceTableOperations.java     |  19 ++++
 .../gravitino/GravitinoLanceTableOperations.java   |  58 +++++++++++
 .../lance/service/rest/LanceTableOperations.java   |  53 ++++++++++
 .../lance/integration/test/LanceRESTServiceIT.java |  76 +++++++++++++++
 .../service/rest/TestLanceNamespaceOperations.java | 107 +++++++++++++++++++++
 5 files changed, 313 insertions(+)

diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
index 7f2f1df52f..97b65d9bf0 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceTableOperations.java
@@ -23,6 +23,7 @@ import com.lancedb.lance.namespace.model.CreateTableRequest;
 import com.lancedb.lance.namespace.model.CreateTableResponse;
 import com.lancedb.lance.namespace.model.DeregisterTableResponse;
 import com.lancedb.lance.namespace.model.DescribeTableResponse;
+import com.lancedb.lance.namespace.model.DropTableResponse;
 import com.lancedb.lance.namespace.model.RegisterTableRequest;
 import com.lancedb.lance.namespace.model.RegisterTableResponse;
 import java.util.Map;
@@ -94,4 +95,22 @@ public interface LanceTableOperations {
    * @return the response of the deregister table operation
    */
   DeregisterTableResponse deregisterTable(String tableId, String delimiter);
+
+  /**
+   * Check if a table exists.
+   *
+   * @param tableId table ids are in the format of 
"{namespace}{delimiter}{table_name}"
+   * @param delimiter the delimiter used in the namespace
+   * @return true if the table exists, false otherwise
+   */
+  boolean tableExists(String tableId, String delimiter);
+
+  /**
+   * Drop a table. It will delete the underlying lance data.
+   *
+   * @param tableId table ids are in the format of 
"{namespace}{delimiter}{table_name}"
+   * @param delimiter the delimiter used in the namespace
+   * @return the response of the drop table operation
+   */
+  DropTableResponse dropTable(String tableId, String delimiter);
 }
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
index 8bb6bf37ea..68add7a863 100644
--- 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceTableOperations.java
@@ -34,6 +34,7 @@ import 
com.lancedb.lance.namespace.model.CreateTableRequest.ModeEnum;
 import com.lancedb.lance.namespace.model.CreateTableResponse;
 import com.lancedb.lance.namespace.model.DeregisterTableResponse;
 import com.lancedb.lance.namespace.model.DescribeTableResponse;
+import com.lancedb.lance.namespace.model.DropTableResponse;
 import com.lancedb.lance.namespace.model.JsonArrowSchema;
 import com.lancedb.lance.namespace.model.RegisterTableRequest;
 import com.lancedb.lance.namespace.model.RegisterTableResponse;
@@ -266,6 +267,63 @@ public class GravitinoLanceTableOperations implements 
LanceTableOperations {
     return response;
   }
 
+  @Override
+  public boolean tableExists(String tableId, String delimiter) {
+    ObjectIdentifier nsId = ObjectIdentifier.of(tableId, 
Pattern.quote(delimiter));
+    Preconditions.checkArgument(
+        nsId.levels() == 3, "Expected at 3-level namespace but got: %s", 
nsId.levels());
+
+    String catalogName = nsId.levelAtListPos(0);
+    Catalog catalog = 
namespaceWrapper.loadAndValidateLakehouseCatalog(catalogName);
+
+    NameIdentifier tableIdentifier =
+        NameIdentifier.of(nsId.levelAtListPos(1), nsId.levelAtListPos(2));
+
+    return catalog.asTableCatalog().tableExists(tableIdentifier);
+  }
+
+  @Override
+  public DropTableResponse dropTable(String tableId, String delimiter) {
+    ObjectIdentifier nsId = ObjectIdentifier.of(tableId, 
Pattern.quote(delimiter));
+    Preconditions.checkArgument(
+        nsId.levels() == 3, "Expected at 3-level namespace but got: %s", 
nsId.levels());
+
+    String catalogName = nsId.levelAtListPos(0);
+    Catalog catalog = 
namespaceWrapper.loadAndValidateLakehouseCatalog(catalogName);
+
+    NameIdentifier tableIdentifier =
+        NameIdentifier.of(nsId.levelAtListPos(1), nsId.levelAtListPos(2));
+
+    Table table;
+    try {
+      table = catalog.asTableCatalog().loadTable(tableIdentifier);
+    } catch (NoSuchTableException e) {
+      throw LanceNamespaceException.notFound(
+          "Table not found: " + tableId,
+          NoSuchTableException.class.getSimpleName(),
+          tableId,
+          CommonUtil.formatCurrentStackTrace());
+    }
+
+    boolean deleted = catalog.asTableCatalog().purgeTable(tableIdentifier);
+    if (!deleted) {
+      throw LanceNamespaceException.notFound(
+          "Table not found: " + tableId,
+          NoSuchTableException.class.getSimpleName(),
+          tableId,
+          CommonUtil.formatCurrentStackTrace());
+    }
+
+    DropTableResponse response = new DropTableResponse();
+    response.setId(nsId.listStyleId());
+    response.setLocation(table.properties().get(LANCE_LOCATION));
+    response.setProperties(table.properties());
+    // TODO Support transaction ids later
+    response.setTransactionId(List.of());
+
+    return response;
+  }
+
   private List<Column> 
extractColumns(org.apache.arrow.vector.types.pojo.Schema arrowSchema) {
     List<Column> columns = new ArrayList<>();
 
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
index bbef8e3b6a..ab46353c83 100644
--- 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java
@@ -34,9 +34,12 @@ import 
com.lancedb.lance.namespace.model.DeregisterTableRequest;
 import com.lancedb.lance.namespace.model.DeregisterTableResponse;
 import com.lancedb.lance.namespace.model.DescribeTableRequest;
 import com.lancedb.lance.namespace.model.DescribeTableResponse;
+import com.lancedb.lance.namespace.model.DropTableRequest;
+import com.lancedb.lance.namespace.model.DropTableResponse;
 import com.lancedb.lance.namespace.model.RegisterTableRequest;
 import com.lancedb.lance.namespace.model.RegisterTableRequest.ModeEnum;
 import com.lancedb.lance.namespace.model.RegisterTableResponse;
+import com.lancedb.lance.namespace.model.TableExistsRequest;
 import java.util.Map;
 import java.util.Optional;
 import javax.inject.Inject;
@@ -192,6 +195,46 @@ public class LanceTableOperations {
     }
   }
 
+  @POST
+  @Path("/exists")
+  @Timed(name = "table-exists." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
+  @ResponseMetered(name = "table-exists", absolute = true)
+  public Response tableExists(
+      @PathParam("id") String tableId,
+      @QueryParam("delimiter") @DefaultValue("$") String delimiter,
+      @Context HttpHeaders headers,
+      TableExistsRequest tableExistsRequest) {
+    try {
+      validateTableExists(tableExistsRequest);
+      // True if exists, false if not found and, otherwise throws exception
+      boolean exists = lanceNamespace.asTableOps().tableExists(tableId, 
delimiter);
+      if (exists) {
+        return Response.status(Response.Status.OK).build();
+      }
+      return Response.status(Response.Status.NOT_FOUND).build();
+    } catch (Exception e) {
+      return LanceExceptionMapper.toRESTResponse(tableId, e);
+    }
+  }
+
+  @POST
+  @Path("/drop")
+  @Timed(name = "drop-table." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "drop-table", absolute = true)
+  public Response dropTable(
+      @PathParam("id") String tableId,
+      @QueryParam("delimiter") @DefaultValue("$") String delimiter,
+      @Context HttpHeaders headers,
+      DropTableRequest dropTableRequest) {
+    try {
+      validateDropTableRequest(dropTableRequest);
+      DropTableResponse response = 
lanceNamespace.asTableOps().dropTable(tableId, delimiter);
+      return Response.ok(response).build();
+    } catch (Exception e) {
+      return LanceExceptionMapper.toRESTResponse(tableId, e);
+    }
+  }
+
   private void validateCreateEmptyTableRequest(
       @SuppressWarnings("unused") CreateEmptyTableRequest request) {
     // No specific fields to validate for now
@@ -213,4 +256,14 @@ public class LanceTableOperations {
     // We will ignore the id in the request body since it's already provided 
in the path param
     // No specific fields to validate for now
   }
+
+  private void validateTableExists(@SuppressWarnings("unused") 
TableExistsRequest request) {
+    // We will ignore the id in the request body since it's already provided 
in the path param
+    // No specific fields to validate for now
+  }
+
+  private void validateDropTableRequest(@SuppressWarnings("unused") 
DropTableRequest request) {
+    // We will ignore the id in the request body since it's already provided 
in the path param
+    // No specific fields to validate for now
+  }
 }
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 51826c3add..028d98215e 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
@@ -39,6 +39,8 @@ import com.lancedb.lance.namespace.model.DescribeTableRequest;
 import com.lancedb.lance.namespace.model.DescribeTableResponse;
 import com.lancedb.lance.namespace.model.DropNamespaceRequest;
 import com.lancedb.lance.namespace.model.DropNamespaceResponse;
+import com.lancedb.lance.namespace.model.DropTableRequest;
+import com.lancedb.lance.namespace.model.DropTableResponse;
 import com.lancedb.lance.namespace.model.ErrorResponse;
 import com.lancedb.lance.namespace.model.JsonArrowField;
 import com.lancedb.lance.namespace.model.ListNamespacesRequest;
@@ -48,6 +50,7 @@ import 
com.lancedb.lance.namespace.model.NamespaceExistsRequest;
 import com.lancedb.lance.namespace.model.RegisterTableRequest;
 import com.lancedb.lance.namespace.model.RegisterTableRequest.ModeEnum;
 import com.lancedb.lance.namespace.model.RegisterTableResponse;
+import com.lancedb.lance.namespace.model.TableExistsRequest;
 import com.lancedb.lance.namespace.rest.RestNamespaceConfig;
 import java.io.File;
 import java.io.IOException;
@@ -686,6 +689,79 @@ public class LanceRESTServiceIT extends BaseIT {
     Assertions.assertEquals(406, lanceNamespaceException.getCode());
   }
 
+  @Test
+  void testTableExists() {
+    catalog = createCatalog(CATALOG_NAME);
+    createSchema();
+
+    List<String> ids = List.of(CATALOG_NAME, SCHEMA_NAME, "table_exists");
+    CreateEmptyTableRequest createEmptyTableRequest = new 
CreateEmptyTableRequest();
+    String location = tempDir + "/" + "table_exists/";
+    createEmptyTableRequest.setLocation(location);
+    createEmptyTableRequest.setProperties(ImmutableMap.of());
+    createEmptyTableRequest.setId(ids);
+    CreateEmptyTableResponse response =
+        Assertions.assertDoesNotThrow(() -> 
ns.createEmptyTable(createEmptyTableRequest));
+    Assertions.assertNotNull(response);
+    Assertions.assertEquals(location, response.getLocation());
+
+    // Test existing table
+    TableExistsRequest tableExistsReq = new TableExistsRequest();
+    tableExistsReq.setId(ids);
+    Assertions.assertDoesNotThrow(() -> ns.tableExists(tableExistsReq));
+
+    // Test non-existing table
+    List<String> nonExistingIds = List.of(CATALOG_NAME, SCHEMA_NAME, 
"non_existing_table");
+    tableExistsReq.setId(nonExistingIds);
+    LanceNamespaceException exception =
+        Assertions.assertThrows(
+            LanceNamespaceException.class, () -> 
ns.tableExists(tableExistsReq));
+    Assertions.assertEquals(404, exception.getCode());
+    Assertions.assertTrue(exception.getMessage().contains("Not Found"));
+  }
+
+  @Test
+  void testDropTable() {
+    catalog = createCatalog(CATALOG_NAME);
+    createSchema();
+
+    List<String> ids = List.of(CATALOG_NAME, SCHEMA_NAME, "table_to_drop");
+    CreateEmptyTableRequest createEmptyTableRequest = new 
CreateEmptyTableRequest();
+    String location = tempDir + "/" + "table_to_drop/";
+    createEmptyTableRequest.setLocation(location);
+    createEmptyTableRequest.setProperties(ImmutableMap.of());
+    createEmptyTableRequest.setId(ids);
+    CreateEmptyTableResponse response =
+        Assertions.assertDoesNotThrow(() -> 
ns.createEmptyTable(createEmptyTableRequest));
+    Assertions.assertNotNull(response);
+    Assertions.assertEquals(location, response.getLocation());
+
+    // Drop the table
+    DropTableRequest dropTableRequest = new DropTableRequest();
+    dropTableRequest.setId(ids);
+    DropTableResponse dropTableResponse =
+        Assertions.assertDoesNotThrow(() -> ns.dropTable(dropTableRequest));
+    Assertions.assertNotNull(dropTableResponse);
+    Assertions.assertEquals(location, dropTableResponse.getLocation());
+    Assertions.assertFalse(
+        new File(location).exists(), "Data should be deleted after dropping 
the table.");
+
+    // Describe the dropped table should fail
+    DescribeTableRequest describeTableRequest = new DescribeTableRequest();
+    describeTableRequest.setId(ids);
+    LanceNamespaceException exception =
+        Assertions.assertThrows(
+            LanceNamespaceException.class, () -> 
ns.describeTable(describeTableRequest));
+    Assertions.assertEquals(404, exception.getCode());
+
+    // Drop a non-existing table should fail
+    dropTableRequest.setId(ids);
+    exception =
+        Assertions.assertThrows(
+            LanceNamespaceException.class, () -> 
ns.dropTable(dropTableRequest));
+    Assertions.assertEquals(404, exception.getCode());
+  }
+
   private GravitinoMetalake createMetalake(String metalakeName) {
     return client.createMetalake(metalakeName, "metalake for lance rest 
service tests", null);
   }
diff --git 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
index 0fc4df9cfd..02d5a6e812 100644
--- 
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
+++ 
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
@@ -21,11 +21,13 @@ package org.apache.gravitino.lance.service.rest;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.lancedb.lance.namespace.LanceNamespaceException;
 import com.lancedb.lance.namespace.model.CreateEmptyTableRequest;
@@ -40,6 +42,7 @@ import com.lancedb.lance.namespace.model.DescribeTableRequest;
 import com.lancedb.lance.namespace.model.DescribeTableResponse;
 import com.lancedb.lance.namespace.model.DropNamespaceRequest;
 import com.lancedb.lance.namespace.model.DropNamespaceResponse;
+import com.lancedb.lance.namespace.model.DropTableResponse;
 import com.lancedb.lance.namespace.model.ErrorResponse;
 import com.lancedb.lance.namespace.model.ListNamespacesResponse;
 import com.lancedb.lance.namespace.model.RegisterTableRequest;
@@ -665,4 +668,108 @@ public class TestLanceNamespaceOperations extends 
JerseyTest {
     Assertions.assertEquals("Runtime exception", errorResp.getError());
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResp.getType());
   }
+
+  @Test
+  void testTableExists() {
+    String tableIds = "catalog.scheme.table_exists";
+    String delimiter = ".";
+
+    doReturn(true).when(tableOps).tableExists(any(), any());
+
+    Response resp =
+        target(String.format("/v1/table/%s/exists", tableIds))
+            .queryParam("delimiter", delimiter)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .post(null);
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+
+    // test throw exception
+    doThrow(
+            LanceNamespaceException.notFound(
+                "Table not found", "NoSuchTableException", tableIds, ""))
+        .when(tableOps)
+        .tableExists(any(), any());
+    resp =
+        target(String.format("/v1/table/%s/exists", tableIds))
+            .queryParam("delimiter", delimiter)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .post(null);
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+
+    ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(404, errorResp.getCode());
+    Assertions.assertEquals("Table not found", errorResp.getError());
+    Assertions.assertEquals("NoSuchTableException", errorResp.getType());
+
+    // Test runtime exception
+    Mockito.reset(tableOps);
+    doThrow(new RuntimeException("Runtime 
exception")).when(tableOps).tableExists(any(), any());
+    resp =
+        target(String.format("/v1/table/%s/exists", tableIds))
+            .queryParam("delimiter", delimiter)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .post(null);
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+  }
+
+  @Test
+  void testDropTable() {
+    String tableIds = "catalog.scheme.drop_table";
+    String delimiter = ".";
+
+    DropTableResponse dropTableResponse = new DropTableResponse();
+    dropTableResponse.setId(Lists.newArrayList("catalog", "scheme", 
"drop_table"));
+    dropTableResponse.setProperties(ImmutableMap.of("key", "value"));
+    dropTableResponse.setLocation("/path/to/drop_table");
+    Mockito.doReturn(dropTableResponse).when(tableOps).dropTable(any(), any());
+
+    Response resp =
+        target(String.format("/v1/table/%s/drop", tableIds))
+            .queryParam("delimiter", delimiter)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .post(null);
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    DropTableResponse response = resp.readEntity(DropTableResponse.class);
+    Assertions.assertEquals(dropTableResponse.getId(), response.getId());
+    Assertions.assertEquals(dropTableResponse.getProperties(), 
response.getProperties());
+    Assertions.assertEquals(dropTableResponse.getLocation(), 
response.getLocation());
+
+    // test throw exception
+    doThrow(
+            LanceNamespaceException.notFound(
+                "Table not found", "NoSuchTableException", tableIds, ""))
+        .when(tableOps)
+        .dropTable(any(), any());
+    resp =
+        target(String.format("/v1/table/%s/drop", tableIds))
+            .queryParam("delimiter", delimiter)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .post(null);
+
+    Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+
+    ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(404, errorResp.getCode());
+    Assertions.assertEquals("Table not found", errorResp.getError());
+    Assertions.assertEquals("NoSuchTableException", errorResp.getType());
+
+    // Test runtime exception
+    Mockito.reset(tableOps);
+    doThrow(new RuntimeException("Runtime 
exception")).when(tableOps).dropTable(any(), any());
+    resp =
+        target(String.format("/v1/table/%s/drop", tableIds))
+            .queryParam("delimiter", delimiter)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .post(null);
+    Assertions.assertEquals(
+        Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
resp.getStatus());
+    Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, 
resp.getMediaType());
+  }
 }

Reply via email to