diff --git a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitino.java b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitino.java index 3cff6940c6e..4b05b973848 100644 --- a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitino.java +++ b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitino.java @@ -119,6 +119,14 @@ public void start() throws Exception { this.port = jettyServerConfig.getHttpPort(); String URI = String.format("http://%s:%d", host, port); + // Add for lance rest service uri + if (!context.ignoreAuxRestService) { + properties.put("gravitino.lance-rest.gravitino.uri", URI); + serverConfig.loadFromProperties(properties); + ITUtils.overwriteConfigFile( + ITUtils.joinPath(mockConfDir.getAbsolutePath(), "gravitino.conf"), properties); + } + List authenticators = new ArrayList<>(); String authenticatorStr = context.customConfig.get(Configs.AUTHENTICATORS.getKey()); if (authenticatorStr != null) { diff --git a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java index 1ae60a0fecb..45943d231d7 100644 --- a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java +++ b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java @@ -133,7 +133,7 @@ public void registerCustomConfigs(Map configs) { customConfigs.putAll(configs); } - private void rewriteGravitinoServerConfig() throws IOException { + protected void rewriteGravitinoServerConfig() throws IOException { String gravitinoHome = System.getenv("GRAVITINO_HOME"); Path configPath = Paths.get(gravitinoHome, "conf", GravitinoServer.CONF_FILE); if (originConfig == null) { @@ -421,7 +421,7 @@ protected String readGitCommitIdFromGitFile() { } } - private static boolean isDeploy() { + public static boolean isDeploy() { String mode = System.getProperty(ITUtils.TEST_MODE) == null ? ITUtils.EMBEDDED_TEST_MODE diff --git a/lance/lance-common/build.gradle.kts b/lance/lance-common/build.gradle.kts index 5be0eae4bf9..0178fa3fc28 100644 --- a/lance/lance-common/build.gradle.kts +++ b/lance/lance-common/build.gradle.kts @@ -44,3 +44,16 @@ dependencies { testImplementation(libs.junit.jupiter.params) testRuntimeOnly(libs.junit.jupiter.engine) } + +val testJar by tasks.registering(Jar::class) { + archiveClassifier.set("tests") + from(sourceSets["test"].output) +} + +configurations { + create("testArtifacts") +} + +artifacts { + add("testArtifacts", testJar) +} 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 8a356fb1359..b8d65d49c9b 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 @@ -18,26 +18,66 @@ */ package org.apache.gravitino.lance.common.ops; +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.RegisterTableRequest; import com.lancedb.lance.namespace.model.RegisterTableResponse; import java.util.Map; public interface LanceTableOperations { - DescribeTableResponse describeTable(String tableId, String delimiter); + /** + * Describe the details of a table. + * + * @param tableId table ids are in the format of "namespace/delimiter/table_name" + * @param delimiter the delimiter used in the namespace + * @param version the version of the table to describe, if null, describe the latest version + * @return the table description + */ + DescribeTableResponse describeTable(String tableId, String delimiter, Long version); + /** + * Create a new table. + * + * @param tableId table ids are in the format of "namespace/delimiter/table_name" + * @param mode it can be CREATE, OVERWRITE, or EXIST_OK + * @param delimiter the delimiter used in the namespace + * @param tableLocation the location where the table data will be stored + * @param tableProperties the properties of the table + * @param arrowStreamBody the arrow stream bytes containing the schema and data + * @return the response of the create table operation + */ CreateTableResponse createTable( String tableId, - String mode, + CreateTableRequest.ModeEnum mode, String delimiter, String tableLocation, Map tableProperties, byte[] arrowStreamBody); + /** + * Register an existing table. + * + * @param tableId table ids are in the format of "namespace/delimiter/table_name" + * @param mode it can be REGISTER or OVERWRITE. + * @param delimiter the delimiter used in the namespace + * @param tableProperties the properties of the table, it should contain the table location + * @return + */ RegisterTableResponse registerTable( - String tableId, String mode, String delimiter, Map tableProperties); + String tableId, + RegisterTableRequest.ModeEnum mode, + String delimiter, + Map tableProperties); + /** + * Deregister a table. It will not 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 deregister table operation + */ DeregisterTableResponse deregisterTable(String tableId, String delimiter); } diff --git a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java index 865313b31c1..cc346fe60dd 100644 --- a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java +++ b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/gravitino/GravitinoLanceNamespaceWrapper.java @@ -32,8 +32,8 @@ import com.lancedb.lance.namespace.LanceNamespaceException; import com.lancedb.lance.namespace.ObjectIdentifier; import com.lancedb.lance.namespace.model.CreateNamespaceRequest; -import com.lancedb.lance.namespace.model.CreateNamespaceRequest.ModeEnum; import com.lancedb.lance.namespace.model.CreateNamespaceResponse; +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.DescribeNamespaceResponse; @@ -48,7 +48,6 @@ import com.lancedb.lance.namespace.util.CommonUtil; import com.lancedb.lance.namespace.util.JsonArrowSchemaConverter; import com.lancedb.lance.namespace.util.PageUtil; -import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -61,9 +60,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.arrow.memory.BufferAllocator; -import org.apache.arrow.memory.RootAllocator; -import org.apache.arrow.vector.ipc.ArrowStreamReader; import org.apache.arrow.vector.types.pojo.Field; import org.apache.commons.lang3.StringUtils; import org.apache.gravitino.Catalog; @@ -83,6 +79,7 @@ import org.apache.gravitino.lance.common.ops.LanceNamespaceOperations; import org.apache.gravitino.lance.common.ops.LanceTableOperations; import org.apache.gravitino.lance.common.ops.NamespaceWrapper; +import org.apache.gravitino.lance.common.utils.ArrowUtils; import org.apache.gravitino.rel.Column; import org.apache.gravitino.rel.Table; import org.slf4j.Logger; @@ -516,7 +513,8 @@ public ListTablesResponse listTables( } @Override - public DescribeTableResponse describeTable(String tableId, String delimiter) { + public DescribeTableResponse describeTable(String tableId, String delimiter, Long version) { + // TODO Currently we do not support versioned table description. ObjectIdentifier nsId = ObjectIdentifier.of(tableId, Pattern.quote(delimiter)); Preconditions.checkArgument( nsId.levels() == 3, "Expected at 3-level namespace but got: %s", nsId.levels()); @@ -537,7 +535,7 @@ public DescribeTableResponse describeTable(String tableId, String delimiter) { @Override public CreateTableResponse createTable( String tableId, - String mode, + CreateTableRequest.ModeEnum mode, String delimiter, String tableLocation, Map tableProperties, @@ -549,7 +547,8 @@ public CreateTableResponse createTable( // Parser column information. List columns = Lists.newArrayList(); if (arrowStreamBody != null) { - org.apache.arrow.vector.types.pojo.Schema schema = parseArrowIpcStream(arrowStreamBody); + org.apache.arrow.vector.types.pojo.Schema schema = + ArrowUtils.parseArrowIpcStream(arrowStreamBody); columns = extractColumns(schema); } @@ -561,11 +560,10 @@ public CreateTableResponse createTable( Map createTableProperties = Maps.newHashMap(tableProperties); createTableProperties.put("location", tableLocation); - createTableProperties.put("mode", mode); - // TODO considering the mode (create, exist_ok, overwrite) + createTableProperties.put("mode", mode.getValue()); + createTableProperties.put("format", "lance"); - ModeEnum createMode = ModeEnum.fromValue(mode.toLowerCase()); - switch (createMode) { + switch (mode) { case EXIST_OK: if (catalog.asTableCatalog().tableExists(tableIdentifier)) { CreateTableResponse response = new CreateTableResponse(); @@ -598,10 +596,7 @@ public CreateTableResponse createTable( catalog .asTableCatalog() .createTable( - tableIdentifier, - columns.toArray(new Column[0]), - tableLocation, - createTableProperties); + tableIdentifier, columns.toArray(new Column[0]), null, createTableProperties); CreateTableResponse response = new CreateTableResponse(); response.setProperties(t.properties()); @@ -612,7 +607,10 @@ public CreateTableResponse createTable( @Override public RegisterTableResponse registerTable( - String tableId, String mode, String delimiter, Map tableProperties) { + String tableId, + RegisterTableRequest.ModeEnum mode, + String delimiter, + Map tableProperties) { ObjectIdentifier nsId = ObjectIdentifier.of(tableId, Pattern.quote(delimiter)); Preconditions.checkArgument( nsId.levels() == 3, "Expected at 3-level namespace but got: %s", nsId.levels()); @@ -623,9 +621,7 @@ public RegisterTableResponse registerTable( NameIdentifier.of(nsId.levelAtListPos(1), nsId.levelAtListPos(2)); // TODO Support real register API - RegisterTableRequest.ModeEnum createMode = - RegisterTableRequest.ModeEnum.fromValue(mode.toUpperCase()); - if (createMode == RegisterTableRequest.ModeEnum.CREATE + if (mode == RegisterTableRequest.ModeEnum.CREATE && catalog.asTableCatalog().tableExists(tableIdentifier)) { throw LanceNamespaceException.conflict( "Table already exists: " + tableId, @@ -634,14 +630,16 @@ public RegisterTableResponse registerTable( CommonUtil.formatCurrentStackTrace()); } - if (createMode == RegisterTableRequest.ModeEnum.OVERWRITE + if (mode == RegisterTableRequest.ModeEnum.OVERWRITE && catalog.asTableCatalog().tableExists(tableIdentifier)) { LOG.info("Overwriting existing table: {}", tableId); - catalog.asTableCatalog().dropTable(tableIdentifier); + catalog.asTableCatalog().purgeTable(tableIdentifier); } Table t = - catalog.asTableCatalog().createTable(tableIdentifier, new Column[] {}, "", tableProperties); + catalog + .asTableCatalog() + .createTable(tableIdentifier, new Column[] {}, null, tableProperties); RegisterTableResponse response = new RegisterTableResponse(); response.setProperties(t.properties()); @@ -651,7 +649,6 @@ public RegisterTableResponse registerTable( @Override public DeregisterTableResponse deregisterTable(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()); @@ -664,7 +661,14 @@ public DeregisterTableResponse deregisterTable(String tableId, String delimiter) Table t = catalog.asTableCatalog().loadTable(tableIdentifier); Map properties = t.properties(); // TODO Support real deregister API. - catalog.asTableCatalog().purgeTable(tableIdentifier); + boolean result = catalog.asTableCatalog().purgeTable(tableIdentifier); + if (!result) { + throw LanceNamespaceException.notFound( + "Table not found: " + tableId, + NoSuchSchemaException.class.getSimpleName(), + tableId, + CommonUtil.formatCurrentStackTrace()); + } DeregisterTableResponse response = new DeregisterTableResponse(); response.setProperties(properties); @@ -682,24 +686,8 @@ private JsonArrowSchema toJsonArrowSchema(Column[] columns) { new org.apache.arrow.vector.types.pojo.Schema(fields)); } - @VisibleForTesting - org.apache.arrow.vector.types.pojo.Schema parseArrowIpcStream(byte[] stream) { - org.apache.arrow.vector.types.pojo.Schema schema; - try (BufferAllocator allocator = new RootAllocator(); - ByteArrayInputStream bais = new ByteArrayInputStream(stream); - ArrowStreamReader reader = new ArrowStreamReader(bais, allocator)) { - schema = reader.getVectorSchemaRoot().getSchema(); - } catch (Exception e) { - throw new RuntimeException("Failed to parse Arrow IPC stream", e); - } - - Preconditions.checkArgument(schema != null, "No schema found in Arrow IPC stream"); - return schema; - } - private List extractColumns(org.apache.arrow.vector.types.pojo.Schema arrowSchema) { List columns = new ArrayList<>(); - for (org.apache.arrow.vector.types.pojo.Field field : arrowSchema.getFields()) { columns.add( Column.of( diff --git a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/ArrowUtils.java b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/ArrowUtils.java new file mode 100644 index 00000000000..b683259f7c7 --- /dev/null +++ b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/utils/ArrowUtils.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.lance.common.utils; + +import com.google.common.base.Preconditions; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.ipc.ArrowStreamReader; +import org.apache.arrow.vector.ipc.ArrowStreamWriter; +import org.apache.arrow.vector.types.pojo.Schema; + +public class ArrowUtils { + public static byte[] generateIpcStream(Schema arrowSchema) throws IOException { + try (BufferAllocator allocator = new RootAllocator()) { + + // Create an empty VectorSchemaRoot with the schema + try (VectorSchemaRoot root = VectorSchemaRoot.create(arrowSchema, allocator)) { + // Allocate empty vectors (0 rows) + root.allocateNew(); + root.setRowCount(0); + + // Write to IPC stream + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (ArrowStreamWriter writer = + new ArrowStreamWriter(root, null, Channels.newChannel(outputStream))) { + writer.start(); + writer.writeBatch(); + writer.end(); + } + + return outputStream.toByteArray(); + } + } catch (Exception e) { + throw new IOException("Failed to create empty Arrow IPC stream: " + e.getMessage(), e); + } + } + + public static org.apache.arrow.vector.types.pojo.Schema parseArrowIpcStream(byte[] stream) { + org.apache.arrow.vector.types.pojo.Schema schema; + try (BufferAllocator allocator = new RootAllocator(); + ByteArrayInputStream bais = new ByteArrayInputStream(stream); + ArrowStreamReader reader = new ArrowStreamReader(bais, allocator)) { + schema = reader.getVectorSchemaRoot().getSchema(); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse Arrow IPC stream", e); + } + + Preconditions.checkArgument(schema != null, "No schema found in Arrow IPC stream"); + return schema; + } +} diff --git a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNamespaceWrapper.java b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNamespaceWrapper.java index b0ddb980ab0..6b89f6ee1f8 100644 --- a/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNamespaceWrapper.java +++ b/lance/lance-common/src/test/java/org/apache/gravitino/lance/common/ops/gravitino/TestGravitinoLanceNamespaceWrapper.java @@ -18,17 +18,11 @@ */ package org.apache.gravitino.lance.common.ops.gravitino; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.channels.Channels; import java.util.Arrays; -import org.apache.arrow.memory.BufferAllocator; -import org.apache.arrow.memory.RootAllocator; -import org.apache.arrow.vector.VectorSchemaRoot; -import org.apache.arrow.vector.ipc.ArrowStreamWriter; import org.apache.arrow.vector.types.pojo.ArrowType; import org.apache.arrow.vector.types.pojo.Field; import org.apache.arrow.vector.types.pojo.Schema; +import org.apache.gravitino.lance.common.utils.ArrowUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -42,35 +36,9 @@ public void testParseArrowIpcStream() throws Exception { Field.nullable("id", new ArrowType.Int(32, true)), Field.nullable("value", new ArrowType.Utf8()))); - GravitinoLanceNamespaceWrapper wrapper = new GravitinoLanceNamespaceWrapper(); - byte[] ipcStream = generateIpcStream(schema); - Schema parsedSchema = wrapper.parseArrowIpcStream(ipcStream); + byte[] ipcStream = ArrowUtils.generateIpcStream(schema); + Schema parsedSchema = ArrowUtils.parseArrowIpcStream(ipcStream); Assertions.assertEquals(schema, parsedSchema); } - - private byte[] generateIpcStream(Schema arrowSchema) throws IOException { - try (BufferAllocator allocator = new RootAllocator()) { - - // Create an empty VectorSchemaRoot with the schema - try (VectorSchemaRoot root = VectorSchemaRoot.create(arrowSchema, allocator)) { - // Allocate empty vectors (0 rows) - root.allocateNew(); - root.setRowCount(0); - - // Write to IPC stream - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (ArrowStreamWriter writer = - new ArrowStreamWriter(root, null, Channels.newChannel(outputStream))) { - writer.start(); - writer.writeBatch(); - writer.end(); - } - - return outputStream.toByteArray(); - } - } catch (Exception e) { - throw new IOException("Failed to create empty Arrow IPC stream: " + e.getMessage(), e); - } - } } diff --git a/lance/lance-rest-server/build.gradle.kts b/lance/lance-rest-server/build.gradle.kts index 7befc28b35a..665b6bcaa3e 100644 --- a/lance/lance-rest-server/build.gradle.kts +++ b/lance/lance-rest-server/build.gradle.kts @@ -53,7 +53,27 @@ dependencies { implementation(libs.jackson.datatype.jdk8) implementation(libs.jackson.datatype.jsr310) + testImplementation(project(":server")) + testImplementation(project(":clients:client-java")) + testImplementation(project(":integration-test-common", "testArtifacts")) + testImplementation(project(":lance:lance-common", "testArtifacts")) + + testImplementation(libs.awaitility) + testImplementation(libs.commons.io) + testImplementation(libs.lance.namespace.core) + testImplementation(libs.jersey.test.framework.core) { + exclude(group = "org.junit.jupiter") + } + testImplementation(libs.jersey.test.framework.provider.jetty) { + exclude(group = "org.junit.jupiter") + } + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.mockito.core) + testImplementation(libs.mysql.driver) + testImplementation(libs.postgresql.driver) + testImplementation(libs.testcontainers) + testRuntimeOnly(libs.junit.jupiter.engine) } @@ -91,3 +111,13 @@ tasks { dependsOn(copyDepends) } } + +tasks.test { + val skipITs = project.hasProperty("skipITs") + if (skipITs) { + // Exclude integration tests + exclude("**/integration/test/**") + } else { + dependsOn(tasks.jar) + } +} 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 5590eef9bdc..25f24753eeb 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 @@ -24,15 +24,21 @@ import com.codahale.metrics.annotation.ResponseMetered; import com.codahale.metrics.annotation.Timed; -import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; +import com.lancedb.lance.namespace.model.CreateEmptyTableRequest; +import com.lancedb.lance.namespace.model.CreateEmptyTableResponse; +import com.lancedb.lance.namespace.model.CreateTableRequest; import com.lancedb.lance.namespace.model.CreateTableResponse; 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.RegisterTableRequest; +import com.lancedb.lance.namespace.model.RegisterTableRequest.ModeEnum; import com.lancedb.lance.namespace.model.RegisterTableResponse; import com.lancedb.lance.namespace.util.JsonUtil; +import java.util.HashMap; import java.util.Map; import javax.inject.Inject; import javax.ws.rs.Consumes; @@ -70,10 +76,11 @@ public LanceTableOperations(NamespaceWrapper lanceNamespace) { @ResponseMetered(name = "describe-table", absolute = true) public Response describeTable( @PathParam("id") String tableId, - @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") String delimiter) { + @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) @QueryParam("delimiter") String delimiter, + DescribeTableRequest request) { try { DescribeTableResponse response = - lanceNamespace.asTableOps().describeTable(tableId, delimiter); + lanceNamespace.asTableOps().describeTable(tableId, delimiter, request.getVersion()); return Response.ok(response).build(); } catch (Exception e) { return LanceExceptionMapper.toRESTResponse(tableId, e); @@ -97,13 +104,28 @@ public Response createTable( MultivaluedMap headersMap = headers.getRequestHeaders(); String tableLocation = headersMap.getFirst(LANCE_TABLE_LOCATION_HEADER); String tableProperties = headersMap.getFirst(LANCE_TABLE_PROPERTIES_PREFIX_HEADER); + CreateTableRequest.ModeEnum modeEnum = CreateTableRequest.ModeEnum.fromValue(mode); Map props = - JsonUtil.mapper().readValue(tableProperties, new TypeReference<>() {}); + StringUtils.isBlank(tableProperties) + ? ImmutableMap.of() + : JsonUtil.parse( + tableProperties, + jsonNode -> { + Map map = new HashMap<>(); + jsonNode + .fields() + .forEachRemaining( + entry -> { + map.put(entry.getKey(), entry.getValue().asText()); + }); + return map; + }); + CreateTableResponse response = lanceNamespace .asTableOps() - .createTable(tableId, mode, delimiter, tableLocation, props, arrowStreamBody); + .createTable(tableId, modeEnum, delimiter, tableLocation, props, arrowStreamBody); return Response.ok(response).build(); } catch (Exception e) { return LanceExceptionMapper.toRESTResponse(tableId, e); @@ -116,24 +138,30 @@ public Response createTable( @ResponseMetered(name = "create-empty-table", absolute = true) public Response createEmptyTable( @PathParam("id") String tableId, - @QueryParam("mode") @DefaultValue("create") String mode, // create, exist_ok, overwrite @QueryParam("delimiter") @DefaultValue(NAMESPACE_DELIMITER_DEFAULT) String delimiter, + CreateEmptyTableRequest request, @Context HttpHeaders headers) { try { - // Extract table properties from header - MultivaluedMap headersMap = headers.getRequestHeaders(); - String tableLocation = headersMap.getFirst(LANCE_TABLE_LOCATION_HEADER); - String tableProperties = headersMap.getFirst(LANCE_TABLE_PROPERTIES_PREFIX_HEADER); - + String tableLocation = request.getLocation(); Map props = - StringUtils.isBlank(tableProperties) - ? Map.of() - : JsonUtil.mapper().readValue(tableProperties, new TypeReference<>() {}); + request.getProperties() == null + ? Maps.newHashMap() + : Maps.newHashMap(request.getProperties()); + props.put("format", "lance"); CreateTableResponse response = lanceNamespace .asTableOps() - .createTable(tableId, mode, delimiter, tableLocation, props, null); - return Response.ok(response).build(); + .createTable( + tableId, + CreateTableRequest.ModeEnum.CREATE, + delimiter, + tableLocation, + props, + null); + CreateEmptyTableResponse responseObj = new CreateEmptyTableResponse(); + responseObj.setProperties(response.getProperties()); + responseObj.setLocation(response.getLocation()); + return Response.ok(responseObj).build(); } catch (Exception e) { return LanceExceptionMapper.toRESTResponse(tableId, e); } @@ -145,7 +173,6 @@ public Response createEmptyTable( @ResponseMetered(name = "register-table", absolute = true) public Response registerTable( @PathParam("id") String tableId, - @QueryParam("mode") @DefaultValue("create") String mode, // overwrite or @QueryParam("delimiter") @DefaultValue("$") String delimiter, @Context HttpHeaders headers, RegisterTableRequest registerTableRequest) { @@ -157,6 +184,7 @@ public Response registerTable( props.put("register", "true"); props.put("location", registerTableRequest.getLocation()); props.put("format", "lance"); + ModeEnum mode = registerTableRequest.getMode(); RegisterTableResponse response = lanceNamespace.asTableOps().registerTable(tableId, mode, delimiter, props); 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 new file mode 100644 index 00000000000..7e71b3818e7 --- /dev/null +++ b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java @@ -0,0 +1,344 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.lance.integration.test; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.lancedb.lance.namespace.LanceNamespaceException; +import com.lancedb.lance.namespace.client.apache.ApiClient; +import com.lancedb.lance.namespace.client.apache.ApiException; +import com.lancedb.lance.namespace.model.CreateEmptyTableRequest; +import com.lancedb.lance.namespace.model.CreateEmptyTableResponse; +import com.lancedb.lance.namespace.model.CreateTableRequest; +import com.lancedb.lance.namespace.model.CreateTableResponse; +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.JsonArrowField; +import com.lancedb.lance.namespace.model.ListTablesRequest; +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.rest.RestNamespace; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.arrow.vector.types.pojo.Schema; +import org.apache.gravitino.Catalog; +import org.apache.gravitino.client.GravitinoMetalake; +import org.apache.gravitino.integration.test.util.BaseIT; +import org.apache.gravitino.integration.test.util.GravitinoITUtils; +import org.apache.gravitino.lance.common.utils.ArrowUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.shaded.com.google.common.base.Joiner; + +public class LanceRESTServiceIT extends BaseIT { + private static final Logger LOG = LoggerFactory.getLogger(LanceRESTServiceIT.class); + + public static final String METALAKE_NAME = GravitinoITUtils.genRandomName("lance_reset_metalake"); + public static final String CATALOG_NAME = GravitinoITUtils.genRandomName("lance_rest_catalog"); + public static final String SCHEMA_NAME = GravitinoITUtils.genRandomName("lance_rest_schema"); + private static final String DEFAULT_LANCE_REST_URL = "http://localhost:9101/lance"; + + protected GravitinoMetalake metalake; + protected Catalog catalog; + private String tempDirectory; + private RestNamespace restNameSpace; + + @BeforeAll + public void setup() throws Exception { + startIntegrationTest(); + + createMetalake(); + createCatalog(); + createSchema(); + + // Create a temp directory for test use + Path tempDir = Files.createTempDirectory("myTempDir"); + tempDirectory = tempDir.toString(); + File file = new File(tempDirectory); + file.deleteOnExit(); + + ApiClient apiClient = new ApiClient(); + String uri = DEFAULT_LANCE_REST_URL; + if (serverConfig.getAllConfig().containsKey("gravitino.lance-rest.httpPort")) { + int port = Integer.parseInt(serverConfig.getAllConfig().get("gravitino.lance-rest.httpPort")); + uri = "http://localhost:" + port + "/lance"; + LOG.info("Lance REST HTTP Port: {}", port); + } + apiClient.setBasePath(uri); + + restNameSpace = new RestNamespace(); + Map configs = ImmutableMap.of("delimiter", ".", "uri", uri); + + restNameSpace.initialize(configs, new RootAllocator()); + } + + public void startIntegrationTest() throws Exception { + this.ignoreAuxRestService = false; + customConfigs.put("gravitino.lance-rest.gravitino.metalake-name", METALAKE_NAME); + super.startIntegrationTest(); + } + + private void createMetalake() { + client.createMetalake(METALAKE_NAME, "comment", Collections.emptyMap()); + GravitinoMetalake loadMetalake = client.loadMetalake(METALAKE_NAME); + Assertions.assertEquals(METALAKE_NAME, loadMetalake.name()); + metalake = loadMetalake; + } + + private void createCatalog() { + Map properties = Maps.newHashMap(); + metalake.createCatalog( + CATALOG_NAME, Catalog.Type.RELATIONAL, "generic-lakehouse", "comment", properties); + catalog = metalake.loadCatalog(CATALOG_NAME); + } + + protected void createSchema() { + Map schemaProperties = Maps.newHashMap(); + String comment = "comment"; + catalog.asSchemas().createSchema(SCHEMA_NAME, comment, schemaProperties); + catalog.asSchemas().loadSchema(SCHEMA_NAME); + } + + @Test + void testCreateEmptyTable() throws ApiException { + CreateEmptyTableRequest request = new CreateEmptyTableRequest(); + String location = tempDirectory + "/" + "empty_table/"; + request.setLocation(location); + request.setProperties(ImmutableMap.of()); + request.setId(List.of(CATALOG_NAME, SCHEMA_NAME, "empty_table")); + + CreateEmptyTableResponse response = restNameSpace.createEmptyTable(request); + Assertions.assertNotNull(response); + + DescribeTableRequest describeTableRequest = new DescribeTableRequest(); + describeTableRequest.setId(List.of(CATALOG_NAME, SCHEMA_NAME, "empty_table")); + + DescribeTableResponse loadTable = restNameSpace.describeTable(describeTableRequest); + Assertions.assertNotNull(loadTable); + Assertions.assertEquals(location, loadTable.getLocation()); + + // Try to create the same table again should fail + LanceNamespaceException exception = + Assertions.assertThrows( + LanceNamespaceException.class, + () -> { + restNameSpace.createEmptyTable(request); + }); + Assertions.assertTrue(exception.getMessage().contains("Table already exists")); + Assertions.assertEquals(409, exception.getCode()); + } + + @Test + void testCreateTable() throws IOException, ApiException { + String location = tempDirectory + "/" + "table/"; + List ids = List.of(CATALOG_NAME, SCHEMA_NAME, "table"); + // TODO add more types here to verify it's okay. + Schema schema = + new Schema( + Arrays.asList( + Field.nullable("id", new ArrowType.Int(32, true)), + Field.nullable("value", new ArrowType.Utf8()))); + byte[] body = ArrowUtils.generateIpcStream(schema); + + CreateTableRequest request = new CreateTableRequest(); + request.setId(ids); + request.setLocation(location); + CreateTableResponse response = restNameSpace.createTable(request, body); + Assertions.assertNotNull(response); + Assertions.assertEquals(location, response.getLocation()); + + DescribeTableRequest describeTableRequest = new DescribeTableRequest(); + describeTableRequest.setId(ids); + DescribeTableResponse loadTable = restNameSpace.describeTable(describeTableRequest); + Assertions.assertNotNull(loadTable); + Assertions.assertEquals(location, loadTable.getLocation()); + + List jsonArrowFields = loadTable.getSchema().getFields(); + for (int i = 0; i < jsonArrowFields.size(); i++) { + JsonArrowField jsonArrowField = jsonArrowFields.get(i); + Field originalField = schema.getFields().get(i); + Assertions.assertEquals(originalField.getName(), jsonArrowField.getName()); + + if (i == 0) { + Assertions.assertEquals("int32", jsonArrowField.getType().getType()); + } else if (i == 1) { + Assertions.assertEquals("utf8", jsonArrowField.getType().getType()); + } + } + // Check the location exists + Assertions.assertTrue(new File(location).exists()); + + // Check overwrite mode + String newLocation = tempDirectory + "/" + "table_new/"; + request.setLocation(newLocation); + request.setMode(CreateTableRequest.ModeEnum.OVERWRITE); + + response = Assertions.assertDoesNotThrow(() -> restNameSpace.createTable(request, body)); + + Assertions.assertNotNull(response); + Assertions.assertEquals(newLocation, response.getLocation()); + Assertions.assertTrue(new File(newLocation).exists()); + Assertions.assertFalse(new File(location).exists()); + + // Check exist_ok mode + request.setMode(CreateTableRequest.ModeEnum.EXIST_OK); + response = Assertions.assertDoesNotThrow(() -> restNameSpace.createTable(request, body)); + + Assertions.assertNotNull(response); + Assertions.assertEquals(newLocation, response.getLocation()); + Assertions.assertTrue(new File(newLocation).exists()); + + // Create table again without overwrite or exist_ok should fail + request.setMode(CreateTableRequest.ModeEnum.CREATE); + LanceNamespaceException exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> restNameSpace.createTable(request, body)); + Assertions.assertTrue(exception.getMessage().contains("already exists")); + Assertions.assertEquals(409, exception.getCode()); + + // Create table with invalid schema should fail + byte[] invalidBody = "invalid schema".getBytes(Charset.defaultCharset()); + CreateTableRequest invalidRequest = new CreateTableRequest(); + invalidRequest.setId(List.of(CATALOG_NAME, SCHEMA_NAME, "invalid_table")); + invalidRequest.setLocation(tempDirectory + "/" + "invalid_table/"); + LanceNamespaceException apiException = + Assertions.assertThrows( + LanceNamespaceException.class, + () -> restNameSpace.createTable(invalidRequest, invalidBody)); + Assertions.assertTrue(apiException.getMessage().contains("Failed to parse Arrow IPC stream")); + Assertions.assertEquals(400, apiException.getCode()); + + // Create table with wrong ids should fail + CreateTableRequest wrongIdRequest = new CreateTableRequest(); + wrongIdRequest.setId(List.of(CATALOG_NAME, "wrong_schema")); // This is a schema NOT a talbe. + wrongIdRequest.setLocation(tempDirectory + "/" + "wrong_id_table/"); + LanceNamespaceException wrongIdException = + Assertions.assertThrows( + LanceNamespaceException.class, () -> restNameSpace.createTable(wrongIdRequest, body)); + Assertions.assertTrue(wrongIdException.getMessage().contains("Expected at 3-level namespace")); + Assertions.assertEquals(400, wrongIdException.getCode()); + + // Now test list tables + ListTablesRequest listRequest = new ListTablesRequest(); + listRequest.setId(List.of(CATALOG_NAME, SCHEMA_NAME)); + var listResponse = restNameSpace.listTables(listRequest); + Set stringSet = listResponse.getTables(); + Assertions.assertEquals(1, stringSet.size()); + Assertions.assertTrue(stringSet.contains(Joiner.on(".").join(ids))); + } + + @Test + void testRegisterTable() { + String location = tempDirectory + "/" + "register/"; + List ids = List.of(CATALOG_NAME, SCHEMA_NAME, "table_register"); + RegisterTableRequest registerTableRequest = new RegisterTableRequest(); + registerTableRequest.setLocation(location); + registerTableRequest.setMode(ModeEnum.CREATE); + registerTableRequest.setId(ids); + registerTableRequest.setProperties(ImmutableMap.of("key1", "value1")); + + RegisterTableResponse response = restNameSpace.registerTable(registerTableRequest); + Assertions.assertNotNull(response); + + DescribeTableRequest describeTableRequest = new DescribeTableRequest(); + describeTableRequest.setId(ids); + DescribeTableResponse loadTable = restNameSpace.describeTable(describeTableRequest); + Assertions.assertNotNull(loadTable); + Assertions.assertEquals(location, loadTable.getLocation()); + Assertions.assertTrue(loadTable.getProperties().containsKey("key1")); + + // Test register again with OVERWRITE mode + String newLocation = tempDirectory + "/" + "register_new/"; + registerTableRequest.setMode(ModeEnum.OVERWRITE); + registerTableRequest.setLocation(newLocation); + response = + Assertions.assertDoesNotThrow(() -> restNameSpace.registerTable(registerTableRequest)); + Assertions.assertNotNull(response); + Assertions.assertEquals(newLocation, response.getLocation()); + + // Test deregister table + DeregisterTableRequest deregisterTableRequest = new DeregisterTableRequest(); + deregisterTableRequest.setId(ids); + DeregisterTableResponse deregisterTableResponse = + restNameSpace.deregisterTable(deregisterTableRequest); + Assertions.assertNotNull(deregisterTableResponse); + Assertions.assertEquals(newLocation, deregisterTableResponse.getLocation()); + } + + @Test + void testDeregisterNonExistingTable() { + List ids = List.of(CATALOG_NAME, SCHEMA_NAME, "non_existing_table"); + DeregisterTableRequest deregisterTableRequest = new DeregisterTableRequest(); + deregisterTableRequest.setId(ids); + + LanceNamespaceException exception = + Assertions.assertThrows( + LanceNamespaceException.class, + () -> restNameSpace.deregisterTable(deregisterTableRequest)); + Assertions.assertEquals(404, exception.getCode()); + Assertions.assertTrue(exception.getMessage().contains("does not exist")); + + // Try to create a table and then deregister table + CreateEmptyTableRequest createEmptyTableRequest = new CreateEmptyTableRequest(); + String location = tempDirectory + "/" + "to_be_deregistered_table/"; + ids = List.of(CATALOG_NAME, SCHEMA_NAME, "to_be_deregistered_table"); + createEmptyTableRequest.setLocation(location); + createEmptyTableRequest.setProperties(ImmutableMap.of()); + createEmptyTableRequest.setId(ids); + CreateEmptyTableResponse response = + Assertions.assertDoesNotThrow( + () -> restNameSpace.createEmptyTable(createEmptyTableRequest)); + Assertions.assertNotNull(response); + Assertions.assertEquals(location, response.getLocation()); + + // Now try to deregister + deregisterTableRequest.setId(ids); + DeregisterTableResponse deregisterTableResponse = + Assertions.assertDoesNotThrow(() -> restNameSpace.deregisterTable(deregisterTableRequest)); + Assertions.assertNotNull(deregisterTableResponse); + Assertions.assertEquals(location, deregisterTableResponse.getLocation()); + Assertions.assertTrue( + new File(location).exists(), "Data should still exist after deregistering the table."); + + // Now try to describe the table, should fail + DescribeTableRequest describeTableRequest = new DescribeTableRequest(); + describeTableRequest.setId(ids); + LanceNamespaceException lanceNamespaceException = + Assertions.assertThrows( + LanceNamespaceException.class, () -> restNameSpace.describeTable(describeTableRequest)); + Assertions.assertEquals(404, lanceNamespaceException.getCode()); + } +} diff --git a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/test/ServletRequestFactoryBase.java b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/test/ServletRequestFactoryBase.java new file mode 100644 index 00000000000..6fa1c9da99d --- /dev/null +++ b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/test/ServletRequestFactoryBase.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.lance.service.test; + +import java.util.function.Supplier; +import javax.servlet.http.HttpServletRequest; +import org.glassfish.hk2.api.Factory; + +abstract class ServletRequestFactoryBase + implements Factory, Supplier { + + @Override + public HttpServletRequest provide() { + return get(); + } + + @Override + public void dispose(HttpServletRequest instance) {} +} diff --git a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/test/TestLanceNamespaceOperations.java b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/test/TestLanceNamespaceOperations.java new file mode 100644 index 00000000000..3996a159ef0 --- /dev/null +++ b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/test/TestLanceNamespaceOperations.java @@ -0,0 +1,630 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.lance.service.test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +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.Sets; +import com.lancedb.lance.namespace.LanceNamespaceException; +import com.lancedb.lance.namespace.model.CreateEmptyTableRequest; +import com.lancedb.lance.namespace.model.CreateEmptyTableResponse; +import com.lancedb.lance.namespace.model.CreateNamespaceRequest; +import com.lancedb.lance.namespace.model.CreateNamespaceResponse; +import com.lancedb.lance.namespace.model.CreateTableResponse; +import com.lancedb.lance.namespace.model.DeregisterTableRequest; +import com.lancedb.lance.namespace.model.DeregisterTableResponse; +import com.lancedb.lance.namespace.model.DescribeNamespaceResponse; +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.ErrorResponse; +import com.lancedb.lance.namespace.model.ListNamespacesResponse; +import com.lancedb.lance.namespace.model.RegisterTableRequest; +import com.lancedb.lance.namespace.model.RegisterTableResponse; +import java.io.IOException; +import java.util.regex.Pattern; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.apache.gravitino.exceptions.NoSuchCatalogException; +import org.apache.gravitino.lance.common.ops.LanceTableOperations; +import org.apache.gravitino.lance.common.ops.NamespaceWrapper; +import org.apache.gravitino.lance.service.rest.LanceNamespaceOperations; +import org.apache.gravitino.rest.RESTUtils; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class TestLanceNamespaceOperations extends JerseyTest { + private static class MockServletRequestFactory extends ServletRequestFactoryBase { + @Override + public HttpServletRequest get() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRemoteUser()).thenReturn(null); + return request; + } + } + + private static NamespaceWrapper namespaceWrapper = mock(NamespaceWrapper.class); + private static org.apache.gravitino.lance.common.ops.LanceNamespaceOperations namespaceOps = + mock(org.apache.gravitino.lance.common.ops.LanceNamespaceOperations.class); + private static LanceTableOperations tableOps = + mock(org.apache.gravitino.lance.common.ops.LanceTableOperations.class); + + @Override + protected Application configure() { + try { + forceSet( + TestProperties.CONTAINER_PORT, String.valueOf(RESTUtils.findAvailablePort(2000, 3000))); + } catch (IOException e) { + throw new RuntimeException(e); + } + + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig.register(LanceNamespaceOperations.class); + resourceConfig.register(org.apache.gravitino.lance.service.rest.LanceTableOperations.class); + resourceConfig.register( + new AbstractBinder() { + @Override + protected void configure() { + bind(namespaceWrapper).to(NamespaceWrapper.class).ranked(2); + bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class); + } + }); + + return resourceConfig; + } + + @BeforeAll + public static void setup() { + when(namespaceWrapper.asNamespaceOps()).thenReturn(namespaceOps); + when(namespaceWrapper.asTableOps()).thenReturn(tableOps); + } + + @Test + public void testListNamespaces() { + String namespaceId = "ns1.ns2"; + String delimiter = "."; + ListNamespacesResponse listNamespacesResp = new ListNamespacesResponse(); + listNamespacesResp.setNamespaces(Sets.newHashSet(namespaceId.split(delimiter))); + + when(namespaceOps.listNamespaces(any(), any(), any(), any())).thenReturn(listNamespacesResp); + + Response resp = + target("/v1/namespace/ns1.ns2/list") + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .get(); + + Mockito.verify(namespaceOps) + .listNamespaces(eq(namespaceId), eq(Pattern.quote(delimiter)), any(), any()); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + ListNamespacesResponse respEntity = resp.readEntity(ListNamespacesResponse.class); + Assertions.assertEquals(listNamespacesResp.getNamespaces(), respEntity.getNamespaces()); + Assertions.assertEquals(listNamespacesResp.getPageToken(), respEntity.getPageToken()); + + // list namespaces under root + resp = + target("/v1/namespace/./list") + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .get(); + + Mockito.verify(namespaceOps) + .listNamespaces(eq("."), eq(Pattern.quote(delimiter)), any(), any()); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + respEntity = resp.readEntity(ListNamespacesResponse.class); + Assertions.assertEquals(listNamespacesResp.getNamespaces(), respEntity.getNamespaces()); + Assertions.assertEquals(listNamespacesResp.getPageToken(), respEntity.getPageToken()); + + // test throw exception + when(namespaceOps.listNamespaces(any(), any(), any(), any())) + .thenThrow(new RuntimeException("Test exception")); + resp = + target("/v1/namespace/ns1.ns2/list") + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .get(); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + ErrorResponse errorResp = resp.readEntity(ErrorResponse.class); + Assertions.assertEquals(500, errorResp.getCode()); + Assertions.assertEquals("Test exception", errorResp.getError()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp.getType()); + Assertions.assertEquals("ns1.ns2", errorResp.getInstance()); + Assertions.assertNotNull(errorResp.getDetail()); + Assertions.assertTrue(errorResp.getDetail().contains("Test exception")); + } + + @Test + public void testDescribeNamespace() { + String namespaceId = "ns1.ns2"; + String delimiter = "."; + DescribeNamespaceResponse describeNamespaceResp = new DescribeNamespaceResponse(); + describeNamespaceResp.setProperties(ImmutableMap.of("key", "value")); + + when(namespaceOps.describeNamespace(any(), any())).thenReturn(describeNamespaceResp); + + Response resp = + target("/v1/namespace/ns1.ns2/describe") + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(null); + + Mockito.verify(namespaceOps).describeNamespace(eq(namespaceId), eq(Pattern.quote(delimiter))); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + DescribeNamespaceResponse respEntity = resp.readEntity(DescribeNamespaceResponse.class); + Assertions.assertEquals(describeNamespaceResp.getProperties(), respEntity.getProperties()); + + // test throw exception + when(namespaceOps.describeNamespace(any(), any())) + .thenThrow(new RuntimeException("Test exception")); + resp = + target("/v1/namespace/ns1.ns2/describe") + .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()); + + ErrorResponse errorResp = resp.readEntity(ErrorResponse.class); + Assertions.assertEquals(500, errorResp.getCode()); + Assertions.assertEquals("Test exception", errorResp.getError()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp.getType()); + } + + @Test + public void testCreateNamespace() { + String namespaceId = "ns1.ns2"; + String delimiter = "."; + CreateNamespaceRequest createNamespaceReq = new CreateNamespaceRequest(); + createNamespaceReq.setProperties(ImmutableMap.of("key", "value")); + + CreateNamespaceResponse createNamespaceResp = new CreateNamespaceResponse(); + createNamespaceResp.setProperties(ImmutableMap.of("key", "value")); + + when(namespaceOps.createNamespace(any(), any(), any(), any())).thenReturn(createNamespaceResp); + + Response resp = + target("/v1/namespace/ns1.ns2/create") + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(createNamespaceReq, MediaType.APPLICATION_JSON_TYPE)); + + Mockito.verify(namespaceOps) + .createNamespace( + eq(namespaceId), + eq(Pattern.quote(delimiter)), + eq(CreateNamespaceRequest.ModeEnum.CREATE), + eq(createNamespaceReq.getProperties())); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + CreateNamespaceResponse respEntity = resp.readEntity(CreateNamespaceResponse.class); + Assertions.assertEquals(createNamespaceResp.getProperties(), respEntity.getProperties()); + + // test throw exception + when(namespaceOps.createNamespace(any(), any(), any(), any())) + .thenThrow(new RuntimeException("Test exception")); + resp = + target("/v1/namespace/ns1.ns2/create") + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(createNamespaceReq, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + ErrorResponse errorResp = resp.readEntity(ErrorResponse.class); + Assertions.assertEquals(500, errorResp.getCode()); + Assertions.assertEquals("Test exception", errorResp.getError()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp.getType()); + } + + @Test + public void testNamespaceExists() { + String namespaceId = "ns1.ns2"; + String delimiter = "."; + + doNothing().when(namespaceOps).namespaceExists(any(), any()); + + Response resp = + target("/v1/namespace/ns1.ns2/exists") + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(null); + + Mockito.verify(namespaceOps).namespaceExists(eq(namespaceId), eq(Pattern.quote(delimiter))); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + + // test throw exception + doThrow(new NoSuchCatalogException("Not found")) + .when(namespaceOps) + .namespaceExists(any(), any()); + resp = + target("/v1/namespace/ns1.ns2/exists") + .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("Not found", errorResp.getError()); + Assertions.assertEquals(NoSuchCatalogException.class.getSimpleName(), errorResp.getType()); + } + + @Test + public void testDropNamespace() { + String namespaceId = "ns1.ns2"; + String delimiter = "."; + DropNamespaceRequest dropNamespaceReq = new DropNamespaceRequest(); + + DropNamespaceResponse dropNamespaceResp = new DropNamespaceResponse(); + when(namespaceOps.dropNamespace(any(), any(), any(), any())).thenReturn(dropNamespaceResp); + + Response resp = + target("/v1/namespace/ns1.ns2/drop") + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(dropNamespaceReq, MediaType.APPLICATION_JSON_TYPE)); + + Mockito.verify(namespaceOps) + .dropNamespace( + eq(namespaceId), + eq(Pattern.quote(delimiter)), + eq(DropNamespaceRequest.ModeEnum.FAIL), + eq(DropNamespaceRequest.BehaviorEnum.RESTRICT)); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + // test throw exception + when(namespaceOps.dropNamespace(any(), any(), any(), any())) + .thenThrow(new RuntimeException("Test exception")); + resp = + target("/v1/namespace/ns1.ns2/drop") + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(dropNamespaceReq, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + ErrorResponse errorResp = resp.readEntity(ErrorResponse.class); + Assertions.assertEquals(500, errorResp.getCode()); + Assertions.assertEquals("Test exception", errorResp.getError()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp.getType()); + } + + @Test + void testCreateTable() { + String tableIds = "catalog.scheme.create_table"; + String delimiter = "."; + + // Test normal + CreateTableResponse createTableResponse = new CreateTableResponse(); + when(tableOps.createTable(any(), any(), any(), any(), any(), any())) + .thenReturn(createTableResponse); + + byte[] bytes = new byte[] {0x01, 0x02, 0x03}; + Response resp = + target(String.format("/v1/table/%s/create", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(bytes, "application/vnd.apache.arrow.stream")); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + // Test illegal argument + when(tableOps.createTable(any(), any(), any(), any(), any(), any())) + .thenThrow(new IllegalArgumentException("Illegal argument")); + + resp = + target(String.format("/v1/table/%s/create", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(bytes, "application/vnd.apache.arrow.stream")); + Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + // Test runtime exception + Mockito.reset(tableOps); + when(tableOps.createTable(any(), any(), any(), any(), any(), any())) + .thenThrow(new RuntimeException("Runtime exception")); + resp = + target(String.format("/v1/table/%s/create", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(bytes, "application/vnd.apache.arrow.stream")); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + ErrorResponse errorResp = resp.readEntity(ErrorResponse.class); + Assertions.assertEquals("Runtime exception", errorResp.getError()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp.getType()); + } + + @Test + void testCreateEmptyTable() { + String tableIds = "catalog.scheme.create_empty_table"; + String delimiter = "."; + + // Test normal + CreateTableResponse createTableResponse = new CreateTableResponse(); + createTableResponse.setLocation("/path/to/table"); + createTableResponse.setProperties(ImmutableMap.of("key", "value")); + when(tableOps.createTable(any(), any(), any(), any(), any(), any())) + .thenReturn(createTableResponse); + + CreateEmptyTableRequest tableRequest = new CreateEmptyTableRequest(); + tableRequest.setLocation("/path/to/table"); + + Response resp = + target(String.format("/v1/table/%s/create-empty", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + CreateEmptyTableResponse response = resp.readEntity(CreateEmptyTableResponse.class); + Assertions.assertEquals(createTableResponse.getLocation(), response.getLocation()); + Assertions.assertEquals(createTableResponse.getProperties(), response.getProperties()); + + Mockito.reset(tableOps); + // Test illegal argument + when(tableOps.createTable(any(), any(), any(), any(), any(), any())) + .thenThrow(new IllegalArgumentException("Illegal argument")); + + resp = + target(String.format("/v1/table/%s/create-empty", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + // Test runtime exception + Mockito.reset(tableOps); + when(tableOps.createTable(any(), any(), any(), any(), any(), any())) + .thenThrow(new RuntimeException("Runtime exception")); + resp = + target(String.format("/v1/table/%s/create-empty", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + ErrorResponse errorResp = resp.readEntity(ErrorResponse.class); + Assertions.assertEquals("Runtime exception", errorResp.getError()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp.getType()); + } + + @Test + void testRegisterTable() { + String tableIds = "catalog.scheme.register_table"; + String delimiter = "."; + + // Test normal + RegisterTableResponse registerTableResponse = new RegisterTableResponse(); + registerTableResponse.setLocation("/path/to/registered_table"); + registerTableResponse.setProperties(ImmutableMap.of("key", "value")); + when(tableOps.registerTable(any(), any(), any(), any())).thenReturn(registerTableResponse); + + RegisterTableRequest tableRequest = new RegisterTableRequest(); + tableRequest.setLocation("/path/to/registered_table"); + tableRequest.setMode(RegisterTableRequest.ModeEnum.CREATE); + + Response resp = + target(String.format("/v1/table/%s/register", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + RegisterTableResponse response = resp.readEntity(RegisterTableResponse.class); + Assertions.assertEquals(registerTableResponse.getLocation(), response.getLocation()); + Assertions.assertEquals(registerTableResponse.getProperties(), response.getProperties()); + + // Test illegal argument + Mockito.reset(tableOps); + when(tableOps.registerTable(any(), any(), any(), any())) + .thenThrow(new IllegalArgumentException("Illegal argument")); + resp = + target(String.format("/v1/table/%s/register", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + // Test runtime exception + Mockito.reset(tableOps); + when(tableOps.registerTable(any(), any(), any(), any())) + .thenThrow(new RuntimeException("Runtime exception")); + resp = + target(String.format("/v1/table/%s/register", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + ErrorResponse errorResp = resp.readEntity(ErrorResponse.class); + Assertions.assertEquals("Runtime exception", errorResp.getError()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp.getType()); + } + + @Test + void testDeregisterTable() { + String tableIds = "catalog.scheme.deregister_table"; + String delimiter = "."; + + DeregisterTableRequest tableRequest = new DeregisterTableRequest(); + + DeregisterTableResponse deregisterTableResponse = new DeregisterTableResponse(); + deregisterTableResponse.setLocation("/path/to/deregistered_table"); + deregisterTableResponse.setProperties(ImmutableMap.of("key", "value")); + // Test normal + when(tableOps.deregisterTable(any(), any())).thenReturn(deregisterTableResponse); + + Response resp = + target(String.format("/v1/table/%s/deregister", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + DeregisterTableResponse response = resp.readEntity(DeregisterTableResponse.class); + Assertions.assertEquals(deregisterTableResponse.getLocation(), response.getLocation()); + Assertions.assertEquals(deregisterTableResponse.getProperties(), response.getProperties()); + + // Test illegal argument + Mockito.reset(tableOps); + when(tableOps.deregisterTable(any(), any())) + .thenThrow(new IllegalArgumentException("Illegal argument")); + resp = + target(String.format("/v1/table/%s/deregister", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + // Test not found exception + Mockito.reset(tableOps); + when(tableOps.deregisterTable(any(), any())) + .thenThrow( + LanceNamespaceException.notFound( + "Table not found", "NoSuchTableException", tableIds, "")); + resp = + target(String.format("/v1/table/%s/deregister", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resp.getStatus()); + + // Test runtime exception + Mockito.reset(tableOps); + when(tableOps.deregisterTable(any(), any())) + .thenThrow(new RuntimeException("Runtime exception")); + resp = + target(String.format("/v1/table/%s/deregister", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + ErrorResponse errorResp = resp.readEntity(ErrorResponse.class); + Assertions.assertEquals("Runtime exception", errorResp.getError()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp.getType()); + } + + @Test + void testDescribeTable() { + String tableIds = "catalog.scheme.describe_table"; + String delimiter = "."; + + // Test normal + DescribeTableResponse createTableResponse = new DescribeTableResponse(); + createTableResponse.setLocation("/path/to/describe_table"); + createTableResponse.setProperties(ImmutableMap.of("key", "value")); + when(tableOps.describeTable(any(), any(), any())).thenReturn(createTableResponse); + + DescribeTableRequest tableRequest = new DescribeTableRequest(); + Response resp = + target(String.format("/v1/table/%s/describe", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + DescribeTableResponse response = resp.readEntity(DescribeTableResponse.class); + Assertions.assertEquals(createTableResponse.getLocation(), response.getLocation()); + Assertions.assertEquals(createTableResponse.getProperties(), response.getProperties()); + + // Test not found exception + Mockito.reset(tableOps); + when(tableOps.describeTable(any(), any(), any())) + .thenThrow( + LanceNamespaceException.notFound( + "Table not found", "NoSuchTableException", tableIds, "")); + resp = + target(String.format("/v1/table/%s/describe", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resp.getStatus()); + + // Test runtime exception + Mockito.reset(tableOps); + when(tableOps.describeTable(any(), any(), any())) + .thenThrow(new RuntimeException("Runtime exception")); + resp = + target(String.format("/v1/table/%s/describe", tableIds)) + .queryParam("delimiter", delimiter) + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(tableRequest, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + ErrorResponse errorResp = resp.readEntity(ErrorResponse.class); + Assertions.assertEquals("Runtime exception", errorResp.getError()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResp.getType()); + } +} diff --git a/lance/lance-rest-server/src/test/resources/log4j2.properties b/lance/lance-rest-server/src/test/resources/log4j2.properties new file mode 100644 index 00000000000..e6c8916fb20 --- /dev/null +++ b/lance/lance-rest-server/src/test/resources/log4j2.properties @@ -0,0 +1,73 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +# Set to debug or trace if log4j initialization is failing +status = info + +# Name of the configuration +name = ConsoleLogConfig + +# Console appender configuration +appender.console.type = Console +appender.console.name = consoleLogger +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p [%t] %c{1}:%L - %m%n + +# Log files location +property.logPath = ${sys:gravitino.log.path:-build/catalog-hive-integration-test.log} + +# File appender configuration +appender.file.type = File +appender.file.name = fileLogger +appender.file.fileName = ${logPath} +appender.file.layout.type = PatternLayout +appender.file.layout.pattern = %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %c - %m%n + +# Root logger level +rootLogger.level = info + +# Root logger referring to console and file appenders +rootLogger.appenderRef.stdout.ref = consoleLogger +rootLogger.appenderRef.file.ref = fileLogger + +# File appender configuration for testcontainers +appender.testcontainersFile.type = File +appender.testcontainersFile.name = testcontainersLogger +appender.testcontainersFile.fileName = build/testcontainers.log +appender.testcontainersFile.layout.type = PatternLayout +appender.testcontainersFile.layout.pattern = %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %c - %m%n + +# Logger for testcontainers +logger.testcontainers.name = org.testcontainers +logger.testcontainers.level = debug +logger.testcontainers.additivity = false +logger.testcontainers.appenderRef.file.ref = testcontainersLogger + +logger.tc.name = tc +logger.tc.level = debug +logger.tc.additivity = false +logger.tc.appenderRef.file.ref = testcontainersLogger + +logger.docker.name = com.github.dockerjava +logger.docker.level = warn +logger.docker.additivity = false +logger.docker.appenderRef.file.ref = testcontainersLogger + +logger.http.name = com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire +logger.http.level = off