use std::collections::BTreeMap;
use std::path::Path;
use arrow::datatypes::Schema as ArrowSchema;
use crate::catalog::TableDescriptor;
use crate::metadata::{CollectionKind, FileInfo};
use crate::{StorageError, StorageResult};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DatasetFacts {
pub kind: Option<String>,
pub rows: Option<u64>,
pub cols: Option<u64>,
pub nnz: Option<u64>,
pub num_nodes: Option<u64>,
pub weight_type: Option<String>,
pub node_id_width: Option<String>,
pub weighted: Option<String>,
}
impl DatasetFacts {
pub fn from_schema(schema: &ArrowSchema) -> Self {
let md = schema.metadata();
let num = |key: &str| md.get(key).and_then(|v| v.parse::<u64>().ok());
Self {
kind: md.get("kind").cloned(),
rows: num("rows"),
cols: num("cols"),
nnz: num("nnz"),
num_nodes: num("num_nodes"),
weight_type: md.get("weight_type").cloned(),
node_id_width: md.get("node_id_width").cloned(),
weighted: md.get("weighted").cloned(),
}
}
pub fn from_file_info(info: &FileInfo) -> Self {
Self::from_registry_parts(
&info.filetype,
info.kind,
info.rows,
info.cols,
info.nnz,
&info.properties,
)
}
pub fn from_descriptor(descriptor: &TableDescriptor) -> Self {
let num = |key: &str| {
descriptor
.properties
.get(key)
.and_then(|v| v.parse::<u64>().ok())
};
let rows = num("rows").map(|v| v as usize);
let cols = num("cols").map(|v| v as usize);
let nnz = num("nnz").map(|v| v as usize);
Self::from_registry_parts(
&descriptor.properties["filetype"],
Some(descriptor.kind),
rows.unwrap_or(0),
cols.unwrap_or(0),
nnz,
&descriptor.properties,
)
}
fn from_registry_parts(
filetype: &str,
explicit_kind: Option<CollectionKind>,
rows: usize,
cols: usize,
nnz: Option<usize>,
properties: &BTreeMap<String, String>,
) -> Self {
let kind = match filetype {
"vectors" | "vector" => Some(
explicit_kind
.unwrap_or(CollectionKind::VectorSpace)
.as_str()
.to_string(),
),
"graph" => Some(
explicit_kind
.unwrap_or(CollectionKind::Graph)
.as_str()
.to_string(),
),
_ => None,
};
let shape = match filetype {
"sparse" => (Some(rows as u64), Some(cols as u64)),
_ => (None, None),
};
let num = |key: &str| properties.get(key).and_then(|v| v.parse::<u64>().ok());
Self {
kind,
rows: shape.0,
cols: shape.1,
nnz: if filetype == "sparse" {
nnz.map(|n| n as u64)
} else {
None
},
num_nodes: num("num_nodes"),
weight_type: properties.get("weight_type").cloned(),
node_id_width: properties.get("node_id_width").cloned(),
weighted: properties.get("weighted").cloned(),
}
}
}
pub fn verify_dataset_schema(dir: &Path, expected: &DatasetFacts) -> StorageResult<()> {
let schema = super::read_schema(dir)?;
let stamped = DatasetFacts::from_schema(&schema);
verify_facts(dir, expected, &stamped)
}
fn verify_facts(dir: &Path, expected: &DatasetFacts, stamped: &DatasetFacts) -> StorageResult<()> {
for (fact, want, have) in [
("rows", expected.rows, stamped.rows),
("cols", expected.cols, stamped.cols),
("nnz", expected.nnz, stamped.nnz),
("num_nodes", expected.num_nodes, stamped.num_nodes),
] {
let Some(want) = want else { continue };
match have {
None => return Err(missing_stamp(dir, fact, want)),
Some(have) if have != want => {
return Err(StorageError::DimensionMismatch {
expected: format!("{fact}={want}"),
found: format!("{fact}={have}"),
});
}
Some(_) => {}
}
}
for (fact, want, have) in [
("kind", &expected.kind, &stamped.kind),
("weight_type", &expected.weight_type, &stamped.weight_type),
(
"node_id_width",
&expected.node_id_width,
&stamped.node_id_width,
),
("weighted", &expected.weighted, &stamped.weighted),
] {
let Some(want) = want else { continue };
match have {
None => return Err(missing_stamp(dir, fact, want)),
Some(have) if have != want => {
return Err(StorageError::Invalid(format!(
"dataset {dir:?} stamps {fact} {have:?}, registry expects {want:?}"
)));
}
Some(_) => {}
}
}
Ok(())
}
fn missing_stamp(dir: &Path, fact: &str, want: impl std::fmt::Display) -> StorageError {
StorageError::Invalid(format!(
"dataset {dir:?} carries no {fact:?} stamp; registry expects {want}"
))
}