use std::collections::HashMap;
use std::sync::Arc;
use anyhow::{Context, Result};
use lance::Dataset;
use lance::dataset::builder::DatasetBuilder;
use lance::dataset::{WriteMode, WriteParams};
use lance::io::{ObjectStore, ObjectStoreParams, ObjectStoreRegistry, StorageOptionsAccessor};
use object_store::path::Path as ObjectPath;
const LANCE_EXTENSION: &str = "lance";
#[derive(Clone)]
pub struct LanceDirectory {
base_uri: String,
store: Arc<ObjectStore>,
base_path: ObjectPath,
storage_options: Option<HashMap<String, String>>,
}
impl std::fmt::Debug for LanceDirectory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LanceDirectory")
.field("base_uri", &self.base_uri)
.field("base_path", &self.base_path)
.finish_non_exhaustive()
}
}
impl LanceDirectory {
pub async fn connect(
base_uri: &str,
storage_options: Option<HashMap<String, String>>,
) -> Result<Self> {
let params = ObjectStoreParams {
storage_options_accessor: storage_options
.clone()
.map(|opts| Arc::new(StorageOptionsAccessor::with_static_options(opts))),
..Default::default()
};
let registry = Arc::new(ObjectStoreRegistry::default());
let (store, base_path) = ObjectStore::from_uri_and_params(registry, base_uri, ¶ms)
.await
.with_context(|| format!("open object store for '{base_uri}'"))?;
Ok(Self {
base_uri: base_uri.to_string(),
store,
base_path,
storage_options,
})
}
#[must_use]
pub fn base_uri(&self) -> &str {
&self.base_uri
}
#[must_use]
pub fn dataset_uri(&self, table: &str) -> String {
if self.base_uri.ends_with('/') {
format!("{}{table}.{LANCE_EXTENSION}", self.base_uri)
} else {
format!("{}/{table}.{LANCE_EXTENSION}", self.base_uri)
}
}
pub async fn table_names(&self) -> Result<Vec<String>> {
let entries = match self.store.read_dir(self.base_path.clone()).await {
Ok(entries) => entries,
Err(e) if is_not_found(&e) => return Ok(Vec::new()),
Err(e) => {
return Err(anyhow::anyhow!(e))
.with_context(|| format!("list tables under '{}'", self.base_uri));
}
};
let mut names: Vec<String> = entries
.iter()
.filter_map(|entry| entry.strip_suffix(&format!(".{LANCE_EXTENSION}")))
.map(String::from)
.collect();
names.sort();
Ok(names)
}
pub async fn open(&self, table: &str) -> Result<Dataset> {
let uri = self.dataset_uri(table);
self.builder(&uri)
.load()
.await
.with_context(|| format!("open table '{table}' at '{uri}'"))
}
pub async fn open_at_version(&self, table: &str, version: u64) -> Result<Dataset> {
let uri = self.dataset_uri(table);
self.builder(&uri)
.with_version(version)
.load()
.await
.with_context(|| format!("open table '{table}' at version {version} ('{uri}')"))
}
fn builder(&self, uri: &str) -> DatasetBuilder {
let builder = DatasetBuilder::from_uri(uri);
match &self.storage_options {
Some(opts) => builder.with_storage_options(opts.clone()),
None => builder,
}
}
#[must_use]
pub fn storage_options(&self) -> Option<&HashMap<String, String>> {
self.storage_options.as_ref()
}
#[must_use]
pub fn write_params(&self, mode: WriteMode) -> WriteParams {
WriteParams {
mode,
store_params: self.storage_options.clone().map(|opts| ObjectStoreParams {
storage_options_accessor: Some(Arc::new(
StorageOptionsAccessor::with_static_options(opts),
)),
..Default::default()
}),
..Default::default()
}
}
pub async fn remove_table(&self, table: &str) -> Result<()> {
let path = self
.base_path
.clone()
.join(format!("{table}.{LANCE_EXTENSION}"));
self.store
.remove_dir_all(path)
.await
.map_err(|e| anyhow::anyhow!(e))
.with_context(|| format!("drop table '{table}'"))
}
}
fn is_not_found(e: &lance::Error) -> bool {
matches!(e, lance::Error::NotFound { .. })
|| matches!(e, lance::Error::DatasetNotFound { .. })
|| e.to_string().contains("No such file or directory")
|| e.to_string().contains("NotFound")
}