use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use nodedb_graph::params::{AlgoParams, GraphAlgorithm};
use nodedb_mem::ScopedMemory;
use nodedb_types::Namespace;
use nodedb_types::id::EdgeId;
use nodedb_types::result::QueryResult;
use nodedb_types::value::Value;
use crate::engine::graph::index::CsrIndex;
use crate::error::LiteError;
use crate::storage::engine::StorageEngine;
use super::algorithms;
const TRAILER_LEN: usize = 8;
fn decode_system_to(value: &[u8]) -> Option<i64> {
if value.len() < TRAILER_LEN {
return None;
}
let bytes: [u8; 8] = value[value.len() - TRAILER_LEN..].try_into().ok()?;
Some(u64::from_be_bytes(bytes) as i64)
}
fn parse_edge_key(edge_key: &str) -> Option<(&str, &str, &str)> {
let arrow_pos = edge_key.find("->")?;
let src = &edge_key[..arrow_pos];
let rest = &edge_key[arrow_pos + 2..];
let colon_pos = rest.rfind(':')?;
let dst = &rest[..colon_pos];
let label = &rest[colon_pos + 1..];
Some((src, dst, label))
}
fn parse_history_edge_key(edge_key: &str) -> Option<(String, String, String)> {
if let Ok(id) = edge_key.parse::<EdgeId>() {
return Some((
id.src.as_str().to_owned(),
id.dst.as_str().to_owned(),
id.label,
));
}
parse_edge_key(edge_key)
.map(|(src, dst, label)| (src.to_owned(), dst.to_owned(), label.to_owned()))
}
async fn build_temporal_snapshot<S: StorageEngine>(
storage: &Arc<S>,
collection: &str,
system_as_of_ms: i64,
memory: &ScopedMemory,
) -> Result<CsrIndex, LiteError> {
let prefix = {
let mut p = collection.as_bytes().to_vec();
p.push(b':');
p
};
let entries = storage
.scan_prefix(Namespace::GraphHistory, &prefix)
.await?;
let mut snapshot = CsrIndex::new(memory.clone());
struct EdgeVersion {
system_from: i64,
src: String,
dst: String,
label: String,
}
let mut edge_map: HashMap<String, EdgeVersion> = HashMap::new();
for (raw_key, raw_val) in &entries {
let key_after_prefix = &raw_key[collection.len() + 1..];
if key_after_prefix.len() < TRAILER_LEN + 2 {
continue;
}
let separator_at = key_after_prefix.len() - TRAILER_LEN - 1;
if key_after_prefix[separator_at] != b':' {
continue;
}
let edge_key_bytes = &key_after_prefix[..separator_at];
let from_bytes: [u8; 8] = key_after_prefix[key_after_prefix.len() - TRAILER_LEN..]
.try_into()
.unwrap_or([0; 8]);
let system_from = u64::from_be_bytes(from_bytes) as i64;
if system_from > system_as_of_ms {
continue; }
let system_to = decode_system_to(raw_val).unwrap_or(i64::MAX);
if system_as_of_ms >= system_to {
continue;
}
let edge_key_str = String::from_utf8_lossy(edge_key_bytes).into_owned();
let keep = edge_map
.get(&edge_key_str)
.is_none_or(|ev| system_from > ev.system_from);
if keep && let Some((src, dst, label)) = parse_history_edge_key(&edge_key_str) {
let ev = EdgeVersion {
system_from,
src,
dst,
label,
};
edge_map.insert(edge_key_str, ev);
}
}
for ev in edge_map.values() {
let _ = snapshot.add_edge(&ev.src, &ev.label, &ev.dst);
}
Ok(snapshot)
}
pub struct TemporalNeighborsParams<'a> {
pub collection: &'a str,
pub node_id: &'a str,
pub edge_label: Option<&'a str>,
pub direction: nodedb_graph::Direction,
pub system_as_of_ms: Option<i64>,
pub valid_at_ms: Option<i64>,
}
pub async fn temporal_neighbors<S: StorageEngine>(
storage: &Arc<S>,
csr_map: &Arc<Mutex<HashMap<String, CsrIndex>>>,
memory: &ScopedMemory,
params: TemporalNeighborsParams<'_>,
) -> Result<QueryResult, LiteError> {
let TemporalNeighborsParams {
collection,
node_id,
edge_label,
direction,
system_as_of_ms,
valid_at_ms,
} = params;
let Some(as_of) = system_as_of_ms else {
return super::traversal::neighbors(
csr_map,
collection,
node_id,
edge_label.as_slice(),
direction,
);
};
let snapshot = build_temporal_snapshot(storage, collection, as_of, memory).await?;
let nbrs = snapshot.neighbors(node_id, edge_label.as_slice(), direction);
let columns = vec!["label".to_string(), "neighbor".to_string()];
let rows = nbrs
.into_iter()
.map(|(lbl, nb)| vec![Value::String(lbl), Value::String(nb)])
.collect();
let _ = valid_at_ms;
Ok(QueryResult {
columns,
rows,
rows_affected: 0,
command: None,
})
}
pub async fn temporal_algorithm<S: StorageEngine>(
storage: &Arc<S>,
csr_map: &Arc<Mutex<HashMap<String, CsrIndex>>>,
memory: &ScopedMemory,
algorithm: GraphAlgorithm,
params: &AlgoParams,
system_as_of_ms: Option<i64>,
) -> Result<QueryResult, LiteError> {
let Some(as_of) = system_as_of_ms else {
return algorithms::run_algo(csr_map, algorithm, params);
};
let snapshot = build_temporal_snapshot(storage, ¶ms.collection, as_of, memory).await?;
let mut tmp_map = HashMap::new();
tmp_map.insert(params.collection.clone(), snapshot);
let tmp_arc = Arc::new(Mutex::new(tmp_map));
algorithms::run_algo(&tmp_arc, algorithm, params)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_edge_key_roundtrip() {
let (src, dst, label) = parse_edge_key("alice->bob:KNOWS").unwrap();
assert_eq!(src, "alice");
assert_eq!(dst, "bob");
assert_eq!(label, "KNOWS");
}
#[test]
fn parse_edge_key_no_arrow() {
assert!(parse_edge_key("alice_bob_KNOWS").is_none());
}
#[test]
fn parse_history_edge_key_reads_both_forms() {
let arrow = parse_history_edge_key("alice->bob:KNOWS").unwrap();
assert_eq!(arrow, ("alice".into(), "bob".into(), "KNOWS".into()));
let id = EdgeId::try_first(
nodedb_types::id::NodeId::from_validated("carol".to_string()),
nodedb_types::id::NodeId::from_validated("dave".to_string()),
"LIKES",
)
.unwrap();
let display = parse_history_edge_key(&id.to_string()).unwrap();
assert_eq!(display, ("carol".into(), "dave".into(), "LIKES".into()));
}
#[tokio::test]
async fn snapshot_strips_separator_and_reads_both_key_forms() {
use crate::config::LiteConfig;
use crate::engine::graph::history;
use crate::storage::pagedb_storage::PagedbStorageMem;
let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
let governor = Arc::new(
nodedb_mem::MemoryGovernor::new(LiteConfig::default().to_governor_config()).unwrap(),
);
let memory = ScopedMemory::new(
governor,
nodedb_types::DatabaseId::DEFAULT,
nodedb_types::TenantId::new(0),
nodedb_mem::EngineId::Graph,
);
history::record_edge_insert(&*storage, "social", "alice->bob:KNOWS", &Value::Null, 1_000)
.await
.unwrap();
let id = EdgeId::try_first(
nodedb_types::id::NodeId::from_validated("carol".to_string()),
nodedb_types::id::NodeId::from_validated("dave".to_string()),
"LIKES",
)
.unwrap();
history::record_edge_insert(&*storage, "social", &id.to_string(), &Value::Null, 1_000)
.await
.unwrap();
let snapshot = build_temporal_snapshot(&storage, "social", 2_000, &memory)
.await
.unwrap();
assert_eq!(
snapshot.neighbors("alice", &[], nodedb_graph::Direction::Out),
vec![("KNOWS".to_string(), "bob".to_string())]
);
assert_eq!(
snapshot.neighbors("carol", &[], nodedb_graph::Direction::Out),
vec![("LIKES".to_string(), "dave".to_string())]
);
}
#[test]
fn decode_system_to_max() {
let sentinel = i64::MAX as u64;
let mut val = vec![0u8; 8];
val.extend_from_slice(&sentinel.to_be_bytes());
assert_eq!(decode_system_to(&val), Some(i64::MAX));
}
}