use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::StorageError;
use crate::StorageResult;
use crate::graph::GraphSource;
pub use crate::metadata::CollectionKind;
use crate::metadata::{FileInfo, GeneMetadata};
pub(crate) const RESERVED_PROPERTIES: [&str; 6] = [
"filetype",
"storage_format",
"rows",
"cols",
"nnz",
"size_bytes",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableDescriptor {
pub name: String,
pub format: String,
pub base_location: PathBuf,
pub kind: CollectionKind,
pub properties: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphStaleness {
Unlinked,
Fresh,
Stale,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VectorSpaceDescriptor {
pub vectors: TableDescriptor,
pub graph: Option<TableDescriptor>,
pub graph_staleness: GraphStaleness,
}
pub trait Catalog {
fn list_tables(&self) -> StorageResult<Vec<TableDescriptor>>;
fn table_exists(&self, name: &str) -> StorageResult<bool>;
fn describe_table(&self, name: &str) -> StorageResult<TableDescriptor>;
fn register_table(&mut self, table: TableDescriptor) -> StorageResult<()>;
fn deregister_table(&mut self, name: &str) -> StorageResult<()>;
fn describe_vector_space(&self, name: &str) -> StorageResult<VectorSpaceDescriptor> {
let vectors = self.describe_table(name)?;
if vectors.kind != CollectionKind::VectorSpace {
return Err(StorageError::Invalid(format!(
"collection '{name}' has kind '{}', not a vector space",
vectors.kind.as_str()
)));
}
let graph = match vectors.properties.get("graph") {
Some(graph_name) => Some(self.describe_table(graph_name)?),
None => None,
};
let graph_staleness = match &graph {
None => GraphStaleness::Unlinked,
Some(g) => {
let rows = vectors
.properties
.get("rows")
.and_then(|v| v.parse::<usize>().ok());
match (g.properties.get("source"), g.properties.get("source_rows")) {
(Some(source), Some(source_rows)) => {
if source == &vectors.name && source_rows.parse::<usize>().ok() == rows {
GraphStaleness::Fresh
} else {
GraphStaleness::Stale
}
}
_ => {
match (
g.properties
.get("num_nodes")
.and_then(|v| v.parse::<usize>().ok()),
rows,
) {
(Some(num_nodes), Some(rows)) if num_nodes != rows => {
GraphStaleness::Stale
}
_ => GraphStaleness::Unknown,
}
}
}
}
};
Ok(VectorSpaceDescriptor {
vectors,
graph,
graph_staleness,
})
}
fn graph_source(&self, name: &str) -> StorageResult<GraphSource> {
let table = self.describe_table(name)?;
if table.kind != CollectionKind::VectorSpace {
return Err(StorageError::Invalid(format!(
"graph source '{name}' has kind '{}', not a vector space",
table.kind.as_str()
)));
}
let rows = table
.properties
.get("rows")
.and_then(|v| v.parse::<usize>().ok())
.ok_or_else(|| {
StorageError::Invalid(format!(
"graph source '{name}' carries no usable 'rows' fact"
))
})?;
Ok(GraphSource {
name: table.name,
rows,
})
}
}
#[derive(Debug, Clone)]
pub struct LocalRegistry {
metadata: GeneMetadata,
base: PathBuf,
}
impl LocalRegistry {
pub fn new(metadata: GeneMetadata, base: PathBuf) -> Self {
Self { metadata, base }
}
pub fn metadata(&self) -> &GeneMetadata {
&self.metadata
}
pub fn into_metadata(self) -> GeneMetadata {
self.metadata
}
fn descriptor(&self, key: &str, info: &FileInfo) -> TableDescriptor {
let kind = info
.kind
.or_else(|| CollectionKind::for_filetype(&info.filetype))
.unwrap_or(CollectionKind::Table);
let mut properties = BTreeMap::new();
properties.insert("filetype".to_string(), info.filetype.clone());
properties.insert("storage_format".to_string(), info.storage_format.clone());
properties.insert("rows".to_string(), info.rows.to_string());
properties.insert("cols".to_string(), info.cols.to_string());
if let Some(nnz) = info.nnz {
properties.insert("nnz".to_string(), nnz.to_string());
}
if let Some(size) = info.size_bytes {
properties.insert("size_bytes".to_string(), size.to_string());
}
for (k, v) in &info.properties {
properties.insert(k.clone(), v.clone());
}
properties.insert("kind".to_string(), kind.as_str().to_string());
let format = info
.storage_format
.split(' ')
.next()
.unwrap_or_default()
.to_string();
TableDescriptor {
name: key.to_string(),
format,
base_location: self.base.join(&info.filename),
kind,
properties,
}
}
}
const SUPPORTED_TABLE_FORMATS: [&str; 3] = ["lance", "arrow-ipc", "arrow-ipc-stream"];
impl Catalog for LocalRegistry {
fn list_tables(&self) -> StorageResult<Vec<TableDescriptor>> {
let mut tables: Vec<TableDescriptor> = self
.metadata
.files
.iter()
.map(|(key, info)| self.descriptor(key, info))
.collect();
tables.sort_by(|a, b| a.name.cmp(&b.name));
Ok(tables)
}
fn table_exists(&self, name: &str) -> StorageResult<bool> {
Ok(self.metadata.files.contains_key(name))
}
fn describe_table(&self, name: &str) -> StorageResult<TableDescriptor> {
let info =
self.metadata.files.get(name).ok_or_else(|| {
StorageError::Invalid(format!("table '{name}' is not registered"))
})?;
Ok(self.descriptor(name, info))
}
fn register_table(&mut self, table: TableDescriptor) -> StorageResult<()> {
if !SUPPORTED_TABLE_FORMATS.contains(&table.format.as_str()) {
return Err(StorageError::UnsupportedFormat(format!(
"unsupported table format '{}'",
table.format
)));
}
let prop = |key: &str| table.properties.get(key).cloned();
let parse = |key: &str| -> StorageResult<Option<usize>> {
prop(key)
.map(|v| {
v.parse::<usize>().map_err(|_| {
StorageError::Invalid(format!(
"property '{key}' = '{v}' is not a valid integer"
))
})
})
.transpose()
};
let kind = match prop("kind") {
Some(k) => {
let prop_kind = CollectionKind::parse(&k)?;
if prop_kind != table.kind {
return Err(StorageError::Invalid(format!(
"kind mismatch for table '{}': typed field is '{}', \
'kind' property is '{}'",
table.name,
table.kind.as_str(),
prop_kind.as_str()
)));
}
Some(prop_kind)
}
None => Some(table.kind),
};
let user_properties: BTreeMap<String, String> = table
.properties
.iter()
.filter(|(k, _)| !RESERVED_PROPERTIES.contains(&k.as_str()) && k.as_str() != "kind")
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let mut info = FileInfo::new(
table
.base_location
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| {
crate::generations::artifact_file_name(&self.metadata.name_id, &table.name)
}),
prop("filetype").as_deref().unwrap_or("vector"),
(parse("rows")?.unwrap_or(0), parse("cols")?.unwrap_or(0)),
parse("nnz")?,
None,
)?;
info.kind = kind;
if table.format != "lance" {
info.storage_format = table.format.clone();
}
info.properties = user_properties;
self.metadata.files.insert(table.name, info);
Ok(())
}
fn deregister_table(&mut self, name: &str) -> StorageResult<()> {
if self.metadata.files.remove(name).is_none() {
return Err(StorageError::Invalid(format!(
"table '{name}' is not registered"
)));
}
Ok(())
}
}