diff --git a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergBaseIT.java b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergBaseIT.java index b0d4f3b5eb2..82aedaff7df 100644 --- a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergBaseIT.java +++ b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergBaseIT.java @@ -124,7 +124,7 @@ public abstract class CatalogIcebergBaseIT extends BaseIT { @BeforeAll public void startup() throws Exception { - super.ignoreAuxRestService = false; + super.ignoreIcebergAuxRestService = false; super.startIntegrationTest(); containerSuite.startHiveContainer(); initIcebergCatalogProperties(); diff --git a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergKerberosHiveIT.java b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergKerberosHiveIT.java index cce787ec850..5591a4f26fc 100644 --- a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergKerberosHiveIT.java +++ b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/integration/test/CatalogIcebergKerberosHiveIT.java @@ -127,7 +127,7 @@ public void startIntegrationTest() { // Config kerberos configuration for Gravitino server addKerberosConfig(); - super.ignoreAuxRestService = false; + super.ignoreIcebergAuxRestService = false; // Start Gravitino server super.startIntegrationTest(); } catch (Exception e) { diff --git a/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/integration/test/FlinkEnvIT.java b/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/integration/test/FlinkEnvIT.java index ca79144f5b6..db04715a4ce 100644 --- a/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/integration/test/FlinkEnvIT.java +++ b/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/integration/test/FlinkEnvIT.java @@ -112,7 +112,7 @@ protected String flinkByPass(String key) { protected abstract String getProvider(); private void initIcebergRestServiceEnv() { - super.ignoreAuxRestService = false; + super.ignoreIcebergAuxRestService = false; Map icebergRestServiceConfigs = new HashMap<>(); icebergRestServiceConfigs.put( "gravitino." diff --git a/integration-test-common/build.gradle.kts b/integration-test-common/build.gradle.kts index bd15dc2a34f..8aac2cafad3 100644 --- a/integration-test-common/build.gradle.kts +++ b/integration-test-common/build.gradle.kts @@ -30,6 +30,9 @@ dependencies { testImplementation(project(":clients:client-java")) testImplementation(project(":common")) testImplementation(project(":core")) + testImplementation(project(":lance:lance-common")) { + exclude("*") + } testImplementation(project(":server")) testImplementation(project(":server-common")) testImplementation(libs.bundles.jetty) 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..eb26fb37e4a 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 @@ -19,6 +19,10 @@ package org.apache.gravitino.integration.test; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; +import static org.apache.gravitino.lance.common.config.LanceConfig.GRAVITINO_NAMESPACE_BACKEND; +import static org.apache.gravitino.lance.common.config.LanceConfig.LANCE_CONFIG_PREFIX; +import static org.apache.gravitino.lance.common.config.LanceConfig.NAMESPACE_BACKEND; +import static org.apache.gravitino.lance.common.config.LanceConfig.NAMESPACE_BACKEND_URI; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; @@ -27,6 +31,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,6 +55,7 @@ import org.apache.gravitino.server.GravitinoServer; import org.apache.gravitino.server.ServerConfig; import org.apache.gravitino.server.web.JettyServerConfig; +import org.junit.platform.commons.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,11 +84,32 @@ public MiniGravitino(MiniGravitinoContext context) throws IOException { mockConfDir.mkdirs(); } - private void removeAuxRestConfiguration(Properties properties) { - // Disable Iceberg REST service - properties.remove( - AuxiliaryServiceManager.GRAVITINO_AUX_SERVICE_PREFIX - + AuxiliaryServiceManager.AUX_SERVICE_NAMES); + private void removeAuxRestConfiguration(Properties properties, String serviceToRemove) { + String value = + properties.getProperty( + AuxiliaryServiceManager.GRAVITINO_AUX_SERVICE_PREFIX + + AuxiliaryServiceManager.AUX_SERVICE_NAMES); + if (StringUtils.isNotBlank(value) && StringUtils.isNotBlank(serviceToRemove)) { + List serviceNames = COMMA.splitToList(value); + List updatedServiceNames = new ArrayList<>(); + for (String serviceName : serviceNames) { + if (!serviceName.equalsIgnoreCase(serviceToRemove)) { + updatedServiceNames.add(serviceName); + } + } + + String updatedValue = String.join(",", updatedServiceNames); + if (StringUtils.isBlank(updatedValue)) { + properties.remove( + AuxiliaryServiceManager.GRAVITINO_AUX_SERVICE_PREFIX + + AuxiliaryServiceManager.AUX_SERVICE_NAMES); + } else { + properties.setProperty( + AuxiliaryServiceManager.GRAVITINO_AUX_SERVICE_PREFIX + + AuxiliaryServiceManager.AUX_SERVICE_NAMES, + updatedValue); + } + } } public void start() throws Exception { @@ -103,9 +130,18 @@ public void start() throws Exception { serverConfig.loadPropertiesFromFile( new File(ITUtils.joinPath(mockConfDir.getAbsolutePath(), "gravitino.conf"))); - // Disable auxiliary rest service. - if (context.ignoreAuxRestService) { - removeAuxRestConfiguration(properties); + if (context.ignoreIcebergAuxRestService) { + // Disable auxiliary rest service. + removeAuxRestConfiguration(properties, "iceberg-rest"); + LOG.info("Iceberg auxiliary REST service is disabled for MiniGravitino."); + ITUtils.overwriteConfigFile( + ITUtils.joinPath(mockConfDir.getAbsolutePath(), "gravitino.conf"), properties); + } + + if (context.ignoreLanceAuxRestService) { + // Disable auxiliary rest service. + removeAuxRestConfiguration(properties, "lance-rest"); + LOG.info("Lance auxiliary REST service is disabled for MiniGravitino."); ITUtils.overwriteConfigFile( ITUtils.joinPath(mockConfDir.getAbsolutePath(), "gravitino.conf"), properties); } @@ -230,20 +266,38 @@ Map getIcebergRestServiceConfigs() throws IOException { return customConfigs; } - private Map getLanceRestServiceConfigs() throws IOException { + private Map getLanceRestServiceConfigs(Map configMap) + throws IOException { + if (context.ignoreLanceAuxRestService) { + return Collections.emptyMap(); + } + Map customConfigs = new HashMap<>(); String lanceJarPath = Paths.get("lance", "lance-rest-server", "build", "libs").toString(); String lanceConfigPath = Paths.get("lance", "lance-rest-server", "src", "main", "resources").toString(); customConfigs.put( - "gravitino.lance-rest." + AuxiliaryServiceManager.AUX_SERVICE_CLASSPATH, + LANCE_CONFIG_PREFIX + AuxiliaryServiceManager.AUX_SERVICE_CLASSPATH, String.join(",", lanceJarPath, lanceConfigPath)); customConfigs.put( - "gravitino.lance-rest." + JettyServerConfig.WEBSERVER_HTTP_PORT.getKey(), + LANCE_CONFIG_PREFIX + JettyServerConfig.WEBSERVER_HTTP_PORT.getKey(), String.valueOf(RESTUtils.findAvailablePort(4000, 5000))); - return customConfigs; + + if (GRAVITINO_NAMESPACE_BACKEND.equals( + configMap.getOrDefault(NAMESPACE_BACKEND.getKey(), NAMESPACE_BACKEND.getDefaultValue()))) { + // Set the Lance REST service to use the Gravitino server URI + String gravitinoUri = + String.format( + "http://%s:%s", + "localhost", + configMap.get( + GravitinoServer.WEBSERVER_CONF_PREFIX + + JettyServerConfig.WEBSERVER_HTTP_PORT.getKey())); + customConfigs.put(LANCE_CONFIG_PREFIX + NAMESPACE_BACKEND_URI.getKey(), gravitinoUri); + } + return ImmutableMap.copyOf(customConfigs); } // Customize the config file @@ -255,7 +309,7 @@ private void customizeConfigFile(String configTempFileName, String configFileNam String.valueOf(RESTUtils.findAvailablePort(2000, 3000))); configMap.putAll(getIcebergRestServiceConfigs()); - configMap.putAll(getLanceRestServiceConfigs()); + configMap.putAll(getLanceRestServiceConfigs(configMap)); configMap.putAll(context.customConfig); ITUtils.rewriteConfigFile(configTempFileName, configFileName, configMap); diff --git a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitinoContext.java b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitinoContext.java index 601138ecfd0..cf02bb3f3c3 100644 --- a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitinoContext.java +++ b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitinoContext.java @@ -23,10 +23,15 @@ public class MiniGravitinoContext { Map customConfig; - final boolean ignoreAuxRestService; + final boolean ignoreIcebergAuxRestService; + final boolean ignoreLanceAuxRestService; - public MiniGravitinoContext(Map customConfig, boolean ignoreAuxRestService) { + public MiniGravitinoContext( + Map customConfig, + boolean ignoreIcebergAuxRestService, + boolean ignoreLanceAuxRestService) { this.customConfig = customConfig; - this.ignoreAuxRestService = ignoreAuxRestService; + this.ignoreIcebergAuxRestService = ignoreIcebergAuxRestService; + this.ignoreLanceAuxRestService = ignoreLanceAuxRestService; } } 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..9497d5bd237 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 @@ -21,6 +21,8 @@ import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PATH; import static org.apache.gravitino.integration.test.util.TestDatabaseName.PG_CATALOG_POSTGRESQL_IT; import static org.apache.gravitino.integration.test.util.TestDatabaseName.PG_JDBC_BACKEND; +import static org.apache.gravitino.lance.common.config.LanceConfig.LANCE_CONFIG_PREFIX; +import static org.apache.gravitino.lance.common.config.LanceConfig.METALAKE_NAME; import static org.apache.gravitino.server.GravitinoServer.WEBSERVER_CONF_PREFIX; import com.google.common.base.Splitter; @@ -102,7 +104,9 @@ public class BaseIT { protected Map customConfigs = new HashMap<>(); - protected boolean ignoreAuxRestService = true; + protected boolean ignoreIcebergAuxRestService = true; + + protected boolean ignoreLanceAuxRestService = true; public String DOWNLOAD_MYSQL_JDBC_DRIVER_URL = "https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.26/mysql-connector-java-8.0.26.jar"; @@ -133,6 +137,16 @@ public void registerCustomConfigs(Map configs) { customConfigs.putAll(configs); } + protected int getLanceRESTServerPort() { + JettyServerConfig lanceServerConfig = + JettyServerConfig.fromConfig(serverConfig, LANCE_CONFIG_PREFIX); + return lanceServerConfig.getHttpPort(); + } + + protected String getLanceRESTServerMetalakeName() { + return serverConfig.getRawString(LANCE_CONFIG_PREFIX + METALAKE_NAME.getKey()); + } + private void rewriteGravitinoServerConfig() throws IOException { String gravitinoHome = System.getenv("GRAVITINO_HOME"); Path configPath = Paths.get(gravitinoHome, "conf", GravitinoServer.CONF_FILE); @@ -329,8 +343,15 @@ public void startIntegrationTest() throws Exception { serverConfig = new ServerConfig(); customConfigs.put(ENTITY_RELATIONAL_JDBC_BACKEND_PATH.getKey(), file.getAbsolutePath()); + if (!ignoreLanceAuxRestService) { + customConfigs.put( + LANCE_CONFIG_PREFIX + METALAKE_NAME.getKey(), + GravitinoITUtils.genRandomName("LanceRESTService_metalake")); + } if (testMode != null && testMode.equals(ITUtils.EMBEDDED_TEST_MODE)) { - MiniGravitinoContext context = new MiniGravitinoContext(customConfigs, ignoreAuxRestService); + MiniGravitinoContext context = + new MiniGravitinoContext( + customConfigs, ignoreIcebergAuxRestService, ignoreLanceAuxRestService); miniGravitino = new MiniGravitino(context); miniGravitino.start(); serverConfig = miniGravitino.getServerConfig(); 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..dd2c4629b4f 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 @@ -74,6 +74,7 @@ import org.apache.gravitino.SchemaChange; import org.apache.gravitino.client.GravitinoClient; import org.apache.gravitino.exceptions.CatalogAlreadyExistsException; +import org.apache.gravitino.exceptions.CatalogInUseException; import org.apache.gravitino.exceptions.NoSuchCatalogException; import org.apache.gravitino.exceptions.NoSuchSchemaException; import org.apache.gravitino.exceptions.NonEmptyCatalogException; @@ -332,7 +333,8 @@ private CreateNamespaceResponse createOrUpdateCatalog( // Catalog exists, handle based on mode switch (mode) { case EXIST_OK: - response.setProperties(Maps.newHashMap()); + response.setProperties( + Optional.ofNullable(catalog.properties()).orElse(Collections.emptyMap())); return response; case CREATE: throw LanceNamespaceException.conflict( @@ -344,7 +346,7 @@ private CreateNamespaceResponse createOrUpdateCatalog( CatalogChange[] changes = buildChanges( properties, - catalog.properties(), + removeInUseProperty(catalog.properties()), CatalogChange::setProperty, CatalogChange::removeProperty, CatalogChange[]::new); @@ -356,6 +358,12 @@ private CreateNamespaceResponse createOrUpdateCatalog( } } + private Map removeInUseProperty(Map properties) { + return properties.entrySet().stream() + .filter(e -> !e.getKey().equalsIgnoreCase(Catalog.PROPERTY_IN_USE)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + private CreateNamespaceResponse createOrUpdateSchema( String catalogName, String schemaName, @@ -378,7 +386,8 @@ private CreateNamespaceResponse createOrUpdateSchema( // Schema exists, handle based on mode switch (mode) { case EXIST_OK: - response.setProperties(Maps.newHashMap()); + response.setProperties( + Optional.ofNullable(schema.properties()).orElse(Collections.emptyMap())); return response; case CREATE: throw LanceNamespaceException.conflict( @@ -422,9 +431,9 @@ private DropNamespaceResponse dropCatalog( } return new DropNamespaceResponse(); // SKIP mode } - } catch (NonEmptyCatalogException e) { + } catch (NonEmptyCatalogException | CatalogInUseException e) { throw LanceNamespaceException.badRequest( - String.format("Catalog %s is not empty.", catalogName), + String.format("Catalog %s is not empty or in used", catalogName), NonEmptyCatalogException.class.getSimpleName(), catalogName, CommonUtil.formatCurrentStackTrace()); diff --git a/lance/lance-rest-server/build.gradle.kts b/lance/lance-rest-server/build.gradle.kts index 7befc28b35a..b3e06f67074 100644 --- a/lance/lance-rest-server/build.gradle.kts +++ b/lance/lance-rest-server/build.gradle.kts @@ -53,7 +53,24 @@ dependencies { implementation(libs.jackson.datatype.jdk8) implementation(libs.jackson.datatype.jsr310) + testImplementation(project(":clients:client-java")) + testImplementation(project(":server")) + testImplementation(project(":integration-test-common", "testArtifacts")) + + testImplementation(libs.commons.io) + 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.inline) + testImplementation(libs.mysql.driver) + testImplementation(libs.postgresql.driver) + testImplementation(libs.testcontainers) + testRuntimeOnly(libs.junit.jupiter.engine) } 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..f1fe0087820 --- /dev/null +++ b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java @@ -0,0 +1,391 @@ +/* + * 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.google.common.collect.Sets; +import com.lancedb.lance.namespace.LanceNamespace; +import com.lancedb.lance.namespace.LanceNamespaceException; +import com.lancedb.lance.namespace.LanceNamespaces; +import com.lancedb.lance.namespace.model.CreateNamespaceRequest; +import com.lancedb.lance.namespace.model.CreateNamespaceResponse; +import com.lancedb.lance.namespace.model.DescribeNamespaceRequest; +import com.lancedb.lance.namespace.model.DescribeNamespaceResponse; +import com.lancedb.lance.namespace.model.DropNamespaceRequest; +import com.lancedb.lance.namespace.model.DropNamespaceResponse; +import com.lancedb.lance.namespace.model.ListNamespacesRequest; +import com.lancedb.lance.namespace.model.ListNamespacesResponse; +import com.lancedb.lance.namespace.model.NamespaceExistsRequest; +import com.lancedb.lance.namespace.rest.RestNamespaceConfig; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.gravitino.Catalog; +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.Schema; +import org.apache.gravitino.client.GravitinoMetalake; +import org.apache.gravitino.integration.test.util.BaseIT; +import org.apache.gravitino.integration.test.util.GravitinoITUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class LanceRESTServiceIT extends BaseIT { + + private GravitinoMetalake metalake; + private Map properties = + new HashMap<>() { + { + put("key1", "value1"); + } + }; + private final BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE); + private LanceNamespace ns; + private Path tempDir; + + @BeforeAll + public void startIntegrationTest() throws Exception { + super.ignoreLanceAuxRestService = false; + super.startIntegrationTest(); + this.metalake = createMetalake(getLanceRESTServerMetalakeName()); + + HashMap props = Maps.newHashMap(); + props.put(RestNamespaceConfig.URI, getLanceRestServiceUrl()); + props.put(RestNamespaceConfig.DELIMITER, RestNamespaceConfig.DELIMITER_DEFAULT); + this.ns = LanceNamespaces.connect("rest", props, null, allocator); + + this.tempDir = Files.createTempDirectory("test_lance_rest_service_it_"); + } + + @AfterAll + public void clean() { + client.dropMetalake(getLanceRESTServerMetalakeName(), true); + tempDir.toFile().deleteOnExit(); + } + + @AfterEach + public void clearMetalake() { + Arrays.stream(metalake.listCatalogs()).forEach(c -> metalake.dropCatalog(c, true)); + } + + @Test + public void testListNamespaces() { + Catalog catalog1 = createCatalog(GravitinoITUtils.genRandomName("lance_catalog_1")); + Catalog catalog2 = createCatalog(GravitinoITUtils.genRandomName("lance_catalog_2")); + Schema schema1 = + catalog1 + .asSchemas() + .createSchema("lance_schema_1", "schema for lance rest service tests", null); + + // test list catalogs via lance rest namespace client + ListNamespacesRequest listNamespacesReq = new ListNamespacesRequest(); + ListNamespacesResponse listNamespacesResp = ns.listNamespaces(listNamespacesReq); + + Assertions.assertEquals( + Sets.newHashSet(catalog1.name(), catalog2.name()), listNamespacesResp.getNamespaces()); + + // test list schemas via lance rest namespace client + listNamespacesReq.addIdItem(catalog1.name()); + listNamespacesResp = ns.listNamespaces(listNamespacesReq); + + Assertions.assertEquals(Sets.newHashSet(schema1.name()), listNamespacesResp.getNamespaces()); + } + + @Test + public void testDescribeNamespace() { + Catalog catalog = createCatalog(GravitinoITUtils.genRandomName("lance_catalog")); + Map schemaProps = + new HashMap<>() { + { + put("schema_key1", "schema_value1"); + } + }; + Schema schema = catalog.asSchemas().createSchema("lance_schema", null, schemaProps); + + // test describe catalog via lance rest namespace client + DescribeNamespaceRequest describeNamespaceReq = new DescribeNamespaceRequest(); + describeNamespaceReq.addIdItem(catalog.name()); + DescribeNamespaceResponse describeNamespaceResp = ns.describeNamespace(describeNamespaceReq); + + Assertions.assertEquals(catalog.properties(), describeNamespaceResp.getProperties()); + + // test describe schema via lance rest namespace client + describeNamespaceReq.addIdItem(schema.name()); + describeNamespaceResp = ns.describeNamespace(describeNamespaceReq); + + Assertions.assertEquals(schema.properties(), describeNamespaceResp.getProperties()); + + // test describe the root namespace + DescribeNamespaceRequest rootDescNamespaceReq = new DescribeNamespaceRequest(); + LanceNamespaceException exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.describeNamespace(rootDescNamespaceReq)); + + Assertions.assertEquals(400, exception.getCode()); + Assertions.assertTrue(exception.getErrorResponse().isPresent()); + Assertions.assertTrue( + exception + .getErrorResponse() + .get() + .getError() + .contains("Expected at most 2-level and at least 1-level namespace")); + Assertions.assertEquals( + IllegalArgumentException.class.getSimpleName(), + exception.getErrorResponse().get().getType()); + + // test describe a non-existent catalog namespace + DescribeNamespaceRequest nonExistentCatalogReq = new DescribeNamespaceRequest(); + nonExistentCatalogReq.addIdItem("non_existent_catalog"); + exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.describeNamespace(nonExistentCatalogReq)); + Assertions.assertEquals(404, exception.getCode()); + + // test describe a non-existent schema namespace + DescribeNamespaceRequest nonExistentSchemaReq = new DescribeNamespaceRequest(); + nonExistentSchemaReq.addIdItem(catalog.name()); + nonExistentSchemaReq.addIdItem("non_existent_schema"); + exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.describeNamespace(nonExistentSchemaReq)); + Assertions.assertEquals(404, exception.getCode()); + } + + @Test + public void testCreateNamespace() { + String catalogName = GravitinoITUtils.genRandomName("lance_catalog"); + Map catalogProps = + new HashMap<>() { + { + put("catalog_key1", "catalog_value1"); + } + }; + + // test create catalog via lance rest namespace client + CreateNamespaceRequest createNamespaceReq = new CreateNamespaceRequest(); + createNamespaceReq.addIdItem(catalogName); + createNamespaceReq.setProperties(catalogProps); + CreateNamespaceResponse createNamespaceResp = ns.createNamespace(createNamespaceReq); + + Catalog catalog = metalake.loadCatalog(catalogName); + Assertions.assertEquals(catalog.properties(), createNamespaceResp.getProperties()); + + // create catalog again with default mode (create) should fail + LanceNamespaceException exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.createNamespace(createNamespaceReq)); + Assertions.assertEquals(409, exception.getCode()); + + // create catalog again with exist_ok mode should succeed + createNamespaceReq.setMode(CreateNamespaceRequest.ModeEnum.EXIST_OK); + createNamespaceResp = ns.createNamespace(createNamespaceReq); + Assertions.assertEquals(catalog.properties(), createNamespaceResp.getProperties()); + + // create catalog again with overwrite mode should succeed and update properties + Map newProps = + new HashMap<>(catalogProps) { + { + put("catalog_key2", "catalog_value2"); + } + }; + createNamespaceReq.setMode(CreateNamespaceRequest.ModeEnum.OVERWRITE); + createNamespaceReq.setProperties(newProps); + createNamespaceResp = ns.createNamespace(createNamespaceReq); + + catalog = metalake.loadCatalog(catalogName); + Assertions.assertEquals(catalog.properties(), createNamespaceResp.getProperties()); + Assertions.assertEquals(catalog.properties(), createNamespaceResp.getProperties()); + + // test create schema via lance rest namespace client + CreateNamespaceRequest createSchemaReq = new CreateNamespaceRequest(); + String schemaName = "lance_schema"; + Map schemaProps = + new HashMap<>() { + { + put("schema_key1", "schema_value1"); + } + }; + createSchemaReq.addIdItem(catalogName); + createSchemaReq.addIdItem(schemaName); + createSchemaReq.setProperties(schemaProps); + createNamespaceResp = ns.createNamespace(createSchemaReq); + + Schema schema = catalog.asSchemas().loadSchema(schemaName); + Assertions.assertEquals(schema.properties(), createNamespaceResp.getProperties()); + + // create schema again with default mode (create) should fail + exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.createNamespace(createSchemaReq)); + Assertions.assertEquals(409, exception.getCode()); + + // create schema again with exist_ok mode should succeed + createSchemaReq.setMode(CreateNamespaceRequest.ModeEnum.EXIST_OK); + createNamespaceResp = ns.createNamespace(createSchemaReq); + Assertions.assertEquals(schema.properties(), createNamespaceResp.getProperties()); + + // create schema again with overwrite mode should succeed and update properties + Map newSchemaProps = + new HashMap<>(schemaProps) { + { + put("schema_key2", "schema_value2"); + } + }; + createSchemaReq.setMode(CreateNamespaceRequest.ModeEnum.OVERWRITE); + createSchemaReq.setProperties(newSchemaProps); + createNamespaceResp = ns.createNamespace(createSchemaReq); + + schema = catalog.asSchemas().loadSchema(schemaName); + Assertions.assertEquals(schema.properties(), createNamespaceResp.getProperties()); + } + + @Test + public void testDropNamespace() { + Catalog catalog = createCatalog(GravitinoITUtils.genRandomName("lance_catalog")); + Schema schema = catalog.asSchemas().createSchema("lance_schema", null, null); + + // test drop a non-existent namespace (catalog) with default mode (FAIL) should fail + DropNamespaceRequest dropNamespaceReq = new DropNamespaceRequest(); + dropNamespaceReq.addIdItem("non_existent_catalog"); + LanceNamespaceException exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.dropNamespace(dropNamespaceReq)); + Assertions.assertEquals(404, exception.getCode()); + + // test drop a non-existent namespace (catalog) with SKIP mode should succeed + dropNamespaceReq.setMode(DropNamespaceRequest.ModeEnum.SKIP); + DropNamespaceResponse dropNamespaceResp = ns.dropNamespace(dropNamespaceReq); + Assertions.assertTrue(dropNamespaceResp.getTransactionId().isEmpty()); + + // test drop a non-existent namespace (schema) with default mode (FAIL) should fail + DropNamespaceRequest dropSchemaReq = new DropNamespaceRequest(); + dropSchemaReq.addIdItem(catalog.name()); + dropSchemaReq.addIdItem("non_existent_schema"); + exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.dropNamespace(dropSchemaReq)); + Assertions.assertEquals(404, exception.getCode()); + + // test drop a non-existent namespace (schema) with SKIP mode should succeed + dropSchemaReq.setMode(DropNamespaceRequest.ModeEnum.SKIP); + dropNamespaceResp = ns.dropNamespace(dropSchemaReq); + Assertions.assertTrue(dropNamespaceResp.getTransactionId().isEmpty()); + + // test drop a non-empty namespace (catalog) with default behavior (RESTRICT) should fail + DropNamespaceRequest dropNonEmptyCatalogReq = new DropNamespaceRequest(); + dropNonEmptyCatalogReq.addIdItem(catalog.name()); + exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.dropNamespace(dropNonEmptyCatalogReq)); + Assertions.assertEquals(400, exception.getCode()); + + // test drop a non-empty namespace (catalog) with CASCADE behavior should succeed + dropNonEmptyCatalogReq.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE); + dropNamespaceResp = ns.dropNamespace(dropNonEmptyCatalogReq); + Assertions.assertTrue(dropNamespaceResp.getTransactionId().isEmpty()); + Assertions.assertFalse(metalake.catalogExists(catalog.name())); + + // recreate catalog, schema, and table for next test + catalog = createCatalog(catalog.name()); + schema = catalog.asSchemas().createSchema(schema.name(), null, null); + String tableName = GravitinoITUtils.genRandomName("test_lance_table"); + String tableLocation = + Path.of(tempDir.toString(), catalog.name(), schema.name(), tableName).toString(); + catalog + .asTableCatalog() + .createTable( + NameIdentifier.of(schema.name(), tableName), + null, + null, + ImmutableMap.of("location", tableLocation, "format", "lance")); + // test drop a non-empty namespace (schema) with default behavior (RESTRICT) should fail + DropNamespaceRequest dropNonEmptySchemaReq = new DropNamespaceRequest(); + dropNonEmptySchemaReq.addIdItem(catalog.name()); + dropNonEmptySchemaReq.addIdItem(schema.name()); + exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.dropNamespace(dropNonEmptySchemaReq)); + Assertions.assertEquals(400, exception.getCode()); + Assertions.assertTrue(catalog.asSchemas().schemaExists(schema.name())); + + // test drop a non-empty namespace (schema) with CASCADE behavior should succeed + dropNonEmptySchemaReq.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE); + dropNamespaceResp = ns.dropNamespace(dropNonEmptySchemaReq); + Assertions.assertTrue(dropNamespaceResp.getTransactionId().isEmpty()); + Assertions.assertFalse(catalog.asSchemas().schemaExists(schema.name())); + } + + @Test + public void testNamespaceExists() { + Catalog catalog = createCatalog(GravitinoITUtils.genRandomName("lance_catalog")); + Schema schema = catalog.asSchemas().createSchema("lance_schema", null, null); + + // test existing catalog + NamespaceExistsRequest catalogExistsReq = new NamespaceExistsRequest(); + catalogExistsReq.addIdItem(catalog.name()); + Assertions.assertDoesNotThrow(() -> ns.namespaceExists(catalogExistsReq)); + + // test non-existing catalog + NamespaceExistsRequest nonExistentCatalogReq = new NamespaceExistsRequest(); + nonExistentCatalogReq.addIdItem("non_existent_catalog"); + LanceNamespaceException exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.namespaceExists(nonExistentCatalogReq)); + Assertions.assertEquals(404, exception.getCode()); + + // test existing schema + NamespaceExistsRequest schemaExistsReq = new NamespaceExistsRequest(); + schemaExistsReq.addIdItem(catalog.name()); + schemaExistsReq.addIdItem(schema.name()); + Assertions.assertDoesNotThrow(() -> ns.namespaceExists(schemaExistsReq)); + + // test non-existing schema + NamespaceExistsRequest nonExistentSchemaReq = new NamespaceExistsRequest(); + nonExistentSchemaReq.addIdItem(catalog.name()); + nonExistentSchemaReq.addIdItem("non_existent_schema"); + exception = + Assertions.assertThrows( + LanceNamespaceException.class, () -> ns.namespaceExists(nonExistentSchemaReq)); + Assertions.assertEquals(404, exception.getCode()); + } + + private GravitinoMetalake createMetalake(String metalakeName) { + return client.createMetalake(metalakeName, "metalake for lance rest service tests", null); + } + + private Catalog createCatalog(String catalogName) { + return metalake.createCatalog( + catalogName, + Catalog.Type.RELATIONAL, + "generic-lakehouse", + "catalog for lance rest service tests", + properties); + } + + private String getLanceRestServiceUrl() { + return String.format("http://%s:%d/lance", "localhost", getLanceRESTServerPort()); + } +} diff --git a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/ServletRequestFactoryBase.java b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/ServletRequestFactoryBase.java new file mode 100644 index 00000000000..68b557955cd --- /dev/null +++ b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/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.rest; + +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/rest/TestLanceNamespaceOperations.java b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java new file mode 100644 index 00000000000..0ba8bf79b70 --- /dev/null +++ b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java @@ -0,0 +1,326 @@ +/* + * 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.rest; + +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.model.CreateNamespaceRequest; +import com.lancedb.lance.namespace.model.CreateNamespaceResponse; +import com.lancedb.lance.namespace.model.DescribeNamespaceResponse; +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 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.NamespaceWrapper; +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); + + @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( + 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); + } + + @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()); + } +} diff --git a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/SparkEnvIT.java b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/SparkEnvIT.java index 9c2d2d5110b..f3d891ad390 100644 --- a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/SparkEnvIT.java +++ b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/SparkEnvIT.java @@ -155,7 +155,7 @@ private void initHiveEnv() { protected void initCatalogEnv() throws Exception {} private void initIcebergRestServiceEnv() { - super.ignoreAuxRestService = false; + super.ignoreIcebergAuxRestService = false; Map icebergRestServiceConfigs = new HashMap<>(); icebergRestServiceConfigs.put( "gravitino."