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());
+ }
}