use std::collections::BTreeSet;
use anyhow::Result;
use mnem_core::codec::from_canonical_bytes;
use mnem_core::objects::AdjacencyBucket;
use mnem_core::prolly::tree::{TreeChunk, load_tree_chunk};
use super::super::index_set;
use crate::server::Server;
pub(in crate::tools) fn schema(server: &mut Server) -> Result<String> {
let repo = server.load_repo()?;
let Some(set) = index_set(server, &repo)? else {
return Ok("schema: <no IndexSet on current commit>\n".to_string());
};
let mut out = String::new();
out.push_str("mnem schema (from current IndexSet)\n");
out.push_str(" node labels:\n");
if set.nodes_by_label.is_empty() {
out.push_str(" <none>\n");
} else {
for label in set.nodes_by_label.keys() {
let props: Vec<&String> = set
.nodes_by_prop
.get(label)
.map(|m| m.keys().collect::<Vec<_>>())
.unwrap_or_default();
out.push_str(&format!(
" - {label} [indexed props: {}]\n",
if props.is_empty() {
"none".into()
} else {
props
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
}
));
}
}
out.push_str(" edge types:\n");
match collect_edge_types(server, &set.outgoing) {
Ok(Some(etypes)) if !etypes.is_empty() => {
for etype in &etypes {
out.push_str(&format!(" - {etype}\n"));
}
}
Ok(Some(_)) => {
out.push_str(" <none - index present but contains no edges>\n");
}
_ => {
out.push_str(" (index not built - run `mnem embed --reindex` to populate)\n");
}
}
out.push_str(" outgoing-adjacency index: ");
out.push_str(if set.outgoing.is_some() {
"present\n"
} else {
"absent\n"
});
out.push_str(" incoming-adjacency index: ");
out.push_str(if set.incoming.is_some() {
"present\n"
} else {
"absent\n"
});
Ok(out)
}
fn collect_edge_types(
server: &mut Server,
outgoing_cid: &Option<mnem_core::id::Cid>,
) -> Result<Option<BTreeSet<String>>> {
let bs = server.stores()?.0;
collect_edge_types_from_bs(bs.as_ref(), outgoing_cid)
}
fn collect_edge_types_from_bs(
bs: &dyn mnem_core::store::Blockstore,
outgoing_cid: &Option<mnem_core::id::Cid>,
) -> Result<Option<BTreeSet<String>>> {
let root_cid = match outgoing_cid {
Some(c) => c.clone(),
None => return Ok(None),
};
let mut etypes: BTreeSet<String> = BTreeSet::new();
let mut stack = vec![root_cid];
while let Some(cid) = stack.pop() {
let chunk = load_tree_chunk(bs, &cid)?;
match chunk {
TreeChunk::Internal(internal) => {
stack.extend(internal.children);
}
TreeChunk::Leaf(leaf) => {
for (_key, bucket_cid) in &leaf.entries {
let bucket_bytes = bs.get(bucket_cid)?.ok_or_else(|| {
anyhow::anyhow!("AdjacencyBucket block {bucket_cid} missing")
})?;
let bucket: AdjacencyBucket = from_canonical_bytes(&bucket_bytes)?;
for entry in &bucket.edges {
etypes.insert(entry.label.clone());
}
}
}
}
}
Ok(Some(etypes))
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use mnem_core::codec::hash_to_cid;
use mnem_core::id::NodeId;
use mnem_core::objects::AdjacencyBucket;
use mnem_core::prolly::{ProllyKey, build_tree};
use mnem_core::store::{Blockstore, MemoryBlockstore};
use super::collect_edge_types_from_bs;
#[test]
fn collect_edge_types_from_bs_empty_bucket_returns_some_empty_set() {
let bs = MemoryBlockstore::default();
let empty_bucket = AdjacencyBucket {
edges: vec![],
extra: BTreeMap::new(),
};
let (bucket_bytes, bucket_cid) = hash_to_cid(&empty_bucket).expect("hash bucket");
bs.put_trusted(bucket_cid.clone(), bucket_bytes)
.expect("put bucket");
let src_id = NodeId::from_bytes_raw([0u8; 16]);
let mut entries: BTreeMap<ProllyKey, _> = BTreeMap::new();
entries.insert(ProllyKey::from(src_id), bucket_cid);
let tree_cid = build_tree(&bs, entries).expect("build tree");
let result =
collect_edge_types_from_bs(&bs, &Some(tree_cid)).expect("collect must not error");
match result {
Some(set) => assert!(
set.is_empty(),
"expected empty edge-type set for empty-bucket tree, got: {set:?}"
),
None => panic!("expected Some(empty_set) but got None"),
}
}
#[test]
fn collect_edge_types_from_bs_none_cid_returns_ok_none() {
let bs = MemoryBlockstore::default();
let result = collect_edge_types_from_bs(&bs, &None).expect("must not error");
assert!(result.is_none(), "expected None for absent outgoing CID");
}
}