diff --git a/java/lance-jni/Cargo.lock b/java/lance-jni/Cargo.lock index 90cb7765da9..2ec40b9e342 100644 --- a/java/lance-jni/Cargo.lock +++ b/java/lance-jni/Cargo.lock @@ -800,6 +800,61 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backon" version = "1.6.0" @@ -2667,6 +2722,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "humantime" version = "2.3.0" @@ -2687,6 +2748,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -3418,6 +3480,7 @@ dependencies = [ "arrow", "arrow-schema", "async-trait", + "bytes", "chrono", "env_logger", "jni", @@ -3430,11 +3493,13 @@ dependencies = [ "lance-io", "lance-linalg", "lance-namespace", + "lance-namespace-impls", "log", "object_store", "prost", "prost-types", "roaring", + "serde", "serde_json", "snafu", "tokio", @@ -3469,6 +3534,35 @@ dependencies = [ "snafu", ] +[[package]] +name = "lance-namespace-impls" +version = "1.0.0-beta.4" +dependencies = [ + "arrow", + "arrow-ipc", + "arrow-schema", + "async-trait", + "axum", + "bytes", + "futures", + "lance", + "lance-core", + "lance-index", + "lance-io", + "lance-namespace", + "log", + "object_store", + "rand 0.9.2", + "reqwest", + "serde", + "serde_json", + "snafu", + "tokio", + "tower", + "tower-http 0.5.2", + "url", +] + [[package]] name = "lance-namespace-reqwest-client" version = "0.0.18" @@ -3730,6 +3824,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matrixmultiply" version = "0.3.10" @@ -4914,7 +5014,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower", - "tower-http", + "tower-http 0.6.6", "tower-service", "url", "wasm-bindgen", @@ -5267,6 +5367,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -5938,6 +6049,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -6003,6 +6115,24 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", ] [[package]] @@ -6041,6 +6171,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/java/lance-jni/Cargo.toml b/java/lance-jni/Cargo.toml index 1578ce95b79..fa198dcb092 100644 --- a/java/lance-jni/Cargo.toml +++ b/java/lance-jni/Cargo.toml @@ -20,6 +20,7 @@ lance-linalg = { path = "../../rust/lance-linalg" } lance-index = { path = "../../rust/lance-index" } lance-io = { path = "../../rust/lance-io" } lance-namespace = { path = "../../rust/lance-namespace" } +lance-namespace-impls = { path = "../../rust/lance-namespace-impls", features = ["rest", "rest-adapter"] } lance-core = { path = "../../rust/lance-core" } lance-file = { path = "../../rust/lance-file" } arrow = { version = "56.1", features = ["ffi"] } @@ -34,7 +35,9 @@ tokio = { version = "1.23", features = [ async-trait = "0.1" snafu = "0.8" jni = "0.21.1" +serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1" } +bytes = "1.11" log = "0.4" env_logger = "0.11.7" uuid = { version = "1.17.0", features = ["v4"] } diff --git a/java/lance-jni/src/lib.rs b/java/lance-jni/src/lib.rs index 50cf1379937..0cc43865538 100644 --- a/java/lance-jni/src/lib.rs +++ b/java/lance-jni/src/lib.rs @@ -48,6 +48,7 @@ mod file_reader; mod file_writer; mod fragment; mod merge_insert; +mod namespace; mod optimize; mod schema; mod sql; diff --git a/java/lance-jni/src/namespace.rs b/java/lance-jni/src/namespace.rs new file mode 100644 index 00000000000..cdfc2575768 --- /dev/null +++ b/java/lance-jni/src/namespace.rs @@ -0,0 +1,1343 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +use bytes::Bytes; +use jni::objects::{JByteArray, JMap, JObject, JString}; +use jni::sys::{jbyteArray, jlong, jstring}; +use jni::JNIEnv; +use lance_namespace::models::*; +use lance_namespace::LanceNamespace as LanceNamespaceTrait; +use lance_namespace_impls::{ + ConnectBuilder, DirectoryNamespace, DirectoryNamespaceBuilder, RestAdapter, RestAdapterConfig, + RestNamespace, RestNamespaceBuilder, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use crate::error::{Error, Result}; +use crate::utils::to_rust_map; +use crate::RT; + +/// Blocking wrapper for DirectoryNamespace +pub struct BlockingDirectoryNamespace { + pub(crate) inner: DirectoryNamespace, +} + +/// Blocking wrapper for RestNamespace +pub struct BlockingRestNamespace { + pub(crate) inner: RestNamespace, +} + +// ============================================================================ +// DirectoryNamespace JNI Functions +// ============================================================================ + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_createNative( + mut env: JNIEnv, + _obj: JObject, + properties_map: JObject, +) -> jlong { + ok_or_throw_with_return!( + env, + create_directory_namespace_internal(&mut env, properties_map), + 0 + ) +} + +fn create_directory_namespace_internal(env: &mut JNIEnv, properties_map: JObject) -> Result { + // Convert Java HashMap to Rust HashMap + let jmap = JMap::from_env(env, &properties_map)?; + let properties = to_rust_map(env, &jmap)?; + + // Build DirectoryNamespace using builder + let builder = DirectoryNamespaceBuilder::from_properties(properties, None).map_err(|e| { + Error::runtime_error(format!("Failed to create DirectoryNamespaceBuilder: {}", e)) + })?; + + let namespace = RT + .block_on(builder.build()) + .map_err(|e| Error::runtime_error(format!("Failed to build DirectoryNamespace: {}", e)))?; + + let blocking_namespace = BlockingDirectoryNamespace { inner: namespace }; + let handle = Box::into_raw(Box::new(blocking_namespace)) as jlong; + Ok(handle) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_releaseNative( + _env: JNIEnv, + _obj: JObject, + handle: jlong, +) { + if handle != 0 { + unsafe { + let _ = Box::from_raw(handle as *mut BlockingDirectoryNamespace); + } + } +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_namespaceIdNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, +) -> jstring { + let namespace = unsafe { &*(handle as *const BlockingDirectoryNamespace) }; + let namespace_id = namespace.inner.namespace_id(); + ok_or_throw_with_return!( + env, + env.new_string(namespace_id).map_err(Error::from), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_listNamespacesNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.list_namespaces(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_describeNamespaceNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.describe_namespace(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_createNamespaceNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.create_namespace(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_dropNamespaceNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.drop_namespace(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_namespaceExistsNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) { + ok_or_throw_without_return!( + env, + call_namespace_void_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.namespace_exists(req)) + }) + ) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_listTablesNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.list_tables(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_describeTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.describe_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_registerTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.register_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_tableExistsNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) { + ok_or_throw_without_return!( + env, + call_namespace_void_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.table_exists(req)) + }) + ) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_dropTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.drop_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_deregisterTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.deregister_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_countTableRowsNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jlong { + ok_or_throw_with_return!( + env, + call_namespace_count_method(&mut env, handle, request_json), + 0 + ) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_createTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, + request_data: JByteArray, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_with_data_method( + &mut env, + handle, + request_json, + request_data, + |ns, req, data| { RT.block_on(ns.inner.create_table(req, data)) } + ), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_createEmptyTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.create_empty_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_insertIntoTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, + request_data: JByteArray, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_with_data_method( + &mut env, + handle, + request_json, + request_data, + |ns, req, data| { RT.block_on(ns.inner.insert_into_table(req, data)) } + ), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_mergeInsertIntoTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, + request_data: JByteArray, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_with_data_method( + &mut env, + handle, + request_json, + request_data, + |ns, req, data| { RT.block_on(ns.inner.merge_insert_into_table(req, data)) } + ), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_updateTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.update_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_deleteFromTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.delete_from_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_queryTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jbyteArray { + ok_or_throw_with_return!( + env, + call_namespace_query_method(&mut env, handle, request_json), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_createTableIndexNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.create_table_index(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_listTableIndicesNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.list_table_indices(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_describeTableIndexStatsNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.describe_table_index_stats(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_describeTransactionNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.describe_transaction(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_DirectoryNamespace_alterTransactionNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.alter_transaction(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +// ============================================================================ +// RestNamespace JNI Functions +// ============================================================================ + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_createNative( + mut env: JNIEnv, + _obj: JObject, + properties_map: JObject, +) -> jlong { + ok_or_throw_with_return!( + env, + create_rest_namespace_internal(&mut env, properties_map), + 0 + ) +} + +fn create_rest_namespace_internal(env: &mut JNIEnv, properties_map: JObject) -> Result { + // Convert Java HashMap to Rust HashMap + let jmap = JMap::from_env(env, &properties_map)?; + let properties = to_rust_map(env, &jmap)?; + + // Build RestNamespace using builder + let builder = RestNamespaceBuilder::from_properties(properties).map_err(|e| { + Error::runtime_error(format!("Failed to create RestNamespaceBuilder: {}", e)) + })?; + + let namespace = builder.build(); + + let blocking_namespace = BlockingRestNamespace { inner: namespace }; + let handle = Box::into_raw(Box::new(blocking_namespace)) as jlong; + Ok(handle) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_releaseNative( + _env: JNIEnv, + _obj: JObject, + handle: jlong, +) { + if handle != 0 { + unsafe { + let _ = Box::from_raw(handle as *mut BlockingRestNamespace); + } + } +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_namespaceIdNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, +) -> jstring { + let namespace = unsafe { &*(handle as *const BlockingRestNamespace) }; + let namespace_id = namespace.inner.namespace_id(); + ok_or_throw_with_return!( + env, + env.new_string(namespace_id).map_err(Error::from), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_listNamespacesNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.list_namespaces(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_describeNamespaceNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.describe_namespace(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_createNamespaceNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.create_namespace(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_dropNamespaceNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.drop_namespace(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_namespaceExistsNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) { + ok_or_throw_without_return!( + env, + call_rest_namespace_void_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.namespace_exists(req)) + }) + ) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_listTablesNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.list_tables(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_describeTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.describe_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_registerTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.register_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_tableExistsNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) { + ok_or_throw_without_return!( + env, + call_rest_namespace_void_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.table_exists(req)) + }) + ) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_dropTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.drop_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_deregisterTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.deregister_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_countTableRowsNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jlong { + ok_or_throw_with_return!( + env, + call_rest_namespace_count_method(&mut env, handle, request_json), + 0 + ) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_createTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, + request_data: JByteArray, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_with_data_method( + &mut env, + handle, + request_json, + request_data, + |ns, req, data| { RT.block_on(ns.inner.create_table(req, data)) } + ), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_createEmptyTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.create_empty_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_insertIntoTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, + request_data: JByteArray, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_with_data_method( + &mut env, + handle, + request_json, + request_data, + |ns, req, data| { RT.block_on(ns.inner.insert_into_table(req, data)) } + ), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_mergeInsertIntoTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, + request_data: JByteArray, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_with_data_method( + &mut env, + handle, + request_json, + request_data, + |ns, req, data| { RT.block_on(ns.inner.merge_insert_into_table(req, data)) } + ), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_updateTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.update_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_deleteFromTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.delete_from_table(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_queryTableNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jbyteArray { + ok_or_throw_with_return!( + env, + call_rest_namespace_query_method(&mut env, handle, request_json), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_createTableIndexNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.create_table_index(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_listTableIndicesNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.list_table_indices(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_describeTableIndexStatsNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.describe_table_index_stats(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_describeTransactionNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.describe_transaction(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestNamespace_alterTransactionNative( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, + request_json: JString, +) -> jstring { + ok_or_throw_with_return!( + env, + call_rest_namespace_method(&mut env, handle, request_json, |ns, req| { + RT.block_on(ns.inner.alter_transaction(req)) + }), + std::ptr::null_mut() + ) + .into_raw() +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Helper function to call namespace methods that return a response object (DirectoryNamespace) +fn call_namespace_method<'local, Req, Resp, F>( + env: &mut JNIEnv<'local>, + handle: jlong, + request_json: JString, + f: F, +) -> Result> +where + Req: for<'de> Deserialize<'de>, + Resp: Serialize, + F: FnOnce(&BlockingDirectoryNamespace, Req) -> lance_core::Result, +{ + let namespace = unsafe { &*(handle as *const BlockingDirectoryNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: Req = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + let response = f(namespace, request) + .map_err(|e| Error::runtime_error(format!("Namespace operation failed: {}", e)))?; + + let response_json = serde_json::to_string(&response) + .map_err(|e| Error::runtime_error(format!("Failed to serialize response: {}", e)))?; + + env.new_string(response_json).map_err(Into::into) +} + +/// Helper function for void methods (DirectoryNamespace) +fn call_namespace_void_method( + env: &mut JNIEnv, + handle: jlong, + request_json: JString, + f: F, +) -> Result<()> +where + Req: for<'de> Deserialize<'de>, + F: FnOnce(&BlockingDirectoryNamespace, Req) -> lance_core::Result<()>, +{ + let namespace = unsafe { &*(handle as *const BlockingDirectoryNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: Req = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + f(namespace, request) + .map_err(|e| Error::runtime_error(format!("Namespace operation failed: {}", e)))?; + + Ok(()) +} + +/// Helper function for count methods (DirectoryNamespace) +fn call_namespace_count_method( + env: &mut JNIEnv, + handle: jlong, + request_json: JString, +) -> Result { + let namespace = unsafe { &*(handle as *const BlockingDirectoryNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: CountTableRowsRequest = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + let count = RT + .block_on(namespace.inner.count_table_rows(request)) + .map_err(|e| Error::runtime_error(format!("Count table rows failed: {}", e)))?; + + Ok(count) +} + +/// Helper function for methods with data parameter (DirectoryNamespace) +fn call_namespace_with_data_method<'local, Req, Resp, F>( + env: &mut JNIEnv<'local>, + handle: jlong, + request_json: JString, + request_data: JByteArray, + f: F, +) -> Result> +where + Req: for<'de> Deserialize<'de>, + Resp: Serialize, + F: FnOnce(&BlockingDirectoryNamespace, Req, Bytes) -> lance_core::Result, +{ + let namespace = unsafe { &*(handle as *const BlockingDirectoryNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: Req = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + let data_vec = env.convert_byte_array(request_data)?; + let data = bytes::Bytes::from(data_vec); + + let response = f(namespace, request, data) + .map_err(|e| Error::runtime_error(format!("Namespace operation failed: {}", e)))?; + + let response_json = serde_json::to_string(&response) + .map_err(|e| Error::runtime_error(format!("Failed to serialize response: {}", e)))?; + + env.new_string(response_json).map_err(Into::into) +} + +/// Helper function for query methods that return byte arrays (DirectoryNamespace) +fn call_namespace_query_method<'local>( + env: &mut JNIEnv<'local>, + handle: jlong, + request_json: JString, +) -> Result> { + let namespace = unsafe { &*(handle as *const BlockingDirectoryNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: QueryTableRequest = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + let result_bytes = RT + .block_on(namespace.inner.query_table(request)) + .map_err(|e| Error::runtime_error(format!("Query table failed: {}", e)))?; + + let byte_array = env.byte_array_from_slice(&result_bytes)?; + Ok(byte_array) +} + +/// Helper function to call namespace methods that return a response object (RestNamespace) +fn call_rest_namespace_method<'local, Req, Resp, F>( + env: &mut JNIEnv<'local>, + handle: jlong, + request_json: JString, + f: F, +) -> Result> +where + Req: for<'de> Deserialize<'de>, + Resp: Serialize, + F: FnOnce(&BlockingRestNamespace, Req) -> lance_core::Result, +{ + let namespace = unsafe { &*(handle as *const BlockingRestNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: Req = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + let response = f(namespace, request) + .map_err(|e| Error::runtime_error(format!("Namespace operation failed: {}", e)))?; + + let response_json = serde_json::to_string(&response) + .map_err(|e| Error::runtime_error(format!("Failed to serialize response: {}", e)))?; + + env.new_string(response_json).map_err(Into::into) +} + +/// Helper function for void methods (RestNamespace) +fn call_rest_namespace_void_method( + env: &mut JNIEnv, + handle: jlong, + request_json: JString, + f: F, +) -> Result<()> +where + Req: for<'de> Deserialize<'de>, + F: FnOnce(&BlockingRestNamespace, Req) -> lance_core::Result<()>, +{ + let namespace = unsafe { &*(handle as *const BlockingRestNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: Req = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + f(namespace, request) + .map_err(|e| Error::runtime_error(format!("Namespace operation failed: {}", e)))?; + + Ok(()) +} + +/// Helper function for count methods (RestNamespace) +fn call_rest_namespace_count_method( + env: &mut JNIEnv, + handle: jlong, + request_json: JString, +) -> Result { + let namespace = unsafe { &*(handle as *const BlockingRestNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: CountTableRowsRequest = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + let count = RT + .block_on(namespace.inner.count_table_rows(request)) + .map_err(|e| Error::runtime_error(format!("Count table rows failed: {}", e)))?; + + Ok(count) +} + +/// Helper function for methods with data parameter (RestNamespace) +fn call_rest_namespace_with_data_method<'local, Req, Resp, F>( + env: &mut JNIEnv<'local>, + handle: jlong, + request_json: JString, + request_data: JByteArray, + f: F, +) -> Result> +where + Req: for<'de> Deserialize<'de>, + Resp: Serialize, + F: FnOnce(&BlockingRestNamespace, Req, Bytes) -> lance_core::Result, +{ + let namespace = unsafe { &*(handle as *const BlockingRestNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: Req = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + let data_vec = env.convert_byte_array(request_data)?; + let data = bytes::Bytes::from(data_vec); + + let response = f(namespace, request, data) + .map_err(|e| Error::runtime_error(format!("Namespace operation failed: {}", e)))?; + + let response_json = serde_json::to_string(&response) + .map_err(|e| Error::runtime_error(format!("Failed to serialize response: {}", e)))?; + + env.new_string(response_json).map_err(Into::into) +} + +/// Helper function for query methods that return byte arrays (RestNamespace) +fn call_rest_namespace_query_method<'local>( + env: &mut JNIEnv<'local>, + handle: jlong, + request_json: JString, +) -> Result> { + let namespace = unsafe { &*(handle as *const BlockingRestNamespace) }; + let request_str: String = env.get_string(&request_json)?.into(); + let request: QueryTableRequest = serde_json::from_str(&request_str) + .map_err(|e| Error::input_error(format!("Failed to parse request JSON: {}", e)))?; + + let result_bytes = RT + .block_on(namespace.inner.query_table(request)) + .map_err(|e| Error::runtime_error(format!("Query table failed: {}", e)))?; + + let byte_array = env.byte_array_from_slice(&result_bytes)?; + Ok(byte_array) +} +// ============================================================================ +// RestAdapter - Server for testing +// ============================================================================ + +/// Wrapper for RestAdapter that manages the server lifecycle +pub struct BlockingRestAdapter { + backend: Arc, + config: RestAdapterConfig, + server_handle: Option>, +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestAdapter_createNative( + mut env: JNIEnv, + _obj: JObject, + namespace_impl: JString, + properties_map: JObject, + host: JString, + port: jni::sys::jint, +) -> jlong { + ok_or_throw_with_return!( + env, + create_rest_adapter_internal(&mut env, namespace_impl, properties_map, host, port), + 0 + ) +} + +fn create_rest_adapter_internal( + env: &mut JNIEnv, + namespace_impl: JString, + properties_map: JObject, + host: JString, + port: jni::sys::jint, +) -> Result { + // Get namespace implementation type + let impl_str: String = env.get_string(&namespace_impl)?.into(); + + // Convert Java HashMap to Rust HashMap + let jmap = JMap::from_env(env, &properties_map)?; + let properties = to_rust_map(env, &jmap)?; + + // Build backend namespace using ConnectBuilder + let mut builder = ConnectBuilder::new(impl_str); + for (k, v) in properties { + builder = builder.property(k, v); + } + + let backend = RT + .block_on(builder.connect()) + .map_err(|e| Error::runtime_error(format!("Failed to build backend namespace: {}", e)))?; + + // Get host string + let host_str: String = env.get_string(&host)?.into(); + + let config = RestAdapterConfig { + host: host_str, + port: port as u16, + }; + + let adapter = BlockingRestAdapter { + backend, + config, + server_handle: None, + }; + + let handle = Box::into_raw(Box::new(adapter)) as jlong; + Ok(handle) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestAdapter_serve( + mut env: JNIEnv, + _obj: JObject, + handle: jlong, +) { + ok_or_throw_without_return!(env, serve_internal(handle)) +} + +fn serve_internal(handle: jlong) -> Result<()> { + let adapter = unsafe { &mut *(handle as *mut BlockingRestAdapter) }; + + let rest_adapter = RestAdapter::new(adapter.backend.clone(), adapter.config.clone()); + + // Spawn server in background + let server_handle = RT.spawn(async move { + let _ = rest_adapter.serve().await; + }); + + adapter.server_handle = Some(server_handle); + + // Give server time to start + std::thread::sleep(std::time::Duration::from_millis(500)); + + Ok(()) +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestAdapter_stop( + _env: JNIEnv, + _obj: JObject, + handle: jlong, +) { + let adapter = unsafe { &mut *(handle as *mut BlockingRestAdapter) }; + + if let Some(server_handle) = adapter.server_handle.take() { + server_handle.abort(); + } +} + +#[no_mangle] +pub extern "system" fn Java_com_lancedb_lance_namespace_RestAdapter_releaseNative( + _env: JNIEnv, + _obj: JObject, + handle: jlong, +) { + if handle != 0 { + unsafe { + let mut adapter = Box::from_raw(handle as *mut BlockingRestAdapter); + if let Some(server_handle) = adapter.server_handle.take() { + server_handle.abort(); + } + } + } +} diff --git a/java/pom.xml b/java/pom.xml index 37510482f07..10432294e50 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -110,6 +110,11 @@ lance-namespace-core 0.0.20 + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + software.amazon.awssdk diff --git a/java/src/main/java/com/lancedb/lance/namespace/DirectoryNamespace.java b/java/src/main/java/com/lancedb/lance/namespace/DirectoryNamespace.java new file mode 100644 index 00000000000..a2ed29aac5b --- /dev/null +++ b/java/src/main/java/com/lancedb/lance/namespace/DirectoryNamespace.java @@ -0,0 +1,394 @@ +/* + * 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 com.lancedb.lance.namespace; + +import com.lancedb.lance.JniLoader; +import com.lancedb.lance.namespace.model.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.arrow.memory.BufferAllocator; + +import java.io.Closeable; +import java.util.Map; + +/** + * DirectoryNamespace implementation that provides Lance namespace functionality for directory-based + * storage. + * + *

Supported storage backends: + * + *

    + *
  • Local filesystem + *
  • AWS S3 (s3://bucket/path) + *
  • Azure Blob Storage (az://container/path) + *
  • Google Cloud Storage (gs://bucket/path) + *
+ * + *

This class wraps the native Rust implementation and provides a Java interface that implements + * the LanceNamespace interface from lance-namespace-core. + * + *

Configuration properties: + * + *

    + *
  • root (required): Root directory path or URI (e.g., /path/to/dir, s3://bucket/path, + * az://container/path, gs://bucket/path) + *
  • manifest_enabled (optional): "true" or "false" (default: true) + *
  • dir_listing_enabled (optional): "true" or "false" (default: true) + *
  • inline_optimization_enabled (optional): "true" or "false" (default: true) + *
  • storage.* (optional): Storage options for cloud providers (e.g., storage.region=us-east-1 + * for S3, storage.account_name=myaccount for Azure) + *
+ * + *

Example usage (local filesystem): + * + *

{@code
+ * Map properties = new HashMap<>();
+ * properties.put("root", "/tmp/lance-data");
+ * properties.put("manifest_enabled", "true");
+ *
+ * DirectoryNamespace namespace = new DirectoryNamespace();
+ * namespace.initialize(properties, allocator);
+ *
+ * // Use namespace...
+ * ListTablesResponse tables = namespace.listTables(request);
+ *
+ * // Clean up
+ * namespace.close();
+ * }
+ * + *

Example usage (AWS S3): + * + *

{@code
+ * Map properties = new HashMap<>();
+ * properties.put("root", "s3://my-bucket/lance-data");
+ * properties.put("storage.region", "us-east-1");
+ * // AWS credentials can be provided via environment variables or IAM roles
+ *
+ * DirectoryNamespace namespace = new DirectoryNamespace();
+ * namespace.initialize(properties, allocator);
+ * // Use namespace...
+ * namespace.close();
+ * }
+ */ +public class DirectoryNamespace implements LanceNamespace, Closeable { + static { + JniLoader.ensureLoaded(); + } + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private long nativeDirectoryNamespaceHandle; + private BufferAllocator allocator; + + /** Creates a new DirectoryNamespace. Must call initialize() before use. */ + public DirectoryNamespace() {} + + @Override + public void initialize(Map configProperties, BufferAllocator allocator) { + if (this.nativeDirectoryNamespaceHandle != 0) { + throw new IllegalStateException("DirectoryNamespace already initialized"); + } + this.allocator = allocator; + this.nativeDirectoryNamespaceHandle = createNative(configProperties); + } + + @Override + public String namespaceId() { + ensureInitialized(); + return namespaceIdNative(nativeDirectoryNamespaceHandle); + } + + @Override + public ListNamespacesResponse listNamespaces(ListNamespacesRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = listNamespacesNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, ListNamespacesResponse.class); + } + + @Override + public DescribeNamespaceResponse describeNamespace(DescribeNamespaceRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = describeNamespaceNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, DescribeNamespaceResponse.class); + } + + @Override + public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = createNamespaceNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, CreateNamespaceResponse.class); + } + + @Override + public DropNamespaceResponse dropNamespace(DropNamespaceRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = dropNamespaceNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, DropNamespaceResponse.class); + } + + @Override + public void namespaceExists(NamespaceExistsRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + namespaceExistsNative(nativeDirectoryNamespaceHandle, requestJson); + } + + @Override + public ListTablesResponse listTables(ListTablesRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = listTablesNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, ListTablesResponse.class); + } + + @Override + public DescribeTableResponse describeTable(DescribeTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = describeTableNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, DescribeTableResponse.class); + } + + @Override + public RegisterTableResponse registerTable(RegisterTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = registerTableNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, RegisterTableResponse.class); + } + + @Override + public void tableExists(TableExistsRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + tableExistsNative(nativeDirectoryNamespaceHandle, requestJson); + } + + @Override + public DropTableResponse dropTable(DropTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = dropTableNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, DropTableResponse.class); + } + + @Override + public DeregisterTableResponse deregisterTable(DeregisterTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = deregisterTableNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, DeregisterTableResponse.class); + } + + @Override + public Long countTableRows(CountTableRowsRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + return countTableRowsNative(nativeDirectoryNamespaceHandle, requestJson); + } + + @Override + public CreateTableResponse createTable(CreateTableRequest request, byte[] requestData) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = + createTableNative(nativeDirectoryNamespaceHandle, requestJson, requestData); + return fromJson(responseJson, CreateTableResponse.class); + } + + @Override + public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = createEmptyTableNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, CreateEmptyTableResponse.class); + } + + @Override + public InsertIntoTableResponse insertIntoTable( + InsertIntoTableRequest request, byte[] requestData) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = + insertIntoTableNative(nativeDirectoryNamespaceHandle, requestJson, requestData); + return fromJson(responseJson, InsertIntoTableResponse.class); + } + + @Override + public MergeInsertIntoTableResponse mergeInsertIntoTable( + MergeInsertIntoTableRequest request, byte[] requestData) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = + mergeInsertIntoTableNative(nativeDirectoryNamespaceHandle, requestJson, requestData); + return fromJson(responseJson, MergeInsertIntoTableResponse.class); + } + + @Override + public UpdateTableResponse updateTable(UpdateTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = updateTableNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, UpdateTableResponse.class); + } + + @Override + public DeleteFromTableResponse deleteFromTable(DeleteFromTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = deleteFromTableNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, DeleteFromTableResponse.class); + } + + @Override + public byte[] queryTable(QueryTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + return queryTableNative(nativeDirectoryNamespaceHandle, requestJson); + } + + @Override + public CreateTableIndexResponse createTableIndex(CreateTableIndexRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = createTableIndexNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, CreateTableIndexResponse.class); + } + + @Override + public ListTableIndicesResponse listTableIndices(ListTableIndicesRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = listTableIndicesNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, ListTableIndicesResponse.class); + } + + @Override + public DescribeTableIndexStatsResponse describeTableIndexStats( + DescribeTableIndexStatsRequest request, String indexName) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = + describeTableIndexStatsNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, DescribeTableIndexStatsResponse.class); + } + + @Override + public DescribeTransactionResponse describeTransaction(DescribeTransactionRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = describeTransactionNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, DescribeTransactionResponse.class); + } + + @Override + public AlterTransactionResponse alterTransaction(AlterTransactionRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = alterTransactionNative(nativeDirectoryNamespaceHandle, requestJson); + return fromJson(responseJson, AlterTransactionResponse.class); + } + + @Override + public void close() { + if (nativeDirectoryNamespaceHandle != 0) { + releaseNative(nativeDirectoryNamespaceHandle); + nativeDirectoryNamespaceHandle = 0; + } + } + + private void ensureInitialized() { + if (nativeDirectoryNamespaceHandle == 0) { + throw new IllegalStateException( + "DirectoryNamespace not initialized. Call initialize() first."); + } + } + + private static String toJson(Object obj) { + try { + return OBJECT_MAPPER.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize request to JSON", e); + } + } + + private static T fromJson(String json, Class clazz) { + try { + return OBJECT_MAPPER.readValue(json, clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to deserialize response from JSON", e); + } + } + + // Native methods + private native long createNative(Map properties); + + private native void releaseNative(long handle); + + private native String namespaceIdNative(long handle); + + private native String listNamespacesNative(long handle, String requestJson); + + private native String describeNamespaceNative(long handle, String requestJson); + + private native String createNamespaceNative(long handle, String requestJson); + + private native String dropNamespaceNative(long handle, String requestJson); + + private native void namespaceExistsNative(long handle, String requestJson); + + private native String listTablesNative(long handle, String requestJson); + + private native String describeTableNative(long handle, String requestJson); + + private native String registerTableNative(long handle, String requestJson); + + private native void tableExistsNative(long handle, String requestJson); + + private native String dropTableNative(long handle, String requestJson); + + private native String deregisterTableNative(long handle, String requestJson); + + private native long countTableRowsNative(long handle, String requestJson); + + private native String createTableNative(long handle, String requestJson, byte[] requestData); + + private native String createEmptyTableNative(long handle, String requestJson); + + private native String insertIntoTableNative(long handle, String requestJson, byte[] requestData); + + private native String mergeInsertIntoTableNative( + long handle, String requestJson, byte[] requestData); + + private native String updateTableNative(long handle, String requestJson); + + private native String deleteFromTableNative(long handle, String requestJson); + + private native byte[] queryTableNative(long handle, String requestJson); + + private native String createTableIndexNative(long handle, String requestJson); + + private native String listTableIndicesNative(long handle, String requestJson); + + private native String describeTableIndexStatsNative(long handle, String requestJson); + + private native String describeTransactionNative(long handle, String requestJson); + + private native String alterTransactionNative(long handle, String requestJson); +} diff --git a/java/src/main/java/com/lancedb/lance/namespace/RestAdapter.java b/java/src/main/java/com/lancedb/lance/namespace/RestAdapter.java new file mode 100644 index 00000000000..f9d1ffac2b9 --- /dev/null +++ b/java/src/main/java/com/lancedb/lance/namespace/RestAdapter.java @@ -0,0 +1,138 @@ +/* + * 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 com.lancedb.lance.namespace; + +import com.lancedb.lance.JniLoader; + +import java.io.Closeable; +import java.util.Map; + +/** + * REST adapter server for testing namespace implementations. + * + *

This class wraps a namespace backend (e.g., DirectoryNamespace) and exposes it via a REST API. + * It's primarily used for testing RestNamespace implementations. + * + *

Example usage: + * + *

{@code
+ * Map backendConfig = new HashMap<>();
+ * backendConfig.put("root", "/tmp/test-data");
+ *
+ * try (RestAdapter adapter = new RestAdapter("dir", backendConfig, "127.0.0.1", 8080)) {
+ *     adapter.serve();
+ *
+ *     // Now you can connect with RestNamespace
+ *     Map clientConfig = new HashMap<>();
+ *     clientConfig.put("uri", "http://127.0.0.1:8080");
+ *     RestNamespace client = new RestNamespace();
+ *     client.initialize(clientConfig, allocator);
+ *
+ *     // Use the client...
+ * }
+ * }
+ */ +public class RestAdapter implements Closeable, AutoCloseable { + static { + JniLoader.ensureLoaded(); + } + + private long nativeRestAdapterHandle; + private boolean serverStarted = false; + + /** + * Creates a new REST adapter with the given backend namespace. + * + * @param namespaceImpl The namespace implementation type (e.g., "dir" for DirectoryNamespace) + * @param backendConfig Configuration properties for the backend namespace + * @param host Host to bind the server to + * @param port Port to bind the server to + */ + public RestAdapter( + String namespaceImpl, Map backendConfig, String host, int port) { + if (namespaceImpl == null || namespaceImpl.isEmpty()) { + throw new IllegalArgumentException("namespace implementation cannot be null or empty"); + } + if (backendConfig == null) { + throw new IllegalArgumentException("backend config cannot be null"); + } + if (host == null || host.isEmpty()) { + throw new IllegalArgumentException("host cannot be null or empty"); + } + if (port <= 0 || port > 65535) { + throw new IllegalArgumentException("port must be between 1 and 65535"); + } + + this.nativeRestAdapterHandle = createNative(namespaceImpl, backendConfig, host, port); + } + + /** + * Creates a new REST adapter with default host (127.0.0.1) and port (2333). + * + * @param namespaceImpl The namespace implementation type + * @param backendConfig Configuration properties for the backend namespace + */ + public RestAdapter(String namespaceImpl, Map backendConfig) { + this(namespaceImpl, backendConfig, "127.0.0.1", 2333); + } + + /** + * Start the REST server in the background. + * + *

This method returns immediately after starting the server. The server runs in a background + * thread until {@link #stop()} is called or the adapter is closed. + */ + public void serve() { + if (nativeRestAdapterHandle == 0) { + throw new IllegalStateException("RestAdapter not initialized"); + } + if (serverStarted) { + throw new IllegalStateException("Server already started"); + } + + serve(nativeRestAdapterHandle); + serverStarted = true; + } + + /** + * Stop the REST server. + * + *

This method is idempotent - calling it multiple times has no effect. + */ + public void stop() { + if (nativeRestAdapterHandle != 0 && serverStarted) { + stop(nativeRestAdapterHandle); + serverStarted = false; + } + } + + @Override + public void close() { + stop(); + if (nativeRestAdapterHandle != 0) { + releaseNative(nativeRestAdapterHandle); + nativeRestAdapterHandle = 0; + } + } + + // Native methods + private native long createNative( + String namespaceImpl, Map backendConfig, String host, int port); + + private native void serve(long handle); + + private native void stop(long handle); + + private native void releaseNative(long handle); +} diff --git a/java/src/main/java/com/lancedb/lance/namespace/RestNamespace.java b/java/src/main/java/com/lancedb/lance/namespace/RestNamespace.java new file mode 100644 index 00000000000..218822ed7be --- /dev/null +++ b/java/src/main/java/com/lancedb/lance/namespace/RestNamespace.java @@ -0,0 +1,368 @@ +/* + * 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 com.lancedb.lance.namespace; + +import com.lancedb.lance.JniLoader; +import com.lancedb.lance.namespace.model.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.arrow.memory.BufferAllocator; + +import java.io.Closeable; +import java.util.Map; + +/** + * RestNamespace implementation that provides Lance namespace functionality via REST API endpoints. + * + *

This class wraps the native Rust implementation and provides a Java interface that implements + * the LanceNamespace interface from lance-namespace-core. + * + *

Configuration properties: + * + *

    + *
  • uri (required): REST API endpoint URL + *
  • delimiter (optional): Namespace delimiter (default: "$") + *
  • header.* (optional): HTTP headers (e.g., header.Authorization=Bearer token) + *
  • tls.cert_file (optional): Path to client certificate file + *
  • tls.key_file (optional): Path to client key file + *
  • tls.ssl_ca_cert (optional): Path to CA certificate file + *
  • tls.assert_hostname (optional): "true" or "false" (default: true) + *
+ * + *

Example usage: + * + *

{@code
+ * Map properties = new HashMap<>();
+ * properties.put("uri", "https://api.example.com");
+ * properties.put("delimiter", ".");
+ * properties.put("header.Authorization", "Bearer my-token");
+ *
+ * RestNamespace namespace = new RestNamespace();
+ * namespace.initialize(properties, allocator);
+ *
+ * // Use namespace...
+ * ListTablesResponse tables = namespace.listTables(request);
+ *
+ * // Clean up
+ * namespace.close();
+ * }
+ */ +public class RestNamespace implements LanceNamespace, Closeable { + static { + JniLoader.ensureLoaded(); + } + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private long nativeRestNamespaceHandle; + private BufferAllocator allocator; + + /** Creates a new RestNamespace. Must call initialize() before use. */ + public RestNamespace() {} + + @Override + public void initialize(Map configProperties, BufferAllocator allocator) { + if (this.nativeRestNamespaceHandle != 0) { + throw new IllegalStateException("RestNamespace already initialized"); + } + this.allocator = allocator; + this.nativeRestNamespaceHandle = createNative(configProperties); + } + + @Override + public String namespaceId() { + ensureInitialized(); + return namespaceIdNative(nativeRestNamespaceHandle); + } + + @Override + public ListNamespacesResponse listNamespaces(ListNamespacesRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = listNamespacesNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, ListNamespacesResponse.class); + } + + @Override + public DescribeNamespaceResponse describeNamespace(DescribeNamespaceRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = describeNamespaceNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, DescribeNamespaceResponse.class); + } + + @Override + public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = createNamespaceNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, CreateNamespaceResponse.class); + } + + @Override + public DropNamespaceResponse dropNamespace(DropNamespaceRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = dropNamespaceNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, DropNamespaceResponse.class); + } + + @Override + public void namespaceExists(NamespaceExistsRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + namespaceExistsNative(nativeRestNamespaceHandle, requestJson); + } + + @Override + public ListTablesResponse listTables(ListTablesRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = listTablesNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, ListTablesResponse.class); + } + + @Override + public DescribeTableResponse describeTable(DescribeTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = describeTableNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, DescribeTableResponse.class); + } + + @Override + public RegisterTableResponse registerTable(RegisterTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = registerTableNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, RegisterTableResponse.class); + } + + @Override + public void tableExists(TableExistsRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + tableExistsNative(nativeRestNamespaceHandle, requestJson); + } + + @Override + public DropTableResponse dropTable(DropTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = dropTableNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, DropTableResponse.class); + } + + @Override + public DeregisterTableResponse deregisterTable(DeregisterTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = deregisterTableNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, DeregisterTableResponse.class); + } + + @Override + public Long countTableRows(CountTableRowsRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + return countTableRowsNative(nativeRestNamespaceHandle, requestJson); + } + + @Override + public CreateTableResponse createTable(CreateTableRequest request, byte[] requestData) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = createTableNative(nativeRestNamespaceHandle, requestJson, requestData); + return fromJson(responseJson, CreateTableResponse.class); + } + + @Override + public CreateEmptyTableResponse createEmptyTable(CreateEmptyTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = createEmptyTableNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, CreateEmptyTableResponse.class); + } + + @Override + public InsertIntoTableResponse insertIntoTable( + InsertIntoTableRequest request, byte[] requestData) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = + insertIntoTableNative(nativeRestNamespaceHandle, requestJson, requestData); + return fromJson(responseJson, InsertIntoTableResponse.class); + } + + @Override + public MergeInsertIntoTableResponse mergeInsertIntoTable( + MergeInsertIntoTableRequest request, byte[] requestData) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = + mergeInsertIntoTableNative(nativeRestNamespaceHandle, requestJson, requestData); + return fromJson(responseJson, MergeInsertIntoTableResponse.class); + } + + @Override + public UpdateTableResponse updateTable(UpdateTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = updateTableNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, UpdateTableResponse.class); + } + + @Override + public DeleteFromTableResponse deleteFromTable(DeleteFromTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = deleteFromTableNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, DeleteFromTableResponse.class); + } + + @Override + public byte[] queryTable(QueryTableRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + return queryTableNative(nativeRestNamespaceHandle, requestJson); + } + + @Override + public CreateTableIndexResponse createTableIndex(CreateTableIndexRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = createTableIndexNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, CreateTableIndexResponse.class); + } + + @Override + public ListTableIndicesResponse listTableIndices(ListTableIndicesRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = listTableIndicesNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, ListTableIndicesResponse.class); + } + + @Override + public DescribeTableIndexStatsResponse describeTableIndexStats( + DescribeTableIndexStatsRequest request, String indexName) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = describeTableIndexStatsNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, DescribeTableIndexStatsResponse.class); + } + + @Override + public DescribeTransactionResponse describeTransaction(DescribeTransactionRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = describeTransactionNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, DescribeTransactionResponse.class); + } + + @Override + public AlterTransactionResponse alterTransaction(AlterTransactionRequest request) { + ensureInitialized(); + String requestJson = toJson(request); + String responseJson = alterTransactionNative(nativeRestNamespaceHandle, requestJson); + return fromJson(responseJson, AlterTransactionResponse.class); + } + + @Override + public void close() { + if (nativeRestNamespaceHandle != 0) { + releaseNative(nativeRestNamespaceHandle); + nativeRestNamespaceHandle = 0; + } + } + + private void ensureInitialized() { + if (nativeRestNamespaceHandle == 0) { + throw new IllegalStateException("RestNamespace not initialized. Call initialize() first."); + } + } + + private static String toJson(Object obj) { + try { + return OBJECT_MAPPER.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize request to JSON", e); + } + } + + private static T fromJson(String json, Class clazz) { + try { + return OBJECT_MAPPER.readValue(json, clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to deserialize response from JSON", e); + } + } + + // Native methods + private native long createNative(Map properties); + + private native void releaseNative(long handle); + + private native String namespaceIdNative(long handle); + + private native String listNamespacesNative(long handle, String requestJson); + + private native String describeNamespaceNative(long handle, String requestJson); + + private native String createNamespaceNative(long handle, String requestJson); + + private native String dropNamespaceNative(long handle, String requestJson); + + private native void namespaceExistsNative(long handle, String requestJson); + + private native String listTablesNative(long handle, String requestJson); + + private native String describeTableNative(long handle, String requestJson); + + private native String registerTableNative(long handle, String requestJson); + + private native void tableExistsNative(long handle, String requestJson); + + private native String dropTableNative(long handle, String requestJson); + + private native String deregisterTableNative(long handle, String requestJson); + + private native long countTableRowsNative(long handle, String requestJson); + + private native String createTableNative(long handle, String requestJson, byte[] requestData); + + private native String createEmptyTableNative(long handle, String requestJson); + + private native String insertIntoTableNative(long handle, String requestJson, byte[] requestData); + + private native String mergeInsertIntoTableNative( + long handle, String requestJson, byte[] requestData); + + private native String updateTableNative(long handle, String requestJson); + + private native String deleteFromTableNative(long handle, String requestJson); + + private native byte[] queryTableNative(long handle, String requestJson); + + private native String createTableIndexNative(long handle, String requestJson); + + private native String listTableIndicesNative(long handle, String requestJson); + + private native String describeTableIndexStatsNative(long handle, String requestJson); + + private native String describeTransactionNative(long handle, String requestJson); + + private native String alterTransactionNative(long handle, String requestJson); +} diff --git a/java/src/test/java/com/lancedb/lance/namespace/DirectoryNamespaceTest.java b/java/src/test/java/com/lancedb/lance/namespace/DirectoryNamespaceTest.java new file mode 100644 index 00000000000..2982255cbea --- /dev/null +++ b/java/src/test/java/com/lancedb/lance/namespace/DirectoryNamespaceTest.java @@ -0,0 +1,308 @@ +/* + * 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 com.lancedb.lance.namespace; + +import com.lancedb.lance.namespace.model.*; + +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.IntVector; +import org.apache.arrow.vector.VarCharVector; +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.FieldType; +import org.apache.arrow.vector.types.pojo.Schema; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.ByteArrayOutputStream; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** Tests for DirectoryNamespace implementation. */ +public class DirectoryNamespaceTest { + @TempDir Path tempDir; + + private BufferAllocator allocator; + private DirectoryNamespace namespace; + + @BeforeEach + void setUp() { + allocator = new RootAllocator(Long.MAX_VALUE); + namespace = new DirectoryNamespace(); + + Map config = new HashMap<>(); + config.put("root", tempDir.toString()); + namespace.initialize(config, allocator); + } + + @AfterEach + void tearDown() { + if (namespace != null) { + namespace.close(); + } + if (allocator != null) { + allocator.close(); + } + } + + private byte[] createTestTableData() throws Exception { + Schema schema = + new Schema( + Arrays.asList( + new Field("id", FieldType.nullable(new ArrowType.Int(32, true)), null), + new Field("name", FieldType.nullable(new ArrowType.Utf8()), null), + new Field("age", FieldType.nullable(new ArrowType.Int(32, true)), null))); + + try (VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) { + IntVector idVector = (IntVector) root.getVector("id"); + VarCharVector nameVector = (VarCharVector) root.getVector("name"); + IntVector ageVector = (IntVector) root.getVector("age"); + + // Allocate space for 3 rows + idVector.allocateNew(3); + nameVector.allocateNew(3); + ageVector.allocateNew(3); + + idVector.set(0, 1); + nameVector.set(0, "Alice".getBytes()); + ageVector.set(0, 30); + + idVector.set(1, 2); + nameVector.set(1, "Bob".getBytes()); + ageVector.set(1, 25); + + idVector.set(2, 3); + nameVector.set(2, "Charlie".getBytes()); + ageVector.set(2, 35); + + // Set value counts + idVector.setValueCount(3); + nameVector.setValueCount(3); + ageVector.setValueCount(3); + root.setRowCount(3); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (ArrowStreamWriter writer = new ArrowStreamWriter(root, null, out)) { + writer.writeBatch(); + } + return out.toByteArray(); + } + } + + @Test + void testNamespaceId() { + String namespaceId = namespace.namespaceId(); + assertNotNull(namespaceId); + assertTrue(namespaceId.contains("DirectoryNamespace")); + } + + @Test + void testCreateAndListNamespaces() { + // Create a namespace + CreateNamespaceRequest createReq = new CreateNamespaceRequest().id(Arrays.asList("workspace")); + CreateNamespaceResponse createResp = namespace.createNamespace(createReq); + assertNotNull(createResp); + + // List namespaces + ListNamespacesRequest listReq = new ListNamespacesRequest(); + ListNamespacesResponse listResp = namespace.listNamespaces(listReq); + assertNotNull(listResp); + assertNotNull(listResp.getNamespaces()); + assertTrue(listResp.getNamespaces().contains("workspace")); + } + + @Test + void testDescribeNamespace() { + // Create a namespace + CreateNamespaceRequest createReq = new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createReq); + + // Describe namespace + DescribeNamespaceRequest descReq = + new DescribeNamespaceRequest().id(Arrays.asList("workspace")); + DescribeNamespaceResponse descResp = namespace.describeNamespace(descReq); + assertNotNull(descResp); + assertNotNull(descResp.getProperties()); + } + + @Test + void testNamespaceExists() { + // Create a namespace + CreateNamespaceRequest createReq = new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createReq); + + // Check existence + NamespaceExistsRequest existsReq = new NamespaceExistsRequest().id(Arrays.asList("workspace")); + assertDoesNotThrow(() -> namespace.namespaceExists(existsReq)); + + // Check non-existent namespace + NamespaceExistsRequest notExistsReq = + new NamespaceExistsRequest().id(Arrays.asList("nonexistent")); + assertThrows(RuntimeException.class, () -> namespace.namespaceExists(notExistsReq)); + } + + @Test + void testDropNamespace() { + // Create a namespace + CreateNamespaceRequest createReq = new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createReq); + + // Drop namespace + DropNamespaceRequest dropReq = new DropNamespaceRequest().id(Arrays.asList("workspace")); + DropNamespaceResponse dropResp = namespace.dropNamespace(dropReq); + assertNotNull(dropResp); + + // Verify it's gone + NamespaceExistsRequest existsReq = new NamespaceExistsRequest().id(Arrays.asList("workspace")); + assertThrows(RuntimeException.class, () -> namespace.namespaceExists(existsReq)); + } + + @Test + void testCreateTable() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create table with data + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + CreateTableResponse createResp = namespace.createTable(createReq, tableData); + + assertNotNull(createResp); + assertNotNull(createResp.getLocation()); + assertTrue(createResp.getLocation().contains("test_table")); + assertEquals(Long.valueOf(1), createResp.getVersion()); + } + + @Test + void testListTables() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create a table + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + namespace.createTable(createReq, tableData); + + // List tables + ListTablesRequest listReq = new ListTablesRequest().id(Arrays.asList("workspace")); + ListTablesResponse listResp = namespace.listTables(listReq); + + assertNotNull(listResp); + assertNotNull(listResp.getTables()); + assertTrue(listResp.getTables().contains("test_table")); + } + + @Test + void testDescribeTable() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create a table + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + namespace.createTable(createReq, tableData); + + // Describe table + DescribeTableRequest descReq = + new DescribeTableRequest().id(Arrays.asList("workspace", "test_table")); + DescribeTableResponse descResp = namespace.describeTable(descReq); + + assertNotNull(descResp); + assertNotNull(descResp.getLocation()); + assertTrue(descResp.getLocation().contains("test_table")); + } + + @Test + void testTableExists() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create a table + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + namespace.createTable(createReq, tableData); + + // Check existence + TableExistsRequest existsReq = + new TableExistsRequest().id(Arrays.asList("workspace", "test_table")); + assertDoesNotThrow(() -> namespace.tableExists(existsReq)); + + // Check non-existent table + TableExistsRequest notExistsReq = + new TableExistsRequest().id(Arrays.asList("workspace", "nonexistent")); + assertThrows(RuntimeException.class, () -> namespace.tableExists(notExistsReq)); + } + + @Test + void testDropTable() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create a table + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + namespace.createTable(createReq, tableData); + + // Drop table + DropTableRequest dropReq = new DropTableRequest().id(Arrays.asList("workspace", "test_table")); + DropTableResponse dropResp = namespace.dropTable(dropReq); + assertNotNull(dropResp); + + // Verify it's gone + TableExistsRequest existsReq = + new TableExistsRequest().id(Arrays.asList("workspace", "test_table")); + assertThrows(RuntimeException.class, () -> namespace.tableExists(existsReq)); + } + + @Test + void testCreateEmptyTable() { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create empty table (metadata-only operation) + CreateEmptyTableRequest createReq = + new CreateEmptyTableRequest().id(Arrays.asList("workspace", "empty_table")); + + CreateEmptyTableResponse createResp = namespace.createEmptyTable(createReq); + + assertNotNull(createResp); + assertNotNull(createResp.getLocation()); + } +} diff --git a/java/src/test/java/com/lancedb/lance/namespace/RestNamespaceTest.java b/java/src/test/java/com/lancedb/lance/namespace/RestNamespaceTest.java new file mode 100644 index 00000000000..85348dadc2a --- /dev/null +++ b/java/src/test/java/com/lancedb/lance/namespace/RestNamespaceTest.java @@ -0,0 +1,331 @@ +/* + * 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 com.lancedb.lance.namespace; + +import com.lancedb.lance.namespace.model.*; + +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.IntVector; +import org.apache.arrow.vector.VarCharVector; +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.FieldType; +import org.apache.arrow.vector.types.pojo.Schema; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.ByteArrayOutputStream; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for RestNamespace implementation using RestAdapter with DirectoryNamespace backend. + * + *

This mirrors DirectoryNamespaceTest to ensure parity between DirectoryNamespace and + * RestNamespace implementations. + */ +public class RestNamespaceTest { + @TempDir Path tempDir; + + private BufferAllocator allocator; + private RestAdapter adapter; + private RestNamespace namespace; + private int port; + + @BeforeEach + void setUp() { + allocator = new RootAllocator(Long.MAX_VALUE); + + // Use a random port to avoid conflicts + port = 4000 + new Random().nextInt(10000); + + // Create backend configuration for DirectoryNamespace + Map backendConfig = new HashMap<>(); + backendConfig.put("root", tempDir.toString()); + + // Create and start REST adapter + adapter = new RestAdapter("dir", backendConfig, "127.0.0.1", port); + adapter.serve(); + + // Create REST namespace client + namespace = new RestNamespace(); + Map clientConfig = new HashMap<>(); + clientConfig.put("uri", "http://127.0.0.1:" + port); + namespace.initialize(clientConfig, allocator); + } + + @AfterEach + void tearDown() { + if (namespace != null) { + namespace.close(); + } + if (adapter != null) { + adapter.close(); + } + if (allocator != null) { + allocator.close(); + } + } + + private byte[] createTestTableData() throws Exception { + Schema schema = + new Schema( + Arrays.asList( + new Field("id", FieldType.nullable(new ArrowType.Int(32, true)), null), + new Field("name", FieldType.nullable(new ArrowType.Utf8()), null), + new Field("age", FieldType.nullable(new ArrowType.Int(32, true)), null))); + + try (VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) { + IntVector idVector = (IntVector) root.getVector("id"); + VarCharVector nameVector = (VarCharVector) root.getVector("name"); + IntVector ageVector = (IntVector) root.getVector("age"); + + // Allocate space for 3 rows + idVector.allocateNew(3); + nameVector.allocateNew(3); + ageVector.allocateNew(3); + + idVector.set(0, 1); + nameVector.set(0, "Alice".getBytes()); + ageVector.set(0, 30); + + idVector.set(1, 2); + nameVector.set(1, "Bob".getBytes()); + ageVector.set(1, 25); + + idVector.set(2, 3); + nameVector.set(2, "Charlie".getBytes()); + ageVector.set(2, 35); + + // Set value counts + idVector.setValueCount(3); + nameVector.setValueCount(3); + ageVector.setValueCount(3); + root.setRowCount(3); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (ArrowStreamWriter writer = new ArrowStreamWriter(root, null, out)) { + writer.writeBatch(); + } + return out.toByteArray(); + } + } + + @Test + void testNamespaceId() { + String namespaceId = namespace.namespaceId(); + assertNotNull(namespaceId); + assertTrue(namespaceId.contains("RestNamespace")); + } + + @Test + void testCreateAndListNamespaces() { + // Create a namespace + CreateNamespaceRequest createReq = new CreateNamespaceRequest().id(Arrays.asList("workspace")); + CreateNamespaceResponse createResp = namespace.createNamespace(createReq); + assertNotNull(createResp); + + // List namespaces + ListNamespacesRequest listReq = new ListNamespacesRequest(); + ListNamespacesResponse listResp = namespace.listNamespaces(listReq); + assertNotNull(listResp); + assertNotNull(listResp.getNamespaces()); + assertTrue(listResp.getNamespaces().contains("workspace")); + } + + @Test + void testDescribeNamespace() { + // Create a namespace + CreateNamespaceRequest createReq = new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createReq); + + // Describe namespace + DescribeNamespaceRequest descReq = + new DescribeNamespaceRequest().id(Arrays.asList("workspace")); + DescribeNamespaceResponse descResp = namespace.describeNamespace(descReq); + assertNotNull(descResp); + assertNotNull(descResp.getProperties()); + } + + @Test + void testNamespaceExists() { + // Create a namespace + CreateNamespaceRequest createReq = new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createReq); + + // Check existence + NamespaceExistsRequest existsReq = new NamespaceExistsRequest().id(Arrays.asList("workspace")); + assertDoesNotThrow(() -> namespace.namespaceExists(existsReq)); + + // Check non-existent namespace + NamespaceExistsRequest notExistsReq = + new NamespaceExistsRequest().id(Arrays.asList("nonexistent")); + assertThrows(RuntimeException.class, () -> namespace.namespaceExists(notExistsReq)); + } + + @Test + void testDropNamespace() { + // Create a namespace + CreateNamespaceRequest createReq = new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createReq); + + // Drop namespace + DropNamespaceRequest dropReq = new DropNamespaceRequest().id(Arrays.asList("workspace")); + DropNamespaceResponse dropResp = namespace.dropNamespace(dropReq); + assertNotNull(dropResp); + + // Verify it's gone + NamespaceExistsRequest existsReq = new NamespaceExistsRequest().id(Arrays.asList("workspace")); + assertThrows(RuntimeException.class, () -> namespace.namespaceExists(existsReq)); + } + + @Test + void testCreateTable() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create table with data + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + CreateTableResponse createResp = namespace.createTable(createReq, tableData); + + assertNotNull(createResp); + assertNotNull(createResp.getLocation()); + assertTrue(createResp.getLocation().contains("test_table")); + assertEquals(Long.valueOf(1), createResp.getVersion()); + } + + @Test + void testListTables() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create a table + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + namespace.createTable(createReq, tableData); + + // List tables + ListTablesRequest listReq = new ListTablesRequest().id(Arrays.asList("workspace")); + ListTablesResponse listResp = namespace.listTables(listReq); + + assertNotNull(listResp); + assertNotNull(listResp.getTables()); + assertTrue(listResp.getTables().contains("test_table")); + } + + @Test + void testDescribeTable() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create a table + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + namespace.createTable(createReq, tableData); + + // Describe table + DescribeTableRequest descReq = + new DescribeTableRequest().id(Arrays.asList("workspace", "test_table")); + DescribeTableResponse descResp = namespace.describeTable(descReq); + + assertNotNull(descResp); + assertNotNull(descResp.getLocation()); + assertTrue(descResp.getLocation().contains("test_table")); + } + + @Test + void testTableExists() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create a table + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + namespace.createTable(createReq, tableData); + + // Check existence + TableExistsRequest existsReq = + new TableExistsRequest().id(Arrays.asList("workspace", "test_table")); + assertDoesNotThrow(() -> namespace.tableExists(existsReq)); + + // Check non-existent table + TableExistsRequest notExistsReq = + new TableExistsRequest().id(Arrays.asList("workspace", "nonexistent")); + assertThrows(RuntimeException.class, () -> namespace.tableExists(notExistsReq)); + } + + @Test + void testDropTable() throws Exception { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create a table + byte[] tableData = createTestTableData(); + CreateTableRequest createReq = + new CreateTableRequest().id(Arrays.asList("workspace", "test_table")); + namespace.createTable(createReq, tableData); + + // Drop table + DropTableRequest dropReq = new DropTableRequest().id(Arrays.asList("workspace", "test_table")); + DropTableResponse dropResp = namespace.dropTable(dropReq); + assertNotNull(dropResp); + + // Verify it's gone + TableExistsRequest existsReq = + new TableExistsRequest().id(Arrays.asList("workspace", "test_table")); + assertThrows(RuntimeException.class, () -> namespace.tableExists(existsReq)); + } + + @Test + void testCreateEmptyTable() { + // Create parent namespace + CreateNamespaceRequest createNsReq = + new CreateNamespaceRequest().id(Arrays.asList("workspace")); + namespace.createNamespace(createNsReq); + + // Create empty table (metadata-only operation) + CreateEmptyTableRequest createReq = + new CreateEmptyTableRequest().id(Arrays.asList("workspace", "empty_table")); + + CreateEmptyTableResponse createResp = namespace.createEmptyTable(createReq); + + assertNotNull(createResp); + assertNotNull(createResp.getLocation()); + } +}