fields = new ArrayList<>();
- fields.add(idField);
- fields.add(nameField);
-
- JsonArrowSchema schema = new JsonArrowSchema();
- schema.setFields(fields);
- return schema;
- }
-
- private byte[] createTestArrowData() {
- // Create a proper Arrow IPC stream with test schema
- try {
- return org.lance.namespace.util.ArrowIpcUtil.createEmptyArrowIpcStream(
- createTestSchema());
- } catch (Exception e) {
- throw new RuntimeException("Failed to create test Arrow data", e);
- }
- }
}
diff --git a/java/lance-namespace-glue/src/test/java/org/lance/namespace/glue/TestGlueNamespaceIntegration.java b/java/lance-namespace-glue/src/test/java/org/lance/namespace/glue/TestGlueNamespaceIntegration.java
new file mode 100644
index 0000000..e37d6bf
--- /dev/null
+++ b/java/lance-namespace-glue/src/test/java/org/lance/namespace/glue/TestGlueNamespaceIntegration.java
@@ -0,0 +1,322 @@
+/*
+ * Licensed 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.lance.namespace.glue;
+
+import org.lance.namespace.errors.LanceNamespaceException;
+import org.lance.namespace.model.CreateEmptyTableRequest;
+import org.lance.namespace.model.CreateEmptyTableResponse;
+import org.lance.namespace.model.CreateNamespaceRequest;
+import org.lance.namespace.model.CreateNamespaceResponse;
+import org.lance.namespace.model.DeregisterTableRequest;
+import org.lance.namespace.model.DescribeNamespaceRequest;
+import org.lance.namespace.model.DescribeNamespaceResponse;
+import org.lance.namespace.model.DescribeTableRequest;
+import org.lance.namespace.model.DescribeTableResponse;
+import org.lance.namespace.model.DropNamespaceRequest;
+import org.lance.namespace.model.ListNamespacesRequest;
+import org.lance.namespace.model.ListNamespacesResponse;
+import org.lance.namespace.model.ListTablesRequest;
+import org.lance.namespace.model.ListTablesResponse;
+import org.lance.namespace.model.NamespaceExistsRequest;
+import org.lance.namespace.model.TableExistsRequest;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Integration tests for GlueNamespace against a real AWS Glue catalog.
+ *
+ * To run these tests locally:
+ *
+ *
+ * - Configure AWS credentials (via environment variables, ~/.aws/credentials, or IAM role)
+ *
- Set AWS_S3_BUCKET_NAME environment variable
+ *
- Run: make integ-test-glue
+ *
+ *
+ * Tests are automatically skipped if AWS credentials are not available.
+ */
+public class TestGlueNamespaceIntegration {
+
+ private static final String AWS_REGION =
+ System.getenv("AWS_REGION") != null ? System.getenv("AWS_REGION") : "us-east-1";
+ private static final String AWS_S3_BUCKET_NAME = System.getenv("AWS_S3_BUCKET_NAME");
+ private static boolean awsCredentialsAvailable = false;
+ private static String s3Root;
+
+ private GlueNamespace namespace;
+ private BufferAllocator allocator;
+ private String testDatabase;
+ private List createdDatabases;
+
+ @BeforeAll
+ public static void checkAwsCredentialsAvailable() {
+ // Check if S3 bucket is configured
+ if (AWS_S3_BUCKET_NAME == null || AWS_S3_BUCKET_NAME.isEmpty()) {
+ System.out.println("AWS_S3_BUCKET_NAME not set - skipping integration tests");
+ awsCredentialsAvailable = false;
+ return;
+ }
+
+ // Check if AWS credentials are available via environment variables
+ String accessKeyId = System.getenv("AWS_ACCESS_KEY_ID");
+ String secretAccessKey = System.getenv("AWS_SECRET_ACCESS_KEY");
+
+ if (accessKeyId != null
+ && !accessKeyId.isEmpty()
+ && secretAccessKey != null
+ && !secretAccessKey.isEmpty()) {
+ awsCredentialsAvailable = true;
+ System.out.println("AWS credentials found in environment variables");
+ } else {
+ // Try to use default credentials chain by making a simple API call
+ try {
+ software.amazon.awssdk.services.sts.StsClient stsClient =
+ software.amazon.awssdk.services.sts.StsClient.builder()
+ .region(software.amazon.awssdk.regions.Region.of(AWS_REGION))
+ .build();
+ stsClient.getCallerIdentity();
+ stsClient.close();
+ awsCredentialsAvailable = true;
+ System.out.println("AWS credentials found via default credentials chain");
+ } catch (Exception e) {
+ awsCredentialsAvailable = false;
+ System.out.println(
+ "AWS credentials not available (" + e.getMessage() + ") - skipping integration tests");
+ }
+ }
+
+ if (awsCredentialsAvailable) {
+ String uniqueId = UUID.randomUUID().toString().substring(0, 8);
+ s3Root = "s3://" + AWS_S3_BUCKET_NAME + "/lance_glue_test_" + uniqueId;
+ System.out.println("Using S3 root: " + s3Root);
+ }
+ }
+
+ @BeforeEach
+ public void setUp() {
+ Assumptions.assumeTrue(awsCredentialsAvailable, "AWS credentials are not available");
+
+ allocator = new RootAllocator();
+ namespace = new GlueNamespace();
+ createdDatabases = new ArrayList<>();
+
+ String uniqueId = UUID.randomUUID().toString().substring(0, 8);
+ testDatabase = "lance_test_db_" + uniqueId;
+
+ Map config = new HashMap<>();
+ config.put("region", AWS_REGION);
+ config.put("root", s3Root);
+
+ namespace.initialize(config, allocator);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // Clean up test resources
+ for (String dbName : createdDatabases) {
+ try {
+ cleanupDatabase(dbName);
+ } catch (Exception e) {
+ // Ignore cleanup errors
+ }
+ }
+
+ if (namespace != null) {
+ namespace.close();
+ }
+
+ if (allocator != null) {
+ allocator.close();
+ }
+ }
+
+ private void cleanupDatabase(String databaseName) {
+ try {
+ // First, delete all tables in the database
+ ListTablesRequest listRequest = new ListTablesRequest();
+ listRequest.setId(Collections.singletonList(databaseName));
+ ListTablesResponse listResponse = namespace.listTables(listRequest);
+
+ for (String tableName : listResponse.getTables()) {
+ try {
+ DeregisterTableRequest deregRequest = new DeregisterTableRequest();
+ deregRequest.setId(Arrays.asList(databaseName, tableName));
+ namespace.deregisterTable(deregRequest);
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+
+ // Then drop the database
+ DropNamespaceRequest dropRequest = new DropNamespaceRequest();
+ dropRequest.setId(Collections.singletonList(databaseName));
+ namespace.dropNamespace(dropRequest);
+ } catch (Exception e) {
+ // Ignore cleanup errors
+ }
+ }
+
+ private String createTestDatabase(String suffix) {
+ String dbName = "lance_test_" + UUID.randomUUID().toString().substring(0, 8) + suffix;
+ createdDatabases.add(dbName);
+
+ CreateNamespaceRequest createRequest = new CreateNamespaceRequest();
+ createRequest.setId(Collections.singletonList(dbName));
+ createRequest.setProperties(
+ Collections.singletonMap("description", "Lance integration test database"));
+ namespace.createNamespace(createRequest);
+
+ return dbName;
+ }
+
+ @Test
+ public void testNamespaceOperations() {
+ String dbName = "lance_test_" + UUID.randomUUID().toString().substring(0, 8);
+ createdDatabases.add(dbName);
+
+ // Create namespace
+ CreateNamespaceRequest createRequest = new CreateNamespaceRequest();
+ createRequest.setId(Collections.singletonList(dbName));
+ createRequest.setProperties(Collections.singletonMap("description", "Test database for Lance"));
+
+ CreateNamespaceResponse createResponse = namespace.createNamespace(createRequest);
+ assertThat(createResponse).isNotNull();
+
+ // Describe namespace
+ DescribeNamespaceRequest describeRequest = new DescribeNamespaceRequest();
+ describeRequest.setId(Collections.singletonList(dbName));
+
+ DescribeNamespaceResponse describeResponse = namespace.describeNamespace(describeRequest);
+ assertThat(describeResponse).isNotNull();
+ assertThat(describeResponse.getProperties())
+ .containsEntry("description", "Test database for Lance");
+
+ // Check namespace exists
+ NamespaceExistsRequest existsRequest = new NamespaceExistsRequest();
+ existsRequest.setId(Collections.singletonList(dbName));
+ namespace.namespaceExists(existsRequest); // Should not throw
+
+ // List namespaces
+ ListNamespacesRequest listRequest = new ListNamespacesRequest();
+ listRequest.setId(Collections.emptyList());
+ ListNamespacesResponse listResponse = namespace.listNamespaces(listRequest);
+ assertThat(listResponse.getNamespaces()).contains(dbName);
+
+ // Drop namespace
+ DropNamespaceRequest dropRequest = new DropNamespaceRequest();
+ dropRequest.setId(Collections.singletonList(dbName));
+ namespace.dropNamespace(dropRequest);
+ createdDatabases.remove(dbName);
+
+ // Verify namespace doesn't exist
+ assertThatThrownBy(() -> namespace.namespaceExists(existsRequest))
+ .isInstanceOf(LanceNamespaceException.class);
+ }
+
+ @Test
+ public void testTableOperations() {
+ String dbName = createTestDatabase("");
+ String tableName = "test_table_" + UUID.randomUUID().toString().substring(0, 8);
+ String tableLocation = s3Root + "/" + dbName + "/" + tableName + ".lance";
+
+ // Create empty table
+ CreateEmptyTableRequest createRequest = new CreateEmptyTableRequest();
+ createRequest.setId(Arrays.asList(dbName, tableName));
+ createRequest.setLocation(tableLocation);
+
+ CreateEmptyTableResponse createResponse = namespace.createEmptyTable(createRequest);
+ assertThat(createResponse.getLocation()).isNotNull();
+ assertThat(createResponse.getLocation()).isEqualTo(tableLocation);
+
+ // Describe table
+ DescribeTableRequest describeRequest = new DescribeTableRequest();
+ describeRequest.setId(Arrays.asList(dbName, tableName));
+
+ DescribeTableResponse describeResponse = namespace.describeTable(describeRequest);
+ assertThat(describeResponse.getLocation()).isNotNull();
+ assertThat(describeResponse.getLocation()).isEqualTo(tableLocation);
+
+ // Check table exists
+ TableExistsRequest existsRequest = new TableExistsRequest();
+ existsRequest.setId(Arrays.asList(dbName, tableName));
+ namespace.tableExists(existsRequest); // Should not throw
+
+ // List tables
+ ListTablesRequest listRequest = new ListTablesRequest();
+ listRequest.setId(Collections.singletonList(dbName));
+
+ ListTablesResponse listResponse = namespace.listTables(listRequest);
+ assertThat(listResponse.getTables()).contains(tableName);
+
+ // Deregister table
+ DeregisterTableRequest deregisterRequest = new DeregisterTableRequest();
+ deregisterRequest.setId(Arrays.asList(dbName, tableName));
+ namespace.deregisterTable(deregisterRequest);
+
+ // Verify table doesn't exist
+ assertThatThrownBy(() -> namespace.tableExists(existsRequest))
+ .isInstanceOf(LanceNamespaceException.class);
+ }
+
+ @Test
+ public void testMultipleTablesInNamespace() {
+ String dbName = createTestDatabase("");
+ List tableNames = new ArrayList<>();
+
+ // Create multiple tables
+ for (int i = 0; i < 3; i++) {
+ String tableName = "table_" + i + "_" + UUID.randomUUID().toString().substring(0, 6);
+ tableNames.add(tableName);
+
+ String tableLocation = s3Root + "/" + dbName + "/" + tableName + ".lance";
+ CreateEmptyTableRequest createRequest = new CreateEmptyTableRequest();
+ createRequest.setId(Arrays.asList(dbName, tableName));
+ createRequest.setLocation(tableLocation);
+ namespace.createEmptyTable(createRequest);
+ }
+
+ // List tables and verify all are present
+ ListTablesRequest listRequest = new ListTablesRequest();
+ listRequest.setId(Collections.singletonList(dbName));
+
+ ListTablesResponse listResponse = namespace.listTables(listRequest);
+ for (String tableName : tableNames) {
+ assertThat(listResponse.getTables()).contains(tableName);
+ }
+
+ // Clean up tables
+ for (String tableName : tableNames) {
+ DeregisterTableRequest deregisterRequest = new DeregisterTableRequest();
+ deregisterRequest.setId(Arrays.asList(dbName, tableName));
+ namespace.deregisterTable(deregisterRequest);
+ }
+ }
+}
diff --git a/java/lance-namespace-hive2/pom.xml b/java/lance-namespace-hive2/pom.xml
index f21cf78..52585f1 100644
--- a/java/lance-namespace-hive2/pom.xml
+++ b/java/lance-namespace-hive2/pom.xml
@@ -22,6 +22,10 @@
org.lance
lance-core
+
+ org.lance
+ lance-namespace-core
+
org.lance
lance-namespace-apache-client
@@ -159,6 +163,12 @@
4.1.19
test
+
+ org.lance
+ lance-namespace-impls-core
+ ${project.version}
+ test
+
org.junit.jupiter
junit-jupiter
diff --git a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/ClientPoolImpl.java b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/ClientPoolImpl.java
new file mode 100644
index 0000000..e4fcd8c
--- /dev/null
+++ b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/ClientPoolImpl.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed 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.lance.namespace.hive2;
+
+import java.io.Closeable;
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * A simple connection pool implementation for reusing clients. Adapted from Apache Iceberg.
+ *
+ * @param the client type
+ * @param the exception type thrown by client operations
+ */
+public abstract class ClientPoolImpl implements Closeable {
+
+ private final int poolSize;
+ private final Deque clients;
+ private final Class extends E> reconnectExc;
+ private final boolean retryByDefault;
+ private volatile int currentSize;
+ private boolean closed;
+
+ protected ClientPoolImpl(int poolSize, Class extends E> reconnectExc, boolean retryByDefault) {
+ this.poolSize = poolSize;
+ this.clients = new ArrayDeque<>();
+ this.reconnectExc = reconnectExc;
+ this.retryByDefault = retryByDefault;
+ this.currentSize = 0;
+ this.closed = false;
+ }
+
+ public interface Action {
+ R run(C client) throws E;
+ }
+
+ public R run(Action action) throws E, InterruptedException {
+ return run(action, retryByDefault);
+ }
+
+ public R run(Action action, boolean retry) throws E, InterruptedException {
+ C client = get();
+ try {
+ return action.run(client);
+ } catch (Exception exc) {
+ if (retry && isConnectionException(exc)) {
+ try {
+ client = reconnect(client);
+ } catch (Exception reconnectExc) {
+ release(client);
+ throw (E) exc;
+ }
+ return action.run(client);
+ }
+ throw (E) exc;
+ } finally {
+ release(client);
+ }
+ }
+
+ protected abstract C newClient();
+
+ protected abstract C reconnect(C client);
+
+ protected abstract void close(C client);
+
+ protected boolean isConnectionException(Exception exc) {
+ return reconnectExc.isInstance(exc);
+ }
+
+ private synchronized C get() throws InterruptedException {
+ if (closed) {
+ throw new IllegalStateException("Cannot get a client from a closed pool");
+ }
+
+ while (clients.isEmpty() && currentSize >= poolSize) {
+ wait();
+ }
+
+ if (!clients.isEmpty()) {
+ return clients.removeFirst();
+ }
+
+ currentSize++;
+ return newClient();
+ }
+
+ private synchronized void release(C client) {
+ if (closed) {
+ close(client);
+ } else {
+ clients.addFirst(client);
+ notify();
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ this.closed = true;
+ while (!clients.isEmpty()) {
+ close(clients.removeFirst());
+ }
+ notifyAll();
+ }
+}
diff --git a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/CommonUtil.java b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/CommonUtil.java
new file mode 100644
index 0000000..bbfb978
--- /dev/null
+++ b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/CommonUtil.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed 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.lance.namespace.hive2;
+
+/** Common utility methods. */
+public class CommonUtil {
+
+ private CommonUtil() {}
+
+ public static String formatCurrentStackTrace() {
+ StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 2; i < Math.min(stack.length, 10); i++) {
+ sb.append(stack[i].toString()).append("\n");
+ }
+ return sb.toString();
+ }
+
+ public static String makeQualified(String path) {
+ if (path == null) {
+ return null;
+ }
+ return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
+ }
+}
diff --git a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2ClientPool.java b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2ClientPool.java
index 9ee9cb4..eef2bf7 100644
--- a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2ClientPool.java
+++ b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2ClientPool.java
@@ -13,8 +13,6 @@
*/
package org.lance.namespace.hive2;
-import org.lance.namespace.util.ClientPoolImpl;
-
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.metastore.HiveMetaHookLoader;
diff --git a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2Namespace.java b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2Namespace.java
index 1812ac0..7c07ac2 100644
--- a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2Namespace.java
+++ b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2Namespace.java
@@ -13,38 +13,33 @@
*/
package org.lance.namespace.hive2;
-import com.lancedb.lance.Dataset;
-import com.lancedb.lance.WriteParams;
-import org.lance.namespace.Configurable;
+import org.lance.Dataset;
+import org.lance.WriteParams;
import org.lance.namespace.LanceNamespace;
-import org.lance.namespace.LanceNamespaceException;
-import org.lance.namespace.ObjectIdentifier;
+import org.lance.namespace.errors.InvalidInputException;
+import org.lance.namespace.errors.NamespaceAlreadyExistsException;
+import org.lance.namespace.errors.NamespaceNotFoundException;
+import org.lance.namespace.errors.ServiceUnavailableException;
+import org.lance.namespace.errors.TableAlreadyExistsException;
+import org.lance.namespace.errors.TableNotFoundException;
import org.lance.namespace.model.CreateEmptyTableRequest;
import org.lance.namespace.model.CreateEmptyTableResponse;
import org.lance.namespace.model.CreateNamespaceRequest;
import org.lance.namespace.model.CreateNamespaceResponse;
-import org.lance.namespace.model.CreateTableRequest;
-import org.lance.namespace.model.CreateTableResponse;
+import org.lance.namespace.model.DeregisterTableRequest;
+import org.lance.namespace.model.DeregisterTableResponse;
import org.lance.namespace.model.DescribeNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
import org.lance.namespace.model.DescribeTableRequest;
import org.lance.namespace.model.DescribeTableResponse;
import org.lance.namespace.model.DropNamespaceRequest;
import org.lance.namespace.model.DropNamespaceResponse;
-import org.lance.namespace.model.DropTableRequest;
-import org.lance.namespace.model.DropTableResponse;
-import org.lance.namespace.model.JsonArrowSchema;
import org.lance.namespace.model.ListNamespacesRequest;
import org.lance.namespace.model.ListNamespacesResponse;
import org.lance.namespace.model.ListTablesRequest;
import org.lance.namespace.model.ListTablesResponse;
import org.lance.namespace.model.NamespaceExistsRequest;
import org.lance.namespace.model.TableExistsRequest;
-import org.lance.namespace.util.ArrowIpcUtil;
-import org.lance.namespace.util.CommonUtil;
-import org.lance.namespace.util.JsonArrowSchemaConverter;
-import org.lance.namespace.util.PageUtil;
-import org.lance.namespace.util.ValidationUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -60,7 +55,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -73,7 +67,7 @@
import static org.lance.namespace.hive2.Hive2ErrorType.TableAlreadyExists;
import static org.lance.namespace.hive2.Hive2ErrorType.TableNotFound;
-public class Hive2Namespace implements LanceNamespace, Configurable {
+public class Hive2Namespace implements LanceNamespace {
private static final Logger LOG = LoggerFactory.getLogger(Hive2Namespace.class);
private Hive2ClientPool clientPool;
@@ -83,6 +77,16 @@ public class Hive2Namespace implements LanceNamespace, Configurable configProperties, BufferAllocator allocator) {
this.allocator = allocator;
@@ -125,7 +129,7 @@ public ListNamespacesResponse listNamespaces(ListNamespacesRequest request) {
@Override
public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) {
ObjectIdentifier id = ObjectIdentifier.of(request.getId());
- CreateNamespaceRequest.ModeEnum mode = request.getMode();
+ String mode = request.getMode() != null ? request.getMode().toLowerCase() : "create";
Map properties = request.getProperties();
ValidationUtil.checkArgument(
@@ -149,11 +153,10 @@ public void namespaceExists(NamespaceExistsRequest request) {
Database database = Hive2Util.getDatabaseOrNull(clientPool, db);
if (database == null) {
- throw LanceNamespaceException.notFound(
+ throw new NamespaceNotFoundException(
String.format("Namespace does not exist: %s", id.stringStyleId()),
HiveMetaStoreError.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ id.stringStyleId());
}
}
@@ -168,11 +171,10 @@ public DescribeNamespaceResponse describeNamespace(DescribeNamespaceRequest requ
Database database = Hive2Util.getDatabaseOrNull(clientPool, db);
if (database == null) {
- throw LanceNamespaceException.notFound(
+ throw new NamespaceNotFoundException(
String.format("Namespace does not exist: %s", id.stringStyleId()),
HiveMetaStoreError.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ id.stringStyleId());
}
DescribeNamespaceResponse response = new DescribeNamespaceResponse();
@@ -201,20 +203,17 @@ public DescribeNamespaceResponse describeNamespace(DescribeNamespaceRequest requ
@Override
public DropNamespaceResponse dropNamespace(DropNamespaceRequest request) {
+ if ("Cascade".equalsIgnoreCase(request.getBehavior())) {
+ throw new InvalidInputException("Cascade behavior is not supported for this implementation");
+ }
+
ObjectIdentifier id = ObjectIdentifier.of(request.getId());
- DropNamespaceRequest.ModeEnum mode = request.getMode();
- DropNamespaceRequest.BehaviorEnum behavior = request.getBehavior();
+ String mode = request.getMode() != null ? request.getMode().toLowerCase() : "fail";
+ String behavior = request.getBehavior() != null ? request.getBehavior() : "Restrict";
ValidationUtil.checkArgument(
!id.isRoot() && id.levels() <= 1, "Expect a 1-level namespace but get %s", id);
- if (mode == null) {
- mode = DropNamespaceRequest.ModeEnum.FAIL;
- }
- if (behavior == null) {
- behavior = DropNamespaceRequest.BehaviorEnum.RESTRICT;
- }
-
Map properties = doDropNamespace(id, mode, behavior);
DropNamespaceResponse response = new DropNamespaceResponse();
@@ -256,11 +255,10 @@ public void tableExists(TableExistsRequest request) {
Optional hmsTable = Hive2Util.getTable(clientPool, db, table);
if (!hmsTable.isPresent()) {
- throw LanceNamespaceException.notFound(
+ throw new TableNotFoundException(
String.format("Table does not exist: %s", tableId.stringStyleId()),
TableNotFound.getType(),
- tableId.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ tableId.stringStyleId());
}
Hive2Util.validateLanceTable(hmsTable.get());
@@ -268,6 +266,11 @@ public void tableExists(TableExistsRequest request) {
@Override
public DescribeTableResponse describeTable(DescribeTableRequest request) {
+ if (Boolean.TRUE.equals(request.getLoadDetailedMetadata())) {
+ throw new InvalidInputException(
+ "load_detailed_metadata=true is not supported for this implementation");
+ }
+
ObjectIdentifier tableId = ObjectIdentifier.of(request.getId());
ValidationUtil.checkArgument(
@@ -276,11 +279,10 @@ public DescribeTableResponse describeTable(DescribeTableRequest request) {
Optional location = doDescribeTable(tableId);
if (!location.isPresent()) {
- throw LanceNamespaceException.notFound(
+ throw new TableNotFoundException(
String.format("Table does not exist: %s", tableId.stringStyleId()),
TableNotFound.getType(),
- tableId.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ tableId.stringStyleId());
}
DescribeTableResponse response = new DescribeTableResponse();
@@ -289,45 +291,7 @@ public DescribeTableResponse describeTable(DescribeTableRequest request) {
return response;
}
- @Override
- public CreateTableResponse createTable(CreateTableRequest request, byte[] requestData) {
- // Validate that requestData is a valid Arrow IPC stream
- ValidationUtil.checkNotNull(
- requestData, "Request data (Arrow IPC stream) is required for createTable");
- ValidationUtil.checkArgument(
- requestData.length > 0, "Request data (Arrow IPC stream) cannot be empty");
-
- ObjectIdentifier tableId = ObjectIdentifier.of(request.getId());
-
- // Extract schema from Arrow IPC stream
- JsonArrowSchema jsonSchema;
- try {
- jsonSchema = ArrowIpcUtil.extractSchemaFromIpc(requestData);
- } catch (IOException e) {
- throw LanceNamespaceException.badRequest(
- "Invalid Arrow IPC stream: " + e.getMessage(),
- "INVALID_ARROW_IPC",
- tableId.stringStyleId(),
- "Failed to extract schema from Arrow IPC stream");
- }
- Schema schema = JsonArrowSchemaConverter.convertToArrowSchema(jsonSchema);
-
- ValidationUtil.checkArgument(
- tableId.levels() == 2, "Expect 2-level table identifier but get %s", tableId);
-
- String location = request.getLocation();
- if (location == null || location.isEmpty()) {
- location = getDefaultTableLocation(tableId.levelAtListPos(0), tableId.levelAtListPos(1));
- }
-
- doCreateTable(tableId, schema, location, request.getProperties(), requestData);
-
- CreateTableResponse response = new CreateTableResponse();
- response.setLocation(location);
- response.setVersion(1L);
- response.setStorageOptions(config.getStorageOptions());
- return response;
- }
+ // Removed: createTable(CreateTableRequest, byte[]) - using default implementation from interface
@Override
public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request) {
@@ -342,7 +306,7 @@ public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request
}
// Create table in metastore without data (pass null for requestData)
- doCreateTable(tableId, null, location, request.getProperties(), null);
+ doCreateTable(tableId, null, location, null, null);
CreateEmptyTableResponse response = new CreateEmptyTableResponse();
response.setLocation(location);
@@ -351,22 +315,20 @@ public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request
}
@Override
- public DropTableResponse dropTable(DropTableRequest request) {
+ public DeregisterTableResponse deregisterTable(DeregisterTableRequest request) {
ObjectIdentifier tableId = ObjectIdentifier.of(request.getId());
ValidationUtil.checkArgument(
tableId.levels() == 2, "Expect 2-level table identifier but get %s", tableId);
String location = doDropTable(tableId);
- // TODO: remove data
- DropTableResponse response = new DropTableResponse();
- response.setLocation(location);
+ DeregisterTableResponse response = new DeregisterTableResponse();
response.setId(request.getId());
+ response.setLocation(location);
return response;
}
- @Override
public void setConf(Configuration conf) {
this.hadoopConf = conf;
}
@@ -383,16 +345,13 @@ protected List doListNamespaces(ObjectIdentifier parent) {
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed to list namespaces: " + errorMessage,
- HiveMetaStoreError.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(
+ "Failed to list namespaces: " + errorMessage, HiveMetaStoreError.getType(), "");
}
}
protected void doCreateNamespace(
- ObjectIdentifier id, CreateNamespaceRequest.ModeEnum mode, Map properties) {
+ ObjectIdentifier id, String mode, Map properties) {
try {
String db = id.levelAtListPos(0).toLowerCase();
@@ -402,34 +361,26 @@ protected void doCreateNamespace(
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed to create namespace: " + errorMessage,
- HiveMetaStoreError.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(
+ "Failed to create namespace: " + errorMessage, HiveMetaStoreError.getType(), "");
}
}
- private void createDatabase(
- String dbName, CreateNamespaceRequest.ModeEnum mode, Map properties)
+ private void createDatabase(String dbName, String mode, Map properties)
throws TException, InterruptedException {
Database oldDb = Hive2Util.getDatabaseOrNull(clientPool, dbName);
if (oldDb != null) {
- switch (mode) {
- case CREATE:
- throw LanceNamespaceException.conflict(
- String.format("Database %s already exist", dbName),
- DatabaseAlreadyExist.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
- case EXIST_OK:
- return;
- case OVERWRITE:
- clientPool.run(
- client -> {
- client.dropDatabase(dbName, false, true, false);
- return null;
- });
+ if ("create".equals(mode)) {
+ throw new NamespaceAlreadyExistsException(
+ String.format("Database %s already exist", dbName), DatabaseAlreadyExist.getType(), "");
+ } else if ("exist_ok".equals(mode) || "existok".equals(mode)) {
+ return;
+ } else if ("overwrite".equals(mode)) {
+ clientPool.run(
+ client -> {
+ client.dropDatabase(dbName, false, true, false);
+ return null;
+ });
}
}
@@ -491,11 +442,10 @@ protected void doCreateTable(
try {
Optional existing = Hive2Util.getTable(clientPool, db, tableName);
if (existing.isPresent()) {
- throw LanceNamespaceException.conflict(
+ throw new TableAlreadyExistsException(
String.format("Table %s.%s already exists", db, tableName),
TableAlreadyExists.getType(),
- String.format("%s.%s", db, tableName),
- CommonUtil.formatCurrentStackTrace());
+ String.format("%s.%s", db, tableName));
}
Table table = new Table();
@@ -537,11 +487,8 @@ protected void doCreateTable(
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed to create table: " + errorMessage,
- HiveMetaStoreError.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(
+ "Failed to create table: " + errorMessage, HiveMetaStoreError.getType(), "");
}
}
@@ -550,11 +497,8 @@ protected List doListTables(String db) {
// First validate that database exists
Database database = Hive2Util.getDatabaseOrNull(clientPool, db);
if (database == null) {
- throw LanceNamespaceException.notFound(
- String.format("Database %s doesn't exist", db),
- HiveMetaStoreError.getType(),
- db,
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(
+ String.format("Database %s doesn't exist", db), HiveMetaStoreError.getType(), db);
}
List allTables = clientPool.run(client -> client.getAllTables(db));
@@ -581,11 +525,8 @@ protected List doListTables(String db) {
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed to list tables: " + errorMessage,
- HiveMetaStoreError.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(
+ "Failed to list tables: " + errorMessage, HiveMetaStoreError.getType(), "");
}
}
@@ -596,11 +537,10 @@ protected String doDropTable(ObjectIdentifier id) {
try {
Optional hmsTable = Hive2Util.getTable(clientPool, db, tableName);
if (!hmsTable.isPresent()) {
- throw LanceNamespaceException.notFound(
+ throw new TableNotFoundException(
String.format("Table %s.%s does not exist", db, tableName),
TableNotFound.getType(),
- String.format("%s.%s", db, tableName),
- CommonUtil.formatCurrentStackTrace());
+ String.format("%s.%s", db, tableName));
}
Hive2Util.validateLanceTable(hmsTable.get());
@@ -618,63 +558,35 @@ protected String doDropTable(ObjectIdentifier id) {
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed to drop table: " + errorMessage,
- HiveMetaStoreError.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(
+ "Failed to drop table: " + errorMessage, HiveMetaStoreError.getType(), "");
}
}
- protected Map doDropNamespace(
- ObjectIdentifier id,
- DropNamespaceRequest.ModeEnum mode,
- DropNamespaceRequest.BehaviorEnum behavior) {
+ protected Map doDropNamespace(ObjectIdentifier id, String mode, String behavior) {
String db = id.levelAtListPos(0).toLowerCase();
try {
Database database = Hive2Util.getDatabaseOrNull(clientPool, db);
if (database == null) {
- if (mode == DropNamespaceRequest.ModeEnum.SKIP) {
- // Return empty properties for SKIP mode when namespace doesn't exist
+ if ("skip".equals(mode)) {
return new HashMap<>();
} else {
- throw LanceNamespaceException.notFound(
- String.format("Database %s doesn't exist", db),
- HiveMetaStoreError.getType(),
- db,
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(
+ String.format("Database %s doesn't exist", db), HiveMetaStoreError.getType(), db);
}
}
- // Check if database contains tables
- List tables = doListTables(db);
- if (!tables.isEmpty()) {
- if (behavior == DropNamespaceRequest.BehaviorEnum.RESTRICT) {
- throw LanceNamespaceException.badRequest(
+ // Check if database contains tables (RESTRICT behavior only, not for Cascade)
+ boolean cascade = "Cascade".equalsIgnoreCase(behavior);
+ if (!cascade) {
+ List tables = doListTables(db);
+ if (!tables.isEmpty()) {
+ throw new InvalidInputException(
String.format(
"Database %s is not empty. Contains %d tables: %s", db, tables.size(), tables),
HiveMetaStoreError.getType(),
- db,
- CommonUtil.formatCurrentStackTrace());
- } else if (behavior == DropNamespaceRequest.BehaviorEnum.CASCADE) {
- // Drop all tables first
- for (String tableName : tables) {
- try {
- ObjectIdentifier tableId = ObjectIdentifier.of(Lists.newArrayList(db, tableName));
- doDropTable(tableId);
- LOG.info("Dropped table {}.{} during CASCADE operation", db, tableName);
- } catch (Exception e) {
- LOG.warn("Failed to drop table {}.{}: {}", db, tableName, e.getMessage());
- throw LanceNamespaceException.serviceUnavailable(
- String.format(
- "Failed to drop table %s.%s during CASCADE operation: %s",
- db, tableName, e.getMessage()),
- HiveMetaStoreError.getType(),
- String.format("%s.%s", db, tableName),
- CommonUtil.formatCurrentStackTrace());
- }
- }
+ db);
}
}
@@ -697,9 +609,10 @@ protected Map doDropNamespace(
}
// Drop the database
+ final boolean cascadeDrop = cascade;
clientPool.run(
client -> {
- client.dropDatabase(db, false, true, false);
+ client.dropDatabase(db, false, true, cascadeDrop);
return null;
});
@@ -710,11 +623,8 @@ protected Map doDropNamespace(
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed to drop namespace: " + errorMessage,
- HiveMetaStoreError.getType(),
- db,
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(
+ "Failed to drop namespace: " + errorMessage, HiveMetaStoreError.getType(), db);
}
}
diff --git a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2NamespaceConfig.java b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2NamespaceConfig.java
index 9237c92..28a5e41 100644
--- a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2NamespaceConfig.java
+++ b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2NamespaceConfig.java
@@ -13,9 +13,7 @@
*/
package org.lance.namespace.hive2;
-import org.lance.namespace.util.OpenDalUtil;
-import org.lance.namespace.util.PropertyUtil;
-
+import java.util.HashMap;
import java.util.Map;
public class Hive2NamespaceConfig {
@@ -41,12 +39,27 @@ public class Hive2NamespaceConfig {
private final String root;
public Hive2NamespaceConfig(Map properties) {
+ // Inline PropertyUtil.propertyAsInt
+ String clientPoolSizeStr = properties.get(CLIENT_POOL_SIZE);
this.clientPoolSize =
- PropertyUtil.propertyAsInt(properties, CLIENT_POOL_SIZE, CLIENT_POOL_SIZE_DEFAULT);
- this.storageOptions = PropertyUtil.propertiesWithPrefix(properties, STORAGE_OPTIONS_PREFIX);
+ clientPoolSizeStr != null ? Integer.parseInt(clientPoolSizeStr) : CLIENT_POOL_SIZE_DEFAULT;
+
+ // Inline PropertyUtil.propertiesWithPrefix
+ Map filteredStorageOptions = new HashMap<>();
+ for (Map.Entry entry : properties.entrySet()) {
+ if (entry.getKey().startsWith(STORAGE_OPTIONS_PREFIX)) {
+ filteredStorageOptions.put(
+ entry.getKey().substring(STORAGE_OPTIONS_PREFIX.length()), entry.getValue());
+ }
+ }
+ this.storageOptions = filteredStorageOptions;
+
+ // Inline PropertyUtil.propertyAsString and OpenDalUtil.stripTrailingSlash
+ String rootValue = properties.getOrDefault(ROOT, ROOT_DEFAULT);
this.root =
- OpenDalUtil.stripTrailingSlash(
- PropertyUtil.propertyAsString(properties, ROOT, ROOT_DEFAULT));
+ rootValue != null && rootValue.endsWith("/")
+ ? rootValue.substring(0, rootValue.length() - 1)
+ : rootValue;
}
public int getClientPoolSize() {
diff --git a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2Util.java b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2Util.java
index 4f26739..23c5374 100644
--- a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2Util.java
+++ b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/Hive2Util.java
@@ -13,8 +13,8 @@
*/
package org.lance.namespace.hive2;
-import org.lance.namespace.LanceNamespaceException;
-import org.lance.namespace.util.CommonUtil;
+import org.lance.namespace.errors.InvalidInputException;
+import org.lance.namespace.errors.ServiceUnavailableException;
import com.google.common.collect.Maps;
import org.apache.hadoop.hive.metastore.api.Database;
@@ -44,8 +44,7 @@ public static Database getDatabaseOrNull(Hive2ClientPool clientPool, String db)
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
- throw LanceNamespaceException.serviceUnavailable(
- e.getMessage(), HiveMetaStoreError.getType(), "", CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(e.getMessage(), HiveMetaStoreError.getType(), "");
}
}
@@ -103,20 +102,18 @@ public static Optional getTable(Hive2ClientPool clientPool, String db, St
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
- throw LanceNamespaceException.serviceUnavailable(
- e.getMessage(), HiveMetaStoreError.getType(), "", CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(e.getMessage(), HiveMetaStoreError.getType(), "");
}
}
public static void validateLanceTable(Table table) {
Map params = table.getParameters();
if (params == null || !"lance".equalsIgnoreCase(params.get("table_type"))) {
- throw LanceNamespaceException.badRequest(
+ throw new InvalidInputException(
String.format(
"Table %s.%s is not a Lance table", table.getDbName(), table.getTableName()),
InvalidLanceTable.getType(),
- String.format("%s.%s", table.getDbName(), table.getTableName()),
- CommonUtil.formatCurrentStackTrace());
+ String.format("%s.%s", table.getDbName(), table.getTableName()));
}
}
diff --git a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/ObjectIdentifier.java b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/ObjectIdentifier.java
new file mode 100644
index 0000000..af3f0de
--- /dev/null
+++ b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/ObjectIdentifier.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed 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.lance.namespace.hive2;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Represents a hierarchical identifier for namespaces and tables. */
+public class ObjectIdentifier {
+ private final List levels;
+
+ private ObjectIdentifier(List levels) {
+ this.levels = levels != null ? levels : Collections.emptyList();
+ }
+
+ public static ObjectIdentifier of(List levels) {
+ return new ObjectIdentifier(levels);
+ }
+
+ public boolean isRoot() {
+ return levels.isEmpty();
+ }
+
+ public int levels() {
+ return levels.size();
+ }
+
+ public String levelAtListPos(int pos) {
+ if (pos < 0 || pos >= levels.size()) {
+ throw new IndexOutOfBoundsException(
+ "Position " + pos + " is out of bounds for size " + levels.size());
+ }
+ return levels.get(pos);
+ }
+
+ public String stringStyleId() {
+ return String.join(".", levels);
+ }
+
+ @Override
+ public String toString() {
+ return stringStyleId();
+ }
+}
diff --git a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/PageUtil.java b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/PageUtil.java
new file mode 100644
index 0000000..3acf941
--- /dev/null
+++ b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/PageUtil.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed 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.lance.namespace.hive2;
+
+import java.util.List;
+
+/** Utility methods for pagination. */
+public class PageUtil {
+
+ private static final int DEFAULT_PAGE_SIZE = 100;
+
+ private PageUtil() {}
+
+ public static int normalizePageSize(Integer pageSize) {
+ if (pageSize == null || pageSize <= 0) {
+ return DEFAULT_PAGE_SIZE;
+ }
+ return pageSize;
+ }
+
+ public static Page splitPage(List items, String pageToken, int pageSize) {
+ int startIndex = 0;
+ if (pageToken != null && !pageToken.isEmpty()) {
+ try {
+ startIndex = Integer.parseInt(pageToken);
+ } catch (NumberFormatException e) {
+ startIndex = 0;
+ }
+ }
+
+ if (startIndex >= items.size()) {
+ return new Page(java.util.Collections.emptyList(), null);
+ }
+
+ int endIndex = Math.min(startIndex + pageSize, items.size());
+ List pageItems = items.subList(startIndex, endIndex);
+
+ String nextPageToken = endIndex < items.size() ? String.valueOf(endIndex) : null;
+ return new Page(pageItems, nextPageToken);
+ }
+
+ public static class Page {
+ private final List items;
+ private final String nextPageToken;
+
+ public Page(List items, String nextPageToken) {
+ this.items = items;
+ this.nextPageToken = nextPageToken;
+ }
+
+ public List items() {
+ return items;
+ }
+
+ public String nextPageToken() {
+ return nextPageToken;
+ }
+ }
+}
diff --git a/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/ValidationUtil.java b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/ValidationUtil.java
new file mode 100644
index 0000000..3652187
--- /dev/null
+++ b/java/lance-namespace-hive2/src/main/java/org/lance/namespace/hive2/ValidationUtil.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed 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.lance.namespace.hive2;
+
+import org.lance.namespace.errors.InvalidInputException;
+
+/** Utility methods for validation. */
+public class ValidationUtil {
+
+ private ValidationUtil() {}
+
+ public static void checkArgument(boolean expression, String message, Object... args) {
+ if (!expression) {
+ throw new InvalidInputException(String.format(message, args));
+ }
+ }
+
+ public static String checkNotNullOrEmptyString(String value, String message) {
+ if (value == null || value.isEmpty()) {
+ throw new InvalidInputException(message);
+ }
+ return value;
+ }
+}
diff --git a/java/lance-namespace-hive2/src/test/java/org/lance/namespace/hive2/TestHive2Namespace.java b/java/lance-namespace-hive2/src/test/java/org/lance/namespace/hive2/TestHive2Namespace.java
index c6073e8..27bbb82 100644
--- a/java/lance-namespace-hive2/src/test/java/org/lance/namespace/hive2/TestHive2Namespace.java
+++ b/java/lance-namespace-hive2/src/test/java/org/lance/namespace/hive2/TestHive2Namespace.java
@@ -14,20 +14,13 @@
package org.lance.namespace.hive2;
import org.lance.namespace.LanceNamespace;
-import org.lance.namespace.LanceNamespaceException;
-import org.lance.namespace.LanceNamespaces;
-import org.lance.namespace.TestHelper;
+import org.lance.namespace.errors.LanceNamespaceException;
import org.lance.namespace.model.CreateNamespaceRequest;
-import org.lance.namespace.model.CreateTableRequest;
-import org.lance.namespace.model.CreateTableResponse;
import org.lance.namespace.model.DescribeNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
import org.lance.namespace.model.DescribeTableRequest;
-import org.lance.namespace.model.DescribeTableResponse;
import org.lance.namespace.model.DropNamespaceRequest;
import org.lance.namespace.model.DropNamespaceResponse;
-import org.lance.namespace.model.DropTableRequest;
-import org.lance.namespace.model.DropTableResponse;
import org.lance.namespace.model.ListTablesRequest;
import org.lance.namespace.model.ListTablesResponse;
import org.lance.namespace.model.NamespaceExistsRequest;
@@ -41,7 +34,6 @@
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.File;
@@ -74,7 +66,10 @@ public static void setup() throws IOException {
tmpDirBase = file.getAbsolutePath();
HiveConf hiveConf = metastore.hiveConf();
- namespace = LanceNamespaces.connect("hive2", Maps.newHashMap(), hiveConf, allocator);
+ Hive2Namespace hive2Namespace = new Hive2Namespace();
+ hive2Namespace.setHadoopConf(hiveConf);
+ hive2Namespace.initialize(Maps.newHashMap(), allocator);
+ namespace = hive2Namespace;
}
@AfterAll
@@ -97,123 +92,12 @@ public void cleanup() throws Exception {
metastore.reset();
}
- @Disabled("Need to figure out the proper interface")
- @Test
- public void testCreateTable() throws IOException {
- // Setup: Create database
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- // Test: Create table with valid parameters
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_db", "test_table"));
- request.setLocation(tmpDirBase + "/test_db/test_table.lance");
-
- Map properties = Maps.newHashMap();
- properties.put("custom_prop", "custom_value");
- request.setProperties(properties);
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- CreateTableResponse response = namespace.createTable(request, testData);
-
- assertEquals(request.getLocation(), response.getLocation());
- assertEquals(1L, response.getVersion());
- }
-
- @Test
- public void testCreateTableAlreadyExists() throws IOException {
- // Setup: Create database and table
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_db", "test_table"));
- request.setLocation(tmpDirBase + "/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(request, testData);
-
- // Test: Create table that already exists
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.createTable(request, testData));
- assertTrue(error.getMessage().contains("Table test_db.test_table already exists"));
- }
-
- @Test
- public void testCreateTableManagedByImpl() throws IOException {
- // Setup: Create database
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- // Test: Create table with managed_by=impl (not supported)
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_db", "impl_table"));
- request.setLocation(tmpDirBase + "/test_db/impl_table.lance");
-
- Map properties = Maps.newHashMap();
- properties.put("managed_by", "impl");
- request.setProperties(properties);
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- Exception error =
- assertThrows(
- UnsupportedOperationException.class, () -> namespace.createTable(request, testData));
- assertTrue(error.getMessage().contains("managed_by=impl is not supported yet"));
- }
-
- @Test
- public void testCreateTableWithoutData() throws IOException {
- // Setup: Create database
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- // Test: Create table without data
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_db", "no_data_table"));
- request.setLocation(tmpDirBase + "/test_db/no_data_table.lance");
-
- byte[] emptyData = TestHelper.createEmptyArrowData(allocator);
- CreateTableResponse response = namespace.createTable(request, emptyData);
- assertEquals(request.getLocation(), response.getLocation());
- }
-
- @Test
- public void testDescribeTable() throws IOException {
- // Setup: Create database and table
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_db", "test_table"));
- createRequest.setLocation(tmpDirBase + "/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: Describe existing Lance table
- DescribeTableRequest request = new DescribeTableRequest();
- request.setId(Lists.list("test_db", "test_table"));
-
- DescribeTableResponse response = namespace.describeTable(request);
- assertEquals("file:" + tmpDirBase + "/test_db/test_table.lance", response.getLocation());
- }
-
@Test
public void testDescribeNonExistentTable() {
// Setup: Create database
CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ nsRequest.setMode("Create");
namespace.createNamespace(nsRequest);
// Test: Describe non-existent table
@@ -224,133 +108,12 @@ public void testDescribeNonExistentTable() {
assertTrue(error.getMessage().contains("Table does not exist"));
}
- @Test
- public void testDropTable() throws IOException {
- // Setup: Create database and table
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_db", "test_table"));
- createRequest.setLocation(tmpDirBase + "/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: Drop existing table
- DropTableRequest request = new DropTableRequest();
- request.setId(Lists.list("test_db", "test_table"));
-
- DropTableResponse response = namespace.dropTable(request);
- assertEquals("file:" + tmpDirBase + "/test_db/test_table.lance", response.getLocation());
- assertEquals(request.getId(), response.getId());
-
- // Verify table is dropped by trying to describe it
- DescribeTableRequest descRequest = new DescribeTableRequest();
- descRequest.setId(request.getId());
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.describeTable(descRequest));
- assertTrue(error.getMessage().contains("Table does not exist"));
- }
-
- @Test
- public void testDropNonExistentTable() {
- // Setup: Create database
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- // Test: Drop non-existent table
- DropTableRequest request = new DropTableRequest();
- request.setId(Lists.list("test_db", "non_existent"));
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.dropTable(request));
- assertTrue(error.getMessage().contains("Table test_db.non_existent does not exist"));
- }
-
- @Test
- public void testCreateTableWithDefaultLocationFromRoot() throws IOException {
- // With our enhancement, databases created without explicit location
- // will use the root config location instead of Hive warehouse
-
- // Setup: Create namespace with custom root configuration
- Map properties = Maps.newHashMap();
- properties.put("root", tmpDirBase);
-
- HiveConf hiveConf = metastore.hiveConf();
- LanceNamespace customNamespace =
- LanceNamespaces.connect("hive2", properties, hiveConf, allocator);
-
- // Setup: Create database (will use root location)
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- customNamespace.createNamespace(nsRequest);
-
- // Test: Create table without specifying location
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_db", "test_table"));
- // Don't set location - it will be derived from database location
-
- // Create test Arrow IPC data
- byte[] testData = TestHelper.createTestArrowData(allocator);
- CreateTableResponse response = customNamespace.createTable(request, testData);
-
- // Verify: Location should be derived from root-based database location
- // Hive adds file: prefix to locations
- String expectedLocation = "file:" + tmpDirBase + "/test_db/test_table.lance";
- assertEquals(expectedLocation, response.getLocation());
- assertEquals(1L, response.getVersion());
- }
-
- @Test
- public void testCreateTableWithDefaultLocationFromDatabaseLocation() throws IOException {
- // Setup: Create namespace with custom root configuration
- Map properties = Maps.newHashMap();
- properties.put("root", tmpDirBase);
-
- HiveConf hiveConf = metastore.hiveConf();
- LanceNamespace customNamespace =
- LanceNamespaces.connect("hive2", properties, hiveConf, allocator);
-
- // Setup: Create database with specific location
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db_with_location"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
-
- // Set database location - this should take precedence over root config
- String databaseLocation = tmpDirBase + "/custom_db_location";
- Map dbProperties = Maps.newHashMap();
- dbProperties.put("database.location-uri", databaseLocation);
- nsRequest.setProperties(dbProperties);
-
- customNamespace.createNamespace(nsRequest);
-
- // Test: Create table without specifying location (should derive from database location)
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_db_with_location", "test_table"));
- // Don't set location - it should be derived from database location
-
- // Create test Arrow IPC data
- byte[] testData = TestHelper.createTestArrowData(allocator);
- CreateTableResponse response = customNamespace.createTable(request, testData);
-
- // Verify: Location should be derived as {database_location}/{table}.lance
- // Database locations in Hive typically have file: prefix
- String expectedLocation = "file:" + databaseLocation + "/test_table.lance";
- assertEquals(expectedLocation, response.getLocation());
- assertEquals(1L, response.getVersion());
- }
-
@Test
public void testDescribeNamespace() {
// Setup: Create database
CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ nsRequest.setMode("Create");
Map properties = Maps.newHashMap();
properties.put("database.description", "Test database description");
@@ -388,7 +151,7 @@ public void testNamespaceExists() {
// Setup: Create database
CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ nsRequest.setMode("Create");
namespace.createNamespace(nsRequest);
// Test: Check existing namespace
@@ -410,35 +173,12 @@ public void testNamespaceExistsNonExistent() {
assertTrue(error.getMessage().contains("Namespace does not exist"));
}
- @Test
- public void testTableExists() throws IOException {
- // Setup: Create database and table
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_db", "test_table"));
- createRequest.setLocation(tmpDirBase + "/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: Check existing table
- TableExistsRequest request = new TableExistsRequest();
- request.setId(Lists.list("test_db", "test_table"));
-
- // Should not throw exception for existing Lance table
- namespace.tableExists(request);
- }
-
@Test
public void testTableExistsNonExistent() {
// Setup: Create database
CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ nsRequest.setMode("Create");
namespace.createNamespace(nsRequest);
// Test: Check non-existent table
@@ -450,46 +190,12 @@ public void testTableExistsNonExistent() {
assertTrue(error.getMessage().contains("Table does not exist"));
}
- @Test
- public void testListTables() throws IOException {
- // Setup: Create database and multiple tables
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- // Create first table
- CreateTableRequest createRequest1 = new CreateTableRequest();
- createRequest1.setId(Lists.list("test_db", "table1"));
- createRequest1.setLocation(tmpDirBase + "/test_db/table1.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest1, testData);
-
- // Create second table
- CreateTableRequest createRequest2 = new CreateTableRequest();
- createRequest2.setId(Lists.list("test_db", "table2"));
- createRequest2.setLocation(tmpDirBase + "/test_db/table2.lance");
-
- namespace.createTable(createRequest2, testData);
-
- // Test: List tables
- ListTablesRequest request = new ListTablesRequest();
- request.setId(Lists.list("test_db"));
-
- ListTablesResponse response = namespace.listTables(request);
-
- assertEquals(2, response.getTables().size());
- assertTrue(response.getTables().contains("table1"));
- assertTrue(response.getTables().contains("table2"));
- }
-
@Test
public void testListTablesEmpty() {
// Setup: Create empty database
CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
nsRequest.setId(Lists.list("empty_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ nsRequest.setMode("Create");
namespace.createNamespace(nsRequest);
// Test: List tables in empty database
@@ -501,47 +207,6 @@ public void testListTablesEmpty() {
assertEquals(0, response.getTables().size());
}
- @Test
- public void testListTablesWithPagination() throws IOException {
- // Setup: Create database and multiple tables
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- // Create multiple tables
- for (int i = 1; i <= 5; i++) {
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_db", "table" + i));
- createRequest.setLocation(tmpDirBase + "/test_db/table" + i + ".lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
- }
-
- // Test: List tables with pagination (limit 3)
- ListTablesRequest request = new ListTablesRequest();
- request.setId(Lists.list("test_db"));
- request.setLimit(3);
-
- ListTablesResponse response = namespace.listTables(request);
-
- assertEquals(3, response.getTables().size());
- // Should have a page token for remaining results
- assertTrue(response.getPageToken() != null && !response.getPageToken().isEmpty());
-
- // Get remaining tables
- ListTablesRequest nextRequest = new ListTablesRequest();
- nextRequest.setId(Lists.list("test_db"));
- nextRequest.setPageToken(response.getPageToken());
-
- ListTablesResponse nextResponse = namespace.listTables(nextRequest);
-
- assertEquals(2, nextResponse.getTables().size());
- // No more pages
- assertTrue(nextResponse.getPageToken() == null || nextResponse.getPageToken().isEmpty());
- }
-
@Test
public void testListTablesNonExistentDatabase() {
// Test: List tables in non-existent database
@@ -558,7 +223,7 @@ public void testDropNamespaceBasic() {
// Setup: Create database
CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
nsRequest.setId(Lists.list("test_db_basic"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ nsRequest.setMode("Create");
Map properties = Maps.newHashMap();
properties.put("database.description", "Test database for dropping");
@@ -592,7 +257,7 @@ public void testDropNamespaceSkipMode() {
// Test: Drop non-existent namespace with SKIP mode
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Lists.list("non_existent_db"));
- dropRequest.setMode(DropNamespaceRequest.ModeEnum.SKIP);
+ dropRequest.setMode("Skip");
DropNamespaceResponse response = namespace.dropNamespace(dropRequest);
@@ -605,78 +270,10 @@ public void testDropNamespaceFailMode() {
// Test: Drop non-existent namespace with FAIL mode (default)
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Lists.list("non_existent_db"));
- dropRequest.setMode(DropNamespaceRequest.ModeEnum.FAIL);
+ dropRequest.setMode("Fail");
Exception error =
assertThrows(LanceNamespaceException.class, () -> namespace.dropNamespace(dropRequest));
assertTrue(error.getMessage().contains("Database non_existent_db doesn't exist"));
}
-
- @Test
- public void testDropNamespaceRestrictWithTables() throws IOException {
- // Setup: Create database and table
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db_restrict"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_db_restrict", "test_table"));
- createRequest.setLocation(tmpDirBase + "/test_db_restrict/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: Try to drop namespace with RESTRICT behavior (should fail)
- DropNamespaceRequest dropRequest = new DropNamespaceRequest();
- dropRequest.setId(Lists.list("test_db_restrict"));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.RESTRICT);
-
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.dropNamespace(dropRequest));
- assertTrue(error.getMessage().contains("Database test_db_restrict is not empty"));
- assertTrue(error.getMessage().contains("Contains 1 tables"));
- }
-
- @Test
- public void testDropNamespaceCascadeWithTables() throws IOException {
- // Setup: Create database and multiple tables
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_db_cascade"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- // Create first table
- CreateTableRequest createRequest1 = new CreateTableRequest();
- createRequest1.setId(Lists.list("test_db_cascade", "table1"));
- createRequest1.setLocation(tmpDirBase + "/test_db_cascade/table1.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest1, testData);
-
- // Create second table
- CreateTableRequest createRequest2 = new CreateTableRequest();
- createRequest2.setId(Lists.list("test_db_cascade", "table2"));
- createRequest2.setLocation(tmpDirBase + "/test_db_cascade/table2.lance");
-
- namespace.createTable(createRequest2, testData);
-
- // Test: Drop namespace with CASCADE behavior
- DropNamespaceRequest dropRequest = new DropNamespaceRequest();
- dropRequest.setId(Lists.list("test_db_cascade"));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE);
-
- DropNamespaceResponse response = namespace.dropNamespace(dropRequest);
-
- // Verify namespace properties were returned
- assertTrue(response.getProperties().containsKey("database.location-uri"));
-
- // Verify namespace was dropped
- NamespaceExistsRequest existsRequest = new NamespaceExistsRequest();
- existsRequest.setId(Lists.list("test_db_cascade"));
-
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.namespaceExists(existsRequest));
- assertTrue(error.getMessage().contains("Namespace does not exist"));
- }
}
diff --git a/java/lance-namespace-hive2/src/test/java/org/lance/namespace/hive2/TestHive2NamespaceIntegration.java b/java/lance-namespace-hive2/src/test/java/org/lance/namespace/hive2/TestHive2NamespaceIntegration.java
index ca95529..e6ba013 100644
--- a/java/lance-namespace-hive2/src/test/java/org/lance/namespace/hive2/TestHive2NamespaceIntegration.java
+++ b/java/lance-namespace-hive2/src/test/java/org/lance/namespace/hive2/TestHive2NamespaceIntegration.java
@@ -13,17 +13,18 @@
*/
package org.lance.namespace.hive2;
-import org.lance.namespace.LanceNamespaceException;
+import org.lance.namespace.errors.InvalidInputException;
+import org.lance.namespace.errors.LanceNamespaceException;
import org.lance.namespace.model.CreateEmptyTableRequest;
import org.lance.namespace.model.CreateEmptyTableResponse;
import org.lance.namespace.model.CreateNamespaceRequest;
import org.lance.namespace.model.CreateNamespaceResponse;
+import org.lance.namespace.model.DeregisterTableRequest;
import org.lance.namespace.model.DescribeNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
import org.lance.namespace.model.DescribeTableRequest;
import org.lance.namespace.model.DescribeTableResponse;
import org.lance.namespace.model.DropNamespaceRequest;
-import org.lance.namespace.model.DropTableRequest;
import org.lance.namespace.model.ListNamespacesRequest;
import org.lance.namespace.model.ListNamespacesResponse;
import org.lance.namespace.model.ListTablesRequest;
@@ -114,19 +115,13 @@ public void tearDown() {
// Clean up test database
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Collections.singletonList(testDatabase));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE);
+ dropRequest.setBehavior("Restrict");
namespace.dropNamespace(dropRequest);
} catch (Exception e) {
// Ignore cleanup errors
}
- if (namespace != null) {
- try {
- namespace.close();
- } catch (Exception e) {
- // Ignore
- }
- }
+ // Namespace cleanup handled by Hive internals
if (allocator != null) {
allocator.close();
@@ -163,8 +158,8 @@ public void testDatabaseOperations() {
DescribeNamespaceResponse describeResponse = namespace.describeNamespace(describeRequest);
assertThat(describeResponse).isNotNull();
- assertThat(describeResponse.getProperties()).containsEntry(
- "database.description", "Integration test database");
+ assertThat(describeResponse.getProperties())
+ .containsEntry("database.description", "Integration test database");
// List databases
ListNamespacesRequest listRequest = new ListNamespacesRequest();
@@ -190,7 +185,8 @@ public void testTableOperations() {
nsRequest.setId(Collections.singletonList(testDatabase));
namespace.createNamespace(nsRequest);
- String tableName = "test_table_" + UUID.randomUUID().toString().substring(0, 8).replace("-", "");
+ String tableName =
+ "test_table_" + UUID.randomUUID().toString().substring(0, 8).replace("-", "");
// Create empty table (declare table without data)
CreateEmptyTableRequest createRequest = new CreateEmptyTableRequest();
@@ -206,7 +202,6 @@ public void testTableOperations() {
DescribeTableResponse describeResponse = namespace.describeTable(describeRequest);
assertThat(describeResponse.getLocation()).contains(tableName);
- assertThat(describeResponse.getProperties()).containsEntry("table_type", "lance");
// List tables
ListTablesRequest listRequest = new ListTablesRequest();
@@ -215,10 +210,10 @@ public void testTableOperations() {
ListTablesResponse listResponse = namespace.listTables(listRequest);
assertThat(listResponse.getTables()).contains(tableName);
- // Drop table
- DropTableRequest dropRequest = new DropTableRequest();
- dropRequest.setId(Arrays.asList(testDatabase, tableName));
- namespace.dropTable(dropRequest);
+ // Deregister table
+ DeregisterTableRequest deregisterRequest = new DeregisterTableRequest();
+ deregisterRequest.setId(Arrays.asList(testDatabase, tableName));
+ namespace.deregisterTable(deregisterRequest);
// Verify table doesn't exist
assertThatThrownBy(() -> namespace.describeTable(describeRequest))
@@ -226,29 +221,14 @@ public void testTableOperations() {
}
@Test
- public void testCascadeDropDatabase() {
- // Create database
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Collections.singletonList(testDatabase));
- namespace.createNamespace(nsRequest);
-
- // Create a table in the database
- String tableName = "cascade_test_table";
- CreateEmptyTableRequest tableRequest = new CreateEmptyTableRequest();
- tableRequest.setId(Arrays.asList(testDatabase, tableName));
- tableRequest.setLocation("/tmp/lance-integration-test/" + testDatabase + "/" + tableName);
- namespace.createEmptyTable(tableRequest);
-
- // Drop database with cascade
+ public void testCascadeDropDatabaseRejected() {
+ // Drop database with cascade - should be rejected
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Collections.singletonList(testDatabase));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE);
- namespace.dropNamespace(dropRequest);
+ dropRequest.setBehavior("Cascade");
- // Verify database doesn't exist
- DescribeNamespaceRequest describeRequest = new DescribeNamespaceRequest();
- describeRequest.setId(Collections.singletonList(testDatabase));
- assertThatThrownBy(() -> namespace.describeNamespace(describeRequest))
- .isInstanceOf(LanceNamespaceException.class);
+ assertThatThrownBy(() -> namespace.dropNamespace(dropRequest))
+ .isInstanceOf(InvalidInputException.class)
+ .hasMessageContaining("Cascade behavior is not supported");
}
}
diff --git a/java/lance-namespace-hive3/derby.log b/java/lance-namespace-hive3/derby.log
new file mode 100644
index 0000000..9063ad7
--- /dev/null
+++ b/java/lance-namespace-hive3/derby.log
@@ -0,0 +1,13 @@
+----------------------------------------------------------------
+Tue Dec 30 21:25:14 PST 2025:
+Booting Derby version The Apache Software Foundation - Apache Derby - 10.14.2.0 - (1828579): instance a816c00e-019b-72de-0fb6-00000746a618
+on database directory /Users/zhaoqinye/oss/lance-namespace-impls/java/lance-namespace-hive3/metastore_db with class loader jdk.internal.loader.ClassLoaders$AppClassLoader@5ffd2b27
+Loaded from file:/Users/zhaoqinye/.m2/repository/org/apache/derby/derby/10.14.2.0/derby-10.14.2.0.jar
+java.vendor=Amazon.com Inc.
+java.runtime.version=17.0.14+7-LTS
+user.dir=/Users/zhaoqinye/oss/lance-namespace-impls/java/lance-namespace-hive3
+os.name=Mac OS X
+os.arch=aarch64
+os.version=15.5
+derby.system.home=null
+Database Class Loader started - derby.database.classpath=''
diff --git a/java/lance-namespace-hive3/pom.xml b/java/lance-namespace-hive3/pom.xml
index f16eaa9..54616be 100644
--- a/java/lance-namespace-hive3/pom.xml
+++ b/java/lance-namespace-hive3/pom.xml
@@ -22,6 +22,10 @@
org.lance
lance-core
+
+ org.lance
+ lance-namespace-core
+
org.lance
lance-namespace-apache-client
@@ -73,6 +77,12 @@
10.14.2.0
test
+
+ org.lance
+ lance-namespace-impls-core
+ ${project.version}
+ test
+
org.junit.jupiter
junit-jupiter
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/ClientPoolImpl.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/ClientPoolImpl.java
new file mode 100644
index 0000000..2a268df
--- /dev/null
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/ClientPoolImpl.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed 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.lance.namespace.hive3;
+
+import java.io.Closeable;
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * A simple connection pool implementation for reusing clients. Adapted from Apache Iceberg.
+ *
+ * @param the client type
+ * @param the exception type thrown by client operations
+ */
+public abstract class ClientPoolImpl implements Closeable {
+
+ private final int poolSize;
+ private final Deque clients;
+ private final Class extends E> reconnectExc;
+ private final boolean retryByDefault;
+ private volatile int currentSize;
+ private boolean closed;
+
+ protected ClientPoolImpl(int poolSize, Class extends E> reconnectExc, boolean retryByDefault) {
+ this.poolSize = poolSize;
+ this.clients = new ArrayDeque<>();
+ this.reconnectExc = reconnectExc;
+ this.retryByDefault = retryByDefault;
+ this.currentSize = 0;
+ this.closed = false;
+ }
+
+ public interface Action {
+ R run(C client) throws E;
+ }
+
+ public R run(Action action) throws E, InterruptedException {
+ return run(action, retryByDefault);
+ }
+
+ public R run(Action action, boolean retry) throws E, InterruptedException {
+ C client = get();
+ try {
+ return action.run(client);
+ } catch (Exception exc) {
+ if (retry && isConnectionException(exc)) {
+ try {
+ client = reconnect(client);
+ } catch (Exception reconnectExc) {
+ release(client);
+ throw (E) exc;
+ }
+ return action.run(client);
+ }
+ throw (E) exc;
+ } finally {
+ release(client);
+ }
+ }
+
+ protected abstract C newClient();
+
+ protected abstract C reconnect(C client);
+
+ protected abstract void close(C client);
+
+ protected boolean isConnectionException(Exception exc) {
+ return reconnectExc.isInstance(exc);
+ }
+
+ private synchronized C get() throws InterruptedException {
+ if (closed) {
+ throw new IllegalStateException("Cannot get a client from a closed pool");
+ }
+
+ while (clients.isEmpty() && currentSize >= poolSize) {
+ wait();
+ }
+
+ if (!clients.isEmpty()) {
+ return clients.removeFirst();
+ }
+
+ currentSize++;
+ return newClient();
+ }
+
+ private synchronized void release(C client) {
+ if (closed) {
+ close(client);
+ } else {
+ clients.addFirst(client);
+ notify();
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ this.closed = true;
+ while (!clients.isEmpty()) {
+ close(clients.removeFirst());
+ }
+ notifyAll();
+ }
+}
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/CommonUtil.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/CommonUtil.java
new file mode 100644
index 0000000..9d4194f
--- /dev/null
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/CommonUtil.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed 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.lance.namespace.hive3;
+
+/** Common utility methods. */
+public class CommonUtil {
+
+ private CommonUtil() {}
+
+ public static String formatCurrentStackTrace() {
+ StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 2; i < Math.min(stack.length, 10); i++) {
+ sb.append(stack[i].toString()).append("\n");
+ }
+ return sb.toString();
+ }
+
+ public static String makeQualified(String path) {
+ if (path == null) {
+ return null;
+ }
+ return path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
+ }
+}
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/DynMethods.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/DynMethods.java
new file mode 100644
index 0000000..7bf259a
--- /dev/null
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/DynMethods.java
@@ -0,0 +1,491 @@
+/*
+ * Licensed 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.lance.namespace.hive3;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Arrays;
+
+/** Copied from parquet-common */
+public class DynMethods {
+
+ private DynMethods() {}
+
+ /**
+ * Convenience wrapper class around {@link java.lang.reflect.Method}.
+ *
+ * Allows callers to invoke the wrapped method with all Exceptions wrapped by RuntimeException,
+ * or with a single Exception catch block.
+ */
+ public static class UnboundMethod {
+
+ private final Method method;
+ private final String name;
+ private final int argLength;
+
+ UnboundMethod(Method method, String name) {
+ this.method = method;
+ this.name = name;
+ this.argLength =
+ (method == null || method.isVarArgs()) ? -1 : method.getParameterTypes().length;
+ }
+
+ @SuppressWarnings("unchecked")
+ R invokeChecked(Object target, Object... args) throws Exception {
+ try {
+ if (argLength < 0) {
+ return (R) method.invoke(target, args);
+ } else {
+ return (R) method.invoke(target, Arrays.copyOfRange(args, 0, argLength));
+ }
+
+ } catch (InvocationTargetException e) {
+ Throwables.propagateIfInstanceOf(e.getCause(), Exception.class);
+ Throwables.propagateIfInstanceOf(e.getCause(), RuntimeException.class);
+ throw Throwables.propagate(e.getCause());
+ }
+ }
+
+ public R invoke(Object target, Object... args) {
+ try {
+ return this.invokeChecked(target, args);
+ } catch (Exception e) {
+ Throwables.propagateIfInstanceOf(e, RuntimeException.class);
+ throw Throwables.propagate(e);
+ }
+ }
+
+ /**
+ * Returns this method as a BoundMethod for the given receiver.
+ *
+ * @param receiver an Object to receive the method invocation
+ * @return a {@link BoundMethod} for this method and the receiver
+ * @throws IllegalStateException if the method is static
+ * @throws IllegalArgumentException if the receiver's class is incompatible
+ */
+ public BoundMethod bind(Object receiver) {
+ Preconditions.checkState(
+ !isStatic(), "Cannot bind static method %s", method.toGenericString());
+ Preconditions.checkArgument(
+ method.getDeclaringClass().isAssignableFrom(receiver.getClass()),
+ "Cannot bind %s to instance of %s",
+ method.toGenericString(),
+ receiver.getClass());
+
+ return new BoundMethod(this, receiver);
+ }
+
+ /** Returns whether the method is a static method. */
+ public boolean isStatic() {
+ return Modifier.isStatic(method.getModifiers());
+ }
+
+ /** Returns whether the method is a noop. */
+ public boolean isNoop() {
+ return this == NOOP;
+ }
+
+ /**
+ * Returns this method as a StaticMethod.
+ *
+ * @return a {@link StaticMethod} for this method
+ * @throws IllegalStateException if the method is not static
+ */
+ public StaticMethod asStatic() {
+ Preconditions.checkState(isStatic(), "Method is not static");
+ return new StaticMethod(this);
+ }
+
+ @Override
+ public String toString() {
+ return "DynMethods.UnboundMethod(name=" + name + " method=" + method.toGenericString() + ")";
+ }
+
+ /** Singleton {@link UnboundMethod}, performs no operation and returns null. */
+ private static final UnboundMethod NOOP =
+ new UnboundMethod(null, "NOOP") {
+ @Override
+ R invokeChecked(Object target, Object... args) {
+ return null;
+ }
+
+ @Override
+ public BoundMethod bind(Object receiver) {
+ return new BoundMethod(this, receiver);
+ }
+
+ @Override
+ public StaticMethod asStatic() {
+ return new StaticMethod(this);
+ }
+
+ @Override
+ public boolean isStatic() {
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "DynMethods.UnboundMethod(NOOP)";
+ }
+ };
+ }
+
+ public static class BoundMethod {
+ private final UnboundMethod method;
+ private final Object receiver;
+
+ private BoundMethod(UnboundMethod method, Object receiver) {
+ this.method = method;
+ this.receiver = receiver;
+ }
+
+ public R invokeChecked(Object... args) throws Exception {
+ return method.invokeChecked(receiver, args);
+ }
+
+ public R invoke(Object... args) {
+ return method.invoke(receiver, args);
+ }
+ }
+
+ public static class StaticMethod {
+ private final UnboundMethod method;
+
+ private StaticMethod(UnboundMethod method) {
+ this.method = method;
+ }
+
+ public R invokeChecked(Object... args) throws Exception {
+ return method.invokeChecked(null, args);
+ }
+
+ public R invoke(Object... args) {
+ return method.invoke(null, args);
+ }
+ }
+
+ /**
+ * Constructs a new builder for calling methods dynamically.
+ *
+ * @param methodName name of the method the builder will locate
+ * @return a Builder for finding a method
+ */
+ public static Builder builder(String methodName) {
+ return new Builder(methodName);
+ }
+
+ public static class Builder {
+ private final String name;
+ private ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ private UnboundMethod method = null;
+
+ public Builder(String methodName) {
+ this.name = methodName;
+ }
+
+ /**
+ * Set the {@link ClassLoader} used to lookup classes by name.
+ *
+ * If not set, the current thread's ClassLoader is used.
+ *
+ * @param newLoader a ClassLoader
+ * @return this Builder for method chaining
+ */
+ public Builder loader(ClassLoader newLoader) {
+ this.loader = newLoader;
+ return this;
+ }
+
+ /**
+ * If no implementation has been found, adds a NOOP method.
+ *
+ *
Note: calls to impl will not match after this method is called!
+ *
+ * @return this Builder for method chaining
+ */
+ public Builder orNoop() {
+ if (method == null) {
+ this.method = UnboundMethod.NOOP;
+ }
+ return this;
+ }
+
+ /**
+ * Checks for an implementation, first finding the given class by name.
+ *
+ * @param className name of a class
+ * @param methodName name of a method (different from constructor)
+ * @param argClasses argument classes for the method
+ * @return this Builder for method chaining
+ * @see java.lang.Class#forName(String)
+ * @see java.lang.Class#getMethod(String, Class[])
+ */
+ public Builder impl(String className, String methodName, Class>... argClasses) {
+ // don't do any work if an implementation has been found
+ if (method != null) {
+ return this;
+ }
+
+ try {
+ Class> targetClass = Class.forName(className, true, loader);
+ impl(targetClass, methodName, argClasses);
+ } catch (ClassNotFoundException e) {
+ // not the right implementation
+ }
+ return this;
+ }
+
+ /**
+ * Checks for an implementation, first finding the given class by name.
+ *
+ *
The name passed to the constructor is the method name used.
+ *
+ * @param className name of a class
+ * @param argClasses argument classes for the method
+ * @return this Builder for method chaining
+ * @see java.lang.Class#forName(String)
+ * @see java.lang.Class#getMethod(String, Class[])
+ */
+ public Builder impl(String className, Class>... argClasses) {
+ impl(className, name, argClasses);
+ return this;
+ }
+
+ /**
+ * Checks for a method implementation.
+ *
+ * @param targetClass a class instance
+ * @param methodName name of a method (different from constructor)
+ * @param argClasses argument classes for the method
+ * @return this Builder for method chaining
+ * @see java.lang.Class#forName(String)
+ * @see java.lang.Class#getMethod(String, Class[])
+ */
+ public Builder impl(Class> targetClass, String methodName, Class>... argClasses) {
+ // don't do any work if an implementation has been found
+ if (method != null) {
+ return this;
+ }
+
+ try {
+ this.method = new UnboundMethod(targetClass.getMethod(methodName, argClasses), name);
+ } catch (NoSuchMethodException e) {
+ // not the right implementation
+ }
+ return this;
+ }
+
+ /**
+ * Checks for a method implementation.
+ *
+ *
The name passed to the constructor is the method name used.
+ *
+ * @param targetClass a class instance
+ * @param argClasses argument classes for the method
+ * @return this Builder for method chaining
+ * @see java.lang.Class#forName(String)
+ * @see java.lang.Class#getMethod(String, Class[])
+ */
+ public Builder impl(Class> targetClass, Class>... argClasses) {
+ impl(targetClass, name, argClasses);
+ return this;
+ }
+
+ /**
+ * Checks for an implementation, first finding the given class by name.
+ *
+ * @param className name of a class
+ * @param methodName name of a method (different from constructor)
+ * @param argClasses argument classes for the method
+ * @return this Builder for method chaining
+ * @see java.lang.Class#forName(String)
+ * @see java.lang.Class#getMethod(String, Class[])
+ */
+ public Builder hiddenImpl(String className, String methodName, Class>... argClasses) {
+ // don't do any work if an implementation has been found
+ if (method != null) {
+ return this;
+ }
+
+ try {
+ Class> targetClass = Class.forName(className, true, loader);
+ hiddenImpl(targetClass, methodName, argClasses);
+ } catch (ClassNotFoundException e) {
+ // not the right implementation
+ }
+ return this;
+ }
+
+ /**
+ * Checks for an implementation, first finding the given class by name.
+ *
+ *
The name passed to the constructor is the method name used.
+ *
+ * @param className name of a class
+ * @param argClasses argument classes for the method
+ * @return this Builder for method chaining
+ * @see java.lang.Class#forName(String)
+ * @see java.lang.Class#getMethod(String, Class[])
+ */
+ public Builder hiddenImpl(String className, Class>... argClasses) {
+ hiddenImpl(className, name, argClasses);
+ return this;
+ }
+
+ /**
+ * Checks for a method implementation.
+ *
+ * @param targetClass a class instance
+ * @param methodName name of a method (different from constructor)
+ * @param argClasses argument classes for the method
+ * @return this Builder for method chaining
+ * @see java.lang.Class#forName(String)
+ * @see java.lang.Class#getMethod(String, Class[])
+ */
+ public Builder hiddenImpl(Class> targetClass, String methodName, Class>... argClasses) {
+ // don't do any work if an implementation has been found
+ if (method != null) {
+ return this;
+ }
+
+ try {
+ Method hidden = targetClass.getDeclaredMethod(methodName, argClasses);
+ AccessController.doPrivileged(new MakeAccessible(hidden));
+ this.method = new UnboundMethod(hidden, name);
+ } catch (SecurityException | NoSuchMethodException e) {
+ // unusable or not the right implementation
+ }
+ return this;
+ }
+
+ /**
+ * Checks for a method implementation.
+ *
+ *
The name passed to the constructor is the method name used.
+ *
+ * @param targetClass a class instance
+ * @param argClasses argument classes for the method
+ * @return this Builder for method chaining
+ * @see java.lang.Class#forName(String)
+ * @see java.lang.Class#getMethod(String, Class[])
+ */
+ public Builder hiddenImpl(Class> targetClass, Class>... argClasses) {
+ hiddenImpl(targetClass, name, argClasses);
+ return this;
+ }
+
+ /**
+ * Returns the first valid implementation as a UnboundMethod or throws a RuntimeError if there
+ * is none.
+ *
+ * @return a {@link UnboundMethod} with a valid implementation
+ * @throws RuntimeException if no implementation was found
+ */
+ public UnboundMethod build() {
+ if (method != null) {
+ return method;
+ } else {
+ throw new RuntimeException("Cannot find method: " + name);
+ }
+ }
+
+ /**
+ * Returns the first valid implementation as a BoundMethod or throws a RuntimeError if there is
+ * none.
+ *
+ * @param receiver an Object to receive the method invocation
+ * @return a {@link BoundMethod} with a valid implementation and receiver
+ * @throws IllegalStateException if the method is static
+ * @throws IllegalArgumentException if the receiver's class is incompatible
+ * @throws RuntimeException if no implementation was found
+ */
+ public BoundMethod build(Object receiver) {
+ return build().bind(receiver);
+ }
+
+ /**
+ * Returns the first valid implementation as a UnboundMethod or throws a NoSuchMethodException
+ * if there is none.
+ *
+ * @return a {@link UnboundMethod} with a valid implementation
+ * @throws NoSuchMethodException if no implementation was found
+ */
+ public UnboundMethod buildChecked() throws NoSuchMethodException {
+ if (method != null) {
+ return method;
+ } else {
+ throw new NoSuchMethodException("Cannot find method: " + name);
+ }
+ }
+
+ /**
+ * Returns the first valid implementation as a BoundMethod or throws a NoSuchMethodException if
+ * there is none.
+ *
+ * @param receiver an Object to receive the method invocation
+ * @return a {@link BoundMethod} with a valid implementation and receiver
+ * @throws IllegalStateException if the method is static
+ * @throws IllegalArgumentException if the receiver's class is incompatible
+ * @throws NoSuchMethodException if no implementation was found
+ */
+ public BoundMethod buildChecked(Object receiver) throws NoSuchMethodException {
+ return buildChecked().bind(receiver);
+ }
+
+ /**
+ * Returns the first valid implementation as a StaticMethod or throws a NoSuchMethodException if
+ * there is none.
+ *
+ * @return a {@link StaticMethod} with a valid implementation
+ * @throws IllegalStateException if the method is not static
+ * @throws NoSuchMethodException if no implementation was found
+ */
+ public StaticMethod buildStaticChecked() throws NoSuchMethodException {
+ return buildChecked().asStatic();
+ }
+
+ /**
+ * Returns the first valid implementation as a StaticMethod or throws a RuntimeException if
+ * there is none.
+ *
+ * @return a {@link StaticMethod} with a valid implementation
+ * @throws IllegalStateException if the method is not static
+ * @throws RuntimeException if no implementation was found
+ */
+ public StaticMethod buildStatic() {
+ return build().asStatic();
+ }
+ }
+
+ private static class MakeAccessible implements PrivilegedAction {
+ private final Method hidden;
+
+ MakeAccessible(Method hidden) {
+ this.hidden = hidden;
+ }
+
+ @Override
+ public Void run() {
+ hidden.setAccessible(true);
+ return null;
+ }
+ }
+}
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3ClientPool.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3ClientPool.java
index 8e9f858..cf276ad 100644
--- a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3ClientPool.java
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3ClientPool.java
@@ -13,9 +13,6 @@
*/
package org.lance.namespace.hive3;
-import org.lance.namespace.util.ClientPoolImpl;
-import org.lance.namespace.util.DynMethods;
-
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.metastore.HiveMetaHookLoader;
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3Namespace.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3Namespace.java
index 3f46207..258332a 100644
--- a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3Namespace.java
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3Namespace.java
@@ -13,38 +13,34 @@
*/
package org.lance.namespace.hive3;
-import com.lancedb.lance.Dataset;
-import com.lancedb.lance.WriteParams;
-import org.lance.namespace.Configurable;
+import org.lance.Dataset;
+import org.lance.WriteParams;
import org.lance.namespace.LanceNamespace;
-import org.lance.namespace.LanceNamespaceException;
-import org.lance.namespace.ObjectIdentifier;
+import org.lance.namespace.errors.InternalException;
+import org.lance.namespace.errors.InvalidInputException;
+import org.lance.namespace.errors.NamespaceAlreadyExistsException;
+import org.lance.namespace.errors.NamespaceNotFoundException;
+import org.lance.namespace.errors.ServiceUnavailableException;
+import org.lance.namespace.errors.TableAlreadyExistsException;
+import org.lance.namespace.errors.TableNotFoundException;
import org.lance.namespace.model.CreateEmptyTableRequest;
import org.lance.namespace.model.CreateEmptyTableResponse;
import org.lance.namespace.model.CreateNamespaceRequest;
import org.lance.namespace.model.CreateNamespaceResponse;
-import org.lance.namespace.model.CreateTableRequest;
-import org.lance.namespace.model.CreateTableResponse;
+import org.lance.namespace.model.DeregisterTableRequest;
+import org.lance.namespace.model.DeregisterTableResponse;
import org.lance.namespace.model.DescribeNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
import org.lance.namespace.model.DescribeTableRequest;
import org.lance.namespace.model.DescribeTableResponse;
import org.lance.namespace.model.DropNamespaceRequest;
import org.lance.namespace.model.DropNamespaceResponse;
-import org.lance.namespace.model.DropTableRequest;
-import org.lance.namespace.model.DropTableResponse;
-import org.lance.namespace.model.JsonArrowSchema;
import org.lance.namespace.model.ListNamespacesRequest;
import org.lance.namespace.model.ListNamespacesResponse;
import org.lance.namespace.model.ListTablesRequest;
import org.lance.namespace.model.ListTablesResponse;
import org.lance.namespace.model.NamespaceExistsRequest;
import org.lance.namespace.model.TableExistsRequest;
-import org.lance.namespace.util.ArrowIpcUtil;
-import org.lance.namespace.util.CommonUtil;
-import org.lance.namespace.util.JsonArrowSchemaConverter;
-import org.lance.namespace.util.PageUtil;
-import org.lance.namespace.util.ValidationUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -61,19 +57,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import static org.lance.namespace.hive3.Hive3ErrorType.DatabaseAlreadyExist;
-import static org.lance.namespace.hive3.Hive3ErrorType.HiveMetaStoreError;
-import static org.lance.namespace.hive3.Hive3ErrorType.TableAlreadyExists;
-import static org.lance.namespace.hive3.Hive3ErrorType.TableNotFound;
-
-public class Hive3Namespace implements LanceNamespace, Configurable {
+public class Hive3Namespace implements LanceNamespace {
private static final Logger LOG = LoggerFactory.getLogger(Hive3Namespace.class);
private Hive3ClientPool clientPool;
@@ -83,6 +73,11 @@ public class Hive3Namespace implements LanceNamespace, Configurable configProperties, BufferAllocator allocator) {
this.allocator = allocator;
@@ -126,7 +121,7 @@ public ListNamespacesResponse listNamespaces(ListNamespacesRequest request) {
@Override
public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) {
ObjectIdentifier id = ObjectIdentifier.of(request.getId());
- CreateNamespaceRequest.ModeEnum mode = request.getMode();
+ String mode = request.getMode() != null ? request.getMode().toLowerCase() : "create";
Map properties = request.getProperties();
ValidationUtil.checkArgument(
@@ -154,11 +149,8 @@ public DescribeNamespaceResponse describeNamespace(DescribeNamespaceRequest requ
Catalog catalogObj = Hive3Util.getCatalogOrNull(clientPool, catalog);
if (catalogObj == null) {
- throw LanceNamespaceException.notFound(
- String.format("Namespace does not exist: %s", id.stringStyleId()),
- HiveMetaStoreError.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(
+ String.format("Namespace does not exist: %s", id.stringStyleId()));
}
if (catalogObj.getDescription() != null) {
@@ -173,11 +165,8 @@ public DescribeNamespaceResponse describeNamespace(DescribeNamespaceRequest requ
Database database = Hive3Util.getDatabaseOrNull(clientPool, catalog, db);
if (database == null) {
- throw LanceNamespaceException.notFound(
- String.format("Namespace does not exist: %s", id.stringStyleId()),
- HiveMetaStoreError.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(
+ String.format("Namespace does not exist: %s", id.stringStyleId()));
}
if (database.getDescription() != null) {
@@ -214,11 +203,8 @@ public void namespaceExists(NamespaceExistsRequest request) {
Catalog catalogObj = Hive3Util.getCatalogOrNull(clientPool, catalog);
if (catalogObj == null) {
- throw LanceNamespaceException.notFound(
- String.format("Namespace does not exist: %s", id.stringStyleId()),
- HiveMetaStoreError.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(
+ String.format("Namespace does not exist: %s", id.stringStyleId()));
}
} else {
String catalog = id.levelAtListPos(0).toLowerCase();
@@ -226,31 +212,25 @@ public void namespaceExists(NamespaceExistsRequest request) {
Database database = Hive3Util.getDatabaseOrNull(clientPool, catalog, db);
if (database == null) {
- throw LanceNamespaceException.notFound(
- String.format("Namespace does not exist: %s", id.stringStyleId()),
- HiveMetaStoreError.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(
+ String.format("Namespace does not exist: %s", id.stringStyleId()));
}
}
}
@Override
public DropNamespaceResponse dropNamespace(DropNamespaceRequest request) {
+ if ("Cascade".equalsIgnoreCase(request.getBehavior())) {
+ throw new InvalidInputException("Cascade behavior is not supported for this implementation");
+ }
+
ObjectIdentifier id = ObjectIdentifier.of(request.getId());
- DropNamespaceRequest.ModeEnum mode = request.getMode();
- DropNamespaceRequest.BehaviorEnum behavior = request.getBehavior();
+ String mode = request.getMode() != null ? request.getMode().toLowerCase() : "fail";
+ String behavior = request.getBehavior() != null ? request.getBehavior() : "Restrict";
ValidationUtil.checkArgument(
!id.isRoot() && id.levels() <= 2, "Expect a 2-level namespace but get %s", id);
- if (mode == null) {
- mode = DropNamespaceRequest.ModeEnum.FAIL;
- }
- if (behavior == null) {
- behavior = DropNamespaceRequest.BehaviorEnum.RESTRICT;
- }
-
Map properties = doDropNamespace(id, mode, behavior);
DropNamespaceResponse response = new DropNamespaceResponse();
@@ -272,11 +252,8 @@ public void tableExists(TableExistsRequest request) {
Optional hmsTable = Hive3Util.getTable(clientPool, catalog, db, table);
if (!hmsTable.isPresent()) {
- throw LanceNamespaceException.notFound(
- String.format("Table does not exist: %s", tableId.stringStyleId()),
- TableNotFound.getType(),
- tableId.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new TableNotFoundException(
+ String.format("Table does not exist: %s", tableId.stringStyleId()));
}
Hive3Util.validateLanceTable(hmsTable.get());
@@ -306,6 +283,11 @@ public ListTablesResponse listTables(ListTablesRequest request) {
@Override
public DescribeTableResponse describeTable(DescribeTableRequest request) {
+ if (Boolean.TRUE.equals(request.getLoadDetailedMetadata())) {
+ throw new InvalidInputException(
+ "load_detailed_metadata=true is not supported for this implementation");
+ }
+
ObjectIdentifier tableId = ObjectIdentifier.of(request.getId());
ValidationUtil.checkArgument(
@@ -314,11 +296,8 @@ public DescribeTableResponse describeTable(DescribeTableRequest request) {
Optional location = doDescribeTable(tableId);
if (!location.isPresent()) {
- throw LanceNamespaceException.notFound(
- String.format("Table does not exist: %s", tableId.stringStyleId()),
- TableNotFound.getType(),
- tableId.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new TableNotFoundException(
+ String.format("Table does not exist: %s", tableId.stringStyleId()));
}
DescribeTableResponse response = new DescribeTableResponse();
@@ -326,46 +305,7 @@ public DescribeTableResponse describeTable(DescribeTableRequest request) {
return response;
}
- @Override
- public CreateTableResponse createTable(CreateTableRequest request, byte[] requestData) {
- // Validate that requestData is a valid Arrow IPC stream
- ValidationUtil.checkNotNull(
- requestData, "Request data (Arrow IPC stream) is required for createTable");
- ValidationUtil.checkArgument(
- requestData.length > 0, "Request data (Arrow IPC stream) cannot be empty");
-
- ObjectIdentifier tableId = ObjectIdentifier.of(request.getId());
-
- // Extract schema from Arrow IPC stream
- JsonArrowSchema jsonSchema;
- try {
- jsonSchema = ArrowIpcUtil.extractSchemaFromIpc(requestData);
- } catch (IOException e) {
- throw LanceNamespaceException.badRequest(
- "Invalid Arrow IPC stream: " + e.getMessage(),
- "INVALID_ARROW_IPC",
- tableId.stringStyleId(),
- "Failed to extract schema from Arrow IPC stream");
- }
- Schema schema = JsonArrowSchemaConverter.convertToArrowSchema(jsonSchema);
-
- ValidationUtil.checkArgument(
- tableId.levels() == 3, "Expect 3-level table identifier but get %s", tableId);
-
- String location = request.getLocation();
- if (location == null || location.isEmpty()) {
- location =
- getDefaultTableLocation(
- tableId.levelAtListPos(0), tableId.levelAtListPos(1), tableId.levelAtListPos(2));
- }
-
- doCreateTable(tableId, schema, location, request.getProperties(), requestData);
-
- CreateTableResponse response = new CreateTableResponse();
- response.setLocation(location);
- response.setVersion(1L);
- return response;
- }
+ // Removed: createTable(CreateTableRequest, byte[]) - using default implementation from interface
@Override
public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request) {
@@ -381,8 +321,8 @@ public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request
tableId.levelAtListPos(0), tableId.levelAtListPos(1), tableId.levelAtListPos(2));
}
- // Create table in metastore without data (pass null for requestData)
- doCreateTable(tableId, null, location, request.getProperties(), null);
+ // Create table in metastore without data (pass null for requestData and properties)
+ doCreateTable(tableId, null, location, null, null);
CreateEmptyTableResponse response = new CreateEmptyTableResponse();
response.setLocation(location);
@@ -390,22 +330,20 @@ public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request
}
@Override
- public DropTableResponse dropTable(DropTableRequest request) {
+ public DeregisterTableResponse deregisterTable(DeregisterTableRequest request) {
ObjectIdentifier tableId = ObjectIdentifier.of(request.getId());
ValidationUtil.checkArgument(
tableId.levels() == 3, "Expect 3-level table identifier but get %s", tableId);
String location = doDropTable(tableId);
- // TODO: remove data
- DropTableResponse response = new DropTableResponse();
- response.setLocation(location);
+ DeregisterTableResponse response = new DeregisterTableResponse();
response.setId(request.getId());
+ response.setLocation(location);
return response;
}
- @Override
public void setConf(Configuration conf) {
this.hadoopConf = conf;
}
@@ -424,16 +362,12 @@ protected List doListNamespaces(ObjectIdentifier parent) {
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed operation: " + errorMessage,
- HiveMetaStoreError.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException("Failed operation: " + errorMessage);
}
}
protected void doCreateNamespace(
- ObjectIdentifier id, CreateNamespaceRequest.ModeEnum mode, Map properties) {
+ ObjectIdentifier id, String mode, Map properties) {
try {
if (id.levels() == 1) {
@@ -449,35 +383,26 @@ protected void doCreateNamespace(
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed operation: " + errorMessage,
- HiveMetaStoreError.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException("Failed operation: " + errorMessage);
}
}
- private void createCatalog(
- String catalogName, CreateNamespaceRequest.ModeEnum mode, Map properties)
+ private void createCatalog(String catalogName, String mode, Map properties)
throws TException, InterruptedException {
Catalog existingCatalog = Hive3Util.getCatalogOrNull(clientPool, catalogName);
if (existingCatalog != null) {
- switch (mode) {
- case CREATE:
- throw LanceNamespaceException.conflict(
- String.format("Catalog %s already exists", catalogName),
- DatabaseAlreadyExist.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
- case EXIST_OK:
- return;
- case OVERWRITE:
- clientPool.run(
- client -> {
- client.dropCatalog(catalogName);
- return null;
- });
+ if ("create".equals(mode)) {
+ throw new NamespaceAlreadyExistsException(
+ String.format("Catalog %s already exists", catalogName));
+ } else if ("exist_ok".equals(mode) || "existok".equals(mode)) {
+ return;
+ } else if ("overwrite".equals(mode)) {
+ clientPool.run(
+ client -> {
+ client.dropCatalog(catalogName);
+ return null;
+ });
}
}
@@ -506,30 +431,23 @@ private void createCatalog(
}
private void createDatabase(
- String catalogName,
- String dbName,
- CreateNamespaceRequest.ModeEnum mode,
- Map properties)
+ String catalogName, String dbName, String mode, Map properties)
throws TException, InterruptedException {
Catalog catalog = Hive3Util.getCatalogOrThrowNotFoundException(clientPool, catalogName);
Database oldDb = Hive3Util.getDatabaseOrNull(clientPool, catalogName, dbName);
if (oldDb != null) {
- switch (mode) {
- case CREATE:
- throw LanceNamespaceException.conflict(
- String.format("Database %s.%s already exist", catalogName, dbName),
- DatabaseAlreadyExist.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
- case EXIST_OK:
- return;
- case OVERWRITE:
- clientPool.run(
- client -> {
- client.dropDatabase(catalogName, dbName, false, true, false);
- return null;
- });
+ if ("create".equals(mode)) {
+ throw new NamespaceAlreadyExistsException(
+ String.format("Database %s.%s already exist", catalogName, dbName));
+ } else if ("exist_ok".equals(mode) || "existok".equals(mode)) {
+ return;
+ } else if ("overwrite".equals(mode)) {
+ clientPool.run(
+ client -> {
+ client.dropDatabase(catalogName, dbName, false, true, false);
+ return null;
+ });
}
}
@@ -580,11 +498,8 @@ protected void doCreateTable(
try {
Optional existing = Hive3Util.getTable(clientPool, catalog, db, tableName);
if (existing.isPresent()) {
- throw LanceNamespaceException.conflict(
- String.format("Table %s.%s.%s already exists", catalog, db, tableName),
- TableAlreadyExists.getType(),
- String.format("%s.%s.%s", catalog, db, tableName),
- CommonUtil.formatCurrentStackTrace());
+ throw new TableAlreadyExistsException(
+ String.format("Table %s.%s.%s already exists", catalog, db, tableName));
}
Table table = new Table();
@@ -614,11 +529,7 @@ protected void doCreateTable(
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
- throw LanceNamespaceException.serverError(
- "Fail to create table: " + e.getMessage(),
- HiveMetaStoreError.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new InternalException("Fail to create table: " + e.getMessage());
}
if (data != null && data.length > 0) {
@@ -636,20 +547,13 @@ protected List doListTables(String catalog, String db) {
// First validate that catalog and database exist
Catalog catalogObj = Hive3Util.getCatalogOrNull(clientPool, catalog);
if (catalogObj == null) {
- throw LanceNamespaceException.notFound(
- String.format("Catalog %s doesn't exist", catalog),
- HiveMetaStoreError.getType(),
- catalog,
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(String.format("Catalog %s doesn't exist", catalog));
}
Database database = Hive3Util.getDatabaseOrNull(clientPool, catalog, db);
if (database == null) {
- throw LanceNamespaceException.notFound(
- String.format("Database %s.%s doesn't exist", catalog, db),
- HiveMetaStoreError.getType(),
- String.format("%s.%s", catalog, db),
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(
+ String.format("Database %s.%s doesn't exist", catalog, db));
}
List allTables = clientPool.run(client -> client.getAllTables(catalog, db));
@@ -676,11 +580,7 @@ protected List doListTables(String catalog, String db) {
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed to list tables: " + errorMessage,
- HiveMetaStoreError.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException("Failed to list tables: " + errorMessage);
}
}
@@ -692,11 +592,8 @@ protected String doDropTable(ObjectIdentifier id) {
try {
Optional hmsTable = Hive3Util.getTable(clientPool, catalog, db, tableName);
if (!hmsTable.isPresent()) {
- throw LanceNamespaceException.notFound(
- String.format("Table %s.%s.%s does not exist", catalog, db, tableName),
- TableNotFound.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new TableNotFoundException(
+ String.format("Table %s.%s.%s does not exist", catalog, db, tableName));
}
Hive3Util.validateLanceTable(hmsTable.get());
@@ -714,18 +611,11 @@ protected String doDropTable(ObjectIdentifier id) {
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed to drop table: " + errorMessage,
- HiveMetaStoreError.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException("Failed to drop table: " + errorMessage);
}
}
- protected Map doDropNamespace(
- ObjectIdentifier id,
- DropNamespaceRequest.ModeEnum mode,
- DropNamespaceRequest.BehaviorEnum behavior) {
+ protected Map doDropNamespace(ObjectIdentifier id, String mode, String behavior) {
try {
if (id.levels() == 1) {
@@ -741,64 +631,30 @@ protected Map doDropNamespace(
Thread.currentThread().interrupt();
}
String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
- throw LanceNamespaceException.serviceUnavailable(
- "Failed to drop namespace: " + errorMessage,
- HiveMetaStoreError.getType(),
- id.stringStyleId(),
- CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException("Failed to drop namespace: " + errorMessage);
}
}
- private Map doDropCatalog(
- String catalog,
- DropNamespaceRequest.ModeEnum mode,
- DropNamespaceRequest.BehaviorEnum behavior)
+ private Map doDropCatalog(String catalog, String mode, String behavior)
throws TException, InterruptedException {
Catalog catalogObj = Hive3Util.getCatalogOrNull(clientPool, catalog);
if (catalogObj == null) {
- if (mode == DropNamespaceRequest.ModeEnum.SKIP) {
+ if ("skip".equals(mode)) {
return new HashMap<>();
} else {
- throw LanceNamespaceException.notFound(
- String.format("Catalog %s doesn't exist", catalog),
- HiveMetaStoreError.getType(),
- catalog,
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(String.format("Catalog %s doesn't exist", catalog));
}
}
- // Check for child databases
- List databases = clientPool.run(client -> client.getAllDatabases(catalog));
- if (!databases.isEmpty()) {
- if (behavior == DropNamespaceRequest.BehaviorEnum.RESTRICT) {
- throw LanceNamespaceException.badRequest(
+ // Check for child databases (RESTRICT behavior only, not for Cascade)
+ boolean cascade = "Cascade".equalsIgnoreCase(behavior);
+ if (!cascade) {
+ List databases = clientPool.run(client -> client.getAllDatabases(catalog));
+ if (!databases.isEmpty()) {
+ throw new InvalidInputException(
String.format(
"Catalog %s is not empty. Contains %d databases: %s",
- catalog, databases.size(), databases),
- HiveMetaStoreError.getType(),
- catalog,
- CommonUtil.formatCurrentStackTrace());
- } else if (behavior == DropNamespaceRequest.BehaviorEnum.CASCADE) {
- // Drop all databases first
- for (String dbName : databases) {
- try {
- doDropDatabase(
- catalog,
- dbName,
- DropNamespaceRequest.ModeEnum.FAIL,
- DropNamespaceRequest.BehaviorEnum.CASCADE);
- LOG.info("Dropped database {}.{} during CASCADE operation", catalog, dbName);
- } catch (Exception e) {
- LOG.warn("Failed to drop database {}.{}: {}", catalog, dbName, e.getMessage());
- throw LanceNamespaceException.serviceUnavailable(
- String.format(
- "Failed to drop database %s.%s during CASCADE operation: %s",
- catalog, dbName, e.getMessage()),
- HiveMetaStoreError.getType(),
- String.format("%s.%s", catalog, dbName),
- CommonUtil.formatCurrentStackTrace());
- }
- }
+ catalog, databases.size(), databases));
}
}
@@ -823,54 +679,27 @@ private Map doDropCatalog(
}
private Map doDropDatabase(
- String catalog,
- String db,
- DropNamespaceRequest.ModeEnum mode,
- DropNamespaceRequest.BehaviorEnum behavior)
+ String catalog, String db, String mode, String behavior)
throws TException, InterruptedException {
Database database = Hive3Util.getDatabaseOrNull(clientPool, catalog, db);
if (database == null) {
- if (mode == DropNamespaceRequest.ModeEnum.SKIP) {
+ if ("skip".equals(mode)) {
return new HashMap<>();
} else {
- throw LanceNamespaceException.notFound(
- String.format("Database %s.%s doesn't exist", catalog, db),
- HiveMetaStoreError.getType(),
- String.format("%s.%s", catalog, db),
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(
+ String.format("Database %s.%s doesn't exist", catalog, db));
}
}
- // Check if database contains tables
- List tables = doListTables(catalog, db);
- if (!tables.isEmpty()) {
- if (behavior == DropNamespaceRequest.BehaviorEnum.RESTRICT) {
- throw LanceNamespaceException.badRequest(
+ // Check if database contains tables (RESTRICT behavior only, not for Cascade)
+ boolean cascade = "Cascade".equalsIgnoreCase(behavior);
+ if (!cascade) {
+ List tables = doListTables(catalog, db);
+ if (!tables.isEmpty()) {
+ throw new InvalidInputException(
String.format(
"Database %s.%s is not empty. Contains %d tables: %s",
- catalog, db, tables.size(), tables),
- HiveMetaStoreError.getType(),
- String.format("%s.%s", catalog, db),
- CommonUtil.formatCurrentStackTrace());
- } else if (behavior == DropNamespaceRequest.BehaviorEnum.CASCADE) {
- // Drop all tables first
- for (String tableName : tables) {
- try {
- ObjectIdentifier tableId =
- ObjectIdentifier.of(Lists.newArrayList(catalog, db, tableName));
- doDropTable(tableId);
- LOG.info("Dropped table {}.{}.{} during CASCADE operation", catalog, db, tableName);
- } catch (Exception e) {
- LOG.warn("Failed to drop table {}.{}.{}: {}", catalog, db, tableName, e.getMessage());
- throw LanceNamespaceException.serviceUnavailable(
- String.format(
- "Failed to drop table %s.%s.%s during CASCADE operation: %s",
- catalog, db, tableName, e.getMessage()),
- HiveMetaStoreError.getType(),
- String.format("%s.%s.%s", catalog, db, tableName),
- CommonUtil.formatCurrentStackTrace());
- }
- }
+ catalog, db, tables.size(), tables));
}
}
@@ -893,9 +722,10 @@ private Map doDropDatabase(
}
// Drop the database
+ final boolean cascadeDrop = cascade;
clientPool.run(
client -> {
- client.dropDatabase(catalog, db, false, true, false);
+ client.dropDatabase(catalog, db, false, true, cascadeDrop);
return null;
});
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3NamespaceConfig.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3NamespaceConfig.java
index 243cd8b..c5ba2a2 100644
--- a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3NamespaceConfig.java
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3NamespaceConfig.java
@@ -13,9 +13,7 @@
*/
package org.lance.namespace.hive3;
-import org.lance.namespace.util.OpenDalUtil;
-import org.lance.namespace.util.PropertyUtil;
-
+import java.util.HashMap;
import java.util.Map;
public class Hive3NamespaceConfig {
@@ -42,12 +40,27 @@ public class Hive3NamespaceConfig {
private final String root;
public Hive3NamespaceConfig(Map properties) {
+ // Inline PropertyUtil.propertyAsInt
+ String clientPoolSizeStr = properties.get(CLIENT_POOL_SIZE);
this.clientPoolSize =
- PropertyUtil.propertyAsInt(properties, CLIENT_POOL_SIZE, CLIENT_POOL_SIZE_DEFAULT);
- this.storageOptions = PropertyUtil.propertiesWithPrefix(properties, STORAGE_OPTIONS_PREFIX);
+ clientPoolSizeStr != null ? Integer.parseInt(clientPoolSizeStr) : CLIENT_POOL_SIZE_DEFAULT;
+
+ // Inline PropertyUtil.propertiesWithPrefix
+ Map filteredStorageOptions = new HashMap<>();
+ for (Map.Entry entry : properties.entrySet()) {
+ if (entry.getKey().startsWith(STORAGE_OPTIONS_PREFIX)) {
+ filteredStorageOptions.put(
+ entry.getKey().substring(STORAGE_OPTIONS_PREFIX.length()), entry.getValue());
+ }
+ }
+ this.storageOptions = filteredStorageOptions;
+
+ // Inline PropertyUtil.propertyAsString and OpenDalUtil.stripTrailingSlash
+ String rootValue = properties.getOrDefault(ROOT, ROOT_DEFAULT);
this.root =
- OpenDalUtil.stripTrailingSlash(
- PropertyUtil.propertyAsString(properties, ROOT, ROOT_DEFAULT));
+ rootValue != null && rootValue.endsWith("/")
+ ? rootValue.substring(0, rootValue.length() - 1)
+ : rootValue;
}
public int getClientPoolSize() {
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3Util.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3Util.java
index 245ec9b..b77eb89 100644
--- a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3Util.java
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/Hive3Util.java
@@ -13,8 +13,9 @@
*/
package org.lance.namespace.hive3;
-import org.lance.namespace.LanceNamespaceException;
-import org.lance.namespace.util.CommonUtil;
+import org.lance.namespace.errors.InvalidInputException;
+import org.lance.namespace.errors.NamespaceNotFoundException;
+import org.lance.namespace.errors.ServiceUnavailableException;
import com.google.common.collect.Maps;
import org.apache.hadoop.hive.metastore.api.Catalog;
@@ -31,10 +32,6 @@
import java.util.Optional;
import java.util.function.Supplier;
-import static org.lance.namespace.hive3.Hive3ErrorType.HiveMetaStoreError;
-import static org.lance.namespace.hive3.Hive3ErrorType.InvalidLanceTable;
-import static org.lance.namespace.hive3.Hive3ErrorType.UnknownCatalog;
-
public class Hive3Util {
public static Catalog getCatalogOrNull(Hive3ClientPool clientPool, String catalog) {
try {
@@ -45,8 +42,7 @@ public static Catalog getCatalogOrNull(Hive3ClientPool clientPool, String catalo
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
- throw LanceNamespaceException.serviceUnavailable(
- e.getMessage(), HiveMetaStoreError.getType(), "", CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(e.getMessage());
}
}
@@ -54,11 +50,7 @@ public static Catalog getCatalogOrThrowNotFoundException(
Hive3ClientPool clientPool, String catalog) {
Catalog catalogObj = getCatalogOrNull(clientPool, catalog);
if (catalogObj == null) {
- throw LanceNamespaceException.notFound(
- String.format("Catalog %s doesn't exist", catalog),
- UnknownCatalog.getType(),
- "",
- CommonUtil.formatCurrentStackTrace());
+ throw new NamespaceNotFoundException(String.format("Catalog %s doesn't exist", catalog));
}
return catalogObj;
}
@@ -72,8 +64,7 @@ public static Database getDatabaseOrNull(Hive3ClientPool clientPool, String cata
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
- throw LanceNamespaceException.serviceUnavailable(
- e.getMessage(), HiveMetaStoreError.getType(), "", CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(e.getMessage());
}
}
@@ -86,8 +77,7 @@ public static Database getDatabaseOrNull(Hive3ClientPool clientPool, String db)
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
- throw LanceNamespaceException.serviceUnavailable(
- e.getMessage(), HiveMetaStoreError.getType(), "", CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(e.getMessage());
}
}
@@ -151,8 +141,7 @@ public static Optional getTable(Hive3ClientPool clientPool, String db, St
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
- throw LanceNamespaceException.serviceUnavailable(
- e.getMessage(), HiveMetaStoreError.getType(), "", CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(e.getMessage());
}
}
@@ -166,20 +155,16 @@ public static Optional getTable(
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
- throw LanceNamespaceException.serviceUnavailable(
- e.getMessage(), HiveMetaStoreError.getType(), "", CommonUtil.formatCurrentStackTrace());
+ throw new ServiceUnavailableException(e.getMessage());
}
}
public static void validateLanceTable(Table table) {
Map params = table.getParameters();
if (params == null || !"lance".equalsIgnoreCase(params.get("table_type"))) {
- throw LanceNamespaceException.badRequest(
+ throw new InvalidInputException(
String.format(
- "Table %s.%s is not a Lance table", table.getDbName(), table.getTableName()),
- InvalidLanceTable.getType(),
- String.format("%s.%s", table.getDbName(), table.getTableName()),
- CommonUtil.formatCurrentStackTrace());
+ "Table %s.%s is not a Lance table", table.getDbName(), table.getTableName()));
}
}
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/ObjectIdentifier.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/ObjectIdentifier.java
new file mode 100644
index 0000000..cfd5e36
--- /dev/null
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/ObjectIdentifier.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed 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.lance.namespace.hive3;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Represents a hierarchical identifier for namespaces and tables. */
+public class ObjectIdentifier {
+ private final List levels;
+
+ private ObjectIdentifier(List levels) {
+ this.levels = levels != null ? levels : Collections.emptyList();
+ }
+
+ public static ObjectIdentifier of(List levels) {
+ return new ObjectIdentifier(levels);
+ }
+
+ public boolean isRoot() {
+ return levels.isEmpty();
+ }
+
+ public int levels() {
+ return levels.size();
+ }
+
+ public String levelAtListPos(int pos) {
+ if (pos < 0 || pos >= levels.size()) {
+ throw new IndexOutOfBoundsException(
+ "Position " + pos + " is out of bounds for size " + levels.size());
+ }
+ return levels.get(pos);
+ }
+
+ public String stringStyleId() {
+ return String.join(".", levels);
+ }
+
+ @Override
+ public String toString() {
+ return stringStyleId();
+ }
+}
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/PageUtil.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/PageUtil.java
new file mode 100644
index 0000000..62475cc
--- /dev/null
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/PageUtil.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed 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.lance.namespace.hive3;
+
+import java.util.List;
+
+/** Utility methods for pagination. */
+public class PageUtil {
+
+ private static final int DEFAULT_PAGE_SIZE = 100;
+
+ private PageUtil() {}
+
+ public static int normalizePageSize(Integer pageSize) {
+ if (pageSize == null || pageSize <= 0) {
+ return DEFAULT_PAGE_SIZE;
+ }
+ return pageSize;
+ }
+
+ public static Page splitPage(List items, String pageToken, int pageSize) {
+ int startIndex = 0;
+ if (pageToken != null && !pageToken.isEmpty()) {
+ try {
+ startIndex = Integer.parseInt(pageToken);
+ } catch (NumberFormatException e) {
+ startIndex = 0;
+ }
+ }
+
+ if (startIndex >= items.size()) {
+ return new Page(java.util.Collections.emptyList(), null);
+ }
+
+ int endIndex = Math.min(startIndex + pageSize, items.size());
+ List pageItems = items.subList(startIndex, endIndex);
+
+ String nextPageToken = endIndex < items.size() ? String.valueOf(endIndex) : null;
+ return new Page(pageItems, nextPageToken);
+ }
+
+ public static class Page {
+ private final List items;
+ private final String nextPageToken;
+
+ public Page(List items, String nextPageToken) {
+ this.items = items;
+ this.nextPageToken = nextPageToken;
+ }
+
+ public List items() {
+ return items;
+ }
+
+ public String nextPageToken() {
+ return nextPageToken;
+ }
+ }
+}
diff --git a/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/ValidationUtil.java b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/ValidationUtil.java
new file mode 100644
index 0000000..c2dbc3a
--- /dev/null
+++ b/java/lance-namespace-hive3/src/main/java/org/lance/namespace/hive3/ValidationUtil.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed 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.lance.namespace.hive3;
+
+import org.lance.namespace.errors.InvalidInputException;
+
+/** Utility methods for validation. */
+public class ValidationUtil {
+
+ private ValidationUtil() {}
+
+ public static void checkArgument(boolean expression, String message, Object... args) {
+ if (!expression) {
+ throw new InvalidInputException(String.format(message, args));
+ }
+ }
+
+ public static String checkNotNullOrEmptyString(String value, String message) {
+ if (value == null || value.isEmpty()) {
+ throw new InvalidInputException(message);
+ }
+ return value;
+ }
+}
diff --git a/java/lance-namespace-hive3/src/test/java/org/lance/namespace/hive3/TestHive3Namespace.java b/java/lance-namespace-hive3/src/test/java/org/lance/namespace/hive3/TestHive3Namespace.java
index 9ae66c4..e98376a 100644
--- a/java/lance-namespace-hive3/src/test/java/org/lance/namespace/hive3/TestHive3Namespace.java
+++ b/java/lance-namespace-hive3/src/test/java/org/lance/namespace/hive3/TestHive3Namespace.java
@@ -14,20 +14,14 @@
package org.lance.namespace.hive3;
import org.lance.namespace.LanceNamespace;
-import org.lance.namespace.LanceNamespaceException;
-import org.lance.namespace.LanceNamespaces;
-import org.lance.namespace.TestHelper;
+import org.lance.namespace.errors.InvalidInputException;
+import org.lance.namespace.errors.LanceNamespaceException;
import org.lance.namespace.model.CreateNamespaceRequest;
-import org.lance.namespace.model.CreateTableRequest;
-import org.lance.namespace.model.CreateTableResponse;
import org.lance.namespace.model.DescribeNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
import org.lance.namespace.model.DescribeTableRequest;
-import org.lance.namespace.model.DescribeTableResponse;
import org.lance.namespace.model.DropNamespaceRequest;
import org.lance.namespace.model.DropNamespaceResponse;
-import org.lance.namespace.model.DropTableRequest;
-import org.lance.namespace.model.DropTableResponse;
import org.lance.namespace.model.ListTablesRequest;
import org.lance.namespace.model.ListTablesResponse;
import org.lance.namespace.model.NamespaceExistsRequest;
@@ -41,7 +35,6 @@
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.File;
@@ -74,7 +67,10 @@ public static void setup() throws IOException {
tmpDirBase = file.getAbsolutePath();
HiveConf hiveConf = metastore.hiveConf();
- namespace = LanceNamespaces.connect("hive3", Maps.newHashMap(), hiveConf, allocator);
+ Hive3Namespace hive3Namespace = new Hive3Namespace();
+ hive3Namespace.setHadoopConf(hiveConf);
+ hive3Namespace.initialize(Maps.newHashMap(), allocator);
+ namespace = hive3Namespace;
// Setup: Create catalog and database for tests
CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
@@ -82,7 +78,7 @@ public static void setup() throws IOException {
properties.put("catalog.location.uri", "file://" + tmpDirBase + "/test_catalog");
nsRequest.setProperties(properties);
nsRequest.setId(Lists.list("test_catalog"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ nsRequest.setMode("Create");
namespace.createNamespace(nsRequest);
nsRequest.setId(Lists.list("test_catalog", "test_db"));
@@ -114,97 +110,13 @@ public void cleanup() throws Exception {
properties.put("catalog.location.uri", "file://" + tmpDirBase + "/test_catalog");
nsRequest.setProperties(properties);
nsRequest.setId(Lists.list("test_catalog"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ nsRequest.setMode("Create");
namespace.createNamespace(nsRequest);
nsRequest.setId(Lists.list("test_catalog", "test_db"));
namespace.createNamespace(nsRequest);
}
- @Disabled("Need to figure out the proper interface")
- @Test
- public void testCreateTable() throws IOException {
- // Test: Create table with valid parameters
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_catalog", "test_db", "test_table"));
- request.setLocation(tmpDirBase + "/test_catalog/test_db/test_table.lance");
-
- Map properties = Maps.newHashMap();
- properties.put("custom_prop", "custom_value");
- request.setProperties(properties);
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- CreateTableResponse response = namespace.createTable(request, testData);
-
- assertEquals(request.getLocation(), response.getLocation());
- assertEquals(1L, response.getVersion());
- }
-
- @Test
- public void testCreateTableAlreadyExists() throws IOException {
- // Setup: Create table
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_catalog", "test_db", "test_table"));
- request.setLocation(tmpDirBase + "/test_catalog/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(request, testData);
-
- // Test: Create table that already exists
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.createTable(request, testData));
- assertTrue(error.getMessage().contains("Table test_catalog.test_db.test_table already exists"));
- }
-
- @Test
- public void testCreateTableManagedByImpl() throws IOException {
- // Test: Create table with managed_by=impl (not supported)
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_catalog", "test_db", "impl_table"));
- request.setLocation(tmpDirBase + "/test_catalog/test_db/impl_table.lance");
-
- Map properties = Maps.newHashMap();
- properties.put("managed_by", "impl");
- request.setProperties(properties);
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- Exception error =
- assertThrows(
- UnsupportedOperationException.class, () -> namespace.createTable(request, testData));
- assertTrue(error.getMessage().contains("managed_by=impl is not supported yet"));
- }
-
- @Test
- public void testCreateTableWithoutData() throws IOException {
- // Test: Create table without data
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_catalog", "test_db", "no_data_table"));
- request.setLocation(tmpDirBase + "/test_catalog/test_db/no_data_table.lance");
-
- byte[] emptyData = TestHelper.createEmptyArrowData(allocator);
- CreateTableResponse response = namespace.createTable(request, emptyData);
- assertEquals(request.getLocation(), response.getLocation());
- }
-
- @Test
- public void testDescribeTable() throws IOException {
- // Setup: Create table
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_catalog", "test_db", "test_table"));
- createRequest.setLocation(tmpDirBase + "/test_catalog/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: Describe existing Lance table
- DescribeTableRequest request = new DescribeTableRequest();
- request.setId(Lists.list("test_catalog", "test_db", "test_table"));
-
- DescribeTableResponse response = namespace.describeTable(request);
- assertEquals(
- "file:" + tmpDirBase + "/test_catalog/test_db/test_table.lance", response.getLocation());
- }
-
@Test
public void testDescribeNonExistentTable() {
// Test: Describe non-existent table
@@ -215,131 +127,6 @@ public void testDescribeNonExistentTable() {
assertTrue(error.getMessage().contains("Table does not exist"));
}
- @Test
- public void testDropTable() throws IOException {
- // Setup: Create table
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_catalog", "test_db", "test_table"));
- createRequest.setLocation(tmpDirBase + "/test_catalog/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: Drop existing table
- DropTableRequest request = new DropTableRequest();
- request.setId(Lists.list("test_catalog", "test_db", "test_table"));
-
- DropTableResponse response = namespace.dropTable(request);
- assertEquals(
- "file:" + tmpDirBase + "/test_catalog/test_db/test_table.lance", response.getLocation());
- assertEquals(request.getId(), response.getId());
-
- // Verify table is dropped by trying to describe it
- DescribeTableRequest descRequest = new DescribeTableRequest();
- descRequest.setId(request.getId());
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.describeTable(descRequest));
- assertTrue(error.getMessage().contains("Table does not exist"));
- }
-
- @Test
- public void testDropNonExistentTable() {
- // Test: Drop non-existent table
- DropTableRequest request = new DropTableRequest();
- request.setId(Lists.list("test_catalog", "test_db", "non_existent"));
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.dropTable(request));
- assertTrue(
- error.getMessage().contains("Table test_catalog.test_db.non_existent does not exist"));
- }
-
- @Test
- public void testCreateTableWithDefaultLocationFromRoot() throws IOException {
- // With our enhancement, databases created without explicit location
- // will use the root config location instead of Hive warehouse
-
- // Setup: Create namespace with custom root configuration
- Map properties = Maps.newHashMap();
- properties.put("root", tmpDirBase);
-
- HiveConf hiveConf = metastore.hiveConf();
- LanceNamespace customNamespace =
- LanceNamespaces.connect("hive3", properties, hiveConf, allocator);
-
- // Setup: Create database (will use root location)
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_catalog", "test_db_root"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- customNamespace.createNamespace(nsRequest);
-
- // Test: Create table without specifying location
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_catalog", "test_db_root", "test_table"));
- // Don't set location - it will be derived from database location
-
- // Create test Arrow IPC data
- byte[] testData = TestHelper.createTestArrowData(allocator);
- CreateTableResponse response = customNamespace.createTable(request, testData);
-
- // Verify: Location should be derived from root-based database location
- // Note: The location may or may not have file: prefix depending on how Hive processes it
- String expectedLocation = tmpDirBase + "/test_db_root/test_table.lance";
- assertTrue(
- response.getLocation().equals(expectedLocation)
- || response.getLocation().equals("file:" + expectedLocation),
- "Expected location (with or without file: prefix): "
- + expectedLocation
- + " but got: "
- + response.getLocation());
- assertEquals(1L, response.getVersion());
- }
-
- @Test
- public void testCreateTableWithExplicitDatabaseLocation() throws IOException {
- // Note: This test verifies that when a database location is explicitly set,
- // it takes precedence over the root config. However, the current implementation
- // may fall back to root config if database location retrieval fails.
-
- // Setup: Create namespace with custom root configuration
- Map properties = Maps.newHashMap();
- properties.put("root", tmpDirBase);
-
- HiveConf hiveConf = metastore.hiveConf();
- LanceNamespace customNamespace =
- LanceNamespaces.connect("hive3", properties, hiveConf, allocator);
-
- // Setup: Create database with specific location
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_catalog", "test_db_with_location"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
-
- // Set database location - this should take precedence over root config
- String databaseLocation = tmpDirBase + "/custom_db_location";
- Map dbProperties = Maps.newHashMap();
- dbProperties.put("database.location-uri", databaseLocation);
- nsRequest.setProperties(dbProperties);
-
- customNamespace.createNamespace(nsRequest);
-
- // Test: Create table without specifying location
- CreateTableRequest request = new CreateTableRequest();
- request.setId(Lists.list("test_catalog", "test_db_with_location", "test_table"));
- // Don't set location - should be derived from database location or root fallback
-
- // Create test Arrow IPC data
- byte[] testData = TestHelper.createTestArrowData(allocator);
- CreateTableResponse response = customNamespace.createTable(request, testData);
-
- // Verify: Location should be derived from either database location or root fallback
- // For now, accept either pattern until database location retrieval is fixed
- assertTrue(
- response.getLocation().contains("custom_db_location/test_table.lance")
- || response.getLocation().contains("test_db_with_location/test_table.lance"),
- "Expected either custom database location or root fallback but got: "
- + response.getLocation());
- assertEquals(1L, response.getVersion());
- }
-
@Test
public void testDescribeNamespaceCatalog() {
// Test: Describe catalog-level namespace
@@ -370,7 +157,7 @@ public void testDescribeNamespaceDatabaseWithCustomProperties() {
// Setup: Create database with custom properties
CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
nsRequest.setId(Lists.list("test_catalog", "custom_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ nsRequest.setMode("Create");
Map properties = Maps.newHashMap();
properties.put("database.description", "Custom database description");
@@ -457,24 +244,6 @@ public void testNamespaceExistsNonExistentDatabase() {
assertTrue(error.getMessage().contains("Namespace does not exist"));
}
- @Test
- public void testTableExists() throws IOException {
- // Setup: Create table
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_catalog", "test_db", "test_table"));
- createRequest.setLocation(tmpDirBase + "/test_catalog/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: Check existing table
- TableExistsRequest request = new TableExistsRequest();
- request.setId(Lists.list("test_catalog", "test_db", "test_table"));
-
- // Should not throw exception for existing Lance table
- namespace.tableExists(request);
- }
-
@Test
public void testTableExistsNonExistent() {
// Test: Check non-existent table
@@ -486,34 +255,6 @@ public void testTableExistsNonExistent() {
assertTrue(error.getMessage().contains("Table does not exist"));
}
- @Test
- public void testListTables() throws IOException {
- // Create first table
- CreateTableRequest createRequest1 = new CreateTableRequest();
- createRequest1.setId(Lists.list("test_catalog", "test_db", "table1"));
- createRequest1.setLocation(tmpDirBase + "/test_catalog/test_db/table1.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest1, testData);
-
- // Create second table
- CreateTableRequest createRequest2 = new CreateTableRequest();
- createRequest2.setId(Lists.list("test_catalog", "test_db", "table2"));
- createRequest2.setLocation(tmpDirBase + "/test_catalog/test_db/table2.lance");
-
- namespace.createTable(createRequest2, testData);
-
- // Test: List tables
- ListTablesRequest request = new ListTablesRequest();
- request.setId(Lists.list("test_catalog", "test_db"));
-
- ListTablesResponse response = namespace.listTables(request);
-
- assertEquals(2, response.getTables().size());
- assertTrue(response.getTables().contains("table1"));
- assertTrue(response.getTables().contains("table2"));
- }
-
@Test
public void testListTablesEmpty() {
// Test: List tables in empty database
@@ -525,67 +266,6 @@ public void testListTablesEmpty() {
assertEquals(0, response.getTables().size());
}
- @Test
- public void testListTablesWithPagination() throws IOException {
- // Create multiple tables
- for (int i = 1; i <= 5; i++) {
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_catalog", "test_db", "table" + i));
- createRequest.setLocation(tmpDirBase + "/test_catalog/test_db/table" + i + ".lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
- }
-
- // Test: List tables with pagination (limit 3)
- ListTablesRequest request = new ListTablesRequest();
- request.setId(Lists.list("test_catalog", "test_db"));
- request.setLimit(3);
-
- ListTablesResponse response = namespace.listTables(request);
-
- assertEquals(3, response.getTables().size());
- // Should have a page token for remaining results
- assertTrue(response.getPageToken() != null && !response.getPageToken().isEmpty());
-
- // Get remaining tables
- ListTablesRequest nextRequest = new ListTablesRequest();
- nextRequest.setId(Lists.list("test_catalog", "test_db"));
- nextRequest.setPageToken(response.getPageToken());
-
- ListTablesResponse nextResponse = namespace.listTables(nextRequest);
-
- assertEquals(2, nextResponse.getTables().size());
- // No more pages
- assertTrue(nextResponse.getPageToken() == null || nextResponse.getPageToken().isEmpty());
- }
-
- @Test
- public void testListTablesWithCustomDatabase() throws IOException {
- // Setup: Create database with custom name
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Lists.list("test_catalog", "custom_db"));
- nsRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(nsRequest);
-
- // Create table in custom database
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_catalog", "custom_db", "custom_table"));
- createRequest.setLocation(tmpDirBase + "/test_catalog/custom_db/custom_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: List tables in custom database
- ListTablesRequest request = new ListTablesRequest();
- request.setId(Lists.list("test_catalog", "custom_db"));
-
- ListTablesResponse response = namespace.listTables(request);
-
- assertEquals(1, response.getTables().size());
- assertTrue(response.getTables().contains("custom_table"));
- }
-
@Test
public void testListTablesNonExistentDatabase() {
// Test: List tables in non-existent database
@@ -613,12 +293,12 @@ public void testDropNamespaceBasicDatabase() throws IOException {
// Setup: Create catalog and database
CreateNamespaceRequest catalogRequest = new CreateNamespaceRequest();
catalogRequest.setId(Lists.list("test_catalog_basic_db"));
- catalogRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ catalogRequest.setMode("Create");
namespace.createNamespace(catalogRequest);
CreateNamespaceRequest dbRequest = new CreateNamespaceRequest();
dbRequest.setId(Lists.list("test_catalog_basic_db", "test_db"));
- dbRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ dbRequest.setMode("Create");
Map properties = Maps.newHashMap();
properties.put("database.description", "Test database for dropping");
@@ -648,35 +328,15 @@ public void testDropNamespaceBasicDatabase() throws IOException {
}
@Test
- public void testDropNamespaceBasicCatalog() {
- // Setup: Create catalog
- CreateNamespaceRequest catalogRequest = new CreateNamespaceRequest();
- catalogRequest.setId(Lists.list("test_catalog_basic"));
- catalogRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
-
- Map properties = Maps.newHashMap();
- properties.put("description", "Test catalog for dropping");
- catalogRequest.setProperties(properties);
-
- namespace.createNamespace(catalogRequest);
-
- // Test: Drop the catalog with CASCADE (since Hive creates default database automatically)
+ public void testDropNamespaceCascadeRejected() {
+ // Test: Drop with CASCADE behavior - should be rejected
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Lists.list("test_catalog_basic"));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE);
-
- DropNamespaceResponse response = namespace.dropNamespace(dropRequest);
-
- // Verify properties were returned
- assertEquals("Test catalog for dropping", response.getProperties().get("description"));
-
- // Verify catalog was dropped
- NamespaceExistsRequest existsRequest = new NamespaceExistsRequest();
- existsRequest.setId(Lists.list("test_catalog_basic"));
+ dropRequest.setBehavior("Cascade");
Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.namespaceExists(existsRequest));
- assertTrue(error.getMessage().contains("Namespace does not exist"));
+ assertThrows(InvalidInputException.class, () -> namespace.dropNamespace(dropRequest));
+ assertTrue(error.getMessage().contains("Cascade behavior is not supported"));
}
@Test
@@ -684,7 +344,7 @@ public void testDropNamespaceSkipMode() {
// Test: Drop non-existent database with SKIP mode
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Lists.list("non_existent_catalog", "non_existent_db"));
- dropRequest.setMode(DropNamespaceRequest.ModeEnum.SKIP);
+ dropRequest.setMode("Skip");
DropNamespaceResponse response = namespace.dropNamespace(dropRequest);
@@ -697,151 +357,34 @@ public void testDropNamespaceFailMode() {
// Test: Drop non-existent database with FAIL mode (default)
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Lists.list("non_existent_catalog", "non_existent_db"));
- dropRequest.setMode(DropNamespaceRequest.ModeEnum.FAIL);
+ dropRequest.setMode("Fail");
Exception error =
assertThrows(LanceNamespaceException.class, () -> namespace.dropNamespace(dropRequest));
assertTrue(error.getMessage().contains("doesn't exist"));
}
- @Test
- public void testDropDatabaseRestrictWithTables() throws IOException {
- // Setup: Create catalog, database and table
- CreateNamespaceRequest catalogRequest = new CreateNamespaceRequest();
- catalogRequest.setId(Lists.list("test_catalog_restrict"));
- catalogRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(catalogRequest);
-
- CreateNamespaceRequest dbRequest = new CreateNamespaceRequest();
- dbRequest.setId(Lists.list("test_catalog_restrict", "test_db"));
- dbRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(dbRequest);
-
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_catalog_restrict", "test_db", "test_table"));
- createRequest.setLocation(tmpDirBase + "/test_catalog_restrict/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: Try to drop database with RESTRICT behavior (should fail)
- DropNamespaceRequest dropRequest = new DropNamespaceRequest();
- dropRequest.setId(Lists.list("test_catalog_restrict", "test_db"));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.RESTRICT);
-
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.dropNamespace(dropRequest));
- assertTrue(error.getMessage().contains("Database test_catalog_restrict.test_db is not empty"));
- assertTrue(error.getMessage().contains("Contains 1 tables"));
- }
-
@Test
public void testDropCatalogRestrictWithDatabases() {
// Setup: Create catalog and database
CreateNamespaceRequest catalogRequest = new CreateNamespaceRequest();
catalogRequest.setId(Lists.list("test_catalog_restrict_db"));
- catalogRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ catalogRequest.setMode("Create");
namespace.createNamespace(catalogRequest);
CreateNamespaceRequest dbRequest = new CreateNamespaceRequest();
dbRequest.setId(Lists.list("test_catalog_restrict_db", "test_db"));
- dbRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
+ dbRequest.setMode("Create");
namespace.createNamespace(dbRequest);
// Test: Try to drop catalog with RESTRICT behavior (should fail)
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Lists.list("test_catalog_restrict_db"));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.RESTRICT);
+ dropRequest.setBehavior("Restrict");
Exception error =
assertThrows(LanceNamespaceException.class, () -> namespace.dropNamespace(dropRequest));
assertTrue(error.getMessage().contains("is not empty"));
assertTrue(error.getMessage().contains("databases"));
}
-
- @Test
- public void testDropDatabaseCascadeWithTables() throws IOException {
- // Setup: Create catalog, database and multiple tables
- CreateNamespaceRequest catalogRequest = new CreateNamespaceRequest();
- catalogRequest.setId(Lists.list("test_catalog_cascade_db"));
- catalogRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(catalogRequest);
-
- CreateNamespaceRequest dbRequest = new CreateNamespaceRequest();
- dbRequest.setId(Lists.list("test_catalog_cascade_db", "test_db"));
- dbRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(dbRequest);
-
- // Create first table
- CreateTableRequest createRequest1 = new CreateTableRequest();
- createRequest1.setId(Lists.list("test_catalog_cascade_db", "test_db", "table1"));
- createRequest1.setLocation(tmpDirBase + "/test_catalog_cascade_db/test_db/table1.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest1, testData);
-
- // Create second table
- CreateTableRequest createRequest2 = new CreateTableRequest();
- createRequest2.setId(Lists.list("test_catalog_cascade_db", "test_db", "table2"));
- createRequest2.setLocation(tmpDirBase + "/test_catalog_cascade_db/test_db/table2.lance");
-
- namespace.createTable(createRequest2, testData);
-
- // Test: Drop database with CASCADE behavior
- DropNamespaceRequest dropRequest = new DropNamespaceRequest();
- dropRequest.setId(Lists.list("test_catalog_cascade_db", "test_db"));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE);
-
- DropNamespaceResponse response = namespace.dropNamespace(dropRequest);
-
- // Verify database properties were returned
- assertTrue(response.getProperties().containsKey("database.location-uri"));
-
- // Verify database was dropped
- NamespaceExistsRequest existsRequest = new NamespaceExistsRequest();
- existsRequest.setId(Lists.list("test_catalog_cascade_db", "test_db"));
-
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.namespaceExists(existsRequest));
- assertTrue(error.getMessage().contains("Namespace does not exist"));
- }
-
- @Test
- public void testDropCatalogCascadeWithDatabasesAndTables() throws IOException {
- // Setup: Create catalog, database and table
- CreateNamespaceRequest catalogRequest = new CreateNamespaceRequest();
- catalogRequest.setId(Lists.list("test_catalog_cascade"));
- catalogRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(catalogRequest);
-
- CreateNamespaceRequest dbRequest = new CreateNamespaceRequest();
- dbRequest.setId(Lists.list("test_catalog_cascade", "test_db"));
- dbRequest.setMode(CreateNamespaceRequest.ModeEnum.CREATE);
- namespace.createNamespace(dbRequest);
-
- CreateTableRequest createRequest = new CreateTableRequest();
- createRequest.setId(Lists.list("test_catalog_cascade", "test_db", "test_table"));
- createRequest.setLocation(tmpDirBase + "/test_catalog_cascade/test_db/test_table.lance");
-
- byte[] testData = TestHelper.createTestArrowData(allocator);
- namespace.createTable(createRequest, testData);
-
- // Test: Drop catalog with CASCADE behavior
- DropNamespaceRequest dropRequest = new DropNamespaceRequest();
- dropRequest.setId(Lists.list("test_catalog_cascade"));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE);
-
- DropNamespaceResponse response = namespace.dropNamespace(dropRequest);
-
- // Verify catalog properties were returned
- assertTrue(response.getProperties().containsKey("catalog.location.uri"));
-
- // Verify catalog was dropped
- NamespaceExistsRequest existsRequest = new NamespaceExistsRequest();
- existsRequest.setId(Lists.list("test_catalog_cascade"));
-
- Exception error =
- assertThrows(LanceNamespaceException.class, () -> namespace.namespaceExists(existsRequest));
- assertTrue(error.getMessage().contains("Namespace does not exist"));
- }
}
diff --git a/java/lance-namespace-hive3/src/test/java/org/lance/namespace/hive3/TestHive3NamespaceIntegration.java b/java/lance-namespace-hive3/src/test/java/org/lance/namespace/hive3/TestHive3NamespaceIntegration.java
index 457f466..8e26396 100644
--- a/java/lance-namespace-hive3/src/test/java/org/lance/namespace/hive3/TestHive3NamespaceIntegration.java
+++ b/java/lance-namespace-hive3/src/test/java/org/lance/namespace/hive3/TestHive3NamespaceIntegration.java
@@ -13,17 +13,18 @@
*/
package org.lance.namespace.hive3;
-import org.lance.namespace.LanceNamespaceException;
+import org.lance.namespace.errors.InvalidInputException;
+import org.lance.namespace.errors.LanceNamespaceException;
import org.lance.namespace.model.CreateEmptyTableRequest;
import org.lance.namespace.model.CreateEmptyTableResponse;
import org.lance.namespace.model.CreateNamespaceRequest;
import org.lance.namespace.model.CreateNamespaceResponse;
+import org.lance.namespace.model.DeregisterTableRequest;
import org.lance.namespace.model.DescribeNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
import org.lance.namespace.model.DescribeTableRequest;
import org.lance.namespace.model.DescribeTableResponse;
import org.lance.namespace.model.DropNamespaceRequest;
-import org.lance.namespace.model.DropTableRequest;
import org.lance.namespace.model.ListNamespacesRequest;
import org.lance.namespace.model.ListNamespacesResponse;
import org.lance.namespace.model.ListTablesRequest;
@@ -31,6 +32,8 @@
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.conf.HiveConf;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
@@ -96,8 +99,12 @@ public void setUp() throws Exception {
testCatalog = "hive"; // Default catalog in Hive 3.x
testDatabase = "test_db_" + uniqueId;
+ // Set up Hadoop configuration with metastore URI
+ Configuration hadoopConf = new Configuration();
+ hadoopConf.set(HiveConf.ConfVars.METASTOREURIS.varname, METASTORE_URI);
+ namespace.setConf(hadoopConf);
+
Map config = new HashMap<>();
- config.put("hive.metastore.uris", METASTORE_URI);
config.put("client.pool-size", "3");
config.put("root", "/tmp/lance-integration-test");
@@ -110,19 +117,13 @@ public void tearDown() {
// Clean up test database
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Arrays.asList(testCatalog, testDatabase));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE);
+ dropRequest.setBehavior("Restrict");
namespace.dropNamespace(dropRequest);
} catch (Exception e) {
// Ignore cleanup errors
}
- if (namespace != null) {
- try {
- namespace.close();
- } catch (Exception e) {
- // Ignore
- }
- }
+ // Namespace cleanup handled by Hive internals
if (allocator != null) {
allocator.close();
@@ -157,8 +158,8 @@ public void testDatabaseOperations() {
DescribeNamespaceResponse describeResponse = namespace.describeNamespace(describeRequest);
assertThat(describeResponse).isNotNull();
- assertThat(describeResponse.getProperties()).containsEntry(
- "database.description", "Integration test database");
+ assertThat(describeResponse.getProperties())
+ .containsEntry("database.description", "Integration test database");
// List databases in catalog
ListNamespacesRequest listRequest = new ListNamespacesRequest();
@@ -184,7 +185,8 @@ public void testTableOperations() {
nsRequest.setId(Arrays.asList(testCatalog, testDatabase));
namespace.createNamespace(nsRequest);
- String tableName = "test_table_" + UUID.randomUUID().toString().substring(0, 8).replace("-", "");
+ String tableName =
+ "test_table_" + UUID.randomUUID().toString().substring(0, 8).replace("-", "");
// Create empty table (declare table without data)
CreateEmptyTableRequest createRequest = new CreateEmptyTableRequest();
@@ -200,7 +202,6 @@ public void testTableOperations() {
DescribeTableResponse describeResponse = namespace.describeTable(describeRequest);
assertThat(describeResponse.getLocation()).contains(tableName);
- assertThat(describeResponse.getProperties()).containsEntry("table_type", "lance");
// List tables
ListTablesRequest listRequest = new ListTablesRequest();
@@ -209,10 +210,10 @@ public void testTableOperations() {
ListTablesResponse listResponse = namespace.listTables(listRequest);
assertThat(listResponse.getTables()).contains(tableName);
- // Drop table
- DropTableRequest dropRequest = new DropTableRequest();
- dropRequest.setId(Arrays.asList(testCatalog, testDatabase, tableName));
- namespace.dropTable(dropRequest);
+ // Deregister table
+ DeregisterTableRequest deregisterRequest = new DeregisterTableRequest();
+ deregisterRequest.setId(Arrays.asList(testCatalog, testDatabase, tableName));
+ namespace.deregisterTable(deregisterRequest);
// Verify table doesn't exist
assertThatThrownBy(() -> namespace.describeTable(describeRequest))
@@ -220,29 +221,14 @@ public void testTableOperations() {
}
@Test
- public void testCascadeDropDatabase() {
- // Create database
- CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
- nsRequest.setId(Arrays.asList(testCatalog, testDatabase));
- namespace.createNamespace(nsRequest);
-
- // Create a table in the database
- String tableName = "cascade_test_table";
- CreateEmptyTableRequest tableRequest = new CreateEmptyTableRequest();
- tableRequest.setId(Arrays.asList(testCatalog, testDatabase, tableName));
- tableRequest.setLocation("/tmp/lance-integration-test/" + testDatabase + "/" + tableName);
- namespace.createEmptyTable(tableRequest);
-
- // Drop database with cascade
+ public void testCascadeDropDatabaseRejected() {
+ // Drop database with cascade - should be rejected
DropNamespaceRequest dropRequest = new DropNamespaceRequest();
dropRequest.setId(Arrays.asList(testCatalog, testDatabase));
- dropRequest.setBehavior(DropNamespaceRequest.BehaviorEnum.CASCADE);
- namespace.dropNamespace(dropRequest);
+ dropRequest.setBehavior("Cascade");
- // Verify database doesn't exist
- DescribeNamespaceRequest describeRequest = new DescribeNamespaceRequest();
- describeRequest.setId(Arrays.asList(testCatalog, testDatabase));
- assertThatThrownBy(() -> namespace.describeNamespace(describeRequest))
- .isInstanceOf(LanceNamespaceException.class);
+ assertThatThrownBy(() -> namespace.dropNamespace(dropRequest))
+ .isInstanceOf(InvalidInputException.class)
+ .hasMessageContaining("Cascade behavior is not supported");
}
}
diff --git a/java/lance-namespace-iceberg/pom.xml b/java/lance-namespace-iceberg/pom.xml
index b486c54..bfa5d9d 100644
--- a/java/lance-namespace-iceberg/pom.xml
+++ b/java/lance-namespace-iceberg/pom.xml
@@ -18,10 +18,18 @@
Iceberg REST Catalog namespace implementation for Lance
+
+ org.lance
+ lance-namespace-impls-core
+
org.lance
lance-core
+
+ org.lance
+ lance-namespace-core
+
org.lance
lance-namespace-apache-client
@@ -48,20 +56,28 @@
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter
test
-
org.mockito
mockito-core
test
-
- org.slf4j
- slf4j-simple
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ ch.qos.logback
+ logback-classic
test
diff --git a/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergModels.java b/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergModels.java
index 4db65a3..b177e58 100644
--- a/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergModels.java
+++ b/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergModels.java
@@ -418,6 +418,31 @@ public void setConfig(Map config) {
}
}
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class ConfigResponse {
+ @JsonProperty("defaults")
+ private Map defaults;
+
+ @JsonProperty("overrides")
+ private Map overrides;
+
+ public Map getDefaults() {
+ return defaults;
+ }
+
+ public void setDefaults(Map defaults) {
+ this.defaults = defaults;
+ }
+
+ public Map getOverrides() {
+ return overrides;
+ }
+
+ public void setOverrides(Map overrides) {
+ this.overrides = overrides;
+ }
+ }
+
public static IcebergSchema createDummySchema() {
IcebergSchema schema = new IcebergSchema();
schema.setType("struct");
@@ -429,7 +454,7 @@ public static IcebergSchema createDummySchema() {
dummyField.setRequired(false);
dummyField.setType("string");
- schema.setFields(List.of(dummyField));
+ schema.setFields(java.util.Collections.singletonList(dummyField));
return schema;
}
}
diff --git a/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergNamespace.java b/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergNamespace.java
index f683f4a..9eb2050 100644
--- a/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergNamespace.java
+++ b/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergNamespace.java
@@ -14,20 +14,26 @@
package org.lance.namespace.iceberg;
import org.lance.namespace.LanceNamespace;
-import org.lance.namespace.LanceNamespaceException;
-import org.lance.namespace.ObjectIdentifier;
+import org.lance.namespace.errors.InternalException;
+import org.lance.namespace.errors.InvalidInputException;
+import org.lance.namespace.errors.NamespaceAlreadyExistsException;
+import org.lance.namespace.errors.NamespaceNotFoundException;
+import org.lance.namespace.errors.TableAlreadyExistsException;
+import org.lance.namespace.errors.TableNotFoundException;
import org.lance.namespace.model.CreateEmptyTableRequest;
import org.lance.namespace.model.CreateEmptyTableResponse;
import org.lance.namespace.model.CreateNamespaceRequest;
import org.lance.namespace.model.CreateNamespaceResponse;
+import org.lance.namespace.model.DeclareTableRequest;
+import org.lance.namespace.model.DeclareTableResponse;
+import org.lance.namespace.model.DeregisterTableRequest;
+import org.lance.namespace.model.DeregisterTableResponse;
import org.lance.namespace.model.DescribeNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
import org.lance.namespace.model.DescribeTableRequest;
import org.lance.namespace.model.DescribeTableResponse;
import org.lance.namespace.model.DropNamespaceRequest;
import org.lance.namespace.model.DropNamespaceResponse;
-import org.lance.namespace.model.DropTableRequest;
-import org.lance.namespace.model.DropTableResponse;
import org.lance.namespace.model.ListNamespacesRequest;
import org.lance.namespace.model.ListNamespacesResponse;
import org.lance.namespace.model.ListTablesRequest;
@@ -35,15 +41,17 @@
import org.lance.namespace.model.NamespaceExistsRequest;
import org.lance.namespace.model.TableExistsRequest;
import org.lance.namespace.rest.RestClient;
+import org.lance.namespace.rest.RestClientException;
+import org.lance.namespace.util.ObjectIdentifier;
import org.lance.namespace.util.ValidationUtil;
import org.apache.arrow.memory.BufferAllocator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.Closeable;
import java.io.IOException;
import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -51,10 +59,22 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
-/** Iceberg REST Catalog namespace implementation for Lance. */
-public class IcebergNamespace implements LanceNamespace {
+/**
+ * Iceberg REST Catalog namespace implementation for Lance.
+ *
+ * The prefix (warehouse) is included in the namespace identifier:
+ *
+ *
+ * - Namespace ID format: [prefix, namespace1, namespace2, ...]
+ *
- Table ID format: [prefix, namespace1, namespace2, ..., table_name]
+ *
+ *
+ * This is consistent with how Polaris handles catalog names.
+ */
+public class IcebergNamespace implements LanceNamespace, Closeable {
private static final Logger LOG = LoggerFactory.getLogger(IcebergNamespace.class);
private static final String TABLE_TYPE_LANCE = "lance";
private static final String TABLE_TYPE_KEY = "table_type";
@@ -63,6 +83,7 @@ public class IcebergNamespace implements LanceNamespace {
private IcebergNamespaceConfig config;
private RestClient restClient;
private BufferAllocator allocator;
+ private final Map prefixCache = new HashMap<>();
public IcebergNamespace() {}
@@ -73,20 +94,16 @@ public void initialize(Map configProperties, BufferAllocator all
RestClient.Builder clientBuilder =
RestClient.builder()
- .baseUrl(config.getFullApiUrl())
- .connectTimeout(config.getConnectTimeout())
- .readTimeout(config.getReadTimeout())
+ .baseUrl(config.getBaseApiUrl())
+ .connectTimeout(config.getConnectTimeout(), TimeUnit.MILLISECONDS)
+ .readTimeout(config.getReadTimeout(), TimeUnit.MILLISECONDS)
.maxRetries(config.getMaxRetries());
- Map headers = new HashMap<>();
if (config.getAuthToken() != null) {
- headers.put("Authorization", "Bearer " + config.getAuthToken());
+ clientBuilder.authToken(config.getAuthToken());
}
if (config.getWarehouse() != null) {
- headers.put("X-Iceberg-Access-Delegation", "vended-credentials");
- }
- if (!headers.isEmpty()) {
- clientBuilder.defaultHeaders(headers);
+ clientBuilder.header("X-Iceberg-Access-Delegation", "vended-credentials");
}
this.restClient = clientBuilder.build();
@@ -98,14 +115,58 @@ public String namespaceId() {
return String.format("IcebergNamespace { endpoint: \"%s\" }", config.getEndpoint());
}
+ private String resolvePrefix(String warehouse) {
+ if (prefixCache.containsKey(warehouse)) {
+ return prefixCache.get(warehouse);
+ }
+
+ try {
+ Map params = new HashMap<>();
+ params.put("warehouse", warehouse);
+ IcebergModels.ConfigResponse response =
+ restClient.get("/v1/config", params, IcebergModels.ConfigResponse.class);
+ if (response != null
+ && response.getDefaults() != null
+ && response.getDefaults().get("prefix") != null) {
+ String prefix = response.getDefaults().get("prefix");
+ prefixCache.put(warehouse, prefix);
+ LOG.debug("Resolved warehouse '{}' to prefix '{}'", warehouse, prefix);
+ return prefix;
+ }
+ } catch (Exception e) {
+ LOG.debug("Failed to resolve prefix for warehouse '{}': {}", warehouse, e.getMessage());
+ }
+
+ prefixCache.put(warehouse, warehouse);
+ return warehouse;
+ }
+
+ private String getPrefixPath(String warehouse) {
+ String prefix = resolvePrefix(warehouse);
+ return "/v1/" + prefix;
+ }
+
@Override
public ListNamespacesResponse listNamespaces(ListNamespacesRequest request) {
- ObjectIdentifier nsId = ObjectIdentifier.of(request.getId());
+ ObjectIdentifier nsId =
+ request.getId() != null
+ ? ObjectIdentifier.of(request.getId())
+ : ObjectIdentifier.of(Collections.emptyList());
+
+ ValidationUtil.checkArgument(
+ nsId.levels() >= 1, "Must specify at least the prefix (warehouse)");
try {
+ String prefix = nsId.levelAtListPos(0);
+ List parentNs =
+ nsId.levels() > 1
+ ? nsId.listStyleId().subList(1, nsId.levels())
+ : Collections.emptyList();
+ String prefixPath = getPrefixPath(prefix);
+
Map params = new HashMap<>();
- if (nsId.levels() > 0) {
- String parent = encodeNamespace(nsId.getIdentifier());
+ if (!parentNs.isEmpty()) {
+ String parent = encodeNamespace(parentNs);
params.put("parent", parent);
}
if (request.getPageToken() != null) {
@@ -113,13 +174,20 @@ public ListNamespacesResponse listNamespaces(ListNamespacesRequest request) {
}
IcebergModels.ListNamespacesResponse response =
- restClient.get("/namespaces", params, IcebergModels.ListNamespacesResponse.class);
+ params.isEmpty()
+ ? restClient.get(
+ prefixPath + "/namespaces", IcebergModels.ListNamespacesResponse.class)
+ : restClient.get(
+ prefixPath + "/namespaces", params, IcebergModels.ListNamespacesResponse.class);
List namespaces = new ArrayList<>();
if (response != null && response.getNamespaces() != null) {
for (List ns : response.getNamespaces()) {
if (!ns.isEmpty()) {
- namespaces.add(ns.get(ns.size() - 1));
+ List fullNs = new ArrayList<>();
+ fullNs.add(prefix);
+ fullNs.addAll(ns);
+ namespaces.add(String.join(".", fullNs));
}
}
}
@@ -130,69 +198,72 @@ public ListNamespacesResponse listNamespaces(ListNamespacesRequest request) {
ListNamespacesResponse result = new ListNamespacesResponse();
result.setNamespaces(resultNamespaces);
return result;
-
- } catch (IOException e) {
- throw new LanceNamespaceException(500, "Failed to list namespaces: " + e.getMessage());
+ } catch (RestClientException e) {
+ throw new InternalException("Failed to list namespaces: " + e.getMessage());
}
}
@Override
public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) {
ObjectIdentifier nsId = ObjectIdentifier.of(request.getId());
- ValidationUtil.checkArgument(nsId.levels() >= 1, "Namespace must have at least one level");
+ ValidationUtil.checkArgument(
+ nsId.levels() >= 2, "Namespace must have at least prefix and namespace levels");
try {
+ String prefix = nsId.levelAtListPos(0);
+ List namespace = nsId.listStyleId().subList(1, nsId.levels());
+ String prefixPath = getPrefixPath(prefix);
+
IcebergModels.CreateNamespaceRequest createRequest =
new IcebergModels.CreateNamespaceRequest();
- createRequest.setNamespace(nsId.getIdentifier());
+ createRequest.setNamespace(namespace);
createRequest.setProperties(request.getProperties());
IcebergModels.CreateNamespaceResponse response =
- restClient.post("/namespaces", createRequest, IcebergModels.CreateNamespaceResponse.class);
+ restClient.post(
+ prefixPath + "/namespaces",
+ createRequest,
+ IcebergModels.CreateNamespaceResponse.class);
+
+ LOG.info("Created namespace: {}.{}", prefix, String.join(".", namespace));
CreateNamespaceResponse result = new CreateNamespaceResponse();
result.setProperties(response != null ? response.getProperties() : null);
return result;
-
- } catch (RestClient.RestClientException e) {
- if (e.getStatusCode() == 409) {
- throw LanceNamespaceException.conflict(
- "Namespace already exists",
- "NAMESPACE_EXISTS",
- request.getId().toString(),
- e.getResponseBody());
+ } catch (RestClientException e) {
+ if (e.isConflict()) {
+ throw new NamespaceAlreadyExistsException(
+ "Namespace already exists: " + nsId.stringStyleId());
}
- throw new LanceNamespaceException(500, "Failed to create namespace: " + e.getMessage());
- } catch (IOException e) {
- throw new LanceNamespaceException(500, "Failed to create namespace: " + e.getMessage());
+ throw new InternalException("Failed to create namespace: " + e.getMessage());
}
}
@Override
public DescribeNamespaceResponse describeNamespace(DescribeNamespaceRequest request) {
ObjectIdentifier nsId = ObjectIdentifier.of(request.getId());
- ValidationUtil.checkArgument(nsId.levels() >= 1, "Namespace must have at least one level");
+ ValidationUtil.checkArgument(
+ nsId.levels() >= 2, "Namespace must have at least prefix and namespace levels");
try {
- String namespacePath = encodeNamespace(nsId.getIdentifier());
+ String prefix = nsId.levelAtListPos(0);
+ List namespace = nsId.listStyleId().subList(1, nsId.levels());
+ String prefixPath = getPrefixPath(prefix);
+ String namespacePath = encodeNamespace(namespace);
+
IcebergModels.GetNamespaceResponse response =
- restClient.get("/namespaces/" + namespacePath, IcebergModels.GetNamespaceResponse.class);
+ restClient.get(
+ prefixPath + "/namespaces/" + namespacePath,
+ IcebergModels.GetNamespaceResponse.class);
DescribeNamespaceResponse result = new DescribeNamespaceResponse();
result.setProperties(response != null ? response.getProperties() : null);
return result;
-
- } catch (RestClient.RestClientException e) {
- if (e.getStatusCode() == 404) {
- throw LanceNamespaceException.notFound(
- "Namespace not found",
- "NAMESPACE_NOT_FOUND",
- request.getId().toString(),
- e.getResponseBody());
+ } catch (RestClientException e) {
+ if (e.isNotFound()) {
+ throw new NamespaceNotFoundException("Namespace not found: " + nsId.stringStyleId());
}
- throw new LanceNamespaceException(500, "Failed to describe namespace: " + e.getMessage());
- } catch (IOException e) {
- throw new LanceNamespaceException(500, "Failed to describe namespace: " + e.getMessage());
+ throw new InternalException("Failed to describe namespace: " + e.getMessage());
}
}
@@ -203,54 +274,61 @@ public void namespaceExists(NamespaceExistsRequest request) {
@Override
public DropNamespaceResponse dropNamespace(DropNamespaceRequest request) {
+ if ("Cascade".equalsIgnoreCase(request.getBehavior())) {
+ throw new InvalidInputException("Cascade behavior is not supported for this implementation");
+ }
+
ObjectIdentifier nsId = ObjectIdentifier.of(request.getId());
- ValidationUtil.checkArgument(nsId.levels() >= 1, "Namespace must have at least one level");
+ ValidationUtil.checkArgument(
+ nsId.levels() >= 2, "Namespace must have at least prefix and namespace levels");
try {
- String namespacePath = encodeNamespace(nsId.getIdentifier());
- restClient.delete("/namespaces/" + namespacePath);
+ String prefix = nsId.levelAtListPos(0);
+ List namespace = nsId.listStyleId().subList(1, nsId.levels());
+ String prefixPath = getPrefixPath(prefix);
+ String namespacePath = encodeNamespace(namespace);
+ restClient.delete(prefixPath + "/namespaces/" + namespacePath);
+ LOG.info("Dropped namespace: {}.{}", prefix, String.join(".", namespace));
return new DropNamespaceResponse();
-
- } catch (RestClient.RestClientException e) {
- if (e.getStatusCode() == 404) {
+ } catch (RestClientException e) {
+ if (e.isNotFound()) {
return new DropNamespaceResponse();
}
- if (e.getStatusCode() == 409) {
- throw LanceNamespaceException.conflict(
- "Namespace not empty",
- "NAMESPACE_NOT_EMPTY",
- request.getId().toString(),
- e.getResponseBody());
- }
- throw new LanceNamespaceException(500, "Failed to drop namespace: " + e.getMessage());
- } catch (IOException e) {
- throw new LanceNamespaceException(500, "Failed to drop namespace: " + e.getMessage());
+ throw new InternalException("Failed to drop namespace: " + e.getMessage());
}
}
@Override
public ListTablesResponse listTables(ListTablesRequest request) {
ObjectIdentifier nsId = ObjectIdentifier.of(request.getId());
- ValidationUtil.checkArgument(nsId.levels() >= 1, "Namespace must have at least one level");
+ ValidationUtil.checkArgument(nsId.levels() >= 2, "Must specify at least prefix and namespace");
try {
- String namespacePath = encodeNamespace(nsId.getIdentifier());
+ String prefix = nsId.levelAtListPos(0);
+ List namespace = nsId.listStyleId().subList(1, nsId.levels());
+ String prefixPath = getPrefixPath(prefix);
+ String namespacePath = encodeNamespace(namespace);
+
Map params = new HashMap<>();
if (request.getPageToken() != null) {
params.put("pageToken", request.getPageToken());
}
IcebergModels.ListTablesResponse response =
- restClient.get(
- "/namespaces/" + namespacePath + "/tables",
- params,
- IcebergModels.ListTablesResponse.class);
+ params.isEmpty()
+ ? restClient.get(
+ prefixPath + "/namespaces/" + namespacePath + "/tables",
+ IcebergModels.ListTablesResponse.class)
+ : restClient.get(
+ prefixPath + "/namespaces/" + namespacePath + "/tables",
+ params,
+ IcebergModels.ListTablesResponse.class);
List tables = new ArrayList<>();
if (response != null && response.getIdentifiers() != null) {
for (IcebergModels.TableIdentifier tableId : response.getIdentifiers()) {
- if (isLanceTable(nsId.getIdentifier(), tableId.getName())) {
+ if (isLanceTable(prefix, namespace, tableId.getName())) {
tables.add(tableId.getName());
}
}
@@ -262,25 +340,31 @@ public ListTablesResponse listTables(ListTablesRequest request) {
ListTablesResponse result = new ListTablesResponse();
result.setTables(resultTables);
return result;
-
- } catch (IOException e) {
- throw new LanceNamespaceException(500, "Failed to list tables: " + e.getMessage());
+ } catch (RestClientException e) {
+ if (e.isNotFound()) {
+ throw new NamespaceNotFoundException("Namespace not found: " + nsId.stringStyleId());
+ }
+ throw new InternalException("Failed to list tables: " + e.getMessage());
}
}
@Override
- public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request) {
+ public DeclareTableResponse declareTable(DeclareTableRequest request) {
ObjectIdentifier tableId = ObjectIdentifier.of(request.getId());
ValidationUtil.checkArgument(
- tableId.levels() >= 2, "Table identifier must have at least namespace and table name");
+ tableId.levels() >= 3, "Table identifier must have prefix, namespace, and table name");
- List namespace = tableId.getIdentifier().subList(0, tableId.levels() - 1);
+ String prefix = tableId.levelAtListPos(0);
+ List namespace = tableId.listStyleId().subList(1, tableId.levels() - 1);
String tableName = tableId.levelAtListPos(tableId.levels() - 1);
try {
+ String prefixPath = getPrefixPath(prefix);
+
String tablePath = request.getLocation();
if (tablePath == null || tablePath.isEmpty()) {
- tablePath = config.getRoot() + "/" + String.join("/", namespace) + "/" + tableName;
+ List pathParts = tableId.listStyleId().subList(0, tableId.levels() - 1);
+ tablePath = config.getRoot() + "/" + String.join("/", pathParts) + "/" + tableName;
}
IcebergModels.CreateTableRequest createRequest = new IcebergModels.CreateTableRequest();
@@ -290,91 +374,92 @@ public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request
Map properties = new HashMap<>();
properties.put(TABLE_TYPE_KEY, TABLE_TYPE_LANCE);
- if (request.getProperties() != null) {
- properties.putAll(request.getProperties());
- }
createRequest.setProperties(properties);
String namespacePath = encodeNamespace(namespace);
- IcebergModels.LoadTableResponse response =
- restClient.post(
- "/namespaces/" + namespacePath + "/tables",
- createRequest,
- IcebergModels.LoadTableResponse.class);
+ restClient.post(
+ prefixPath + "/namespaces/" + namespacePath + "/tables",
+ createRequest,
+ IcebergModels.LoadTableResponse.class);
- CreateEmptyTableResponse result = new CreateEmptyTableResponse();
+ LOG.info("Declared Lance table: {}", tableId.stringStyleId());
+
+ DeclareTableResponse result = new DeclareTableResponse();
result.setLocation(tablePath);
- if (response != null && response.getMetadata() != null) {
- result.setProperties(response.getMetadata().getProperties());
- }
return result;
-
- } catch (RestClient.RestClientException e) {
- if (e.getStatusCode() == 409) {
- throw LanceNamespaceException.conflict(
- "Table already exists",
- "TABLE_EXISTS",
- request.getId().toString(),
- e.getResponseBody());
+ } catch (RestClientException e) {
+ if (e.isConflict()) {
+ throw new TableAlreadyExistsException("Table already exists: " + tableId.stringStyleId());
}
- if (e.getStatusCode() == 404) {
- throw LanceNamespaceException.notFound(
- "Namespace not found",
- "NAMESPACE_NOT_FOUND",
- String.join(".", namespace),
- e.getResponseBody());
+ if (e.isNotFound()) {
+ throw new NamespaceNotFoundException(
+ "Namespace not found: " + prefix + "." + String.join(".", namespace));
}
- throw new LanceNamespaceException(500, "Failed to create empty table: " + e.getMessage());
- } catch (IOException e) {
- throw new LanceNamespaceException(500, "Failed to create empty table: " + e.getMessage());
+ throw new InternalException("Failed to declare table: " + e.getMessage());
}
}
+ /**
+ * @deprecated Use {@link #declareTable(DeclareTableRequest)} instead.
+ */
+ @Deprecated
+ @Override
+ public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request) {
+ DeclareTableRequest declareRequest = new DeclareTableRequest();
+ declareRequest.setId(request.getId());
+ declareRequest.setLocation(request.getLocation());
+ DeclareTableResponse response = declareTable(declareRequest);
+ CreateEmptyTableResponse result = new CreateEmptyTableResponse();
+ result.setLocation(response.getLocation());
+ return result;
+ }
+
@Override
public DescribeTableResponse describeTable(DescribeTableRequest request) {
+ if (Boolean.TRUE.equals(request.getLoadDetailedMetadata())) {
+ throw new InvalidInputException(
+ "load_detailed_metadata=true is not supported for this implementation");
+ }
+
ObjectIdentifier tableId = ObjectIdentifier.of(request.getId());
ValidationUtil.checkArgument(
- tableId.levels() >= 2, "Table identifier must have at least namespace and table name");
+ tableId.levels() >= 3, "Table identifier must have prefix, namespace, and table name");
- List namespace = tableId.getIdentifier().subList(0, tableId.levels() - 1);
+ String prefix = tableId.levelAtListPos(0);
+ List namespace = tableId.listStyleId().subList(1, tableId.levels() - 1);
String tableName = tableId.levelAtListPos(tableId.levels() - 1);
try {
+ String prefixPath = getPrefixPath(prefix);
String namespacePath = encodeNamespace(namespace);
- String encodedTableName = URLEncoder.encode(tableName, StandardCharsets.UTF_8);
+ String encodedTableName = urlEncode(tableName);
IcebergModels.LoadTableResponse response =
restClient.get(
- "/namespaces/" + namespacePath + "/tables/" + encodedTableName,
+ prefixPath + "/namespaces/" + namespacePath + "/tables/" + encodedTableName,
IcebergModels.LoadTableResponse.class);
if (response == null || response.getMetadata() == null) {
- throw LanceNamespaceException.notFound(
- "Table not found", "TABLE_NOT_FOUND", request.getId().toString(), "No metadata");
+ throw new TableNotFoundException("Table not found: " + tableId.stringStyleId());
}
Map props = response.getMetadata().getProperties();
if (props == null || !TABLE_TYPE_LANCE.equalsIgnoreCase(props.get(TABLE_TYPE_KEY))) {
- throw LanceNamespaceException.badRequest(
- "Not a Lance table",
- "INVALID_TABLE",
- request.getId().toString(),
- "Table is not managed by Lance");
+ throw new InvalidInputException(
+ String.format(
+ "Table %s is not a Lance table (missing table_type property)",
+ tableId.stringStyleId()));
}
DescribeTableResponse result = new DescribeTableResponse();
result.setLocation(response.getMetadata().getLocation());
- result.setProperties(props);
+ result.setStorageOptions(props);
return result;
-
- } catch (RestClient.RestClientException e) {
- if (e.getStatusCode() == 404) {
- throw LanceNamespaceException.notFound(
- "Table not found", "TABLE_NOT_FOUND", request.getId().toString(), e.getResponseBody());
+ } catch (RestClientException e) {
+ if (e.isNotFound()) {
+ throw new TableNotFoundException("Table not found: " + tableId.stringStyleId());
}
- throw new LanceNamespaceException(500, "Failed to describe table: " + e.getMessage());
- } catch (IOException e) {
- throw new LanceNamespaceException(500, "Failed to describe table: " + e.getMessage());
+ throw new InternalException("Failed to describe table: " + e.getMessage());
}
}
@@ -384,49 +469,46 @@ public void tableExists(TableExistsRequest request) {
}
@Override
- public DropTableResponse dropTable(DropTableRequest request) {
+ public DeregisterTableResponse deregisterTable(DeregisterTableRequest request) {
ObjectIdentifier tableId = ObjectIdentifier.of(request.getId());
ValidationUtil.checkArgument(
- tableId.levels() >= 2, "Table identifier must have at least namespace and table name");
+ tableId.levels() >= 3, "Table identifier must have prefix, namespace, and table name");
- List namespace = tableId.getIdentifier().subList(0, tableId.levels() - 1);
+ String prefix = tableId.levelAtListPos(0);
+ List namespace = tableId.listStyleId().subList(1, tableId.levels() - 1);
String tableName = tableId.levelAtListPos(tableId.levels() - 1);
try {
+ String prefixPath = getPrefixPath(prefix);
String namespacePath = encodeNamespace(namespace);
- String encodedTableName = URLEncoder.encode(tableName, StandardCharsets.UTF_8);
-
- String tableLocation = null;
- try {
- IcebergModels.LoadTableResponse tableResponse =
- restClient.get(
- "/namespaces/" + namespacePath + "/tables/" + encodedTableName,
- IcebergModels.LoadTableResponse.class);
- if (tableResponse != null && tableResponse.getMetadata() != null) {
- tableLocation = tableResponse.getMetadata().getLocation();
- }
- } catch (RestClient.RestClientException e) {
- if (e.getStatusCode() == 404) {
- DropTableResponse result = new DropTableResponse();
- result.setId(request.getId());
- return result;
- }
+ String encodedTableName = urlEncode(tableName);
+
+ IcebergModels.LoadTableResponse getResponse =
+ restClient.get(
+ prefixPath + "/namespaces/" + namespacePath + "/tables/" + encodedTableName,
+ IcebergModels.LoadTableResponse.class);
+
+ String location = null;
+ if (getResponse != null && getResponse.getMetadata() != null) {
+ location = getResponse.getMetadata().getLocation();
}
- Map params = new HashMap<>();
- params.put("purgeRequested", "false");
- restClient.delete("/namespaces/" + namespacePath + "/tables/" + encodedTableName, params);
+ restClient.delete(
+ prefixPath + "/namespaces/" + namespacePath + "/tables/" + encodedTableName);
+ LOG.info("Deregistered table: {}", tableId.stringStyleId());
- DropTableResponse result = new DropTableResponse();
- result.setId(request.getId());
- result.setLocation(tableLocation);
+ DeregisterTableResponse result = new DeregisterTableResponse();
+ result.setLocation(location);
return result;
-
- } catch (IOException e) {
- throw new LanceNamespaceException(500, "Failed to drop table: " + e.getMessage());
+ } catch (RestClientException e) {
+ if (e.isNotFound()) {
+ throw new TableNotFoundException("Table not found: " + tableId.stringStyleId());
+ }
+ throw new InternalException("Failed to deregister table: " + e.getMessage());
}
}
+ @Override
public void close() throws IOException {
if (restClient != null) {
restClient.close();
@@ -436,19 +518,28 @@ public void close() throws IOException {
private String encodeNamespace(List namespace) {
String joined =
namespace.stream()
- .map(s -> URLEncoder.encode(s, StandardCharsets.UTF_8))
+ .map(this::urlEncode)
.collect(Collectors.joining(String.valueOf(NAMESPACE_SEPARATOR)));
- return URLEncoder.encode(joined, StandardCharsets.UTF_8);
+ return urlEncode(joined);
+ }
+
+ private String urlEncode(String s) {
+ try {
+ return URLEncoder.encode(s, "UTF-8");
+ } catch (java.io.UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported", e);
+ }
}
- private boolean isLanceTable(List namespace, String tableName) {
+ private boolean isLanceTable(String prefix, List namespace, String tableName) {
try {
+ String prefixPath = getPrefixPath(prefix);
String namespacePath = encodeNamespace(namespace);
- String encodedTableName = URLEncoder.encode(tableName, StandardCharsets.UTF_8);
+ String encodedTableName = urlEncode(tableName);
IcebergModels.LoadTableResponse response =
restClient.get(
- "/namespaces/" + namespacePath + "/tables/" + encodedTableName,
+ prefixPath + "/namespaces/" + namespacePath + "/tables/" + encodedTableName,
IcebergModels.LoadTableResponse.class);
if (response != null && response.getMetadata() != null) {
diff --git a/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergNamespaceConfig.java b/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergNamespaceConfig.java
index f5bc7a5..ce1793c 100644
--- a/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergNamespaceConfig.java
+++ b/java/lance-namespace-iceberg/src/main/java/org/lance/namespace/iceberg/IcebergNamespaceConfig.java
@@ -20,7 +20,6 @@ public class IcebergNamespaceConfig {
public static final String ENDPOINT = "endpoint";
public static final String WAREHOUSE = "warehouse";
- public static final String PREFIX = "prefix";
public static final String AUTH_TOKEN = "auth_token";
public static final String CREDENTIAL = "credential";
public static final String CONNECT_TIMEOUT = "connect_timeout";
@@ -30,7 +29,6 @@ public class IcebergNamespaceConfig {
private final String endpoint;
private final String warehouse;
- private final String prefix;
private final String authToken;
private final String credential;
private final int connectTimeout;
@@ -45,7 +43,6 @@ public IcebergNamespaceConfig(Map properties) {
}
this.warehouse = properties.get(WAREHOUSE);
- this.prefix = properties.getOrDefault(PREFIX, "");
this.authToken = properties.get(AUTH_TOKEN);
this.credential = properties.get(CREDENTIAL);
this.connectTimeout = Integer.parseInt(properties.getOrDefault(CONNECT_TIMEOUT, "10000"));
@@ -62,10 +59,6 @@ public String getWarehouse() {
return warehouse;
}
- public String getPrefix() {
- return prefix;
- }
-
public String getAuthToken() {
return authToken;
}
@@ -90,11 +83,7 @@ public String getRoot() {
return root;
}
- public String getFullApiUrl() {
- String base = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint;
- if (prefix != null && !prefix.isEmpty()) {
- return base + "/" + prefix;
- }
- return base;
+ public String getBaseApiUrl() {
+ return endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint;
}
}
diff --git a/java/lance-namespace-iceberg/src/test/java/org/lance/namespace/iceberg/TestIcebergNamespaceIntegration.java b/java/lance-namespace-iceberg/src/test/java/org/lance/namespace/iceberg/TestIcebergNamespaceIntegration.java
new file mode 100644
index 0000000..ad7eb7c
--- /dev/null
+++ b/java/lance-namespace-iceberg/src/test/java/org/lance/namespace/iceberg/TestIcebergNamespaceIntegration.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed 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.lance.namespace.iceberg;
+
+import org.lance.namespace.errors.LanceNamespaceException;
+import org.lance.namespace.model.CreateEmptyTableRequest;
+import org.lance.namespace.model.CreateEmptyTableResponse;
+import org.lance.namespace.model.CreateNamespaceRequest;
+import org.lance.namespace.model.CreateNamespaceResponse;
+import org.lance.namespace.model.DeregisterTableRequest;
+import org.lance.namespace.model.DescribeNamespaceRequest;
+import org.lance.namespace.model.DescribeNamespaceResponse;
+import org.lance.namespace.model.DescribeTableRequest;
+import org.lance.namespace.model.DescribeTableResponse;
+import org.lance.namespace.model.DropNamespaceRequest;
+import org.lance.namespace.model.ListNamespacesRequest;
+import org.lance.namespace.model.ListNamespacesResponse;
+import org.lance.namespace.model.ListTablesRequest;
+import org.lance.namespace.model.ListTablesResponse;
+import org.lance.namespace.model.NamespaceExistsRequest;
+import org.lance.namespace.model.TableExistsRequest;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Integration tests for IcebergNamespace against a running Iceberg REST Catalog.
+ *
+ * This test uses Lakekeeper as the Iceberg REST Catalog implementation. To run these tests,
+ * start the catalog with:
+ *
+ *
+ * cd docker/iceberg && docker-compose up -d
+ *
+ *
+ * Tests are automatically skipped if the catalog is not available.
+ */
+public class TestIcebergNamespaceIntegration {
+
+ private static final String ICEBERG_ENDPOINT = "http://localhost:8282/catalog";
+ private static final String TEST_WAREHOUSE = "test_warehouse";
+ private static boolean icebergAvailable = false;
+
+ private IcebergNamespace namespace;
+ private BufferAllocator allocator;
+ private String testNamespace;
+
+ @BeforeAll
+ public static void checkIcebergAvailable() {
+ try {
+ URL url = new URL(ICEBERG_ENDPOINT + "/v1/config?warehouse=" + TEST_WAREHOUSE);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.setConnectTimeout(5000);
+ conn.setReadTimeout(5000);
+
+ int responseCode = conn.getResponseCode();
+ conn.disconnect();
+
+ icebergAvailable = responseCode == 200;
+
+ if (!icebergAvailable) {
+ System.out.println(
+ "Iceberg REST Catalog is not available at "
+ + ICEBERG_ENDPOINT
+ + " - skipping integration tests");
+ } else {
+ System.out.println(
+ "Iceberg REST Catalog detected at "
+ + ICEBERG_ENDPOINT
+ + " (response code: "
+ + responseCode
+ + ")");
+ }
+ } catch (Exception e) {
+ icebergAvailable = false;
+ System.out.println(
+ "Iceberg REST Catalog is not available at "
+ + ICEBERG_ENDPOINT
+ + " ("
+ + e.getMessage()
+ + ") - skipping integration tests");
+ }
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ Assumptions.assumeTrue(
+ icebergAvailable, "Iceberg REST Catalog is not available at " + ICEBERG_ENDPOINT);
+
+ allocator = new RootAllocator();
+ namespace = new IcebergNamespace();
+
+ String uniqueId = UUID.randomUUID().toString().substring(0, 8);
+ testNamespace = "test_ns_" + uniqueId;
+
+ Map config = new HashMap<>();
+ config.put("endpoint", ICEBERG_ENDPOINT);
+ config.put("root", "s3://warehouse");
+
+ namespace.initialize(config, allocator);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ try {
+ DropNamespaceRequest dropRequest = new DropNamespaceRequest();
+ dropRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+ namespace.dropNamespace(dropRequest);
+ } catch (Exception e) {
+ // Ignore cleanup errors
+ }
+
+ if (allocator != null) {
+ allocator.close();
+ }
+ }
+
+ @Test
+ public void testNamespaceOperations() {
+ // Create namespace
+ CreateNamespaceRequest createRequest = new CreateNamespaceRequest();
+ createRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+ createRequest.setProperties(Collections.singletonMap("description", "Test namespace"));
+
+ CreateNamespaceResponse createResponse = namespace.createNamespace(createRequest);
+ assertThat(createResponse).isNotNull();
+
+ // Describe namespace
+ DescribeNamespaceRequest describeRequest = new DescribeNamespaceRequest();
+ describeRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+
+ DescribeNamespaceResponse describeResponse = namespace.describeNamespace(describeRequest);
+ assertThat(describeResponse).isNotNull();
+
+ // Check namespace exists
+ NamespaceExistsRequest existsRequest = new NamespaceExistsRequest();
+ existsRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+ namespace.namespaceExists(existsRequest);
+
+ // List namespaces
+ ListNamespacesRequest listRequest = new ListNamespacesRequest();
+ listRequest.setId(Collections.singletonList(TEST_WAREHOUSE));
+ ListNamespacesResponse listResponse = namespace.listNamespaces(listRequest);
+ assertThat(listResponse.getNamespaces()).contains(TEST_WAREHOUSE + "." + testNamespace);
+
+ // Drop namespace
+ DropNamespaceRequest dropRequest = new DropNamespaceRequest();
+ dropRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+ namespace.dropNamespace(dropRequest);
+
+ // Verify namespace doesn't exist
+ assertThatThrownBy(() -> namespace.namespaceExists(existsRequest))
+ .isInstanceOf(LanceNamespaceException.class)
+ .hasMessageContaining("not found");
+ }
+
+ @Test
+ public void testTableOperations() {
+ // Create namespace first
+ CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
+ nsRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+ namespace.createNamespace(nsRequest);
+
+ String tableName = "test_table_" + UUID.randomUUID().toString().substring(0, 8);
+
+ // Create empty table
+ CreateEmptyTableRequest createRequest = new CreateEmptyTableRequest();
+ createRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace, tableName));
+ createRequest.setLocation("s3://warehouse/" + testNamespace + "/" + tableName);
+
+ CreateEmptyTableResponse createResponse = namespace.createEmptyTable(createRequest);
+ assertThat(createResponse.getLocation()).isNotNull();
+
+ // Describe table
+ DescribeTableRequest describeRequest = new DescribeTableRequest();
+ describeRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace, tableName));
+
+ DescribeTableResponse describeResponse = namespace.describeTable(describeRequest);
+ assertThat(describeResponse.getLocation()).isNotNull();
+
+ // Check table exists
+ TableExistsRequest existsRequest = new TableExistsRequest();
+ existsRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace, tableName));
+ namespace.tableExists(existsRequest);
+
+ // List tables
+ ListTablesRequest listRequest = new ListTablesRequest();
+ listRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+
+ ListTablesResponse listResponse = namespace.listTables(listRequest);
+ assertThat(listResponse.getTables()).contains(tableName);
+
+ // Deregister table
+ DeregisterTableRequest deregisterRequest = new DeregisterTableRequest();
+ deregisterRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace, tableName));
+ namespace.deregisterTable(deregisterRequest);
+
+ // Verify table doesn't exist
+ assertThatThrownBy(() -> namespace.tableExists(existsRequest))
+ .isInstanceOf(LanceNamespaceException.class)
+ .hasMessageContaining("not found");
+ }
+
+ @Test
+ public void testCreateEmptyTableWithLocation() {
+ // Create namespace first
+ CreateNamespaceRequest nsRequest = new CreateNamespaceRequest();
+ nsRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+ namespace.createNamespace(nsRequest);
+
+ String tableName = "lance_table";
+ CreateEmptyTableRequest createRequest = new CreateEmptyTableRequest();
+ createRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace, tableName));
+ createRequest.setLocation("s3://warehouse/" + testNamespace + "/" + tableName);
+
+ CreateEmptyTableResponse response = namespace.createEmptyTable(createRequest);
+ assertThat(response.getLocation()).isNotNull();
+
+ // Clean up table
+ DeregisterTableRequest deregisterRequest = new DeregisterTableRequest();
+ deregisterRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace, tableName));
+ namespace.deregisterTable(deregisterRequest);
+ }
+
+ @Test
+ public void testNestedNamespace() {
+ String nestedNs = "nested_" + UUID.randomUUID().toString().substring(0, 8);
+
+ // Create parent namespace
+ CreateNamespaceRequest parentRequest = new CreateNamespaceRequest();
+ parentRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+ namespace.createNamespace(parentRequest);
+
+ // Create nested namespace
+ CreateNamespaceRequest nestedRequest = new CreateNamespaceRequest();
+ nestedRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace, nestedNs));
+ nestedRequest.setProperties(Collections.singletonMap("description", "Nested namespace"));
+ namespace.createNamespace(nestedRequest);
+
+ // List nested namespaces
+ ListNamespacesRequest listRequest = new ListNamespacesRequest();
+ listRequest.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace));
+ ListNamespacesResponse listResponse = namespace.listNamespaces(listRequest);
+ assertThat(listResponse.getNamespaces())
+ .contains(TEST_WAREHOUSE + "." + testNamespace + "." + nestedNs);
+
+ // Drop nested namespace first
+ DropNamespaceRequest dropNested = new DropNamespaceRequest();
+ dropNested.setId(Arrays.asList(TEST_WAREHOUSE, testNamespace, nestedNs));
+ namespace.dropNamespace(dropNested);
+ }
+}
diff --git a/java/lance-namespace-impls-core/pom.xml b/java/lance-namespace-impls-core/pom.xml
new file mode 100644
index 0000000..878d8c3
--- /dev/null
+++ b/java/lance-namespace-impls-core/pom.xml
@@ -0,0 +1,97 @@
+
+
+ 4.0.0
+
+
+ org.lance
+ lance-namespace-impls-root
+ 0.0.1
+
+
+ lance-namespace-impls-core
+ ${project.artifactId}
+ Core utilities for Lance Namespace implementations including RestClient
+
+
+
+
+ org.lance
+ lance-namespace-core
+
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.2.1
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.apache.arrow
+ arrow-vector
+
+
+ org.apache.arrow
+ arrow-memory-netty
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+
diff --git a/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/rest/RestClient.java b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/rest/RestClient.java
new file mode 100644
index 0000000..d92cbed
--- /dev/null
+++ b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/rest/RestClient.java
@@ -0,0 +1,412 @@
+/*
+ * Licensed 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.lance.namespace.rest;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.apache.hc.client5.http.classic.methods.HttpDelete;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPatch;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A reusable REST client for making HTTP requests to REST APIs.
+ *
+ * This client provides:
+ *
+ *
+ * - Connection pooling for efficient HTTP connections
+ *
- Configurable timeouts for connect and read operations
+ *
- Retry logic with exponential backoff
+ *
- JSON serialization/deserialization via Jackson
+ *
- Support for common HTTP methods (GET, POST, PUT, PATCH, DELETE)
+ *
+ *
+ * Example usage:
+ *
+ *
{@code
+ * RestClient client = RestClient.builder()
+ * .baseUrl("http://localhost:8080/api")
+ * .header("Authorization", "Bearer token")
+ * .connectTimeout(10, TimeUnit.SECONDS)
+ * .readTimeout(30, TimeUnit.SECONDS)
+ * .maxRetries(3)
+ * .build();
+ *
+ * MyResponse response = client.get("/resource", MyResponse.class);
+ * }
+ */
+public class RestClient implements Closeable {
+ private static final Logger LOG = LoggerFactory.getLogger(RestClient.class);
+
+ private static final int DEFAULT_MAX_CONNECTIONS = 20;
+ private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 10;
+ private static final int DEFAULT_CONNECT_TIMEOUT_MS = 10000;
+ private static final int DEFAULT_READ_TIMEOUT_MS = 30000;
+ private static final int DEFAULT_MAX_RETRIES = 3;
+ private static final long DEFAULT_RETRY_DELAY_MS = 1000;
+
+ private final String baseUrl;
+ private final Map defaultHeaders;
+ private final CloseableHttpClient httpClient;
+ private final ObjectMapper objectMapper;
+ private final int maxRetries;
+ private final long retryDelayMs;
+
+ private RestClient(Builder builder) {
+ this.baseUrl =
+ builder.baseUrl.endsWith("/")
+ ? builder.baseUrl.substring(0, builder.baseUrl.length() - 1)
+ : builder.baseUrl;
+ this.defaultHeaders = new HashMap<>(builder.defaultHeaders);
+ this.maxRetries = builder.maxRetries;
+ this.retryDelayMs = builder.retryDelayMs;
+
+ this.objectMapper =
+ builder.objectMapper != null
+ ? builder.objectMapper
+ : new ObjectMapper()
+ .registerModule(new JavaTimeModule())
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+ connectionManager.setMaxTotal(builder.maxConnections);
+ connectionManager.setDefaultMaxPerRoute(builder.maxConnectionsPerRoute);
+
+ RequestConfig requestConfig =
+ RequestConfig.custom()
+ .setConnectTimeout(Timeout.ofMilliseconds(builder.connectTimeoutMs))
+ .setResponseTimeout(Timeout.ofMilliseconds(builder.readTimeoutMs))
+ .build();
+
+ this.httpClient =
+ HttpClients.custom()
+ .setConnectionManager(connectionManager)
+ .setDefaultRequestConfig(requestConfig)
+ .build();
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public T get(String path, Class responseType) throws RestClientException {
+ return execute(new HttpGet(buildUri(path)), null, responseType);
+ }
+
+ public T get(String path, Map queryParams, Class responseType)
+ throws RestClientException {
+ HttpGet request = new HttpGet(buildUri(path, queryParams));
+ return execute(request, null, responseType);
+ }
+
+ public T getWithHeaders(String path, Map headers, Class responseType)
+ throws RestClientException {
+ HttpGet request = new HttpGet(buildUri(path));
+ headers.forEach(request::addHeader);
+ return execute(request, null, responseType);
+ }
+
+ public T get(
+ String path,
+ Map queryParams,
+ Map headers,
+ Class responseType)
+ throws RestClientException {
+ HttpGet request = new HttpGet(buildUri(path, queryParams));
+ headers.forEach(request::addHeader);
+ return execute(request, null, responseType);
+ }
+
+ public T post(String path, Object body, Class responseType) throws RestClientException {
+ HttpPost request = new HttpPost(buildUri(path));
+ return execute(request, body, responseType);
+ }
+
+ public T post(String path, Object body, Map headers, Class responseType)
+ throws RestClientException {
+ HttpPost request = new HttpPost(buildUri(path));
+ headers.forEach(request::addHeader);
+ return execute(request, body, responseType);
+ }
+
+ public T put(String path, Object body, Class responseType) throws RestClientException {
+ HttpPut request = new HttpPut(buildUri(path));
+ return execute(request, body, responseType);
+ }
+
+ public T put(String path, Object body, Map headers, Class responseType)
+ throws RestClientException {
+ HttpPut request = new HttpPut(buildUri(path));
+ headers.forEach(request::addHeader);
+ return execute(request, body, responseType);
+ }
+
+ public T patch(String path, Object body, Class responseType) throws RestClientException {
+ HttpPatch request = new HttpPatch(buildUri(path));
+ return execute(request, body, responseType);
+ }
+
+ public void delete(String path) throws RestClientException {
+ execute(new HttpDelete(buildUri(path)), null, Void.class);
+ }
+
+ public void delete(String path, Map headers) throws RestClientException {
+ HttpDelete request = new HttpDelete(buildUri(path));
+ headers.forEach(request::addHeader);
+ execute(request, null, Void.class);
+ }
+
+ public T delete(String path, Class responseType) throws RestClientException {
+ return execute(new HttpDelete(buildUri(path)), null, responseType);
+ }
+
+ private URI buildUri(String path) {
+ String fullPath = path.startsWith("/") ? baseUrl + path : baseUrl + "/" + path;
+ return URI.create(fullPath);
+ }
+
+ private URI buildUri(String path, Map queryParams) {
+ String fullPath = path.startsWith("/") ? baseUrl + path : baseUrl + "/" + path;
+ if (queryParams != null && !queryParams.isEmpty()) {
+ StringBuilder sb = new StringBuilder(fullPath);
+ sb.append("?");
+ boolean first = true;
+ for (Map.Entry entry : queryParams.entrySet()) {
+ if (!first) {
+ sb.append("&");
+ }
+ try {
+ sb.append(java.net.URLEncoder.encode(entry.getKey(), "UTF-8"));
+ sb.append("=");
+ sb.append(java.net.URLEncoder.encode(entry.getValue(), "UTF-8"));
+ } catch (java.io.UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported", e);
+ }
+ first = false;
+ }
+ fullPath = sb.toString();
+ }
+ return URI.create(fullPath);
+ }
+
+ private T execute(HttpUriRequestBase request, Object body, Class responseType)
+ throws RestClientException {
+ defaultHeaders.forEach(request::addHeader);
+
+ if (body != null) {
+ try {
+ String jsonBody = objectMapper.writeValueAsString(body);
+ request.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
+ } catch (JsonProcessingException e) {
+ throw new RestClientException(-1, "Failed to serialize request body", e);
+ }
+ }
+
+ int attempt = 0;
+ RestClientException lastException = null;
+
+ while (attempt <= maxRetries) {
+ try {
+ return httpClient.execute(
+ request,
+ response -> {
+ int statusCode = response.getCode();
+ String responseBody =
+ response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null;
+
+ if (statusCode >= 200 && statusCode < 300) {
+ if (responseType == Void.class || responseBody == null || responseBody.isEmpty()) {
+ return null;
+ }
+ try {
+ return objectMapper.readValue(responseBody, responseType);
+ } catch (JsonProcessingException e) {
+ throw new RestClientException(
+ statusCode, "Failed to deserialize response: " + responseBody, e);
+ }
+ } else {
+ throw new RestClientException(statusCode, responseBody);
+ }
+ });
+ } catch (RestClientException e) {
+ lastException = e;
+ if (e.getStatusCode() >= 400 && e.getStatusCode() < 500) {
+ throw e;
+ }
+ attempt++;
+ if (attempt <= maxRetries) {
+ long delay = retryDelayMs * (1L << (attempt - 1));
+ LOG.warn(
+ "Request failed with status {}, retrying in {}ms (attempt {}/{})",
+ e.getStatusCode(),
+ delay,
+ attempt,
+ maxRetries);
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new RestClientException(-1, "Interrupted during retry", ie);
+ }
+ }
+ } catch (IOException e) {
+ lastException = new RestClientException(-1, "IO error: " + e.getMessage(), e);
+ attempt++;
+ if (attempt <= maxRetries) {
+ long delay = retryDelayMs * (1L << (attempt - 1));
+ LOG.warn(
+ "Request failed with IO error, retrying in {}ms (attempt {}/{})",
+ delay,
+ attempt,
+ maxRetries);
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new RestClientException(-1, "Interrupted during retry", ie);
+ }
+ }
+ }
+ }
+
+ throw lastException != null
+ ? lastException
+ : new RestClientException(-1, "Unknown error after retries");
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (httpClient != null) {
+ httpClient.close();
+ }
+ }
+
+ public static class Builder {
+ private String baseUrl;
+ private final Map defaultHeaders = new HashMap<>();
+ private int maxConnections = DEFAULT_MAX_CONNECTIONS;
+ private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE;
+ private int connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS;
+ private int readTimeoutMs = DEFAULT_READ_TIMEOUT_MS;
+ private int maxRetries = DEFAULT_MAX_RETRIES;
+ private long retryDelayMs = DEFAULT_RETRY_DELAY_MS;
+ private ObjectMapper objectMapper;
+
+ public Builder baseUrl(String baseUrl) {
+ this.baseUrl = Objects.requireNonNull(baseUrl, "baseUrl cannot be null");
+ return this;
+ }
+
+ public Builder header(String name, String value) {
+ this.defaultHeaders.put(name, value);
+ return this;
+ }
+
+ public Builder headers(Map headers) {
+ this.defaultHeaders.putAll(headers);
+ return this;
+ }
+
+ public Builder authToken(String token) {
+ if (token != null && !token.isEmpty()) {
+ this.defaultHeaders.put("Authorization", "Bearer " + token);
+ }
+ return this;
+ }
+
+ public Builder maxConnections(int maxConnections) {
+ this.maxConnections = maxConnections;
+ return this;
+ }
+
+ public Builder maxConnectionsPerRoute(int maxConnectionsPerRoute) {
+ this.maxConnectionsPerRoute = maxConnectionsPerRoute;
+ return this;
+ }
+
+ public Builder connectTimeout(int timeout, TimeUnit unit) {
+ this.connectTimeoutMs = (int) unit.toMillis(timeout);
+ return this;
+ }
+
+ public Builder connectTimeoutMs(int connectTimeoutMs) {
+ this.connectTimeoutMs = connectTimeoutMs;
+ return this;
+ }
+
+ public Builder readTimeout(int timeout, TimeUnit unit) {
+ this.readTimeoutMs = (int) unit.toMillis(timeout);
+ return this;
+ }
+
+ public Builder readTimeoutMs(int readTimeoutMs) {
+ this.readTimeoutMs = readTimeoutMs;
+ return this;
+ }
+
+ public Builder maxRetries(int maxRetries) {
+ this.maxRetries = maxRetries;
+ return this;
+ }
+
+ public Builder retryDelay(long delay, TimeUnit unit) {
+ this.retryDelayMs = unit.toMillis(delay);
+ return this;
+ }
+
+ public Builder retryDelayMs(long retryDelayMs) {
+ this.retryDelayMs = retryDelayMs;
+ return this;
+ }
+
+ public Builder objectMapper(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ return this;
+ }
+
+ public RestClient build() {
+ Objects.requireNonNull(baseUrl, "baseUrl is required");
+ return new RestClient(this);
+ }
+ }
+}
diff --git a/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/rest/RestClientException.java b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/rest/RestClientException.java
new file mode 100644
index 0000000..b976b1d
--- /dev/null
+++ b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/rest/RestClientException.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed 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.lance.namespace.rest;
+
+/**
+ * Exception thrown when a REST API call fails.
+ *
+ * Contains the HTTP status code and response body for error diagnosis.
+ */
+public class RestClientException extends RuntimeException {
+ private final int statusCode;
+ private final String responseBody;
+
+ public RestClientException(int statusCode, String responseBody) {
+ super(String.format("HTTP %d: %s", statusCode, responseBody));
+ this.statusCode = statusCode;
+ this.responseBody = responseBody;
+ }
+
+ public RestClientException(int statusCode, String message, Throwable cause) {
+ super(String.format("HTTP %d: %s", statusCode, message), cause);
+ this.statusCode = statusCode;
+ this.responseBody = message;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public String getResponseBody() {
+ return responseBody;
+ }
+
+ public boolean isClientError() {
+ return statusCode >= 400 && statusCode < 500;
+ }
+
+ public boolean isServerError() {
+ return statusCode >= 500;
+ }
+
+ public boolean isNotFound() {
+ return statusCode == 404;
+ }
+
+ public boolean isConflict() {
+ return statusCode == 409;
+ }
+
+ public boolean isBadRequest() {
+ return statusCode == 400;
+ }
+
+ public boolean isUnauthorized() {
+ return statusCode == 401;
+ }
+
+ public boolean isForbidden() {
+ return statusCode == 403;
+ }
+}
diff --git a/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/test/TestHelper.java b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/test/TestHelper.java
new file mode 100644
index 0000000..e4e7d75
--- /dev/null
+++ b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/test/TestHelper.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed 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.lance.namespace.test;
+
+import org.apache.arrow.memory.BufferAllocator;
+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 java.io.ByteArrayOutputStream;
+import java.nio.channels.Channels;
+import java.util.Arrays;
+
+/** Test utilities for creating Arrow IPC data. */
+public final class TestHelper {
+
+ private TestHelper() {}
+
+ /**
+ * Creates test Arrow IPC data with a simple schema (id: int32, name: utf8).
+ *
+ * @param allocator Arrow buffer allocator
+ * @return Arrow IPC stream bytes
+ */
+ public static byte[] createTestArrowData(BufferAllocator allocator) {
+ Schema schema =
+ new Schema(
+ Arrays.asList(
+ Field.nullable("id", new ArrowType.Int(32, true)),
+ Field.nullable("name", ArrowType.Utf8.INSTANCE)));
+
+ return createArrowIpcStream(allocator, schema);
+ }
+
+ /**
+ * Creates empty Arrow IPC data with a simple schema.
+ *
+ * @param allocator Arrow buffer allocator
+ * @return Arrow IPC stream bytes
+ */
+ public static byte[] createEmptyArrowData(BufferAllocator allocator) {
+ return createTestArrowData(allocator);
+ }
+
+ /**
+ * Creates Arrow IPC stream bytes from a schema.
+ *
+ * @param allocator Arrow buffer allocator
+ * @param schema Arrow schema
+ * @return Arrow IPC stream bytes
+ */
+ public static byte[] createArrowIpcStream(BufferAllocator allocator, Schema schema) {
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator);
+ ArrowStreamWriter writer = new ArrowStreamWriter(root, null, Channels.newChannel(out))) {
+ writer.start();
+ writer.end();
+ }
+ return out.toByteArray();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create Arrow IPC stream", e);
+ }
+ }
+}
diff --git a/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/util/ObjectIdentifier.java b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/util/ObjectIdentifier.java
new file mode 100644
index 0000000..6945fbd
--- /dev/null
+++ b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/util/ObjectIdentifier.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed 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.lance.namespace.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a hierarchical object identifier (namespace or table).
+ *
+ *
An identifier consists of one or more levels, where each level is a string. For example:
+ *
+ *
+ * - A root identifier has 0 levels
+ *
- A catalog identifier has 1 level (e.g., ["my_catalog"])
+ *
- A database identifier has 2 levels (e.g., ["my_catalog", "my_database"])
+ *
- A table identifier has 3 levels (e.g., ["my_catalog", "my_database", "my_table"])
+ *
+ */
+public class ObjectIdentifier {
+ private static final ObjectIdentifier ROOT = new ObjectIdentifier(Collections.emptyList());
+ private final List levels;
+
+ private ObjectIdentifier(List levels) {
+ this.levels = Collections.unmodifiableList(new ArrayList<>(levels));
+ }
+
+ public static ObjectIdentifier root() {
+ return ROOT;
+ }
+
+ public static ObjectIdentifier of(List levels) {
+ if (levels == null || levels.isEmpty()) {
+ return ROOT;
+ }
+ return new ObjectIdentifier(levels);
+ }
+
+ public static ObjectIdentifier of(Set levels) {
+ if (levels == null || levels.isEmpty()) {
+ return ROOT;
+ }
+ return new ObjectIdentifier(new ArrayList<>(levels));
+ }
+
+ public static ObjectIdentifier of(String... levels) {
+ if (levels == null || levels.length == 0) {
+ return ROOT;
+ }
+ return new ObjectIdentifier(Arrays.asList(levels));
+ }
+
+ public boolean isRoot() {
+ return levels.isEmpty();
+ }
+
+ public int levels() {
+ return levels.size();
+ }
+
+ public String levelAtListPos(int index) {
+ if (index < 0 || index >= levels.size()) {
+ throw new IndexOutOfBoundsException(
+ "Index " + index + " out of bounds for identifier with " + levels.size() + " levels");
+ }
+ return levels.get(index);
+ }
+
+ public List getLevels() {
+ return levels;
+ }
+
+ public ObjectIdentifier parent() {
+ if (isRoot()) {
+ throw new IllegalStateException("Root identifier has no parent");
+ }
+ return of(levels.subList(0, levels.size() - 1));
+ }
+
+ public String name() {
+ if (isRoot()) {
+ throw new IllegalStateException("Root identifier has no name");
+ }
+ return levels.get(levels.size() - 1);
+ }
+
+ public ObjectIdentifier child(String name) {
+ List newLevels = new ArrayList<>(levels);
+ newLevels.add(name);
+ return of(newLevels);
+ }
+
+ @Override
+ public String toString() {
+ if (isRoot()) {
+ return "[]";
+ }
+ return levels.toString();
+ }
+
+ public String toDelimited(String delimiter) {
+ return String.join(delimiter, levels);
+ }
+
+ public List listStyleId() {
+ return levels;
+ }
+
+ public String stringStyleId() {
+ return String.join(".", levels);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null || getClass() != obj.getClass()) return false;
+ ObjectIdentifier that = (ObjectIdentifier) obj;
+ return Objects.equals(levels, that.levels);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(levels);
+ }
+}
diff --git a/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/util/ValidationUtil.java b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/util/ValidationUtil.java
new file mode 100644
index 0000000..71cd20a
--- /dev/null
+++ b/java/lance-namespace-impls-core/src/main/java/org/lance/namespace/util/ValidationUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed 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.lance.namespace.util;
+
+import org.lance.namespace.errors.InvalidInputException;
+
+/** Utility methods for validation. */
+public final class ValidationUtil {
+
+ private ValidationUtil() {}
+
+ public static void checkArgument(boolean condition, String message, Object... args) {
+ if (!condition) {
+ throw new InvalidInputException(String.format(message, args));
+ }
+ }
+
+ public static void checkNotNull(Object reference, String message, Object... args) {
+ if (reference == null) {
+ throw new InvalidInputException(String.format(message, args));
+ }
+ }
+
+ public static void checkNotEmpty(String value, String message, Object... args) {
+ if (value == null || value.isEmpty()) {
+ throw new InvalidInputException(String.format(message, args));
+ }
+ }
+
+ public static void checkState(boolean condition, String message, Object... args) {
+ if (!condition) {
+ throw new IllegalStateException(String.format(message, args));
+ }
+ }
+}
diff --git a/java/lance-namespace-polaris/pom.xml b/java/lance-namespace-polaris/pom.xml
index 2c0f5cf..cf71b17 100644
--- a/java/lance-namespace-polaris/pom.xml
+++ b/java/lance-namespace-polaris/pom.xml
@@ -31,10 +31,18 @@
Polaris Catalog implementation for Lance namespace management
+
+ org.lance
+ lance-namespace-impls-core
+
org.lance
lance-core
+
+ org.lance
+ lance-namespace-core
+
org.lance
lance-namespace-apache-client
diff --git a/java/lance-namespace-polaris/src/main/java/org/lance/namespace/polaris/PolarisModels.java b/java/lance-namespace-polaris/src/main/java/org/lance/namespace/polaris/PolarisModels.java
index 24fcbbe..595da3c 100644
--- a/java/lance-namespace-polaris/src/main/java/org/lance/namespace/polaris/PolarisModels.java
+++ b/java/lance-namespace-polaris/src/main/java/org/lance/namespace/polaris/PolarisModels.java
@@ -180,21 +180,21 @@ public void setTable(GenericTable table) {
/** Table identifier. */
@JsonIgnoreProperties(ignoreUnknown = true)
public static class TableIdentifier {
- private String namespace;
+ private List namespace;
private String name;
public TableIdentifier() {}
- public TableIdentifier(String namespace, String name) {
+ public TableIdentifier(List namespace, String name) {
this.namespace = namespace;
this.name = name;
}
- public String getNamespace() {
+ public List getNamespace() {
return namespace;
}
- public void setNamespace(String namespace) {
+ public void setNamespace(List namespace) {
this.namespace = namespace;
}
@@ -339,11 +339,11 @@ public static class ListNamespacesResponse {
@JsonProperty("next-page-token")
private String nextPageToken;
- private List namespaces;
+ private List> namespaces;
public ListNamespacesResponse() {}
- public ListNamespacesResponse(String nextPageToken, List namespaces) {
+ public ListNamespacesResponse(String nextPageToken, List> namespaces) {
this.nextPageToken = nextPageToken;
this.namespaces = namespaces;
}
@@ -356,32 +356,13 @@ public void setNextPageToken(String nextPageToken) {
this.nextPageToken = nextPageToken;
}
- public List getNamespaces() {
+ public List> getNamespaces() {
return namespaces;
}
- public void setNamespaces(List namespaces) {
+ public void setNamespaces(List> namespaces) {
this.namespaces = namespaces;
}
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- public static class Namespace {
- private List namespace;
-
- public Namespace() {}
-
- public Namespace(List namespace) {
- this.namespace = namespace;
- }
-
- public List getNamespace() {
- return namespace;
- }
-
- public void setNamespace(List namespace) {
- this.namespace = namespace;
- }
- }
}
/** Create namespace request. */
diff --git a/java/lance-namespace-polaris/src/main/java/org/lance/namespace/polaris/PolarisNamespace.java b/java/lance-namespace-polaris/src/main/java/org/lance/namespace/polaris/PolarisNamespace.java
index e5ef4f3..b70461a 100644
--- a/java/lance-namespace-polaris/src/main/java/org/lance/namespace/polaris/PolarisNamespace.java
+++ b/java/lance-namespace-polaris/src/main/java/org/lance/namespace/polaris/PolarisNamespace.java
@@ -13,23 +13,25 @@
*/
package org.lance.namespace.polaris;
-import com.lancedb.lance.Dataset;
-import com.lancedb.lance.WriteParams;
import org.lance.namespace.LanceNamespace;
-import org.lance.namespace.LanceNamespaceException;
-import org.lance.namespace.ObjectIdentifier;
+import org.lance.namespace.errors.InternalException;
+import org.lance.namespace.errors.InvalidInputException;
+import org.lance.namespace.errors.NamespaceAlreadyExistsException;
+import org.lance.namespace.errors.NamespaceNotFoundException;
+import org.lance.namespace.errors.TableAlreadyExistsException;
+import org.lance.namespace.errors.TableNotFoundException;
import org.lance.namespace.model.CreateEmptyTableRequest;
import org.lance.namespace.model.CreateEmptyTableResponse;
import org.lance.namespace.model.CreateNamespaceRequest;
import org.lance.namespace.model.CreateNamespaceResponse;
+import org.lance.namespace.model.DeregisterTableRequest;
+import org.lance.namespace.model.DeregisterTableResponse;
import org.lance.namespace.model.DescribeNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
import org.lance.namespace.model.DescribeTableRequest;
import org.lance.namespace.model.DescribeTableResponse;
import org.lance.namespace.model.DropNamespaceRequest;
import org.lance.namespace.model.DropNamespaceResponse;
-import org.lance.namespace.model.DropTableRequest;
-import org.lance.namespace.model.DropTableResponse;
import org.lance.namespace.model.ListNamespacesRequest;
import org.lance.namespace.model.ListNamespacesResponse;
import org.lance.namespace.model.ListTablesRequest;
@@ -37,13 +39,15 @@
import org.lance.namespace.model.NamespaceExistsRequest;
import org.lance.namespace.model.TableExistsRequest;
import org.lance.namespace.rest.RestClient;
+import org.lance.namespace.rest.RestClientException;
+import org.lance.namespace.util.ObjectIdentifier;
import org.lance.namespace.util.ValidationUtil;
import org.apache.arrow.memory.BufferAllocator;
-import org.apache.arrow.vector.types.pojo.Schema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
@@ -51,9 +55,10 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
/** Polaris Catalog namespace implementation for Lance. */
-public class PolarisNamespace implements LanceNamespace {
+public class PolarisNamespace implements LanceNamespace, Closeable {
private static final Logger LOG = LoggerFactory.getLogger(PolarisNamespace.class);
private static final String TABLE_FORMAT_LANCE = "lance";
private static final String TABLE_TYPE_KEY = "table_type";
@@ -69,19 +74,15 @@ public void initialize(Map configProperties, BufferAllocator all
this.allocator = allocator;
this.config = new PolarisNamespaceConfig(configProperties);
- // Build REST client with authentication if provided
RestClient.Builder clientBuilder =
RestClient.builder()
.baseUrl(config.getFullApiUrl())
- .connectTimeout(config.getConnectTimeout())
- .readTimeout(config.getReadTimeout())
+ .connectTimeout(config.getConnectTimeout(), TimeUnit.MILLISECONDS)
+ .readTimeout(config.getReadTimeout(), TimeUnit.MILLISECONDS)
.maxRetries(config.getMaxRetries());
- // Add auth token if provided
if (config.getAuthToken() != null) {
- Map headers = new HashMap<>();
- headers.put("Authorization", "Bearer " + config.getAuthToken());
- clientBuilder.defaultHeaders(headers);
+ clientBuilder.authToken(config.getAuthToken());
}
this.restClient = clientBuilder.build();
@@ -98,27 +99,33 @@ public String namespaceId() {
public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) {
ObjectIdentifier namespaceId = ObjectIdentifier.of(request.getId());
ValidationUtil.checkArgument(
- namespaceId.levels() >= 1, "Namespace must have at least one level");
+ namespaceId.levels() >= 2, "Namespace must have at least catalog and namespace levels");
try {
- // Convert request to Polaris format
- List namespace = namespaceId.listStyleId();
+ List parts = namespaceId.listStyleId();
+ String catalog = parts.get(0);
+ List namespace = parts.subList(1, parts.size());
PolarisModels.CreateNamespaceRequest polarisRequest =
new PolarisModels.CreateNamespaceRequest(namespace, request.getProperties());
- // Create namespace using Iceberg REST API endpoint
PolarisModels.NamespaceResponse response =
- restClient.post("/namespaces", polarisRequest, PolarisModels.NamespaceResponse.class);
+ restClient.post(
+ "/v1/" + catalog + "/namespaces",
+ polarisRequest,
+ PolarisModels.NamespaceResponse.class);
- LOG.info("Created namespace: {}", String.join(".", namespace));
+ LOG.info("Created namespace: {}.{}", catalog, String.join(".", namespace));
CreateNamespaceResponse result = new CreateNamespaceResponse();
result.setProperties(response.getProperties());
return result;
- } catch (IOException e) {
- throw LanceNamespaceException.serverError(
- "Failed to create namespace", "ServerError", namespaceId.stringStyleId(), e.getMessage());
+ } catch (RestClientException e) {
+ if (e.isConflict()) {
+ throw new NamespaceAlreadyExistsException(
+ "Namespace already exists: " + namespaceId.stringStyleId());
+ }
+ throw new InternalException("Failed to create namespace: " + e.getMessage());
}
}
@@ -126,31 +133,27 @@ public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) {
public DescribeNamespaceResponse describeNamespace(DescribeNamespaceRequest request) {
ObjectIdentifier namespaceId = ObjectIdentifier.of(request.getId());
ValidationUtil.checkArgument(
- namespaceId.levels() >= 1, "Namespace must have at least one level");
+ namespaceId.levels() >= 2, "Namespace must have at least catalog and namespace levels");
try {
- String namespacePath = namespaceId.stringStyleId();
+ List parts = namespaceId.listStyleId();
+ String catalog = parts.get(0);
+ List namespaceParts = parts.subList(1, parts.size());
+ String namespacePath = String.join(".", namespaceParts);
- // Get namespace properties using Iceberg REST API
PolarisModels.NamespaceResponse response =
- restClient.get("/namespaces/" + namespacePath, PolarisModels.NamespaceResponse.class);
+ restClient.get(
+ "/v1/" + catalog + "/namespaces/" + namespacePath,
+ PolarisModels.NamespaceResponse.class);
DescribeNamespaceResponse result = new DescribeNamespaceResponse();
result.setProperties(response.getProperties());
return result;
- } catch (IOException e) {
- if (e.getMessage() != null && e.getMessage().contains("404")) {
- throw LanceNamespaceException.notFound(
- "Namespace not found",
- "NoSuchNamespace",
- namespaceId.stringStyleId(),
- "Namespace not found: " + namespaceId.stringStyleId());
+ } catch (RestClientException e) {
+ if (e.isNotFound()) {
+ throw new NamespaceNotFoundException("Namespace not found: " + namespaceId.stringStyleId());
}
- throw LanceNamespaceException.serverError(
- "Failed to describe namespace",
- "ServerError",
- namespaceId.stringStyleId(),
- e.getMessage());
+ throw new InternalException("Failed to describe namespace: " + e.getMessage());
}
}
@@ -161,59 +164,61 @@ public ListNamespacesResponse listNamespaces(ListNamespacesRequest request) {
? ObjectIdentifier.of(request.getId())
: ObjectIdentifier.of(Collections.emptyList());
+ ValidationUtil.checkArgument(parentId.levels() >= 1, "Must specify at least the catalog");
+
try {
- String path = "/namespaces";
- if (!parentId.isRoot()) {
- path += "/" + parentId.stringStyleId() + "/namespaces";
+ List parts = parentId.listStyleId();
+ String catalog = parts.get(0);
+ String path = "/v1/" + catalog + "/namespaces";
+ if (parts.size() > 1) {
+ List namespaceParts = parts.subList(1, parts.size());
+ path = "/v1/" + catalog + "/namespaces/" + String.join(".", namespaceParts) + "/namespaces";
}
- // List namespaces using Iceberg REST API
PolarisModels.ListNamespacesResponse response =
restClient.get(path, PolarisModels.ListNamespacesResponse.class);
ListNamespacesResponse result = new ListNamespacesResponse();
- // Convert namespace identifiers to Set with full paths
Set namespaceSet = new LinkedHashSet<>();
if (response.getNamespaces() != null) {
- for (PolarisModels.ListNamespacesResponse.Namespace ns : response.getNamespaces()) {
- namespaceSet.add(String.join(".", ns.getNamespace()));
+ for (List