use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use crate::catalog::{Catalog, CollectionKind, LocalRegistry, TableDescriptor};
use crate::lance_storage_graph::LanceStorageGraph;
use crate::metadata::GeneMetadata;
use crate::tests::tmp_dir;
use crate::traits::metadata::Metadata;
fn registry_with(base: &Path, name_id: &str) -> LocalRegistry {
let metadata = GeneMetadata::new(name_id)
.with_base(base.to_path_buf())
.with_dimensions(100, 50);
let raw_info = metadata
.new_fileinfo("rawinput", "dense", (100, 50), None, None)
.expect("dense filetype");
let adj_info = metadata
.new_fileinfo("adjacency", "sparse", (100, 100), Some(500), None)
.expect("sparse filetype");
let metadata = metadata
.add_file("rawinput", raw_info)
.add_file("adjacency", adj_info);
LocalRegistry::new(metadata, base.to_path_buf())
}
#[tokio::test(flavor = "multi_thread")]
async fn catalog_lists_and_describes_tables() {
let base = tmp_dir("catalog_m_c1").await;
let name_id = "catalog_test";
let registry = registry_with(&base, name_id);
let tables = registry.list_tables().expect("list");
let names: Vec<_> = tables.iter().map(|t| t.name.clone()).collect();
assert_eq!(names, vec!["adjacency", "rawinput"], "sorted by name");
assert!(tables.iter().all(|t| t.format == "lance"));
let adj = registry.describe_table("adjacency").expect("describe");
assert_eq!(
adj.base_location,
base.join(format!("{name_id}_adjacency.lance"))
);
assert_eq!(
adj.properties.get("filetype").map(String::as_str),
Some("sparse")
);
assert_eq!(adj.properties.get("nnz").map(String::as_str), Some("500"));
assert!(registry.table_exists("rawinput").unwrap());
assert!(!registry.table_exists("missing").unwrap());
assert!(registry.describe_table("missing").is_err());
}
#[tokio::test(flavor = "multi_thread")]
async fn catalog_register_and_deregister_roundtrip() {
let base = tmp_dir("catalog_m_c1").await;
let mut registry = registry_with(&base, "catalog_test");
registry
.register_table(TableDescriptor {
name: "norms".to_string(),
format: "lance".to_string(),
base_location: base.join("catalog_test_norms.lance"),
kind: CollectionKind::VectorSpace,
properties: HashMap::from([
("filetype".to_string(), "vector".to_string()),
("rows".to_string(), "100".to_string()),
("cols".to_string(), "1".to_string()),
])
.into_iter()
.collect(),
})
.expect("register");
assert!(registry.table_exists("norms").unwrap());
let norms = registry.describe_table("norms").unwrap();
assert_eq!(norms.base_location, base.join("catalog_test_norms.lance"));
assert_eq!(
norms.properties.get("rows").map(String::as_str),
Some("100")
);
registry.deregister_table("norms").expect("deregister");
assert!(!registry.table_exists("norms").unwrap());
assert!(registry.deregister_table("norms").is_err());
let metadata = registry.into_metadata();
assert!(!metadata.files.contains_key("norms"));
assert!(metadata.files.contains_key("rawinput"));
}
#[tokio::test(flavor = "multi_thread")]
async fn catalog_rejects_non_lance_format_and_bad_properties() {
let base = tmp_dir("catalog_m_c1").await;
let mut registry = registry_with(&base, "catalog_test");
let err = registry
.register_table(TableDescriptor {
name: "delta_thing".to_string(),
format: "delta".to_string(),
base_location: base.join("delta_thing"),
kind: CollectionKind::Table,
properties: Default::default(),
})
.unwrap_err();
assert!(matches!(err, crate::StorageError::UnsupportedFormat(_)));
let err = registry
.register_table(TableDescriptor {
name: "bad_props".to_string(),
format: "lance".to_string(),
base_location: base.join("bad_props.lance"),
kind: CollectionKind::Table,
properties: HashMap::from([("rows".to_string(), "many".to_string())])
.into_iter()
.collect(),
})
.unwrap_err();
assert!(matches!(err, crate::StorageError::Invalid(_)));
}
#[tokio::test(flavor = "multi_thread")]
async fn catalog_derives_kinds_for_legacy_keys() {
let base = tmp_dir("catalog_m_c1").await;
let registry = registry_with(&base, "catalog_test");
let raw = registry.describe_table("rawinput").unwrap();
assert_eq!(raw.kind, CollectionKind::VectorSpace);
assert_eq!(
raw.properties.get("kind").map(String::as_str),
Some("vector-space")
);
let adj = registry.describe_table("adjacency").unwrap();
assert_eq!(adj.kind, CollectionKind::Graph);
assert_eq!(
adj.properties.get("kind").map(String::as_str),
Some("graph")
);
}
#[tokio::test(flavor = "multi_thread")]
async fn catalog_kinds_and_user_properties_roundtrip() {
let base = tmp_dir("catalog_m_c1").await;
let mut registry = registry_with(&base, "catalog_test");
registry
.register_table(TableDescriptor {
name: "edges".to_string(),
format: "lance".to_string(),
base_location: base.join("catalog_test_edges.lance"),
kind: CollectionKind::Graph,
properties: BTreeMap::from([
("filetype".to_string(), "graph".to_string()),
("rows".to_string(), "7".to_string()),
("cols".to_string(), "7".to_string()),
("nnz".to_string(), "12".to_string()),
("node_id_width".to_string(), "u32".to_string()),
]),
})
.expect("register graph collection");
let desc = registry.describe_table("edges").unwrap();
assert_eq!(desc.kind, CollectionKind::Graph);
assert_eq!(
desc.properties.get("node_id_width").map(String::as_str),
Some("u32"),
"user properties pass through"
);
let metadata = registry.into_metadata();
let info = metadata.files.get("edges").unwrap();
assert_eq!(info.kind, Some(CollectionKind::Graph));
assert_eq!(
info.properties.get("node_id_width").map(String::as_str),
Some("u32")
);
}
#[tokio::test(flavor = "multi_thread")]
async fn catalog_legacy_metadata_json_still_loads() {
let legacy_json = r#"{
"name_id": "legacy_instance",
"nrows": 10,
"ncols": 5,
"base": "/tmp/legacy_instance",
"files": {
"rawinput": {
"filename": "legacy_instance_rawinput.lance",
"filetype": "dense",
"storage_format": "lance fixed-row",
"rows": 10,
"cols": 5,
"nnz": null,
"size_bytes": null
}
},
"created_at": "2026-01-01T00:00:00Z"
}"#;
let metadata: GeneMetadata = serde_json::from_str(legacy_json).expect("legacy JSON loads");
assert!(metadata.files["rawinput"].kind.is_none());
assert!(metadata.files["rawinput"].properties.is_empty());
let registry = LocalRegistry::new(metadata, std::path::PathBuf::from("/tmp/legacy_instance"));
let desc = registry.describe_table("rawinput").unwrap();
assert_eq!(desc.kind, CollectionKind::VectorSpace);
}
#[tokio::test(flavor = "multi_thread")]
async fn catalog_describe_vector_space_links_graph() {
let base = tmp_dir("catalog_m_c1").await;
let mut registry = registry_with(&base, "catalog_test");
registry
.register_table(TableDescriptor {
name: "embeddings".to_string(),
format: "lance".to_string(),
base_location: base.join("catalog_test_embeddings.lance"),
kind: CollectionKind::VectorSpace,
properties: BTreeMap::from([
("filetype".to_string(), "vectors".to_string()),
("rows".to_string(), "100".to_string()),
("cols".to_string(), "50".to_string()),
("graph".to_string(), "adjacency".to_string()),
]),
})
.expect("register vector space");
let vs = registry.describe_vector_space("embeddings").unwrap();
assert_eq!(vs.vectors.name, "embeddings");
let graph = vs.graph.expect("linked graph");
assert_eq!(graph.name, "adjacency");
assert_eq!(graph.kind, CollectionKind::Graph);
registry
.register_table(TableDescriptor {
name: "bare".to_string(),
format: "lance".to_string(),
base_location: base.join("catalog_test_bare.lance"),
kind: CollectionKind::VectorSpace,
properties: BTreeMap::from([
("filetype".to_string(), "vectors".to_string()),
("rows".to_string(), "4".to_string()),
("cols".to_string(), "4".to_string()),
]),
})
.expect("register bare vector space");
assert!(
registry
.describe_vector_space("bare")
.unwrap()
.graph
.is_none()
);
let err = registry.describe_vector_space("adjacency").unwrap_err();
assert!(matches!(err, crate::StorageError::Invalid(_)));
assert!(registry.describe_vector_space("missing").is_err());
registry
.register_table(TableDescriptor {
name: "dangling".to_string(),
format: "lance".to_string(),
base_location: base.join("catalog_test_dangling.lance"),
kind: CollectionKind::VectorSpace,
properties: BTreeMap::from([
("filetype".to_string(), "vectors".to_string()),
("graph".to_string(), "no_such_graph".to_string()),
]),
})
.expect("register dangling vector space");
let err = registry.describe_vector_space("dangling").unwrap_err();
assert!(matches!(err, crate::StorageError::Invalid(_)));
}
#[tokio::test(flavor = "multi_thread")]
async fn catalog_locations_match_storage_layout() {
let base = tmp_dir("catalog_m_c1_storage").await;
let name_id = "catalog_storage";
let storage = LanceStorageGraph::new(base.to_string_lossy().to_string(), name_id.to_string());
let metadata = GeneMetadata::seed_metadata(name_id, 10, 5, &storage)
.await
.expect("seed");
let registry = LocalRegistry::new(metadata, base.to_path_buf());
assert!(
registry.list_tables().unwrap().is_empty(),
"fresh dataset has no tables"
);
assert_eq!(
registry.describe_table("rawinput").unwrap_err().to_string(),
"Invalid data: table 'rawinput' is not registered"
);
}