use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fmt,
path::PathBuf,
sync::{Arc, Mutex, OnceLock, Weak},
};
use lance::session::Session;
use lancedb::{
connection::Connection, database::namespace::LanceNamespaceDatabase, embeddings::MemoryRegistry,
};
mod memory;
use memory::LanceMemory;
use crate::databases::shared::DatabaseRuntime;
use crate::{
CommonConfig, DataRequest, DataResponse, IndexRequest, IndexResponse, LanceConfig,
QueryRequest, QueryResponse, StorageError,
};
use super::{data as data_impl, index as index_impl, query as query_impl};
#[derive(Clone)]
pub(crate) struct LanceManager {
connection: Connection,
mutation_lock: Arc<tokio::sync::Mutex<()>>,
runtime: DatabaseRuntime,
config: LanceConfig,
}
impl fmt::Debug for LanceManager {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LanceManager")
.field("mode", &self.backend_mode())
.field("capabilities", &self.capabilities())
.field("config", &self.config)
.finish()
}
}
impl LanceManager {
pub(crate) async fn connect(
common: CommonConfig,
config: LanceConfig,
) -> Result<Self, StorageError> {
use tokio::time::timeout;
let connection = timeout(common.connect_timeout(), Self::connect_native(&config))
.await
.map_err(|_| {
StorageError::backend_op(
"connect",
format!(
"timed out after {} ms while connecting to LanceDB",
common.connect_timeout_ms
),
)
})?
.map_err(|error| StorageError::backend_op("connect", error.to_string()))?;
let mutation_lock = Self::mutation_lock(&config)?;
let capabilities = config.capabilities();
let mode = config.mode_label();
let manager = Self {
connection,
mutation_lock,
runtime: DatabaseRuntime::new(common, capabilities, "LanceDB", mode),
config,
};
manager.enforce_required_capabilities()?;
Ok(manager)
}
async fn connect_native(config: &LanceConfig) -> lancedb::Result<Connection> {
let namespace_session = Arc::new(Session::default());
let table_session = Arc::new(Session::default());
let uri = config.uri_string();
let uri = if uri == "memory://" {
let memory = Arc::new(LanceMemory::default());
namespace_session
.store_registry()
.insert("memory", memory.clone());
table_session.store_registry().insert("memory", memory);
"memory://storage-kit".to_owned()
} else {
std::path::absolute(config.path())
.map_err(|error| lancedb::Error::Runtime {
message: format!("local database path resolution failed: {error}"),
})?
.to_string_lossy()
.into_owned()
};
let namespace_connection = lancedb::connect(&uri)
.session(namespace_session)
.execute()
.await?;
let namespace = namespace_connection.namespace_client().await?;
let (implementation, properties) = namespace_connection.namespace_client_config().await?;
let database = LanceNamespaceDatabase::from_namespace_client(
namespace,
implementation,
properties,
HashMap::new(),
None,
Some(table_session),
HashSet::new(),
);
Ok(Connection::new(
Arc::new(database),
Arc::new(MemoryRegistry::new()),
))
}
}
impl LanceManager {
pub(crate) fn capabilities(&self) -> crate::CapabilitySet {
self.runtime.capabilities()
}
pub(crate) fn backend_mode(&self) -> &'static str {
self.runtime.backend_mode()
}
pub(crate) fn normalize_request<T: crate::core::ApplyDefaultLocator>(&self, request: T) -> T {
self.runtime.normalize_request(request)
}
pub(crate) fn ensure_mutable(&self, operation: &'static str) -> Result<(), StorageError> {
self.runtime.ensure_mutable(operation)
}
pub(crate) fn unsupported(
&self,
capability: &'static str,
context: impl Into<String>,
) -> StorageError {
self.runtime.unsupported(capability, context)
}
pub(crate) fn require_capability(
&self,
capability: crate::CapabilitySet,
label: &'static str,
context: impl Into<String>,
) -> Result<(), StorageError> {
self.runtime.require_capability(capability, label, context)
}
pub(crate) fn map_err(
&self,
operation: &'static str,
error: impl fmt::Display,
) -> StorageError {
self.runtime.map_err(operation, error)
}
fn enforce_required_capabilities(&self) -> Result<(), StorageError> {
self.runtime.enforce_required_capabilities()
}
}
impl LanceManager {
pub(super) fn connection(&self) -> &Connection {
&self.connection
}
pub(super) async fn namespace_exists(
&self,
namespace: &[String],
) -> Result<bool, StorageError> {
for (index, name) in namespace.iter().enumerate() {
if !self.namespace_contains(&namespace[..index], name).await? {
return Ok(false);
}
}
Ok(true)
}
pub(super) async fn ensure_namespace(&self, namespace: &[String]) -> Result<(), StorageError> {
for (index, name) in namespace.iter().enumerate() {
let parent = &namespace[..index];
if self.namespace_contains(parent, name).await? {
continue;
}
let request = serde_json::from_value(serde_json::json!({
"id": &namespace[..=index],
}))
.map_err(|error| StorageError::Serialization(error.to_string()))?;
if let Err(error) = self.connection.create_namespace(request).await
&& !matches!(self.namespace_contains(parent, name).await, Ok(true))
{
return Err(self.map_err("create_namespace", error));
}
}
Ok(())
}
async fn namespace_contains(
&self,
parent: &[String],
name: &str,
) -> Result<bool, StorageError> {
let mut page_token = None;
let mut seen_tokens = BTreeSet::new();
loop {
let request = serde_json::from_value(serde_json::json!({
"id": parent,
"limit": 256,
"page_token": page_token,
}))
.map_err(|error| StorageError::Serialization(error.to_string()))?;
let response = self
.connection
.list_namespaces(request)
.await
.map_err(|error| self.map_err("list_namespaces", error))?;
if response.namespaces.iter().any(|child| child == name) {
return Ok(true);
}
let Some(next) = response.page_token.filter(|token| !token.is_empty()) else {
return Ok(false);
};
if !seen_tokens.insert(next.clone()) {
return Err(StorageError::backend_op(
"list_namespaces",
"LanceDB namespace pagination repeated a continuation token",
));
}
page_token = Some(next);
}
}
pub(super) fn mutation_process_lock(&self) -> Arc<tokio::sync::Mutex<()>> {
Arc::clone(&self.mutation_lock)
}
fn mutation_lock(config: &LanceConfig) -> Result<Arc<tokio::sync::Mutex<()>>, StorageError> {
if config.path() == std::path::Path::new("memory://") {
return Ok(Arc::new(tokio::sync::Mutex::new(())));
}
static LOCKS: OnceLock<Mutex<BTreeMap<PathBuf, Weak<tokio::sync::Mutex<()>>>>> =
OnceLock::new();
let path = std::fs::canonicalize(config.path())
.map_err(|error| StorageError::backend_op("connect", error.to_string()))?;
let mut locks = LOCKS
.get_or_init(|| Mutex::new(BTreeMap::new()))
.lock()
.map_err(|_| {
StorageError::backend_op("connect", "LanceDB mutation lock registry is poisoned")
})?;
locks.retain(|_, lock| lock.strong_count() != 0);
if let Some(lock) = locks.get(&path).and_then(Weak::upgrade) {
return Ok(lock);
}
let lock = Arc::new(tokio::sync::Mutex::new(()));
locks.insert(path, Arc::downgrade(&lock));
Ok(lock)
}
pub(super) fn config(&self) -> &LanceConfig {
&self.config
}
}
impl LanceManager {
pub(super) async fn data(&self, req: DataRequest) -> Result<DataResponse, StorageError> {
data_impl::handle(self, req).await
}
pub(super) async fn query(&self, req: QueryRequest) -> Result<QueryResponse, StorageError> {
query_impl::handle(self, req).await
}
pub(super) async fn index(&self, req: IndexRequest) -> Result<IndexResponse, StorageError> {
index_impl::handle(self, req).await
}
}