use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use nodedb_mem::{EngineId, ScopedMemory};
use nodedb_types::Namespace;
use nodedb_types::id::EdgeId;
use nodedb_types::{DatabaseId, TenantId};
use crate::engine::graph::history::SYSTEM_TO_CURRENT;
use crate::engine::graph::index::CsrIndex;
use crate::nodedb::lock_ext::LockExt;
use crate::storage::engine::StorageEngine;
use crate::nodedb::core::types::NodeDbLite;
const META_GRAPH_BITEMPORAL_PREFIX: &str = "graph_bitemporal:";
const GRAPH_HISTORY_TRAILER_LEN: usize = 8;
impl<S: StorageEngine> NodeDbLite<S> {
pub(crate) async fn rebuild_graph_indices(&self) {
let mut indexed: HashSet<(String, String, String, String)> = HashSet::new();
let memory = ScopedMemory::new(
Arc::clone(&self.governor),
DatabaseId::DEFAULT,
TenantId::new(0),
EngineId::Graph,
);
{
let crdt = self.crdt.lock_or_recover();
let collections = crdt.collection_names();
let mut csr_map = self.csr.lock_or_recover();
for crdt_coll in &collections {
let Some(collection) = crdt_coll.strip_prefix("__edges__") else {
continue;
};
let ids = crdt.list_ids(crdt_coll);
if ids.is_empty() {
continue;
}
let csr = csr_map
.entry(collection.to_string())
.or_insert_with(|| CsrIndex::new(memory.clone()));
for id in &ids {
if let Some(loro_val) = crdt.read(crdt_coll, id) {
let doc = crate::nodedb::convert::loro_value_to_document(id, &loro_val);
let src = doc.fields.get("src").and_then(|v| match v {
nodedb_types::Value::String(s) => Some(s.clone()),
_ => None,
});
let dst = doc.fields.get("dst").and_then(|v| match v {
nodedb_types::Value::String(s) => Some(s.clone()),
_ => None,
});
let label = doc.fields.get("label").and_then(|v| match v {
nodedb_types::Value::String(s) => Some(s.clone()),
_ => None,
});
if let (Some(src), Some(dst), Some(label)) = (src, dst, label) {
let _ = csr.add_edge(&src, &label, &dst);
indexed.insert((collection.to_string(), src, label, dst));
}
}
}
}
}
let graph_entries = match self.storage.scan_prefix(Namespace::Graph, b"").await {
Ok(e) => e,
Err(e) => {
tracing::warn!(
error = %e,
"graph rebuild: failed to scan Namespace::Graph; skipping KV pass"
);
Vec::new()
}
};
{
let mut csr_map = self.csr.lock_or_recover();
for (key, _value) in &graph_entries {
let parts: Vec<&[u8]> = key.splitn(4, |&b| b == 0).collect();
if parts.len() != 4 {
continue;
}
let Ok(collection) = std::str::from_utf8(parts[0]) else {
continue;
};
let Ok(src) = std::str::from_utf8(parts[1]) else {
continue;
};
let Ok(label) = std::str::from_utf8(parts[2]) else {
continue;
};
let Ok(dst) = std::str::from_utf8(parts[3]) else {
continue;
};
let tuple = (
collection.to_string(),
src.to_string(),
label.to_string(),
dst.to_string(),
);
if indexed.contains(&tuple) {
continue;
}
let csr = csr_map
.entry(collection.to_string())
.or_insert_with(|| CsrIndex::new(memory.clone()));
let _ = csr.add_edge(src, label, dst);
indexed.insert(tuple);
}
}
let bitemporal_collections = self.list_bitemporal_graph_collections().await;
for collection in &bitemporal_collections {
let prefix = {
let mut p = collection.as_bytes().to_vec();
p.push(b':');
p
};
let entries = match self
.storage
.scan_prefix(Namespace::GraphHistory, &prefix)
.await
{
Ok(e) => e,
Err(e) => {
tracing::warn!(
collection = %collection,
error = %e,
"graph rebuild: failed to scan GraphHistory; skipping collection"
);
continue;
}
};
let mut edge_latest_system_to: HashMap<String, u64> = HashMap::new();
for (key, value) in &entries {
if value.len() < GRAPH_HISTORY_TRAILER_LEN {
continue;
}
if key.len() < prefix.len() + 1 + GRAPH_HISTORY_TRAILER_LEN {
continue;
}
let edge_key_bytes = &key[prefix.len()..key.len() - 1 - GRAPH_HISTORY_TRAILER_LEN];
let Ok(edge_key) = std::str::from_utf8(edge_key_bytes) else {
continue;
};
let start = value.len() - GRAPH_HISTORY_TRAILER_LEN;
let system_to = u64::from_be_bytes(value[start..].try_into().unwrap_or([0; 8]));
let entry = edge_latest_system_to
.entry(edge_key.to_string())
.or_insert(0);
if system_to > *entry {
*entry = system_to;
}
}
let memory = self.memory_for(nodedb_mem::EngineId::Graph);
let mut csr_map = self.csr.lock_or_recover();
let csr = csr_map
.entry(collection.to_string())
.or_insert_with(|| CsrIndex::new(memory));
for (edge_key, system_to) in &edge_latest_system_to {
if *system_to != SYSTEM_TO_CURRENT {
continue;
}
let Ok(edge_id) = edge_key.parse::<EdgeId>() else {
continue;
};
let src = edge_id.src.as_str();
let label = &edge_id.label;
let dst = edge_id.dst.as_str();
let tuple = (
collection.to_string(),
src.to_string(),
label.to_string(),
dst.to_string(),
);
if indexed.contains(&tuple) {
continue;
}
let _ = csr.add_edge(src, label, dst);
indexed.insert(tuple);
}
}
}
async fn list_bitemporal_graph_collections(&self) -> Vec<String> {
let prefix = META_GRAPH_BITEMPORAL_PREFIX.as_bytes();
let entries = match self.storage.scan_prefix(Namespace::Meta, prefix).await {
Ok(e) => e,
Err(e) => {
tracing::warn!(
error = %e,
"graph rebuild: failed to scan bitemporal graph meta keys"
);
return Vec::new();
}
};
entries
.into_iter()
.filter_map(|(key, value)| {
if value.first().copied() != Some(1) {
return None;
}
let key_str = std::str::from_utf8(&key).ok()?;
key_str
.strip_prefix(META_GRAPH_BITEMPORAL_PREFIX)
.map(str::to_owned)
})
.collect()
}
}