fn graph_filter_names(names: &[&str]) -> Vec<String> {
names.iter().map(|name| (*name).to_string()).collect()
}
fn graph_node_label_filter(names: &[&str], mode: LabelMatchMode) -> NodeLabelFilter {
NodeLabelFilter {
labels: graph_filter_names(names),
mode,
}
}
#[test]
fn test_degree_basic() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), 0);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 2);
assert_eq!(db.degree(b, &DegreeOptions::default()).unwrap(), 0);
assert_eq!(db.degree(b, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), 1);
assert_eq!(db.degree(b, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 1);
db.close().unwrap();
}
#[test]
fn test_degree_direction() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2); assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), 1); assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 3);
db.close().unwrap();
}
#[test]
fn test_degree_label_filter() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "REPORTS_TO", UpsertEdgeOptions::default())
.unwrap();
assert_eq!(
db.degree(a, &DegreeOptions { direction: Direction::Outgoing, edge_label_filter: Some(vec!["KNOWS".to_string()]), ..Default::default() })
.unwrap(),
1
);
assert_eq!(
db.degree(a, &DegreeOptions { direction: Direction::Outgoing, edge_label_filter: Some(vec!["REPORTS_TO".to_string()]), ..Default::default() })
.unwrap(),
1
);
assert_eq!(
db.degree(a, &DegreeOptions { direction: Direction::Outgoing, edge_label_filter: Some(vec!["KNOWS".to_string(), "REPORTS_TO".to_string()]), ..Default::default() })
.unwrap(),
2
);
assert_eq!(
db.degree(a, &DegreeOptions { direction: Direction::Outgoing, edge_label_filter: Some(vec!["MISSING_EDGE_LABEL".to_string()]), ..Default::default() })
.unwrap(),
0
);
db.close().unwrap();
}
#[test]
fn test_degree_self_loop() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), 1);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 1);
db.close().unwrap();
}
#[test]
fn test_degree_self_loop_with_normal_edges() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions::default())
.unwrap(); db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap(); db.upsert_edge(c, a, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2); assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), 2); assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 3);
let sum = db.sum_edge_weights(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap();
assert!((sum - 6.0).abs() < 1e-9);
db.close().unwrap();
}
#[test]
fn test_degree_nonexistent_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
assert_eq!(db.degree(999, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 0);
db.close().unwrap();
}
#[test]
fn test_degree_deleted_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let e = db
.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1);
db.delete_edge(e).unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 0);
db.close().unwrap();
}
#[test]
fn test_degree_deleted_neighbor_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1);
db.delete_node(b).unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 0);
db.close().unwrap();
}
#[test]
fn test_degree_after_flush() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 2);
db.close().unwrap();
}
#[test]
fn test_degree_cross_source_dedup() {
let dir = TempDir::new().unwrap();
let opts = DbOptions {
create_if_missing: true,
wal_sync_mode: WalSyncMode::Immediate,
compact_after_n_flushes: 0,
edge_uniqueness: true,
..Default::default()
};
let db = DatabaseEngine::open(&dir.path().join("db"), &opts).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1);
let sum = db
.sum_edge_weights(a, &DegreeOptions::default())
.unwrap();
assert!((sum - 2.0).abs() < 1e-9);
db.close().unwrap();
}
#[test]
fn test_degree_survives_restart() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("db");
{
let db = open_imm(&db_path);
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.5, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.close().unwrap();
}
let db = open_imm(&db_path);
let a = 1; assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1);
db.close().unwrap();
}
#[test]
fn test_sum_edge_weights_basic() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.5, ..Default::default() })
.unwrap();
let sum = db
.sum_edge_weights(a, &DegreeOptions::default())
.unwrap();
assert!((sum - 5.5).abs() < 1e-9);
assert_eq!(
db.sum_edge_weights(c, &DegreeOptions::default())
.unwrap(),
0.0
);
db.close().unwrap();
}
#[test]
fn test_sum_edge_weights_after_flush() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
let sum = db
.sum_edge_weights(a, &DegreeOptions::default())
.unwrap();
assert!((sum - 5.0).abs() < 1e-9);
db.close().unwrap();
}
#[test]
fn test_avg_edge_weight_basic() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 4.0, ..Default::default() })
.unwrap();
let avg = db
.avg_edge_weight(a, &DegreeOptions::default())
.unwrap();
assert!(avg.is_some());
assert!((avg.unwrap() - 3.0).abs() < 1e-9);
db.close().unwrap();
}
#[test]
fn test_avg_edge_weight_none_for_zero_degree() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
assert_eq!(
db.avg_edge_weight(999, &DegreeOptions { direction: Direction::Both, ..Default::default() })
.unwrap(),
None
);
db.close().unwrap();
}
#[test]
fn test_degree_matches_neighbors_len() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "REPORTS_TO", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(d, a, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.upsert_edge(a, d, "KNOWS", UpsertEdgeOptions { weight: 4.0, ..Default::default() })
.unwrap();
for dir_val in [Direction::Outgoing, Direction::Incoming, Direction::Both] {
for tf in [
None,
Some(vec!["KNOWS"]),
Some(vec!["REPORTS_TO"]),
Some(vec!["KNOWS", "REPORTS_TO"]),
] {
let tf_ref = tf.as_deref();
let deg = db.degree(a, &DegreeOptions { direction: dir_val, edge_label_filter: tf_ref.map(graph_filter_names), ..Default::default() }).unwrap();
let nbrs = db.neighbors(a, &NeighborOptions { direction: dir_val, edge_label_filter: tf_ref.map(graph_filter_names), ..Default::default() }).unwrap();
assert_eq!(
deg,
nbrs.len() as u64,
"degree mismatch for dir={:?} filter={:?}: degree={} neighbors={}",
dir_val,
tf_ref,
deg,
nbrs.len()
);
}
}
db.close().unwrap();
}
#[test]
fn test_sum_weight_matches_neighbors() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.5, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.5, ..Default::default() })
.unwrap();
db.flush().unwrap();
let sum = db
.sum_edge_weights(a, &DegreeOptions::default())
.unwrap();
let nbrs = db
.neighbors(a, &NeighborOptions::default())
.unwrap();
let nbr_sum: f64 = nbrs.iter().map(|e| e.weight as f64).sum();
assert!((sum - nbr_sum).abs() < 1e-9);
db.close().unwrap();
}
#[test]
fn test_degree_respects_prune_policies() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "hub", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "keep", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "prune_me", UpsertNodeOptions { weight: 0.1, ..Default::default() }).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
let sum = db
.sum_edge_weights(a, &DegreeOptions::default())
.unwrap();
assert!((sum - 5.0).abs() < 1e-9);
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1);
let sum = db
.sum_edge_weights(a, &DegreeOptions::default())
.unwrap();
assert!((sum - 2.0).abs() < 1e-9);
let nbrs = db
.neighbors(a, &NeighborOptions::default())
.unwrap();
assert_eq!(
db.degree(a, &DegreeOptions::default()).unwrap(),
nbrs.len() as u64
);
db.close().unwrap();
}
#[test]
fn test_degree_after_compaction() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.compact().unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
let sum = db
.sum_edge_weights(a, &DegreeOptions::default())
.unwrap();
assert!((sum - 3.0).abs() < 1e-9);
db.close().unwrap();
}
#[test]
fn test_degree_self_loop_after_flush() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions { weight: 5.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 1);
let sum = db.sum_edge_weights(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap();
assert!((sum - 5.0).abs() < 1e-9);
db.close().unwrap();
}
#[test]
fn test_degrees_batch_basic() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let degs = db
.degrees(&[a, b, c], &DegreeOptions::default())
.unwrap();
assert_eq!(*degs.get(&a).unwrap(), 2);
assert_eq!(*degs.get(&b).unwrap(), 1);
assert!(!degs.contains_key(&c));
db.close().unwrap();
}
#[test]
fn test_degrees_batch_matches_individual() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "REPORTS_TO", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(d, a, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let ids = [a, b, c, d];
for dir_val in [Direction::Outgoing, Direction::Incoming, Direction::Both] {
for tf in [
None,
Some(vec!["KNOWS"]),
Some(vec!["REPORTS_TO"]),
Some(vec!["KNOWS", "REPORTS_TO"]),
] {
let tf_ref = tf.as_deref();
let batch = db.degrees(&ids, &DegreeOptions { direction: dir_val, edge_label_filter: tf_ref.map(graph_filter_names), ..Default::default() }).unwrap();
for &nid in &ids {
let individual = db.degree(nid, &DegreeOptions { direction: dir_val, edge_label_filter: tf_ref.map(graph_filter_names), ..Default::default() }).unwrap();
let batch_val = batch.get(&nid).copied().unwrap_or(0);
assert_eq!(
batch_val, individual,
"mismatch for node={} dir={:?} filter={:?}: batch={} individual={}",
nid, dir_val, tf_ref, batch_val, individual
);
}
}
}
db.close().unwrap();
}
#[test]
fn test_degrees_batch_cross_segment() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let degs = db
.degrees(&[a, b, c], &DegreeOptions::default())
.unwrap();
assert_eq!(*degs.get(&a).unwrap(), 2);
assert_eq!(*degs.get(&b).unwrap(), 1);
assert!(!degs.contains_key(&c));
db.close().unwrap();
}
#[test]
fn test_degrees_batch_dedup_across_sources() {
let dir = TempDir::new().unwrap();
let opts = DbOptions {
create_if_missing: true,
wal_sync_mode: WalSyncMode::Immediate,
compact_after_n_flushes: 0,
edge_uniqueness: true,
..Default::default()
};
let db = DatabaseEngine::open(&dir.path().join("db"), &opts).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
let degs = db.degrees(&[a], &DegreeOptions::default()).unwrap();
assert_eq!(*degs.get(&a).unwrap(), 1);
db.close().unwrap();
}
#[test]
fn test_degrees_batch_tombstones() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let e1 = db
.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.delete_edge(e1).unwrap();
let degs = db.degrees(&[a], &DegreeOptions::default()).unwrap();
assert_eq!(*degs.get(&a).unwrap(), 1);
db.close().unwrap();
}
#[test]
fn test_degrees_batch_self_loop() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
let degs = db.degrees(&[a], &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap();
assert_eq!(*degs.get(&a).unwrap(), 2);
db.close().unwrap();
}
#[test]
fn test_degrees_batch_empty_input() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let degs = db.degrees(&[], &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap();
assert!(degs.is_empty());
db.close().unwrap();
}
#[test]
fn test_degrees_batch_unsorted_input() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let degs = db
.degrees(&[c, a, b, a], &DegreeOptions::default())
.unwrap();
assert_eq!(*degs.get(&a).unwrap(), 1);
assert_eq!(*degs.get(&b).unwrap(), 1);
assert!(!degs.contains_key(&c));
db.close().unwrap();
}
#[test]
fn test_degrees_batch_after_compaction() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.compact().unwrap();
let degs = db
.degrees(&[a, b, c], &DegreeOptions::default())
.unwrap();
assert_eq!(*degs.get(&a).unwrap(), 2);
assert!(!degs.contains_key(&b));
assert!(!degs.contains_key(&c));
db.close().unwrap();
}
#[test]
fn test_degrees_batch_respects_prune_policies() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "hub", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "keep", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "prune_me", UpsertNodeOptions { weight: 0.1, ..Default::default() }).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let degs = db.degrees(&[a], &DegreeOptions::default()).unwrap();
assert_eq!(*degs.get(&a).unwrap(), 2);
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
let degs = db.degrees(&[a], &DegreeOptions::default()).unwrap();
assert_eq!(*degs.get(&a).unwrap(), 1);
assert_eq!(
degs.get(&a).copied().unwrap_or(0),
db.degree(a, &DegreeOptions::default()).unwrap()
);
db.close().unwrap();
}
#[test]
fn test_degrees_batch_matches_neighbors_batch() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(b, c, "REPORTS_TO", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(d, a, "KNOWS", UpsertEdgeOptions { weight: 4.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions { weight: 5.0, ..Default::default() })
.unwrap();
let ids = [a, b, c, d];
for dir_val in [Direction::Outgoing, Direction::Incoming, Direction::Both] {
let degs = db.degrees(&ids, &DegreeOptions { direction: dir_val, ..Default::default() }).unwrap();
let nbrs = db.neighbors_batch(&ids, &NeighborOptions { direction: dir_val, ..Default::default() }).unwrap();
for &nid in &ids {
let deg = degs.get(&nid).copied().unwrap_or(0);
let nbr_count = nbrs.get(&nid).map(|v| v.len() as u64).unwrap_or(0);
assert_eq!(
deg, nbr_count,
"mismatch for node={} dir={:?}: degrees={} neighbors_batch={}",
nid, dir_val, deg, nbr_count
);
}
}
db.close().unwrap();
}
#[test]
fn test_degree_ignores_expired_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 5.0, valid_from: None, valid_to: Some(1), ..Default::default() })
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 0);
assert_eq!(
db.sum_edge_weights(a, &DegreeOptions::default())
.unwrap(),
0.0
);
assert_eq!(
db.avg_edge_weight(a, &DegreeOptions::default())
.unwrap(),
None
);
db.close().unwrap();
}
#[test]
fn test_degree_ignores_future_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let future = now_millis() + 100_000_000;
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 5.0, valid_from: Some(future), valid_to: None, ..Default::default() })
.unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 0);
assert_eq!(
db.sum_edge_weights(a, &DegreeOptions::default())
.unwrap(),
0.0
);
assert_eq!(
db.avg_edge_weight(a, &DegreeOptions::default())
.unwrap(),
None
);
db.close().unwrap();
}
#[test]
fn test_degree_ignores_invalidated_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let e1 = db
.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 7.0, ..Default::default() })
.unwrap();
db.invalidate_edge(e1, 1).unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1);
assert_eq!(
db.sum_edge_weights(a, &DegreeOptions::default())
.unwrap(),
7.0
);
assert_eq!(
db.avg_edge_weight(a, &DegreeOptions::default())
.unwrap(),
Some(7.0)
);
db.close().unwrap();
}
#[test]
fn test_degrees_batch_ignores_expired_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: None, valid_to: Some(1), ..Default::default() })
.unwrap();
let degs = db.degrees(&[a], &DegreeOptions::default()).unwrap();
assert_eq!(degs.get(&a).copied().unwrap_or(0), 1);
db.close().unwrap();
}
#[test]
fn test_degrees_batch_ignores_future_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let future = now_millis() + 100_000_000;
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(future), valid_to: None, ..Default::default() })
.unwrap();
let degs = db.degrees(&[a], &DegreeOptions::default()).unwrap();
assert_eq!(degs.get(&a).copied().unwrap_or(0), 0);
db.close().unwrap();
}
#[test]
fn test_degrees_batch_ignores_invalidated_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let e1 = db
.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.invalidate_edge(e1, 1).unwrap();
let degs = db.degrees(&[a], &DegreeOptions::default()).unwrap();
assert_eq!(degs.get(&a).copied().unwrap_or(0), 0);
db.close().unwrap();
}
#[test]
fn test_degree_temporal_after_flush() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, valid_from: None, valid_to: Some(1), ..Default::default() })
.unwrap();
db.flush().unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1);
assert_eq!(
db.sum_edge_weights(a, &DegreeOptions::default())
.unwrap(),
2.0
);
let degs = db.degrees(&[a], &DegreeOptions::default()).unwrap();
assert_eq!(degs.get(&a).copied().unwrap_or(0), 1);
db.close().unwrap();
}
#[test]
fn test_degree_at_epoch_parity_with_neighbors() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(1000), valid_to: Some(5000), ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, valid_from: Some(3000), valid_to: Some(8000), ..Default::default() })
.unwrap();
db.flush().unwrap();
let t = Some(500);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() }).unwrap(), 0);
assert_eq!(
db.neighbors(a, &NeighborOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() })
.unwrap()
.len(),
0
);
let t = Some(2000);
assert_eq!(
db.degree(a, &DegreeOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() }).unwrap(),
db.neighbors(a, &NeighborOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() })
.unwrap()
.len() as u64
);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() }).unwrap(), 1);
let t = Some(4000);
assert_eq!(
db.degree(a, &DegreeOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() }).unwrap(),
db.neighbors(a, &NeighborOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() })
.unwrap()
.len() as u64
);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() }).unwrap(), 2);
let t = Some(6000);
assert_eq!(
db.degree(a, &DegreeOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() }).unwrap(),
db.neighbors(a, &NeighborOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() })
.unwrap()
.len() as u64
);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() }).unwrap(), 1);
let t = Some(9000);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() }).unwrap(), 0);
assert_eq!(
db.neighbors(a, &NeighborOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() })
.unwrap()
.len(),
0
);
let t = Some(4000);
let degs = db.degrees(&[a], &DegreeOptions { direction: Direction::Outgoing, at_epoch: t, ..Default::default() }).unwrap();
assert_eq!(degs.get(&a).copied().unwrap_or(0), 2);
db.close().unwrap();
}
#[test]
fn test_degree_invalidate_after_flush_shadows_segment() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let e1 = db
.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 5.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.invalidate_edge(e1, 1).unwrap();
let nbrs = db
.neighbors(a, &NeighborOptions::default())
.unwrap();
assert_eq!(nbrs.len(), 0, "neighbors should see 0 after invalidation");
assert_eq!(
db.degree(a, &DegreeOptions::default()).unwrap(),
0,
"degree must match neighbors after invalidate-after-flush"
);
assert_eq!(
db.sum_edge_weights(a, &DegreeOptions::default())
.unwrap(),
0.0
);
assert_eq!(
db.avg_edge_weight(a, &DegreeOptions::default())
.unwrap(),
None
);
let degs = db.degrees(&[a], &DegreeOptions::default()).unwrap();
assert_eq!(degs.get(&a).copied().unwrap_or(0), 0);
db.close().unwrap();
}
fn build_chain(db: &mut DatabaseEngine) -> Vec<u64> {
let mut nodes = Vec::new();
for i in 0..5u64 {
let key = format!("chain_{}", i);
nodes.push(db.upsert_node("Person", &key, UpsertNodeOptions::default()).unwrap());
}
for i in 0..4 {
db.upsert_edge(nodes[i], nodes[i + 1], "KNOWS", UpsertEdgeOptions::default())
.unwrap();
}
nodes
}
fn build_diamond(db: &mut DatabaseEngine) -> (u64, u64, u64, u64) {
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
(a, b, c, d)
}
#[test]
fn test_shortest_path_direct_neighbors() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let e = db
.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let path = db
.shortest_path(a, b, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.nodes, vec![a, b]);
assert_eq!(p.edges, vec![e]);
assert_eq!(p.total_cost, 1.0);
db.close().unwrap();
}
#[test]
fn test_shortest_path_multi_hop() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let nodes = build_chain(&mut db);
let path = db
.shortest_path(nodes[0], nodes[4], &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.nodes.len(), 5);
assert_eq!(p.nodes[0], nodes[0]);
assert_eq!(p.nodes[4], nodes[4]);
assert_eq!(p.edges.len(), 4);
assert_eq!(p.total_cost, 4.0);
db.close().unwrap();
}
#[test]
fn test_shortest_path_no_path_disconnected() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let path = db
.shortest_path(a, b, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_none());
db.close().unwrap();
}
#[test]
fn test_shortest_path_same_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let path = db
.shortest_path(a, a, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.nodes, vec![a]);
assert!(p.edges.is_empty());
assert_eq!(p.total_cost, 0.0);
db.close().unwrap();
}
#[test]
fn test_path_apis_same_nonexistent_node_return_empty() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
assert!(db
.shortest_path(999999, 999999, &ShortestPathOptions::default())
.unwrap()
.is_none());
assert!(!db
.is_connected(999999, 999999, &IsConnectedOptions::default())
.unwrap());
assert!(db
.all_shortest_paths(999999, 999999, &AllShortestPathsOptions::default())
.unwrap()
.is_empty());
db.close().unwrap();
}
#[test]
fn test_shortest_path_directed_no_reverse() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let path = db
.shortest_path(b, a, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_none());
let path = db
.shortest_path(b, a, &ShortestPathOptions { direction: Direction::Both, ..Default::default() })
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.nodes, vec![b, a]);
assert_eq!(p.total_cost, 1.0);
db.close().unwrap();
}
#[test]
fn test_shortest_path_diamond_finds_shortest() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let (a, _b, _c, d) = build_diamond(&mut db);
let path = db
.shortest_path(a, d, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.total_cost, 2.0);
assert_eq!(p.nodes.len(), 3);
assert_eq!(p.nodes[0], a);
assert_eq!(p.nodes[2], d);
db.close().unwrap();
}
#[test]
fn test_shortest_path_edge_label_filter() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, c, "REPORTS_TO", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions { edge_label_filter: Some(vec!["KNOWS".to_string()]), ..Default::default() })
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.total_cost, 2.0);
assert_eq!(p.nodes, vec![a, b, c]);
let path = db
.shortest_path(a, c, &ShortestPathOptions { edge_label_filter: Some(vec!["REPORTS_TO".to_string()]), ..Default::default() })
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.total_cost, 1.0);
assert_eq!(p.nodes, vec![a, c]);
db.close().unwrap();
}
#[test]
fn test_shortest_path_max_depth_cutoff() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let nodes = build_chain(&mut db);
let path = db
.shortest_path(nodes[0], nodes[4], &ShortestPathOptions { max_depth: Some(2), ..Default::default() })
.unwrap();
assert!(path.is_none());
let path = db
.shortest_path(nodes[0], nodes[4], &ShortestPathOptions { max_depth: Some(4), ..Default::default() })
.unwrap();
assert!(path.is_some());
assert_eq!(path.unwrap().total_cost, 4.0);
db.close().unwrap();
}
#[test]
fn test_shortest_path_temporal_filtering() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(100), valid_to: Some(200), ..Default::default() })
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(100), valid_to: Some(300), ..Default::default() })
.unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions { at_epoch: Some(150), ..Default::default() })
.unwrap();
assert!(path.is_some());
assert_eq!(path.unwrap().total_cost, 2.0);
let path = db
.shortest_path(a, c, &ShortestPathOptions { at_epoch: Some(250), ..Default::default() })
.unwrap();
assert!(path.is_none());
db.close().unwrap();
}
#[test]
fn test_is_connected_basic() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let nodes = build_chain(&mut db);
assert!(db
.is_connected(nodes[0], nodes[4], &IsConnectedOptions::default())
.unwrap());
assert!(db
.is_connected(nodes[0], nodes[2], &IsConnectedOptions::default())
.unwrap());
assert!(!db
.is_connected(nodes[4], nodes[0], &IsConnectedOptions::default())
.unwrap());
assert!(db
.is_connected(nodes[4], nodes[0], &IsConnectedOptions { direction: Direction::Both, ..Default::default() })
.unwrap());
db.close().unwrap();
}
#[test]
fn test_is_connected_same_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
assert!(db
.is_connected(a, a, &IsConnectedOptions::default())
.unwrap());
db.close().unwrap();
}
#[test]
fn test_is_connected_disconnected() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
assert!(!db
.is_connected(a, b, &IsConnectedOptions::default())
.unwrap());
db.close().unwrap();
}
#[test]
fn test_is_connected_max_depth() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let nodes = build_chain(&mut db);
assert!(!db
.is_connected(nodes[0], nodes[4], &IsConnectedOptions { max_depth: Some(2), ..Default::default() })
.unwrap());
assert!(db
.is_connected(nodes[0], nodes[4], &IsConnectedOptions { max_depth: Some(4), ..Default::default() })
.unwrap());
db.close().unwrap();
}
#[test]
fn test_shortest_path_after_flush() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let nodes = build_chain(&mut db);
db.flush().unwrap();
let path = db
.shortest_path(nodes[0], nodes[4], &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.total_cost, 4.0);
assert_eq!(p.nodes.len(), 5);
db.close().unwrap();
}
#[test]
fn test_shortest_path_across_flush_boundary() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
let e = db.upsert_node("Person", "e", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(d, e, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
let path = db
.shortest_path(a, e, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.total_cost, 4.0);
assert_eq!(p.nodes, vec![a, b, c, d, e]);
db.close().unwrap();
}
#[test]
fn test_shortest_path_after_compact() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.compact().unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
assert_eq!(path.unwrap().total_cost, 2.0);
db.close().unwrap();
}
#[test]
fn test_shortest_path_with_deleted_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let direct = db
.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.delete_edge(direct).unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.total_cost, 2.0);
assert_eq!(p.nodes, vec![a, b, c]);
db.close().unwrap();
}
#[test]
fn test_shortest_path_with_deleted_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(d, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.delete_node(b).unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.total_cost, 2.0);
assert_eq!(p.nodes, vec![a, d, c]);
db.close().unwrap();
}
#[test]
fn test_path_apis_respect_deleted_edges_after_flush_and_compact() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
let cheap_ab = db
.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let cheap_bc = db
.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, d, "KNOWS", UpsertEdgeOptions { weight: 5.0, ..Default::default() })
.unwrap();
db.upsert_edge(d, c, "KNOWS", UpsertEdgeOptions { weight: 5.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.delete_edge(cheap_ab).unwrap();
db.delete_edge(cheap_bc).unwrap();
db.flush().unwrap();
db.compact().unwrap();
let bfs = db
.shortest_path(a, c, &ShortestPathOptions::default())
.unwrap()
.unwrap();
assert_eq!(bfs.nodes, vec![a, d, c]);
assert_eq!(bfs.total_cost, 2.0);
assert!(db
.is_connected(a, c, &IsConnectedOptions::default())
.unwrap());
let bfs_paths = db
.all_shortest_paths(a, c, &AllShortestPathsOptions::default())
.unwrap();
assert_eq!(bfs_paths.len(), 1);
assert_eq!(bfs_paths[0].nodes, vec![a, d, c]);
let weighted = db
.shortest_path(a, c, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(weighted.nodes, vec![a, d, c]);
assert_eq!(weighted.total_cost, 10.0);
let weighted_paths = db
.all_shortest_paths(a, c, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap();
assert_eq!(weighted_paths.len(), 1);
assert_eq!(weighted_paths[0].nodes, vec![a, d, c]);
assert_eq!(weighted_paths[0].total_cost, 10.0);
db.close().unwrap();
}
#[test]
fn test_ingest_mode_suppresses_auto_compact() {
let dir = TempDir::new().unwrap();
let db = DatabaseEngine::open(&dir.path().join("db"), &DbOptions::default()).unwrap();
db.ingest_mode().unwrap();
for i in 0..10 {
db.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions::default())
.unwrap();
db.flush().unwrap();
}
assert_eq!(db.segment_count().unwrap(), 10);
for i in 0..10 {
assert!(db.get_node_by_key("Person", &format!("n{}", i)).unwrap().is_some());
}
let stats = db.end_ingest().unwrap().unwrap();
assert_eq!(stats.segments_merged, 10);
assert_eq!(db.segment_count().unwrap(), 1);
for i in 0..10 {
assert!(db.get_node_by_key("Person", &format!("n{}", i)).unwrap().is_some());
}
db.close().unwrap();
}
#[test]
fn test_end_ingest_restores_previous_compact_threshold() {
let dir = TempDir::new().unwrap();
let opts = DbOptions {
compact_after_n_flushes: 5,
..DbOptions::default()
};
let db = DatabaseEngine::open(&dir.path().join("db"), &opts).unwrap();
assert_eq!(db.compact_after_n_flushes_for_test(), 5);
db.ingest_mode().unwrap();
assert_eq!(db.compact_after_n_flushes_for_test(), 0);
assert_eq!(db.ingest_saved_compact_after_n_flushes_for_test(), Some(5));
let _ = db.end_ingest().unwrap();
assert_eq!(db.compact_after_n_flushes_for_test(), 5);
assert_eq!(db.ingest_saved_compact_after_n_flushes_for_test(), None);
db.close().unwrap();
}
#[test]
fn test_ingest_mode_is_idempotent_for_saved_threshold() {
let dir = TempDir::new().unwrap();
let opts = DbOptions {
compact_after_n_flushes: 5,
..DbOptions::default()
};
let db = DatabaseEngine::open(&dir.path().join("db"), &opts).unwrap();
db.ingest_mode().unwrap();
db.ingest_mode().unwrap();
assert_eq!(db.compact_after_n_flushes_for_test(), 0);
assert_eq!(db.ingest_saved_compact_after_n_flushes_for_test(), Some(5));
let _ = db.end_ingest().unwrap();
assert_eq!(db.compact_after_n_flushes_for_test(), 5);
assert_eq!(db.ingest_saved_compact_after_n_flushes_for_test(), None);
db.close().unwrap();
}
#[test]
fn test_shortest_path_large_fan_out() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let end = db.upsert_node("Person", "end", UpsertNodeOptions::default()).unwrap();
let bridge = db.upsert_node("Person", "bridge", UpsertNodeOptions::default()).unwrap();
for i in 0..100 {
let key = format!("dead_s_{}", i);
let n = db.upsert_node("Person", &key, UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, n, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
}
for i in 0..100 {
let key = format!("dead_e_{}", i);
let n = db.upsert_node("Person", &key, UpsertNodeOptions::default()).unwrap();
db.upsert_edge(n, end, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
}
db.upsert_edge(start, bridge, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(bridge, end, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let path = db
.shortest_path(start, end, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.total_cost, 2.0);
assert_eq!(p.nodes, vec![start, bridge, end]);
db.close().unwrap();
}
#[test]
fn test_shortest_path_incoming_direction() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let path = db
.shortest_path(c, a, &ShortestPathOptions { direction: Direction::Incoming, ..Default::default() })
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.nodes.len(), 3);
assert_eq!(p.nodes[0], c);
assert_eq!(p.nodes[2], a);
assert_eq!(p.total_cost, 2.0);
db.close().unwrap();
}
#[test]
fn test_shortest_path_cycle_safe() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let path = db
.shortest_path(a, d, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
let p = path.unwrap();
assert_eq!(p.total_cost, 3.0);
assert_eq!(p.nodes, vec![a, b, c, d]);
db.close().unwrap();
}
#[test]
fn test_shortest_path_nonexistent_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let path = db
.shortest_path(a, 999999, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_none());
let path = db
.shortest_path(999999, a, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_none());
assert!(!db
.is_connected(a, 999999, &IsConnectedOptions::default())
.unwrap());
db.close().unwrap();
}
#[test]
fn test_path_apis_respect_prune_policy_visibility() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let hidden = db.upsert_node("Person", "hidden", UpsertNodeOptions { weight: 0.2, ..Default::default() }).unwrap();
let visible = db.upsert_node("Person", "visible", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, hidden, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(hidden, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, visible, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(visible, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions::default())
.unwrap()
.unwrap();
assert_eq!(path.nodes, vec![a, visible, c]);
assert!(db
.is_connected(a, c, &IsConnectedOptions::default())
.unwrap());
let paths = db
.all_shortest_paths(a, c, &AllShortestPathsOptions::default())
.unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].nodes, vec![a, visible, c]);
assert!(db
.shortest_path(hidden, hidden, &ShortestPathOptions::default())
.unwrap()
.is_none());
assert!(!db
.is_connected(hidden, hidden, &IsConnectedOptions::default())
.unwrap());
assert!(db
.all_shortest_paths(hidden, hidden, &AllShortestPathsOptions::default())
.unwrap()
.is_empty());
db.close().unwrap();
}
#[test]
fn test_path_apis_exclude_pruned_multi_label_nodes() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db
.upsert_node("Person", "a", UpsertNodeOptions::default())
.unwrap();
let hidden = db
.upsert_node(
&["Person", "Hidden"],
"hidden",
UpsertNodeOptions {
weight: 0.2,
..Default::default()
},
)
.unwrap();
let c = db
.upsert_node("Person", "c", UpsertNodeOptions::default())
.unwrap();
db.upsert_edge(a, hidden, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(hidden, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.set_prune_policy(
"hide-low-hidden",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: Some("Hidden".to_string()),
},
)
.unwrap();
assert!(db
.shortest_path(a, c, &ShortestPathOptions::default())
.unwrap()
.is_none());
assert!(!db
.is_connected(a, c, &IsConnectedOptions::default())
.unwrap());
assert!(db
.all_shortest_paths(a, c, &AllShortestPathsOptions::default())
.unwrap()
.is_empty());
db.close().unwrap();
}
#[test]
fn test_weighted_path_apis_respect_prune_policy_visibility() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let hidden = db.upsert_node("Person", "hidden", UpsertNodeOptions { weight: 0.2, ..Default::default() }).unwrap();
let visible = db.upsert_node("Person", "visible", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let mut cheap = BTreeMap::new();
cheap.insert("cost".to_string(), PropValue::Float(1.0));
let mut expensive = BTreeMap::new();
expensive.insert("cost".to_string(), PropValue::Float(10.0));
db.upsert_edge(a, hidden, "KNOWS", UpsertEdgeOptions { props: cheap.clone(), ..Default::default() })
.unwrap();
db.upsert_edge(hidden, c, "KNOWS", UpsertEdgeOptions { props: cheap, ..Default::default() })
.unwrap();
db.upsert_edge(a, visible, "KNOWS", UpsertEdgeOptions { props: expensive.clone(), weight: 10.0, ..Default::default() })
.unwrap();
db.upsert_edge(visible, c, "KNOWS", UpsertEdgeOptions { props: expensive, weight: 10.0, ..Default::default() })
.unwrap();
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.nodes, vec![a, visible, c]);
assert_eq!(path.total_cost, 20.0);
let path = db
.shortest_path(a, c, &ShortestPathOptions { weight_field: Some("cost".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.nodes, vec![a, visible, c]);
assert_eq!(path.total_cost, 20.0);
let paths = db
.all_shortest_paths(a, c, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].nodes, vec![a, visible, c]);
assert_eq!(paths[0].total_cost, 20.0);
db.close().unwrap();
}
#[test]
fn test_dijkstra_weighted_shortest_path() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 10.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
let _ec = db
.upsert_edge(c, b, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
let path = db
.shortest_path(a, b, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 5.0);
assert_eq!(path.nodes.len(), 3);
assert_eq!(*path.nodes.first().unwrap(), a);
assert_eq!(*path.nodes.last().unwrap(), b);
db.close().unwrap();
}
#[test]
fn test_dijkstra_custom_weight_field() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let mut props_ab = BTreeMap::new();
props_ab.insert("cost".to_string(), PropValue::Float(100.0));
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { props: props_ab, weight: 1.0, ..Default::default() }).unwrap();
let mut props_ac = BTreeMap::new();
props_ac.insert("cost".to_string(), PropValue::Float(1.0));
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { props: props_ac, weight: 1.0, ..Default::default() }).unwrap();
let mut props_cb = BTreeMap::new();
props_cb.insert("cost".to_string(), PropValue::Float(2.0));
db.upsert_edge(c, b, "KNOWS", UpsertEdgeOptions { props: props_cb, weight: 1.0, ..Default::default() }).unwrap();
let path = db
.shortest_path(a, b, &ShortestPathOptions { weight_field: Some("cost".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 3.0); assert_eq!(path.nodes, vec![a, c, b]);
db.close().unwrap();
}
#[test]
fn test_dijkstra_max_cost_ceiling() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 5.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(c, b, "KNOWS", UpsertEdgeOptions { weight: 4.0, ..Default::default() })
.unwrap();
let path = db
.shortest_path(a, b, &ShortestPathOptions { weight_field: Some("weight".to_string()), max_cost: Some(4.0), ..Default::default() })
.unwrap();
assert!(path.is_none());
let path = db
.shortest_path(a, b, &ShortestPathOptions { weight_field: Some("weight".to_string()), max_cost: Some(5.0), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 5.0);
db.close().unwrap();
}
#[test]
fn test_dijkstra_negative_weight_error() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: -1.0, ..Default::default() })
.unwrap();
let result = db.shortest_path(a, b, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() });
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("negative"));
db.close().unwrap();
}
#[test]
fn test_dijkstra_nan_weight_error() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let mut props = BTreeMap::new();
props.insert("w".to_string(), PropValue::Float(f64::NAN));
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { props, weight: 1.0, ..Default::default() }).unwrap();
let result = db.shortest_path(a, b, &ShortestPathOptions { weight_field: Some("w".to_string()), ..Default::default() });
assert!(result.is_err());
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let mut props2 = BTreeMap::new();
props2.insert("w".to_string(), PropValue::Float(f64::INFINITY));
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { props: props2, weight: 1.0, ..Default::default() }).unwrap();
let result = db.shortest_path(a, c, &ShortestPathOptions { weight_field: Some("w".to_string()), ..Default::default() });
assert!(result.is_err());
db.close().unwrap();
}
#[test]
fn test_dijkstra_max_depth_bidir_total() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let path = db
.shortest_path(a, d, &ShortestPathOptions { weight_field: Some("weight".to_string()), max_depth: Some(2), ..Default::default() })
.unwrap();
assert!(path.is_none());
let path = db
.shortest_path(a, d, &ShortestPathOptions { weight_field: Some("weight".to_string()), max_depth: Some(3), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 3.0);
assert_eq!(path.edges.len(), 3);
db.close().unwrap();
}
#[test]
fn test_dijkstra_zero_weight_edges() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 0.0, ..Default::default() })
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions { weight: 0.0, ..Default::default() })
.unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 0.0);
assert_eq!(path.nodes, vec![a, b, c]);
db.close().unwrap();
}
#[test]
fn test_dijkstra_same_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let path = db
.shortest_path(a, a, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.nodes, vec![a]);
assert_eq!(path.edges.len(), 0);
assert_eq!(path.total_cost, 0.0);
db.close().unwrap();
}
#[test]
fn test_dijkstra_bidir_termination_non_meeting_path() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, d, "KNOWS", UpsertEdgeOptions { weight: 10.0, ..Default::default() })
.unwrap();
let path = db
.shortest_path(a, d, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 2.0);
assert_eq!(path.nodes.len(), 3);
assert_eq!(*path.nodes.first().unwrap(), a);
assert_eq!(*path.nodes.last().unwrap(), d);
db.close().unwrap();
}
#[test]
fn test_dijkstra_no_path() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let path = db
.shortest_path(a, b, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap();
assert!(path.is_none());
db.close().unwrap();
}
#[test]
fn test_dijkstra_after_flush() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 5.0);
assert_eq!(path.nodes, vec![a, b, c]);
db.close().unwrap();
}
#[test]
fn test_dijkstra_after_compact() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.compact().unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 5.0);
db.close().unwrap();
}
#[test]
fn test_bfs_vs_dijkstra_different_paths() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
let e = db.upsert_node("Person", "e", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 100.0, ..Default::default() })
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions { weight: 100.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(d, e, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(e, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let bfs = db
.shortest_path(a, c, &ShortestPathOptions::default())
.unwrap()
.unwrap();
assert_eq!(bfs.total_cost, 2.0);
let dij = db
.shortest_path(a, c, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(dij.total_cost, 3.0);
assert_eq!(dij.nodes.len(), 4);
db.close().unwrap();
}
#[test]
fn test_dijkstra_max_depth() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let path = db
.shortest_path(a, d, &ShortestPathOptions { weight_field: Some("weight".to_string()), max_depth: Some(1), ..Default::default() })
.unwrap();
assert!(path.is_none());
let path = db
.shortest_path(a, d, &ShortestPathOptions { weight_field: Some("weight".to_string()), max_depth: Some(3), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 3.0);
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_bfs_diamond() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let (a, _b, _c, d) = build_diamond(&mut db);
let paths = db
.all_shortest_paths(a, d, &AllShortestPathsOptions::default())
.unwrap();
assert_eq!(paths.len(), 2);
for p in &paths {
assert_eq!(p.total_cost, 2.0);
assert_eq!(p.nodes.len(), 3);
assert_eq!(*p.nodes.first().unwrap(), a);
assert_eq!(*p.nodes.last().unwrap(), d);
}
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_bfs_max_paths() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let (a, _, _, d) = build_diamond(&mut db);
let paths = db
.all_shortest_paths(a, d, &AllShortestPathsOptions { max_paths: Some(1), ..Default::default() })
.unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].total_cost, 2.0);
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_bfs_same_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let paths = db
.all_shortest_paths(a, a, &AllShortestPathsOptions::default())
.unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].nodes, vec![a]);
assert_eq!(paths[0].total_cost, 0.0);
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_bfs_no_path() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let paths = db
.all_shortest_paths(a, b, &AllShortestPathsOptions::default())
.unwrap();
assert!(paths.is_empty());
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_dijkstra_diamond() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
let paths = db
.all_shortest_paths(a, d, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap();
assert_eq!(paths.len(), 2);
for p in &paths {
assert_eq!(p.total_cost, 6.0);
assert_eq!(*p.nodes.first().unwrap(), a);
assert_eq!(*p.nodes.last().unwrap(), d);
}
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_dijkstra_max_paths() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let paths = db
.all_shortest_paths(a, d, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), max_paths: Some(1), ..Default::default() })
.unwrap();
assert_eq!(paths.len(), 1);
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_dijkstra_no_path() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let paths = db
.all_shortest_paths(a, b, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap();
assert!(paths.is_empty());
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_negative_weight_error() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: -5.0, ..Default::default() })
.unwrap();
let result = db.all_shortest_paths(a, b, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), ..Default::default() });
assert!(result.is_err());
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_bfs_after_flush() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let (a, _, _, d) = build_diamond(&mut db);
db.flush().unwrap();
let paths = db
.all_shortest_paths(a, d, &AllShortestPathsOptions::default())
.unwrap();
assert_eq!(paths.len(), 2);
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_bfs_after_compact() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.compact().unwrap();
let paths = db
.all_shortest_paths(a, d, &AllShortestPathsOptions::default())
.unwrap();
assert_eq!(paths.len(), 2);
for p in &paths {
assert_eq!(p.total_cost, 2.0);
}
db.close().unwrap();
}
#[test]
fn test_dijkstra_custom_int_weight() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let mut props = BTreeMap::new();
props.insert("distance".to_string(), PropValue::Int(7));
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { props, weight: 1.0, ..Default::default() }).unwrap();
let path = db
.shortest_path(a, b, &ShortestPathOptions { weight_field: Some("distance".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 7.0);
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_max_paths_zero_no_limit() {
let dir = TempDir::new().unwrap();
let mut db = open_imm(&dir.path().join("db"));
let (a, _, _, d) = build_diamond(&mut db);
let paths = db
.all_shortest_paths(a, d, &AllShortestPathsOptions { max_paths: Some(0), ..Default::default() })
.unwrap();
assert_eq!(paths.len(), 2);
db.close().unwrap();
}
#[test]
fn test_dijkstra_equal_cost_different_hops_max_depth() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let s = db.upsert_node("Person", "s", UpsertNodeOptions::default()).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let t = db.upsert_node("Person", "t", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(s, a, "KNOWS", UpsertEdgeOptions { weight: 4.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, t, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(s, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, t, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
let path = db
.shortest_path(s, t, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 5.0);
let path = db
.shortest_path(s, t, &ShortestPathOptions { weight_field: Some("weight".to_string()), max_depth: Some(2), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 5.0);
assert_eq!(path.edges.len(), 2);
db.close().unwrap();
}
#[test]
fn test_dijkstra_equal_cost_shorter_hops_arrives_after_settle() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let s = db.upsert_node("Person", "s", UpsertNodeOptions::default()).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let v = db.upsert_node("Person", "v", UpsertNodeOptions::default()).unwrap();
let x = db.upsert_node("Person", "x", UpsertNodeOptions::default()).unwrap();
let t = db.upsert_node("Person", "t", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(s, a, "KNOWS", UpsertEdgeOptions { weight: 0.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 0.0, ..Default::default() })
.unwrap();
db.upsert_edge(b, v, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(s, x, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(x, v, "KNOWS", UpsertEdgeOptions { weight: 0.0, ..Default::default() })
.unwrap();
db.upsert_edge(v, t, "KNOWS", UpsertEdgeOptions { weight: 0.0, ..Default::default() })
.unwrap();
let path = db
.shortest_path(s, t, &ShortestPathOptions { weight_field: Some("weight".to_string()), max_depth: Some(3), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 1.0);
assert_eq!(path.nodes, vec![s, x, v, t]);
db.close().unwrap();
}
#[test]
fn test_dijkstra_max_depth_uses_best_constrained_cost() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let s = db.upsert_node("Person", "s", UpsertNodeOptions::default()).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let t = db.upsert_node("Person", "t", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(s, a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, t, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(s, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(c, t, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
let path = db
.shortest_path(s, t, &ShortestPathOptions { weight_field: Some("weight".to_string()), max_depth: Some(2), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 6.0);
assert_eq!(path.nodes, vec![s, c, t]);
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_zero_weight_cycle() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let t = db.upsert_node("Person", "t", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 0.0, ..Default::default() })
.unwrap();
db.upsert_edge(b, a, "KNOWS", UpsertEdgeOptions { weight: 0.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, t, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let paths = db
.all_shortest_paths(a, t, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap();
assert!(!paths.is_empty());
for p in &paths {
assert_eq!(p.total_cost, 1.0);
assert_eq!(*p.nodes.first().unwrap(), a);
assert_eq!(*p.nodes.last().unwrap(), t);
}
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_dijkstra_max_depth_filtering() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let s = db.upsert_node("Person", "s", UpsertNodeOptions::default()).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let t = db.upsert_node("Person", "t", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(s, a, "KNOWS", UpsertEdgeOptions { weight: 4.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, t, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(s, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, t, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
let paths = db
.all_shortest_paths(s, t, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), max_depth: Some(2), ..Default::default() })
.unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].total_cost, 5.0);
assert_eq!(paths[0].edges.len(), 2);
let paths = db
.all_shortest_paths(s, t, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), max_depth: Some(3), ..Default::default() })
.unwrap();
assert_eq!(paths.len(), 2);
for p in &paths {
assert_eq!(p.total_cost, 5.0);
}
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_dijkstra_max_depth_uses_best_constrained_cost() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let s = db.upsert_node("Person", "s", UpsertNodeOptions::default()).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let t = db.upsert_node("Person", "t", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(s, a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, t, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(s, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(c, t, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
let paths = db
.all_shortest_paths(s, t, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), max_depth: Some(2), ..Default::default() })
.unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].total_cost, 6.0);
assert_eq!(paths[0].nodes, vec![s, c, t]);
db.close().unwrap();
}
#[test]
fn test_all_shortest_paths_dijkstra_after_flush_weighted() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
let paths = db
.all_shortest_paths(a, d, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap();
assert_eq!(paths.len(), 2);
for p in &paths {
assert_eq!(p.total_cost, 6.0);
}
db.close().unwrap();
}
#[test]
fn test_path_apis_after_reopen_weighted_and_unweighted() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("db");
let (a, d) = {
let db = open_imm(&db_path);
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
db.compact().unwrap();
db.close().unwrap();
(a, d)
};
let reopened = open_imm(&db_path);
let bfs_path = reopened
.shortest_path(a, d, &ShortestPathOptions::default())
.unwrap()
.unwrap();
assert_eq!(bfs_path.total_cost, 2.0);
assert_eq!(bfs_path.edges.len(), 2);
let weighted_path = reopened
.shortest_path(a, d, &ShortestPathOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(weighted_path.total_cost, 3.0);
assert!(reopened
.is_connected(a, d, &IsConnectedOptions::default())
.unwrap());
let bfs_paths = reopened
.all_shortest_paths(a, d, &AllShortestPathsOptions::default())
.unwrap();
assert_eq!(bfs_paths.len(), 2);
let weighted_paths = reopened
.all_shortest_paths(a, d, &AllShortestPathsOptions { weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap();
assert_eq!(weighted_paths.len(), 2);
assert!(weighted_paths.iter().all(|path| path.total_cost == 3.0));
reopened.close().unwrap();
}
#[test]
fn test_dijkstra_direction_both() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
let path = db
.shortest_path(c, a, &ShortestPathOptions { direction: Direction::Both, weight_field: Some("weight".to_string()), ..Default::default() })
.unwrap()
.unwrap();
assert_eq!(path.total_cost, 5.0);
db.close().unwrap();
}
#[test]
fn test_shortest_path_memtable_plus_segments() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let path = db
.shortest_path(a, c, &ShortestPathOptions::default())
.unwrap();
assert!(path.is_some());
assert_eq!(path.unwrap().total_cost, 2.0);
assert!(db
.is_connected(a, c, &IsConnectedOptions::default())
.unwrap());
db.close().unwrap();
}
#[test]
fn test_neighbors_paged_basic() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let center = engine
.upsert_node("Person", "center", UpsertNodeOptions::default())
.unwrap();
let mut edge_ids = Vec::new();
for i in 0..8 {
let neighbor = engine
.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions::default())
.unwrap();
let eid = engine
.upsert_edge(center, neighbor, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
edge_ids.push(eid);
}
edge_ids.sort();
let p1 = engine
.neighbors_paged(center, &NeighborOptions::default(), &PageRequest {
limit: Some(3),
after: None,
})
.unwrap();
assert_eq!(p1.items.len(), 3);
let p1_eids: Vec<u64> = p1.items.iter().map(|e| e.edge_id).collect();
assert_eq!(p1_eids, edge_ids[0..3]);
assert!(p1.next_cursor.is_some());
let p2 = engine
.neighbors_paged(center, &NeighborOptions::default(), &PageRequest {
limit: Some(3),
after: p1.next_cursor,
})
.unwrap();
assert_eq!(p2.items.len(), 3);
let p2_eids: Vec<u64> = p2.items.iter().map(|e| e.edge_id).collect();
assert_eq!(p2_eids, edge_ids[3..6]);
assert!(p2.next_cursor.is_some());
let p3 = engine
.neighbors_paged(center, &NeighborOptions::default(), &PageRequest {
limit: Some(3),
after: p2.next_cursor,
})
.unwrap();
assert_eq!(p3.items.len(), 2);
let p3_eids: Vec<u64> = p3.items.iter().map(|e| e.edge_id).collect();
assert_eq!(p3_eids, edge_ids[6..8]);
assert!(p3.next_cursor.is_none());
}
#[test]
fn test_neighbors_paged_cross_source() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let center = engine
.upsert_node("Person", "center", UpsertNodeOptions::default())
.unwrap();
for i in 0..4 {
let n = engine
.upsert_node("Person", &format!("seg{}", i), UpsertNodeOptions::default())
.unwrap();
engine
.upsert_edge(center, n, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
}
engine.flush().unwrap();
for i in 0..4 {
let n = engine
.upsert_node("Person", &format!("mem{}", i), UpsertNodeOptions::default())
.unwrap();
engine
.upsert_edge(center, n, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
}
let mut all_paged: Vec<u64> = Vec::new();
let mut cursor: Option<u64> = None;
loop {
let page = engine
.neighbors_paged(center, &NeighborOptions::default(), &PageRequest {
limit: Some(3),
after: cursor,
})
.unwrap();
all_paged.extend(page.items.iter().map(|e| e.edge_id));
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
assert_eq!(all_paged.len(), 8);
for i in 1..all_paged.len() {
assert!(all_paged[i] > all_paged[i - 1]);
}
}
#[test]
fn test_neighbors_paged_respects_tombstones() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let center = engine
.upsert_node("Person", "center", UpsertNodeOptions::default())
.unwrap();
let n1 = engine.upsert_node("Person", "n1", UpsertNodeOptions::default()).unwrap();
let n2 = engine.upsert_node("Person", "n2", UpsertNodeOptions::default()).unwrap();
let n3 = engine.upsert_node("Person", "n3", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(center, n1, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let e2 = engine
.upsert_edge(center, n2, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(center, n3, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
engine.delete_edge(e2).unwrap();
let result = engine
.neighbors_paged(center, &NeighborOptions::default(), &PageRequest {
limit: Some(10),
after: None,
})
.unwrap();
assert_eq!(result.items.len(), 2);
assert!(result.items.iter().all(|e| e.edge_id != e2));
}
#[test]
fn test_neighbors_paged_with_temporal_filter() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let center = engine
.upsert_node("Person", "center", UpsertNodeOptions::default())
.unwrap();
let n1 = engine.upsert_node("Person", "n1", UpsertNodeOptions::default()).unwrap();
let n2 = engine.upsert_node("Person", "n2", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(center, n1, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(100), valid_to: Some(200), ..Default::default() })
.unwrap();
engine
.upsert_edge(center, n2, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(150), valid_to: Some(300), ..Default::default() })
.unwrap();
let result = engine
.neighbors_paged(center, &NeighborOptions { at_epoch: Some(175), ..Default::default() }, &PageRequest {
limit: Some(10),
after: None,
})
.unwrap();
assert_eq!(result.items.len(), 2);
let result = engine
.neighbors_paged(center, &NeighborOptions { at_epoch: Some(250), ..Default::default() }, &PageRequest {
limit: Some(10),
after: None,
})
.unwrap();
assert_eq!(result.items.len(), 1);
assert_eq!(result.items[0].node_id, n2);
}
#[test]
fn test_neighbors_paged_roundtrip_matches_neighbors() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let center = engine
.upsert_node("Person", "center", UpsertNodeOptions::default())
.unwrap();
for i in 0..15 {
let n = engine
.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions::default())
.unwrap();
engine
.upsert_edge(center, n, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
}
let mut paged_eids: Vec<u64> = Vec::new();
let mut cursor: Option<u64> = None;
loop {
let page = engine
.neighbors_paged(center, &NeighborOptions::default(), &PageRequest {
limit: Some(4),
after: cursor,
})
.unwrap();
paged_eids.extend(page.items.iter().map(|e| e.edge_id));
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
let unpaged = engine
.neighbors(center, &NeighborOptions::default())
.unwrap();
let mut unpaged_eids: Vec<u64> = unpaged.iter().map(|e| e.edge_id).collect();
unpaged_eids.sort();
assert_eq!(paged_eids, unpaged_eids);
}
#[test]
fn test_neighbors_paged_temporal_default_matches_neighbors() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let center = engine
.upsert_node("Person", "center", UpsertNodeOptions::default())
.unwrap();
let n_past = engine.upsert_node("Person", "past", UpsertNodeOptions::default()).unwrap();
let n_current = engine
.upsert_node("Person", "current", UpsertNodeOptions::default())
.unwrap();
let n_future = engine
.upsert_node("Person", "future", UpsertNodeOptions::default())
.unwrap();
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
engine
.upsert_edge(
center, n_past, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(now - 2000), valid_to: Some(now - 1000), ..Default::default() },
)
.unwrap();
engine
.upsert_edge(
center, n_current, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(now - 1000), valid_to: Some(now + 100_000), ..Default::default() },
)
.unwrap();
engine
.upsert_edge(
center, n_future, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(now + 50_000), valid_to: Some(now + 100_000), ..Default::default() },
)
.unwrap();
let unpaged = engine
.neighbors(center, &NeighborOptions::default())
.unwrap();
assert_eq!(unpaged.len(), 1);
assert_eq!(unpaged[0].node_id, n_current);
let paged = engine
.neighbors_paged(center, &NeighborOptions::default(), &PageRequest {
limit: Some(10),
after: None,
})
.unwrap();
assert_eq!(
paged.items.len(),
1,
"paged should filter future/expired edges when at_epoch=None"
);
assert_eq!(paged.items[0].node_id, n_current);
}
#[test]
fn test_neighbors_paged_temporal_cursor_correctness() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let center = engine
.upsert_node("Person", "center", UpsertNodeOptions::default())
.unwrap();
let epoch = 500_000i64;
let mut valid_node_ids = Vec::new();
for i in 0..10u64 {
let n = engine
.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions::default())
.unwrap();
if i % 2 == 0 {
engine
.upsert_edge(
center, n, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(100_000), valid_to: Some(900_000), ..Default::default() },
)
.unwrap();
valid_node_ids.push(n);
} else {
engine
.upsert_edge(
center, n, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(600_000), valid_to: Some(900_000), ..Default::default() },
)
.unwrap();
}
}
let mut paged_nodes: Vec<u64> = Vec::new();
let mut cursor: Option<u64> = None;
let mut page_count = 0;
loop {
let page = engine
.neighbors_paged(center, &NeighborOptions { at_epoch: Some(epoch), ..Default::default() }, &PageRequest {
limit: Some(2),
after: cursor,
})
.unwrap();
paged_nodes.extend(page.items.iter().map(|e| e.node_id));
cursor = page.next_cursor;
page_count += 1;
if cursor.is_none() {
break;
}
}
valid_node_ids.sort();
paged_nodes.sort();
assert_eq!(paged_nodes, valid_node_ids);
assert_eq!(page_count, 3);
}
#[test]
fn test_neighbors_paged_policy_refills_past_sparse_filtered_window() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let center = engine
.upsert_node("Person", "center", UpsertNodeOptions::default())
.unwrap();
let mut visible_neighbors = Vec::new();
for i in 0..17u64 {
let weight = if i < 12 { 0.1 } else { 1.0 };
let node_id = engine
.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions { weight, ..Default::default() })
.unwrap();
engine
.upsert_edge(center, node_id, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
if weight > 0.5 {
visible_neighbors.push(node_id);
}
}
engine
.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
let page1 = engine
.neighbors_paged(center, &NeighborOptions::default(), &PageRequest {
limit: Some(3),
after: None,
})
.unwrap();
assert_eq!(
page1.items.iter().map(|e| e.node_id).collect::<Vec<_>>(),
visible_neighbors[..3].to_vec()
);
assert!(page1.next_cursor.is_some());
let page2 = engine
.neighbors_paged(center, &NeighborOptions::default(), &PageRequest {
limit: Some(3),
after: page1.next_cursor,
})
.unwrap();
assert_eq!(
page2.items.iter().map(|e| e.node_id).collect::<Vec<_>>(),
visible_neighbors[3..].to_vec()
);
assert!(page2.next_cursor.is_none());
}
#[test]
fn test_nodes_by_labels_paged_policy_cursor_correctness() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let mut all_ids = Vec::new();
for i in 0..10 {
let id = engine
.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions::default())
.unwrap();
all_ids.push(id);
}
engine
.set_prune_policy(
"dummy",
PrunePolicy {
max_age_ms: Some(999_999_999),
max_weight: None,
label: Some("SpecialNode999".to_string()), },
)
.unwrap();
let mut collected: Vec<u64> = Vec::new();
let mut cursor: Option<u64> = None;
loop {
let page = engine
.nodes_by_labels_paged("Person",
&PageRequest {
limit: Some(3),
after: cursor,
},
)
.unwrap();
collected.extend(&page.items);
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
all_ids.sort();
collected.sort();
assert_eq!(collected, all_ids);
let deduped: NodeIdSet = collected.iter().copied().collect();
assert_eq!(deduped.len(), all_ids.len());
}
#[test]
fn test_find_nodes_paged_cursor_correctness() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let mut matching_ids = Vec::new();
for i in 0..12 {
let mut props = BTreeMap::new();
props.insert(
"color".to_string(),
PropValue::String(if i % 3 == 0 {
"red".to_string()
} else {
"blue".to_string()
}),
);
let id = engine
.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions { props, ..Default::default() })
.unwrap();
if i % 3 == 0 {
matching_ids.push(id);
}
}
let mut collected: Vec<u64> = Vec::new();
let mut cursor: Option<u64> = None;
loop {
let page = engine
.find_nodes_paged(
"Person",
"color",
&PropValue::String("red".to_string()),
&PageRequest {
limit: Some(2),
after: cursor,
},
)
.unwrap();
collected.extend(&page.items);
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
matching_ids.sort();
collected.sort();
assert_eq!(collected, matching_ids);
let deduped: NodeIdSet = collected.iter().copied().collect();
assert_eq!(deduped.len(), matching_ids.len());
}
#[test]
fn test_find_nodes_paged_cross_source_cursor() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let mut all_matching = Vec::new();
for i in 0..5 {
let mut props = BTreeMap::new();
props.insert("tag".to_string(), PropValue::String("yes".to_string()));
let id = engine
.upsert_node("Person", &format!("pre{}", i), UpsertNodeOptions { props, ..Default::default() })
.unwrap();
all_matching.push(id);
}
engine.flush().unwrap();
for i in 0..5 {
let mut props = BTreeMap::new();
props.insert("tag".to_string(), PropValue::String("yes".to_string()));
let id = engine
.upsert_node("Person", &format!("post{}", i), UpsertNodeOptions { props, ..Default::default() })
.unwrap();
all_matching.push(id);
}
let mut collected: Vec<u64> = Vec::new();
let mut cursor: Option<u64> = None;
loop {
let page = engine
.find_nodes_paged(
"Person",
"tag",
&PropValue::String("yes".to_string()),
&PageRequest {
limit: Some(3),
after: cursor,
},
)
.unwrap();
collected.extend(&page.items);
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
all_matching.sort();
collected.sort();
assert_eq!(collected, all_matching);
}
#[test]
fn test_find_nodes_stale_match_after_update_across_flush() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let mut props = BTreeMap::new();
props.insert("color".to_string(), PropValue::String("red".to_string()));
let n1 = engine.upsert_node("Person", "n1", UpsertNodeOptions { props, ..Default::default() }).unwrap();
engine.flush().unwrap();
let mut props2 = BTreeMap::new();
props2.insert("color".to_string(), PropValue::String("blue".to_string()));
engine.upsert_node("Person", "n1", UpsertNodeOptions { props: props2, ..Default::default() }).unwrap();
let red = engine
.find_nodes("Person", "color", &PropValue::String("red".to_string()))
.unwrap();
assert!(
!red.contains(&n1),
"find_nodes returned stale segment match after update"
);
let blue = engine
.find_nodes("Person", "color", &PropValue::String("blue".to_string()))
.unwrap();
assert!(blue.contains(&n1));
let red_paged = engine
.find_nodes_paged(
"Person",
"color",
&PropValue::String("red".to_string()),
&PageRequest {
limit: Some(10),
after: None,
},
)
.unwrap();
assert!(red_paged.items.is_empty());
let blue_paged = engine
.find_nodes_paged(
"Person",
"color",
&PropValue::String("blue".to_string()),
&PageRequest {
limit: Some(10),
after: None,
},
)
.unwrap();
assert!(blue_paged.items.contains(&n1));
}
#[test]
fn test_find_nodes_stale_match_across_segments() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let mut props = BTreeMap::new();
props.insert("color".to_string(), PropValue::String("red".to_string()));
let n1 = engine.upsert_node("Person", "n1", UpsertNodeOptions { props, ..Default::default() }).unwrap();
engine.flush().unwrap();
let mut props2 = BTreeMap::new();
props2.insert("color".to_string(), PropValue::String("blue".to_string()));
engine.upsert_node("Person", "n1", UpsertNodeOptions { props: props2, ..Default::default() }).unwrap();
engine.flush().unwrap();
let red = engine
.find_nodes("Person", "color", &PropValue::String("red".to_string()))
.unwrap();
assert!(
!red.contains(&n1),
"find_nodes returned stale match from older segment"
);
let blue = engine
.find_nodes("Person", "color", &PropValue::String("blue".to_string()))
.unwrap();
assert!(blue.contains(&n1));
}
#[test]
fn test_traverse_basic_depth_and_order() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let depth1_low = db.upsert_node("Person", "depth1-low", UpsertNodeOptions::default()).unwrap();
let depth1_high = db
.upsert_node("Person", "depth1-high", UpsertNodeOptions::default())
.unwrap();
let depth2_low = db.upsert_node("Person", "depth2-low", UpsertNodeOptions::default()).unwrap();
let depth2_mid = db.upsert_node("Person", "depth2-mid", UpsertNodeOptions::default()).unwrap();
let depth2_high = db.upsert_node("Person", "depth2-high", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, depth1_high, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, depth1_low, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(depth1_high, depth2_high, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(depth1_high, depth2_mid, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(depth1_low, depth2_low, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let result = db
.traverse(start, 2, &TraverseOptions { min_depth: 0, ..Default::default() })
.unwrap();
let ordered: Vec<(u64, u32)> = result.items.iter().map(|hit| (hit.node_id, hit.depth)).collect();
assert_eq!(
ordered,
vec![
(start, 0),
(depth1_low, 1),
(depth1_high, 1),
(depth2_low, 2),
(depth2_mid, 2),
(depth2_high, 2),
]
);
assert_eq!(result.items[0].via_edge_id, None);
assert!(result.items.iter().skip(1).all(|hit| hit.via_edge_id.is_some()));
assert!(result.items.iter().all(|hit| hit.score.is_none()));
assert!(result.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_depth_window_and_limit() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let depth2_only = db
.traverse(a, 2, &TraverseOptions { min_depth: 2, ..Default::default() })
.unwrap();
let depth2_pairs: Vec<(u64, u32)> = depth2_only
.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect();
assert_eq!(depth2_pairs, vec![(c, 2)]);
let limited = db
.traverse(a, 3, &TraverseOptions { decay_lambda: Some(0.5), limit: Some(2), ..Default::default() })
.unwrap();
let limited_pairs: Vec<(u64, u32)> = limited
.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect();
assert_eq!(limited_pairs, vec![(b, 1), (c, 2)]);
assert_eq!(
limited.next_cursor,
Some(TraversalCursor {
depth: 2,
last_node_id: c,
})
);
assert_eq!(limited.items[0].score, Some((-0.5f64).exp()));
assert_eq!(limited.items[1].score, Some((-1.0f64).exp()));
db.close().unwrap();
}
#[test]
fn test_traverse_last_page_cursor_is_none_for_isolated_start() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let page = db
.traverse(start, 3, &TraverseOptions { min_depth: 0, limit: Some(1), ..Default::default() })
.unwrap();
assert_eq!(page.items.len(), 1);
assert_eq!(page.items[0].node_id, start);
assert!(page.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_last_page_cursor_is_none_when_deeper_work_is_filtered_out() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let middle = db.upsert_node("Company", "middle", UpsertNodeOptions::default()).unwrap();
let hidden = db.upsert_node("Company", "hidden", UpsertNodeOptions { weight: 0.2, ..Default::default() }).unwrap();
db.upsert_edge(start, middle, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(middle, hidden, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
let page = db
.traverse(start, 2, &TraverseOptions { emit_node_label_filter: Some(graph_node_label_filter(&["Company"], LabelMatchMode::Any)), limit: Some(1), ..Default::default() })
.unwrap();
assert_eq!(page.items.len(), 1);
assert_eq!(page.items[0].node_id, middle);
assert!(page.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_emit_label_filter_supports_single_any_all_multi_label() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db
.upsert_node("Person", "start", UpsertNodeOptions::default())
.unwrap();
let bridge = db
.upsert_node("Company", "bridge", UpsertNodeOptions::default())
.unwrap();
let article = db
.upsert_node(&["Article", "Featured"], "article", UpsertNodeOptions::default())
.unwrap();
let draft = db
.upsert_node("Article", "draft", UpsertNodeOptions::default())
.unwrap();
db.upsert_edge(start, bridge, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(bridge, article, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(bridge, draft, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let single = db
.traverse(
start,
2,
&TraverseOptions {
emit_node_label_filter: Some(graph_node_label_filter(
&["Article"],
LabelMatchMode::Any,
)),
..Default::default()
},
)
.unwrap();
assert_eq!(
single.items.iter().map(|hit| hit.node_id).collect::<Vec<_>>(),
vec![article, draft]
);
let any = db
.traverse(
start,
2,
&TraverseOptions {
emit_node_label_filter: Some(graph_node_label_filter(
&["Company", "Featured"],
LabelMatchMode::Any,
)),
..Default::default()
},
)
.unwrap();
assert_eq!(
any.items.iter().map(|hit| hit.node_id).collect::<Vec<_>>(),
vec![bridge, article]
);
let all = db
.traverse(
start,
2,
&TraverseOptions {
emit_node_label_filter: Some(graph_node_label_filter(
&["Article", "Featured"],
LabelMatchMode::All,
)),
..Default::default()
},
)
.unwrap();
assert_eq!(all.items.len(), 1);
assert_eq!(all.items[0].node_id, article);
assert_eq!(all.items[0].depth, 2);
db.close().unwrap();
}
#[test]
fn test_traverse_cycle_safe_and_unique() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let result = db
.traverse(a, 3, &TraverseOptions::default())
.unwrap();
let pairs: Vec<(u64, u32)> = result.items.iter().map(|hit| (hit.node_id, hit.depth)).collect();
assert_eq!(pairs, vec![(b, 1), (c, 2), (d, 2)]);
let unique: NodeIdSet = result.items.iter().map(|hit| hit.node_id).collect();
assert_eq!(unique.len(), result.items.len());
assert!(!unique.contains(&a));
db.close().unwrap();
}
#[test]
fn test_traverse_cursor_resume_across_pages_without_dup_or_skip() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let p1 = db
.traverse(start, 2, &TraverseOptions { limit: Some(2), ..Default::default() })
.unwrap();
let p2 = db
.traverse(start, 2, &TraverseOptions { limit: Some(2), cursor: p1.next_cursor.clone(), ..Default::default() })
.unwrap();
let combined: Vec<(u64, u32)> = p1
.items
.iter()
.chain(p2.items.iter())
.map(|hit| (hit.node_id, hit.depth))
.collect();
assert_eq!(combined, vec![(b, 1), (c, 1), (d, 2)]);
assert!(p2.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_cursor_resume_within_same_depth_layer() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let p1 = db
.traverse(start, 1, &TraverseOptions { limit: Some(2), ..Default::default() })
.unwrap();
assert_eq!(
p1.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect::<Vec<_>>(),
vec![(b, 1), (c, 1)]
);
assert_eq!(
p1.next_cursor,
Some(TraversalCursor {
depth: 1,
last_node_id: c,
})
);
let p2 = db
.traverse(start, 1, &TraverseOptions { limit: Some(2), cursor: p1.next_cursor.clone(), ..Default::default() })
.unwrap();
assert_eq!(
p2.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect::<Vec<_>>(),
vec![(d, 1)]
);
assert!(p2.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_exact_page_size_cutoff_is_last_page() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let page = db
.traverse(start, 1, &TraverseOptions { limit: Some(2), ..Default::default() })
.unwrap();
assert_eq!(
page.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect::<Vec<_>>(),
vec![(b, 1), (c, 1)]
);
assert!(page.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_cursor_past_end_returns_empty_page() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let page = db
.traverse(start, 1, &TraverseOptions { min_depth: 0, limit: Some(10), cursor: Some(TraversalCursor {
depth: 99,
last_node_id: u64::MAX,
}), ..Default::default() })
.unwrap();
assert!(page.items.is_empty());
assert!(page.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_edge_filter_and_node_label_filter_is_emission_only() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let middle = db.upsert_node("Company", "middle", UpsertNodeOptions::default()).unwrap();
let target = db.upsert_node("Article", "target", UpsertNodeOptions::default()).unwrap();
let wrong_edge = db.upsert_node("Article", "wrong-edge", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, middle, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(middle, target, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, wrong_edge, "REPORTS_TO", UpsertEdgeOptions::default())
.unwrap();
let result = db
.traverse(start, 2, &TraverseOptions { edge_label_filter: Some(vec!["KNOWS".to_string()]), emit_node_label_filter: Some(graph_node_label_filter(&["Article"], LabelMatchMode::Any)), ..Default::default() })
.unwrap();
let pairs: Vec<(u64, u32)> = result.items.iter().map(|hit| (hit.node_id, hit.depth)).collect();
assert_eq!(pairs, vec![(target, 2)]);
db.close().unwrap();
}
#[test]
fn test_traverse_incoming_and_both_directions() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let incoming = db
.traverse(b, 1, &TraverseOptions { direction: Direction::Incoming, ..Default::default() })
.unwrap();
assert_eq!(
incoming
.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect::<Vec<_>>(),
vec![(a, 1), (c, 1)]
);
let both = db
.traverse(b, 1, &TraverseOptions { direction: Direction::Both, ..Default::default() })
.unwrap();
assert_eq!(
both.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect::<Vec<_>>(),
vec![(a, 1), (c, 1), (d, 1)]
);
db.close().unwrap();
}
#[test]
fn test_traverse_pagination_with_node_label_filter_uses_filtered_path() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let mid_a = db.upsert_node("Company", "mid-a", UpsertNodeOptions::default()).unwrap();
let mid_b = db.upsert_node("Company", "mid-b", UpsertNodeOptions::default()).unwrap();
let hit_a = db.upsert_node("Article", "hit-a", UpsertNodeOptions::default()).unwrap();
let hit_b = db.upsert_node("Article", "hit-b", UpsertNodeOptions::default()).unwrap();
let skip = db.upsert_node("Topic", "skip", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, mid_a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, mid_b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(mid_a, hit_a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(mid_a, skip, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(mid_b, hit_b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let p1 = db
.traverse(start, 2, &TraverseOptions { emit_node_label_filter: Some(graph_node_label_filter(&["Article"], LabelMatchMode::Any)), limit: Some(1), ..Default::default() })
.unwrap();
assert_eq!(
p1.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect::<Vec<_>>(),
vec![(hit_a, 2)]
);
assert_eq!(
p1.next_cursor,
Some(TraversalCursor {
depth: 2,
last_node_id: hit_a,
})
);
let p2 = db
.traverse(start, 2, &TraverseOptions { emit_node_label_filter: Some(graph_node_label_filter(&["Article"], LabelMatchMode::Any)), limit: Some(1), cursor: p1.next_cursor.clone(), ..Default::default() })
.unwrap();
assert_eq!(
p2.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect::<Vec<_>>(),
vec![(hit_b, 2)]
);
assert!(p2.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_respects_deleted_temporal_and_prune_visibility() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let hidden = db.upsert_node("Person", "hidden", UpsertNodeOptions { weight: 0.2, ..Default::default() }).unwrap();
let hidden_target = db
.upsert_node("Person", "hidden-target", UpsertNodeOptions::default())
.unwrap();
let deleted = db.upsert_node("Person", "deleted", UpsertNodeOptions::default()).unwrap();
let future = db.upsert_node("Person", "future", UpsertNodeOptions::default()).unwrap();
let visible = db.upsert_node("Person", "visible", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, hidden, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(0), valid_to: Some(i64::MAX), ..Default::default() })
.unwrap();
db.upsert_edge(
hidden, hidden_target, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(0), valid_to: Some(i64::MAX), ..Default::default() },
)
.unwrap();
db.upsert_edge(start, deleted, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(0), valid_to: Some(i64::MAX), ..Default::default() })
.unwrap();
db.upsert_edge(start, future, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(5_000), valid_to: Some(6_000), ..Default::default() })
.unwrap();
db.upsert_edge(start, visible, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(0), valid_to: Some(i64::MAX), ..Default::default() })
.unwrap();
db.delete_node(deleted).unwrap();
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
let result = db
.traverse(start, 2, &TraverseOptions { at_epoch: Some(1_000), ..Default::default() })
.unwrap();
let pairs: Vec<(u64, u32)> = result.items.iter().map(|hit| (hit.node_id, hit.depth)).collect();
assert_eq!(pairs, vec![(visible, 1)]);
db.close().unwrap();
}
#[test]
fn test_traverse_pagination_with_prune_policy_uses_filtered_path() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let keep_a = db.upsert_node("Person", "keep-a", UpsertNodeOptions::default()).unwrap();
let hidden = db.upsert_node("Person", "hidden", UpsertNodeOptions { weight: 0.2, ..Default::default() }).unwrap();
let keep_b = db.upsert_node("Person", "keep-b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, keep_a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, hidden, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, keep_b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
let p1 = db
.traverse(start, 1, &TraverseOptions { limit: Some(1), ..Default::default() })
.unwrap();
assert_eq!(
p1.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect::<Vec<_>>(),
vec![(keep_a, 1)]
);
assert_eq!(
p1.next_cursor,
Some(TraversalCursor {
depth: 1,
last_node_id: keep_a,
})
);
let p2 = db
.traverse(start, 1, &TraverseOptions { limit: Some(1), cursor: p1.next_cursor.clone(), ..Default::default() })
.unwrap();
assert_eq!(
p2.items
.iter()
.map(|hit| (hit.node_id, hit.depth))
.collect::<Vec<_>>(),
vec![(keep_b, 1)]
);
assert!(p2.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_hidden_or_missing_start_returns_empty() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let hidden_start = db.upsert_node("Person", "hidden-start", UpsertNodeOptions { weight: 0.2, ..Default::default() }).unwrap();
let next = db.upsert_node("Person", "next", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(hidden_start, next, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
let hidden_result = db
.traverse(hidden_start, 1, &TraverseOptions { min_depth: 0, ..Default::default() })
.unwrap();
assert!(hidden_result.items.is_empty());
assert!(hidden_result.next_cursor.is_none());
let missing_result = db
.traverse(999_999, 1, &TraverseOptions { min_depth: 0, ..Default::default() })
.unwrap();
assert!(missing_result.items.is_empty());
assert!(missing_result.next_cursor.is_none());
db.close().unwrap();
}
#[test]
fn test_traverse_flush_compact_and_reopen_parity() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("db");
let db = open_imm(&db_path);
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(start, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let baseline = db
.traverse(start, 2, &TraverseOptions { min_depth: 0, ..Default::default() })
.unwrap();
db.flush().unwrap();
let after_flush = db
.traverse(start, 2, &TraverseOptions { min_depth: 0, ..Default::default() })
.unwrap();
assert_eq!(after_flush, baseline);
db.compact().unwrap();
let after_compact = db
.traverse(start, 2, &TraverseOptions { min_depth: 0, ..Default::default() })
.unwrap();
assert_eq!(after_compact, baseline);
db.close().unwrap();
let reopened = open_imm(&db_path);
let after_reopen = reopened
.traverse(start, 2, &TraverseOptions { min_depth: 0, ..Default::default() })
.unwrap();
assert_eq!(after_reopen, baseline);
reopened.close().unwrap();
}
#[test]
fn test_traverse_via_edge_id_uses_deterministic_tie_break() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("db");
let db = open_imm(&db_path);
let start = db.upsert_node("Person", "start", UpsertNodeOptions::default()).unwrap();
let middle = db.upsert_node("Person", "middle", UpsertNodeOptions::default()).unwrap();
let target = db.upsert_node("Person", "target", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(start, middle, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let older_edge = db
.upsert_edge(middle, target, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
let newer_edge = db
.upsert_edge(middle, target, "BLOCKS", UpsertEdgeOptions::default())
.unwrap();
assert!(older_edge < newer_edge);
let result = db
.traverse(start, 2, &TraverseOptions { min_depth: 2, ..Default::default() })
.unwrap();
assert_eq!(result.items.len(), 1);
assert_eq!(result.items[0].node_id, target);
assert_eq!(result.items[0].via_edge_id, Some(older_edge));
db.close().unwrap();
}
#[allow(clippy::too_many_arguments)]
fn traverse_depth_two_page(
engine: &DatabaseEngine,
start: u64,
direction: Direction,
edge_label_filter: Option<&[&str]>,
node_label_filter: Option<&[&str]>,
at_epoch: Option<i64>,
decay_lambda: Option<f64>,
limit: Option<usize>,
cursor: Option<&TraversalCursor>,
) -> TraversalPageResult {
engine
.traverse(start, 2, &TraverseOptions {
min_depth: 2,
direction,
edge_label_filter: edge_label_filter.map(graph_filter_names),
emit_node_label_filter: node_label_filter
.map(|labels| graph_node_label_filter(labels, LabelMatchMode::Any)),
at_epoch,
decay_lambda,
limit,
cursor: cursor.cloned(),
})
.unwrap()
}
#[test]
fn test_traverse_depth_two_paged_basic() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
let e = engine.upsert_node("Person", "e", UpsertNodeOptions::default()).unwrap();
engine.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
engine.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
engine.upsert_edge(b, d, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
engine.upsert_edge(b, e, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
let p1 = traverse_depth_two_page(
&engine,
a,
Direction::Outgoing,
None,
None,
None,
None,
Some(2),
None,
);
assert_eq!(p1.items.len(), 2);
assert_eq!(p1.items.iter().map(|hit| hit.depth).collect::<Vec<_>>(), vec![2, 2]);
assert!(p1.next_cursor.is_some());
let p2 = traverse_depth_two_page(
&engine,
a,
Direction::Outgoing,
None,
None,
None,
None,
Some(2),
p1.next_cursor.as_ref(),
);
assert_eq!(p2.items.len(), 1);
assert!(p2.next_cursor.is_none());
let mut all_nodes: Vec<u64> = p1
.items
.iter()
.chain(p2.items.iter())
.map(|hit| hit.node_id)
.collect();
all_nodes.sort();
assert_eq!(all_nodes, vec![c, d, e]);
}
#[test]
fn test_traverse_depth_two_paged_roundtrip() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
for i in 0..5 {
let b = engine
.upsert_node("Person", &format!("b{i}"), UpsertNodeOptions::default())
.unwrap();
engine.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
for j in 0..2 {
let c = engine
.upsert_node("Person", &format!("c{i}_{j}"), UpsertNodeOptions::default())
.unwrap();
engine.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
}
}
let all = traverse_depth_two_page(
&engine,
a,
Direction::Outgoing,
None,
None,
None,
None,
None,
None,
);
let mut paged = Vec::new();
let mut cursor = None;
loop {
let page = traverse_depth_two_page(
&engine,
a,
Direction::Outgoing,
None,
None,
None,
None,
Some(3),
cursor.as_ref(),
);
paged.extend(page.items.clone());
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
assert_eq!(paged, all.items);
}
#[test]
fn test_traverse_depth_two_cursor_past_end_returns_empty() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
engine.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
engine.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
let page = traverse_depth_two_page(
&engine,
a,
Direction::Outgoing,
None,
None,
None,
None,
Some(10),
Some(&TraversalCursor {
depth: 2,
last_node_id: u64::MAX - 1,
}),
);
assert!(page.items.is_empty());
assert!(page.next_cursor.is_none());
}
#[test]
fn test_traverse_depth_two_filtered_emission_matches_old_constrained_shape() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Document", "c", UpsertNodeOptions::default()).unwrap();
let d = engine.upsert_node("Document", "d", UpsertNodeOptions::default()).unwrap();
let e = engine.upsert_node("Group", "e", UpsertNodeOptions::default()).unwrap();
engine.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
engine.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
engine.upsert_edge(b, d, "WORKS_AT", UpsertEdgeOptions::default()).unwrap();
engine.upsert_edge(b, e, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
let hits = traverse_depth_two_page(
&engine,
a,
Direction::Outgoing,
Some(&["RELATES_TO"]),
Some(&["Document"]),
None,
None,
None,
None,
);
assert_eq!(hits.items.len(), 1);
assert_eq!(hits.items[0].node_id, c);
}
#[test]
fn test_traverse_depth_two_cross_segment() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
engine.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
engine.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
engine.flush().unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
engine.upsert_edge(b, d, "RELATES_TO", UpsertEdgeOptions::default()).unwrap();
let page = traverse_depth_two_page(
&engine,
a,
Direction::Outgoing,
None,
None,
None,
None,
None,
None,
);
let mut nodes: Vec<u64> = page.items.iter().map(|hit| hit.node_id).collect();
nodes.sort();
assert_eq!(nodes, vec![c, d]);
}
#[test]
fn test_edge_uniqueness_across_flush() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let opts = DbOptions {
edge_uniqueness: true,
..DbOptions::default()
};
let engine = DatabaseEngine::open(&db_path, &opts).unwrap();
let e1 = engine
.upsert_edge(1, 2, "KNOWS", UpsertEdgeOptions { weight: 0.5, ..Default::default() })
.unwrap();
engine.flush().unwrap();
let e2 = engine
.upsert_edge(1, 2, "KNOWS", UpsertEdgeOptions { weight: 0.9, ..Default::default() })
.unwrap();
assert_eq!(e1, e2, "same triple should reuse edge ID across flush");
let edge = engine.get_edge(e1).unwrap().unwrap();
assert!((edge.weight - 0.9).abs() < f32::EPSILON);
let e3 = engine
.upsert_edge(1, 2, "REPORTS_TO", UpsertEdgeOptions::default())
.unwrap();
assert_ne!(e1, e3);
engine.close().unwrap();
}
#[test]
fn test_neighbor_weight_survives_flush() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let n1 = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let n2 = engine.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let eid = engine
.upsert_edge(n1, n2, "RELATES_TO", UpsertEdgeOptions { weight: 0.42, ..Default::default() })
.unwrap();
engine.flush().unwrap();
let nbrs = engine
.neighbors(n1, &NeighborOptions::default())
.unwrap();
assert_eq!(nbrs.len(), 1);
assert_eq!(nbrs[0].edge_id, eid);
assert!(
(nbrs[0].weight - 0.42).abs() < f32::EPSILON,
"weight: {}",
nbrs[0].weight
);
engine.close().unwrap();
}
#[test]
fn test_edge_uniqueness_respects_cross_segment_tombstone() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let opts = DbOptions {
edge_uniqueness: true,
..DbOptions::default()
};
let engine = DatabaseEngine::open(&db_path, &opts).unwrap();
let e1 = engine
.upsert_edge(1, 2, "KNOWS", UpsertEdgeOptions { weight: 0.5, ..Default::default() })
.unwrap();
engine.flush().unwrap();
engine.delete_edge(e1).unwrap();
engine.flush().unwrap();
let e2 = engine
.upsert_edge(1, 2, "KNOWS", UpsertEdgeOptions { weight: 0.9, ..Default::default() })
.unwrap();
assert_ne!(
e1, e2,
"deleted edge should not be resurrected across segments"
);
engine.close().unwrap();
}
#[test]
fn test_top_k_by_weight() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let e = engine.upsert_node("Person", "e", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions { weight: 0.1, ..Default::default() })
.unwrap();
engine
.upsert_edge(a, c, "RELATES_TO", UpsertEdgeOptions { weight: 0.9, ..Default::default() })
.unwrap();
engine
.upsert_edge(a, d, "RELATES_TO", UpsertEdgeOptions { weight: 0.5, ..Default::default() })
.unwrap();
engine
.upsert_edge(a, e, "RELATES_TO", UpsertEdgeOptions { weight: 0.3, ..Default::default() })
.unwrap();
let top2 = engine
.top_k_neighbors(a, 2, &TopKOptions::default())
.unwrap();
assert_eq!(top2.len(), 2);
assert_eq!(top2[0].node_id, c);
assert!((top2[0].weight - 0.9).abs() < 0.01);
assert_eq!(top2[1].node_id, d);
assert!((top2[1].weight - 0.5).abs() < 0.01);
engine.close().unwrap();
}
#[test]
fn test_top_k_by_recency() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions { weight: 1.0, valid_from: Some(1000), valid_to: None, ..Default::default() })
.unwrap();
engine
.upsert_edge(a, c, "RELATES_TO", UpsertEdgeOptions { weight: 1.0, valid_from: Some(3000), valid_to: None, ..Default::default() })
.unwrap();
engine
.upsert_edge(a, d, "RELATES_TO", UpsertEdgeOptions { weight: 1.0, valid_from: Some(2000), valid_to: None, ..Default::default() })
.unwrap();
let top2 = engine
.top_k_neighbors(a, 2, &TopKOptions { scoring: ScoringMode::Recency, ..Default::default() })
.unwrap();
assert_eq!(top2.len(), 2);
assert_eq!(top2[0].node_id, c);
assert_eq!(top2[1].node_id, d);
engine.close().unwrap();
}
#[test]
fn test_top_k_by_decay() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions { weight: 1.0, valid_from: Some(now - 7_200_000), valid_to: None, ..Default::default() })
.unwrap(); engine
.upsert_edge(a, c, "RELATES_TO", UpsertEdgeOptions { weight: 0.8, valid_from: Some(now - 600_000), valid_to: None, ..Default::default() })
.unwrap(); engine
.upsert_edge(a, d, "RELATES_TO", UpsertEdgeOptions { weight: 0.3, valid_from: Some(now - 60_000), valid_to: None, ..Default::default() })
.unwrap();
let top2 = engine
.top_k_neighbors(a, 2, &TopKOptions { scoring: ScoringMode::DecayAdjusted { lambda: 1.0 }, ..Default::default() })
.unwrap();
assert_eq!(top2.len(), 2);
assert_eq!(top2[0].node_id, c);
assert_eq!(top2[1].node_id, d);
engine.close().unwrap();
}
#[test]
fn test_top_k_zero_returns_empty() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let result = engine
.top_k_neighbors(a, 0, &TopKOptions::default())
.unwrap();
assert!(result.is_empty());
engine.close().unwrap();
}
#[test]
fn test_top_k_k_greater_than_neighbors() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions { weight: 0.3, ..Default::default() })
.unwrap();
engine
.upsert_edge(a, c, "RELATES_TO", UpsertEdgeOptions { weight: 0.7, ..Default::default() })
.unwrap();
let result = engine
.top_k_neighbors(a, 10, &TopKOptions::default())
.unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].node_id, c); assert_eq!(result[1].node_id, b);
engine.close().unwrap();
}
#[test]
fn test_top_k_with_label_filter() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions { weight: 0.9, ..Default::default() })
.unwrap();
engine
.upsert_edge(a, c, "WORKS_AT", UpsertEdgeOptions { weight: 0.8, ..Default::default() })
.unwrap();
engine
.upsert_edge(a, d, "RELATES_TO", UpsertEdgeOptions { weight: 0.7, ..Default::default() })
.unwrap();
let result = engine
.top_k_neighbors(a, 1, &TopKOptions { edge_label_filter: Some(vec!["RELATES_TO".to_string()]), ..Default::default() })
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].node_id, b);
engine.close().unwrap();
}
#[test]
fn test_top_k_excludes_deleted_neighbors() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions { weight: 0.9, ..Default::default() })
.unwrap();
engine
.upsert_edge(a, c, "RELATES_TO", UpsertEdgeOptions { weight: 0.8, ..Default::default() })
.unwrap();
engine.delete_node(b).unwrap();
let result = engine
.top_k_neighbors(a, 2, &TopKOptions::default())
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].node_id, c);
engine.close().unwrap();
}
#[test]
fn test_top_k_across_segments() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions { weight: 0.5, ..Default::default() })
.unwrap();
engine.flush().unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
engine
.upsert_edge(a, c, "RELATES_TO", UpsertEdgeOptions { weight: 0.9, ..Default::default() })
.unwrap();
let result = engine
.top_k_neighbors(a, 1, &TopKOptions::default())
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].node_id, c);
engine.close().unwrap();
}
#[test]
fn test_top_k_negative_lambda_returns_error() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let result = engine.top_k_neighbors(a, 5, &TopKOptions { scoring: ScoringMode::DecayAdjusted { lambda: -1.0 }, ..Default::default() });
assert!(result.is_err());
engine.close().unwrap();
}
#[test]
fn test_subgraph_linear_chain_depth_1() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
let e_ab = engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(c, d, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 1, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b]));
assert_eq!(sg.edges.len(), 1);
assert_eq!(sg.edges[0].id, e_ab);
engine.close().unwrap();
}
#[test]
fn test_subgraph_linear_chain_depth_3() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(c, d, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 3, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b, c, d]));
assert_eq!(sg.edges.len(), 3);
engine.close().unwrap();
}
#[test]
fn test_subgraph_diamond_graph() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(a, c, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(b, d, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(c, d, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 2, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b, c, d]));
assert_eq!(sg.edges.len(), 4);
assert_eq!(sg.nodes.iter().filter(|n| n.id == d).count(), 1);
engine.close().unwrap();
}
#[test]
fn test_subgraph_cycle() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let e_ca = engine
.upsert_edge(c, a, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 10, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b, c]));
let edge_ids: NodeIdSet = sg.edges.iter().map(|e| e.id).collect();
assert_eq!(edge_ids.len(), 3);
assert!(edge_ids.contains(&e_ca));
engine.close().unwrap();
}
#[test]
fn test_subgraph_self_loop() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let e_aa = engine
.upsert_edge(a, a, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let e_ab = engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 5, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b]));
let edge_ids: NodeIdSet = sg.edges.iter().map(|e| e.id).collect();
assert!(edge_ids.contains(&e_aa));
assert!(edge_ids.contains(&e_ab));
engine.close().unwrap();
}
#[test]
fn test_subgraph_depth_zero() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 0, &SubgraphOptions::default())
.unwrap();
assert_eq!(sg.nodes.len(), 1);
assert_eq!(sg.nodes[0].id, a);
assert_eq!(sg.edges.len(), 0);
engine.close().unwrap();
}
#[test]
fn test_subgraph_nonexistent_start_node() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let sg = engine
.extract_subgraph(999, 5, &SubgraphOptions::default())
.unwrap();
assert!(sg.nodes.is_empty());
assert!(sg.edges.is_empty());
engine.close().unwrap();
}
#[test]
fn test_subgraph_disconnected_not_reached() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let _c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 10, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b]));
engine.close().unwrap();
}
#[test]
fn test_subgraph_direction_incoming() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(c, 2, &SubgraphOptions { direction: Direction::Incoming, ..Default::default() })
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b, c]));
assert_eq!(sg.edges.len(), 2);
engine.close().unwrap();
}
#[test]
fn test_subgraph_direction_both() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(c, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(b, 1, &SubgraphOptions { direction: Direction::Both, ..Default::default() })
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b, c]));
assert_eq!(sg.edges.len(), 2);
engine.close().unwrap();
}
#[test]
fn test_subgraph_edge_label_filter() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(a, c, "WORKS_AT", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(b, d, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 2, &SubgraphOptions { edge_label_filter: Some(vec!["RELATES_TO".to_string()]), ..Default::default() })
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b, d]));
assert_eq!(sg.edges.len(), 2);
assert!(sg.edges.iter().all(|e| e.label == "RELATES_TO"));
engine.close().unwrap();
}
#[test]
fn test_subgraph_node_label_filter_uses_single_any_all_eligibility() {
let dir = TempDir::new().unwrap();
let engine = DatabaseEngine::open(&dir.path().join("testdb"), &DbOptions::default()).unwrap();
let root = engine
.upsert_node(
&["Person", "Featured"],
"root",
UpsertNodeOptions::default(),
)
.unwrap();
let bridge = engine
.upsert_node("Person", "bridge", UpsertNodeOptions::default())
.unwrap();
let target = engine
.upsert_node(
&["Person", "Featured"],
"target",
UpsertNodeOptions::default(),
)
.unwrap();
let blocked = engine
.upsert_node(
&["Company", "Featured"],
"blocked",
UpsertNodeOptions::default(),
)
.unwrap();
engine
.upsert_edge(root, bridge, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(bridge, target, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(root, target, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(root, blocked, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let person = engine
.extract_subgraph(
root,
2,
&SubgraphOptions {
node_label_filter: Some(graph_node_label_filter(
&["Person"],
LabelMatchMode::Any,
)),
..Default::default()
},
)
.unwrap();
assert_eq!(
person.nodes.iter().map(|node| node.id).collect::<NodeIdSet>(),
NodeIdSet::from_iter([root, bridge, target])
);
assert_eq!(person.edges.len(), 3);
let any_featured_or_company = engine
.extract_subgraph(
root,
1,
&SubgraphOptions {
node_label_filter: Some(graph_node_label_filter(
&["Featured", "Company"],
LabelMatchMode::Any,
)),
..Default::default()
},
)
.unwrap();
assert_eq!(
any_featured_or_company
.nodes
.iter()
.map(|node| node.id)
.collect::<NodeIdSet>(),
NodeIdSet::from_iter([root, target, blocked])
);
assert_eq!(any_featured_or_company.edges.len(), 2);
let all_person_featured = engine
.extract_subgraph(
root,
2,
&SubgraphOptions {
node_label_filter: Some(graph_node_label_filter(
&["Person", "Featured"],
LabelMatchMode::All,
)),
..Default::default()
},
)
.unwrap();
assert_eq!(
all_person_featured
.nodes
.iter()
.map(|node| node.id)
.collect::<NodeIdSet>(),
NodeIdSet::from_iter([root, target])
);
assert_eq!(all_person_featured.edges.len(), 1);
let start_filtered = engine
.extract_subgraph(
bridge,
2,
&SubgraphOptions {
node_label_filter: Some(graph_node_label_filter(
&["Person", "Featured"],
LabelMatchMode::All,
)),
..Default::default()
},
)
.unwrap();
assert!(start_filtered.nodes.is_empty());
assert!(start_filtered.edges.is_empty());
engine.close().unwrap();
}
#[test]
fn test_subgraph_cross_segment() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let opts = DbOptions {
compact_after_n_flushes: 0,
..DbOptions::default()
};
let engine = DatabaseEngine::open(&db_path, &opts).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine.flush().unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 2, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b, c]));
assert_eq!(sg.edges.len(), 2);
engine.close().unwrap();
}
#[test]
fn test_subgraph_with_deleted_node() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine.delete_node(b).unwrap();
let sg = engine
.extract_subgraph(a, 5, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a]));
assert_eq!(sg.edges.len(), 0);
engine.close().unwrap();
}
#[test]
fn test_subgraph_with_deleted_edge() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let e1 = engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(a, c, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine.delete_edge(e1).unwrap();
let sg = engine
.extract_subgraph(a, 1, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, c]));
assert_eq!(sg.edges.len(), 1);
engine.close().unwrap();
}
#[test]
fn test_subgraph_temporal_filter() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions { weight: 1.0, valid_from: Some(100), valid_to: Some(200), ..Default::default() })
.unwrap();
engine
.upsert_edge(a, c, "RELATES_TO", UpsertEdgeOptions { weight: 1.0, valid_from: Some(300), valid_to: Some(400), ..Default::default() })
.unwrap();
let sg_150 = engine
.extract_subgraph(a, 1, &SubgraphOptions { at_epoch: Some(150), ..Default::default() })
.unwrap();
let node_ids_150: NodeIdSet = sg_150.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids_150, NodeIdSet::from_iter([a, b]));
let sg_350 = engine
.extract_subgraph(a, 1, &SubgraphOptions { at_epoch: Some(350), ..Default::default() })
.unwrap();
let node_ids_350: NodeIdSet = sg_350.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids_350, NodeIdSet::from_iter([a, c]));
engine.close().unwrap();
}
#[test]
fn test_subgraph_large_fan_out() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "hub", UpsertNodeOptions::default()).unwrap();
let mut expected = NodeIdSet::from_iter([a]);
for i in 0..50 {
let n = engine
.upsert_node("Person", &format!("spoke_{}", i), UpsertNodeOptions::default())
.unwrap();
engine
.upsert_edge(a, n, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
expected.insert(n);
}
let sg = engine
.extract_subgraph(a, 1, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, expected);
assert_eq!(sg.edges.len(), 50);
engine.close().unwrap();
}
#[test]
fn test_subgraph_depth_limits_traversal() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let a = engine.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = engine.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = engine.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = engine.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
let e = engine.upsert_node("Person", "e", UpsertNodeOptions::default()).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(b, c, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(c, d, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
engine
.upsert_edge(d, e, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 2, &SubgraphOptions::default())
.unwrap();
let node_ids: NodeIdSet = sg.nodes.iter().map(|n| n.id).collect();
assert_eq!(node_ids, NodeIdSet::from_iter([a, b, c]));
assert_eq!(sg.edges.len(), 2);
engine.close().unwrap();
}
#[test]
fn test_subgraph_preserves_node_properties() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("testdb");
let engine = DatabaseEngine::open(&db_path, &DbOptions::default()).unwrap();
let mut props_a = BTreeMap::new();
props_a.insert("name".to_string(), PropValue::String("Alice".to_string()));
let a = engine.upsert_node("Person", "a", UpsertNodeOptions { props: props_a, weight: 0.9, ..Default::default() }).unwrap();
let mut props_b = BTreeMap::new();
props_b.insert("name".to_string(), PropValue::String("Bob".to_string()));
let b = engine.upsert_node("Company", "b", UpsertNodeOptions { props: props_b, weight: 0.7, ..Default::default() }).unwrap();
engine
.upsert_edge(a, b, "RELATES_TO", UpsertEdgeOptions::default())
.unwrap();
let sg = engine
.extract_subgraph(a, 1, &SubgraphOptions::default())
.unwrap();
let node_a = sg.nodes.iter().find(|n| n.id == a).unwrap();
assert_eq!(node_a.labels.as_slice(), ["Person"]);
assert_eq!(node_a.key, "a");
assert_eq!(
node_a.props.get("name"),
Some(&PropValue::String("Alice".to_string()))
);
let node_b = sg.nodes.iter().find(|n| n.id == b).unwrap();
assert_eq!(node_b.labels.as_slice(), ["Company"]);
assert_eq!(
node_b.props.get("name"),
Some(&PropValue::String("Bob".to_string()))
);
engine.close().unwrap();
}
#[test]
fn test_neighbors_batch_basic() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let result = db
.neighbors_batch(&[a, b, c], &NeighborOptions::default())
.unwrap();
assert_eq!(result.get(&a).unwrap().len(), 2); assert_eq!(result.get(&b).unwrap().len(), 1); assert!(!result.contains_key(&c)); db.close().unwrap();
}
#[test]
fn test_neighbors_batch_matches_individual() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let mut ids = Vec::new();
for i in 0..5 {
ids.push(
db.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions { weight: 0.5, ..Default::default() })
.unwrap(),
);
}
db.upsert_edge(ids[0], ids[1], "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(ids[0], ids[2], "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.upsert_edge(ids[1], ids[2], "REPORTS_TO", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
db.upsert_edge(ids[2], ids[3], "KNOWS", UpsertEdgeOptions { weight: 1.5, ..Default::default() })
.unwrap();
db.upsert_edge(ids[3], ids[4], "REPORTS_TO", UpsertEdgeOptions { weight: 0.5, ..Default::default() })
.unwrap();
db.upsert_edge(ids[4], ids[0], "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let batch = db
.neighbors_batch(&ids, &NeighborOptions::default())
.unwrap();
for &nid in &ids {
let individual = db
.neighbors(nid, &NeighborOptions::default())
.unwrap();
let batch_entries = batch.get(&nid).cloned().unwrap_or_default();
let mut ind_edge_ids: Vec<u64> = individual.iter().map(|e| e.edge_id).collect();
let mut bat_edge_ids: Vec<u64> = batch_entries.iter().map(|e| e.edge_id).collect();
ind_edge_ids.sort();
bat_edge_ids.sort();
assert_eq!(ind_edge_ids, bat_edge_ids, "mismatch for node {}", nid);
}
db.close().unwrap();
}
#[test]
fn test_neighbors_batch_with_label_filter() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "REPORTS_TO", UpsertEdgeOptions::default())
.unwrap();
let result = db
.neighbors_batch(&[a], &NeighborOptions { direction: Direction::Outgoing, edge_label_filter: Some(vec!["KNOWS".to_string()]), ..Default::default() })
.unwrap();
assert_eq!(result.get(&a).unwrap().len(), 1);
assert_eq!(result[&a][0].label, "KNOWS".to_string());
db.close().unwrap();
}
#[test]
fn test_neighbors_batch_cross_segment() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() })
.unwrap();
let result = db
.neighbors_batch(&[a, b], &NeighborOptions::default())
.unwrap();
assert_eq!(result.get(&a).unwrap().len(), 2); assert_eq!(result.get(&b).unwrap().len(), 1); db.close().unwrap();
}
#[test]
fn test_neighbors_batch_dedup_across_sources() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 5.0, ..Default::default() })
.unwrap();
let result = db
.neighbors_batch(&[a], &NeighborOptions::default())
.unwrap();
let individual = db
.neighbors(a, &NeighborOptions::default())
.unwrap();
let batch_entries = result.get(&a).unwrap();
assert_eq!(batch_entries.len(), individual.len());
let mut bat_ids: Vec<u64> = batch_entries.iter().map(|e| e.edge_id).collect();
let mut ind_ids: Vec<u64> = individual.iter().map(|e| e.edge_id).collect();
bat_ids.sort();
ind_ids.sort();
assert_eq!(bat_ids, ind_ids);
db.close().unwrap();
}
#[test]
fn test_neighbors_batch_respects_tombstones() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let e1 = db
.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.delete_edge(e1).unwrap();
let result = db
.neighbors_batch(&[a], &NeighborOptions::default())
.unwrap();
let entries = result.get(&a).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].node_id, c);
db.close().unwrap();
}
#[test]
fn test_neighbors_batch_direction_both() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let result = db
.neighbors_batch(&[b], &NeighborOptions { direction: Direction::Both, ..Default::default() })
.unwrap();
let entries = result.get(&b).unwrap();
assert_eq!(entries.len(), 2); db.close().unwrap();
}
#[test]
fn test_neighbors_batch_self_loop_dedup() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let result = db
.neighbors_batch(&[a], &NeighborOptions { direction: Direction::Both, ..Default::default() })
.unwrap();
let individual = db
.neighbors(a, &NeighborOptions { direction: Direction::Both, ..Default::default() })
.unwrap();
let batch_entries = result.get(&a).unwrap();
let mut bat_ids: Vec<u64> = batch_entries.iter().map(|e| e.edge_id).collect();
let mut ind_ids: Vec<u64> = individual.iter().map(|e| e.edge_id).collect();
bat_ids.sort();
ind_ids.sort();
assert_eq!(bat_ids, ind_ids);
assert_eq!(bat_ids.len(), ind_ids.len());
db.close().unwrap();
}
#[test]
fn test_neighbors_batch_empty_input() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let result = db
.neighbors_batch(&[], &NeighborOptions::default())
.unwrap();
assert!(result.is_empty());
}
#[test]
fn test_neighbors_batch_unsorted_input() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions { weight: 0.5, ..Default::default() }).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(c, a, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
let result = db
.neighbors_batch(&[c, a], &NeighborOptions::default())
.unwrap();
assert_eq!(result.get(&a).unwrap().len(), 1);
assert_eq!(result.get(&c).unwrap().len(), 1);
db.close().unwrap();
}
#[test]
fn test_neighbors_batch_large_graph_parity() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let mut node_ids = Vec::new();
for i in 0..50 {
node_ids.push(
db.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions { weight: 0.5, ..Default::default() })
.unwrap(),
);
}
for i in 0..50 {
let t1 = (i + 1) % 50;
let t2 = (i + 25) % 50;
db.upsert_edge(
node_ids[i], node_ids[t1], "KNOWS", UpsertEdgeOptions::default(),
)
.unwrap();
db.upsert_edge(
node_ids[i], node_ids[t2], "REPORTS_TO", UpsertEdgeOptions { weight: 0.5, ..Default::default() },
)
.unwrap();
}
db.flush().unwrap();
for i in 0..10 {
let t = (i + 5) % 50;
db.upsert_edge(
node_ids[i], node_ids[t], "RATES", UpsertEdgeOptions { weight: 0.3, ..Default::default() },
)
.unwrap();
}
let batch = db
.neighbors_batch(&node_ids, &NeighborOptions::default())
.unwrap();
for &nid in &node_ids {
let individual = db
.neighbors(nid, &NeighborOptions::default())
.unwrap();
let batch_entries = batch.get(&nid).cloned().unwrap_or_default();
let mut ind_ids: Vec<u64> = individual.iter().map(|e| e.edge_id).collect();
let mut bat_ids: Vec<u64> = batch_entries.iter().map(|e| e.edge_id).collect();
ind_ids.sort();
bat_ids.sort();
assert_eq!(
ind_ids, bat_ids,
"neighbors_batch mismatch for node {} (individual: {:?}, batch: {:?})",
nid, ind_ids, bat_ids
);
}
db.close().unwrap();
}
#[test]
fn test_self_loop_neighbors_outgoing() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let out = db
.neighbors(a, &NeighborOptions::default())
.unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].node_id, a);
let inc = db
.neighbors(a, &NeighborOptions { direction: Direction::Incoming, ..Default::default() })
.unwrap();
assert_eq!(inc.len(), 1);
assert_eq!(inc[0].node_id, a);
let both = db
.neighbors(a, &NeighborOptions { direction: Direction::Both, ..Default::default() })
.unwrap();
assert_eq!(
both.len(),
1,
"self-loop should appear once in Both, not twice"
);
db.close().unwrap();
}
#[test]
fn test_self_loop_survives_flush_and_compact() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let e = db
.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions { weight: 2.5, ..Default::default() })
.unwrap();
db.flush().unwrap();
let _b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.flush().unwrap();
db.compact().unwrap();
let edge = db.get_edge(e).unwrap().unwrap();
assert_eq!(edge.from, a);
assert_eq!(edge.to, a);
assert_eq!(edge.weight, 2.5);
let nbrs = db
.neighbors(a, &NeighborOptions { direction: Direction::Both, ..Default::default() })
.unwrap();
assert_eq!(nbrs.len(), 1);
let ppr = db
.personalized_pagerank(&[a], &PprOptions::default())
.unwrap();
let a_score = ppr.scores.iter().find(|s| s.0 == a).map(|s| s.1).unwrap();
assert!(a_score > 0.0);
db.close().unwrap();
}
#[test]
fn test_self_loop_in_top_k() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions { weight: 5.0, ..Default::default() })
.unwrap();
let top = db
.top_k_neighbors(a, 10, &TopKOptions::default())
.unwrap();
assert_eq!(top.len(), 1);
assert_eq!(top[0].node_id, a);
assert_eq!(top[0].weight, 5.0);
db.close().unwrap();
}
#[test]
fn test_degree_cache_rebuild_memtable_only() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() }).unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() }).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions { weight: 1.5, ..Default::default() }).unwrap();
let ea = db.degree_cache_entry(a);
assert_eq!(ea.out_degree, 2);
assert_eq!(ea.in_degree, 0);
assert!((ea.out_weight_sum - 5.0).abs() < 1e-10);
assert!((ea.in_weight_sum - 0.0).abs() < 1e-10);
assert_eq!(ea.self_loop_count, 0);
let eb = db.degree_cache_entry(b);
assert_eq!(eb.out_degree, 1);
assert_eq!(eb.in_degree, 1);
assert!((eb.out_weight_sum - 1.5).abs() < 1e-10);
assert!((eb.in_weight_sum - 2.0).abs() < 1e-10);
let ec = db.degree_cache_entry(c);
assert_eq!(ec.out_degree, 0);
assert_eq!(ec.in_degree, 2);
assert!((ec.in_weight_sum - 4.5).abs() < 1e-10);
let ed = db.degree_cache_entry(d);
assert_eq!(ed.out_degree, 0);
assert_eq!(ed.in_degree, 0);
db.close().unwrap();
}
#[test]
fn test_degree_cache_rebuild_segment_only() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
{
let db = open_imm(&path);
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 4.0, ..Default::default() }).unwrap();
db.upsert_edge(b, a, "KNOWS", UpsertEdgeOptions { weight: 2.5, ..Default::default() }).unwrap();
db.flush().unwrap();
db.close().unwrap();
}
let db = open_imm(&path);
let ea = db.degree_cache_entry(1);
assert_eq!(ea.out_degree, 1);
assert_eq!(ea.in_degree, 1);
assert!((ea.out_weight_sum - 4.0).abs() < 1e-10);
assert!((ea.in_weight_sum - 2.5).abs() < 1e-10);
let eb = db.degree_cache_entry(2);
assert_eq!(eb.out_degree, 1);
assert_eq!(eb.in_degree, 1);
assert!((eb.out_weight_sum - 2.5).abs() < 1e-10);
assert!((eb.in_weight_sum - 4.0).abs() < 1e-10);
db.close().unwrap();
}
#[test]
fn test_degree_cache_rebuild_mixed_sources() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
{
let db = open_imm(&path);
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() }).unwrap();
db.close().unwrap();
}
let db = open_imm(&path);
let ea = db.degree_cache_entry(1);
assert_eq!(ea.out_degree, 2); assert!((ea.out_weight_sum - 3.0).abs() < 1e-10);
let eb = db.degree_cache_entry(2);
assert_eq!(eb.in_degree, 1);
let ec = db.degree_cache_entry(3);
assert_eq!(ec.in_degree, 1);
db.close().unwrap();
}
#[test]
fn test_degree_cache_rebuild_with_tombstones() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
{
let db = open_imm(&path);
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let e1 = db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() }).unwrap();
db.flush().unwrap();
db.delete_edge(e1).unwrap();
db.close().unwrap();
}
let db = open_imm(&path);
let ea = db.degree_cache_entry(1);
assert_eq!(ea.out_degree, 1); assert!((ea.out_weight_sum - 2.0).abs() < 1e-10);
let eb = db.degree_cache_entry(2);
assert_eq!(eb.in_degree, 0);
let ec = db.degree_cache_entry(3);
assert_eq!(ec.in_degree, 1);
db.close().unwrap();
}
#[test]
fn test_degree_cache_rebuild_self_loop() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
{
let db = open_imm(&path);
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() }).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.close().unwrap();
}
let db = open_imm(&path);
let ea = db.degree_cache_entry(1);
assert_eq!(ea.out_degree, 2); assert_eq!(ea.in_degree, 1); assert_eq!(ea.self_loop_count, 1);
assert!((ea.self_loop_weight_sum - 3.0).abs() < 1e-10);
let both_degree = (ea.out_degree + ea.in_degree - ea.self_loop_count) as u64;
assert_eq!(both_degree, 2);
assert_eq!(db.degree(1, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), both_degree);
assert_eq!(db.degree(1, &DegreeOptions::default()).unwrap(), ea.out_degree as u64);
assert_eq!(db.degree(1, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), ea.in_degree as u64);
db.close().unwrap();
}
#[test]
fn test_degree_cache_parity_with_walk() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
{
let db = open_imm(&path);
let mut nodes = Vec::new();
for i in 0..10 {
nodes.push(db.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions::default()).unwrap());
}
for i in 0..9 {
db.upsert_edge(nodes[i], nodes[i + 1], "KNOWS", UpsertEdgeOptions { weight: (i as f32) + 0.5, ..Default::default() }).unwrap();
}
for i in 2..10 {
db.upsert_edge(nodes[0], nodes[i], "REPORTS_TO", UpsertEdgeOptions::default()).unwrap();
}
db.upsert_edge(nodes[5], nodes[5], "KNOWS", UpsertEdgeOptions { weight: 7.0, ..Default::default() }).unwrap();
db.flush().unwrap();
db.upsert_edge(nodes[9], nodes[0], "KNOWS", UpsertEdgeOptions { weight: 0.1, ..Default::default() }).unwrap();
db.close().unwrap();
}
let db = open_imm(&path);
for nid in 1..=10 {
let cache = db.degree_cache_entry(nid);
let walk_out_deg = db.degree(nid, &DegreeOptions::default()).unwrap();
let walk_in_deg = db.degree(nid, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap();
let walk_both_deg = db.degree(nid, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap();
let walk_out_w = db.sum_edge_weights(nid, &DegreeOptions::default()).unwrap();
let walk_in_w = db.sum_edge_weights(nid, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap();
assert_eq!(cache.out_degree as u64, walk_out_deg,
"out_degree mismatch for node {}", nid);
assert_eq!(cache.in_degree as u64, walk_in_deg,
"in_degree mismatch for node {}", nid);
let cache_both = (cache.out_degree + cache.in_degree - cache.self_loop_count) as u64;
assert_eq!(cache_both, walk_both_deg,
"both_degree mismatch for node {}", nid);
assert!((cache.out_weight_sum - walk_out_w).abs() < 1e-10,
"out_weight mismatch for node {}", nid);
assert!((cache.in_weight_sum - walk_in_w).abs() < 1e-10,
"in_weight mismatch for node {}", nid);
}
db.close().unwrap();
}
#[test]
fn test_degree_cache_mutation_new_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
assert_eq!(db.degree_cache_entry(a).out_degree, 0);
assert_eq!(db.degree_cache_entry(b).in_degree, 0);
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.5, ..Default::default() }).unwrap();
let ea = db.degree_cache_entry(a);
assert_eq!(ea.out_degree, 1);
assert_eq!(ea.in_degree, 0);
assert!((ea.out_weight_sum - 2.5).abs() < 1e-10);
assert_eq!(ea.self_loop_count, 0);
let eb = db.degree_cache_entry(b);
assert_eq!(eb.out_degree, 0);
assert_eq!(eb.in_degree, 1);
assert!((eb.in_weight_sum - 2.5).abs() < 1e-10);
db.close().unwrap();
}
#[test]
fn test_degree_cache_mutation_update_same_endpoints_weight_change() {
let dir = TempDir::new().unwrap();
let opts = DbOptions { edge_uniqueness: true, ..Default::default() };
let db = DatabaseEngine::open(&dir.path().join("db"), &opts).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() }).unwrap();
assert_eq!(db.degree_cache_entry(a).out_degree, 1);
assert!((db.degree_cache_entry(a).out_weight_sum - 2.0).abs() < 1e-10);
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 5.0, ..Default::default() }).unwrap();
let ea = db.degree_cache_entry(a);
assert_eq!(ea.out_degree, 1, "degree should not change on update");
assert!((ea.out_weight_sum - 5.0).abs() < 1e-10, "weight should update to 5.0");
let eb = db.degree_cache_entry(b);
assert_eq!(eb.in_degree, 1, "in_degree should not change on update");
assert!((eb.in_weight_sum - 5.0).abs() < 1e-10, "in weight should update to 5.0");
db.close().unwrap();
}
#[test]
fn test_degree_cache_mutation_delete_memtable_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let e1 = db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() }).unwrap();
assert_eq!(db.degree_cache_entry(a).out_degree, 1);
db.delete_edge(e1).unwrap();
let ea = db.degree_cache_entry(a);
assert_eq!(ea.out_degree, 0, "out_degree should be 0 after delete");
assert!((ea.out_weight_sum - 0.0).abs() < 1e-10);
let eb = db.degree_cache_entry(b);
assert_eq!(eb.in_degree, 0, "in_degree should be 0 after delete");
db.close().unwrap();
}
#[test]
fn test_degree_cache_mutation_delete_segment_only_edge() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let e1 = db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() }).unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() }).unwrap();
db.flush().unwrap();
assert_eq!(db.degree_cache_entry(a).out_degree, 2);
db.delete_edge(e1).unwrap();
let ea = db.degree_cache_entry(a);
assert_eq!(ea.out_degree, 1, "out_degree should be 1 after segment-only delete");
assert!((ea.out_weight_sum - 3.0).abs() < 1e-10);
let eb = db.degree_cache_entry(b);
assert_eq!(eb.in_degree, 0, "in_degree should be 0 after segment-only delete");
db.close().unwrap();
}
#[test]
fn test_degree_cache_mutation_node_delete_cascade() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(c, a, "KNOWS", UpsertEdgeOptions::default()).unwrap();
assert_eq!(db.degree_cache_entry(a).out_degree, 1);
assert_eq!(db.degree_cache_entry(a).in_degree, 1);
assert_eq!(db.degree_cache_entry(b).out_degree, 1);
assert_eq!(db.degree_cache_entry(b).in_degree, 1);
db.delete_node(b).unwrap();
let ea = db.degree_cache_entry(a);
assert_eq!(ea.out_degree, 0);
assert_eq!(ea.in_degree, 1);
let eb = db.degree_cache_entry(b);
assert_eq!(eb.out_degree, 0);
assert_eq!(eb.in_degree, 0);
let ec = db.degree_cache_entry(c);
assert_eq!(ec.out_degree, 1);
assert_eq!(ec.in_degree, 0);
db.close().unwrap();
}
#[test]
fn test_degree_cache_mutation_self_loop() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let sl = db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions { weight: 4.0, ..Default::default() }).unwrap();
let ea = db.degree_cache_entry(a);
assert_eq!(ea.out_degree, 1);
assert_eq!(ea.in_degree, 1);
assert_eq!(ea.self_loop_count, 1);
assert!((ea.self_loop_weight_sum - 4.0).abs() < 1e-10);
assert_eq!((ea.out_degree + ea.in_degree - ea.self_loop_count) as u64, 1);
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let ea2 = db.degree_cache_entry(a);
assert_eq!(ea2.out_degree, 2);
assert_eq!(ea2.in_degree, 1);
assert_eq!(ea2.self_loop_count, 1);
assert_eq!((ea2.out_degree + ea2.in_degree - ea2.self_loop_count) as u64, 2);
db.delete_edge(sl).unwrap();
let ea3 = db.degree_cache_entry(a);
assert_eq!(ea3.out_degree, 1); assert_eq!(ea3.in_degree, 0);
assert_eq!(ea3.self_loop_count, 0);
assert!((ea3.self_loop_weight_sum - 0.0).abs() < 1e-10);
assert_eq!((ea3.out_degree + ea3.in_degree - ea3.self_loop_count) as u64, 1);
db.close().unwrap();
}
#[test]
fn test_degree_cache_mutation_idempotent_delete() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let e1 = db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.delete_edge(e1).unwrap();
let ea_after_first = db.degree_cache_entry(a);
assert_eq!(ea_after_first.out_degree, 0);
db.delete_edge(e1).unwrap();
let ea_after_second = db.degree_cache_entry(a);
assert_eq!(ea_after_second.out_degree, 0, "double delete should not underflow");
db.close().unwrap();
}
#[test]
fn test_degree_cache_lifecycle_insert_flush_delete_compact_reopen() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
{
let db = open_imm(&path);
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let e1 = db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() }).unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 3.0, ..Default::default() }).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 2);
assert!((db.sum_edge_weights(a, &DegreeOptions::default()).unwrap() - 5.0).abs() < 1e-10);
db.flush().unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
assert_eq!(db.degree(b, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 2);
db.delete_edge(e1).unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1);
assert_eq!(db.degree(b, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), 0);
db.upsert_edge(c, a, "KNOWS", UpsertEdgeOptions { weight: 4.0, ..Default::default() }).unwrap();
db.flush().unwrap();
db.compact().unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 1); assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), 1); assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 2);
assert_eq!(db.degree(c, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 3);
db.close().unwrap();
}
{
let db = open_imm(&path);
assert_eq!(db.degree(1, &DegreeOptions::default()).unwrap(), 1);
assert_eq!(db.degree(1, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), 1);
assert_eq!(db.degree(1, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 2);
assert_eq!(db.degree(3, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap(), 3);
db.close().unwrap();
}
}
#[test]
fn test_degree_cache_filtered_queries_bypass_cache() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(a, c, "REPORTS_TO", UpsertEdgeOptions::default()).unwrap();
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Outgoing, edge_label_filter: Some(vec!["KNOWS".to_string()]), ..Default::default() }).unwrap(), 1);
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Outgoing, edge_label_filter: Some(vec!["REPORTS_TO".to_string()]), ..Default::default() }).unwrap(), 1);
let now = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
assert_eq!(db.degree(a, &DegreeOptions { direction: Direction::Outgoing, at_epoch: Some(now), ..Default::default() }).unwrap(), 2);
db.close().unwrap();
}
#[test]
fn test_degree_cache_temporal_edge_bypasses_cache() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let entry_a = db.degree_cache_entry(a);
assert_eq!(entry_a.temporal_edge_count, 0);
assert_eq!(entry_a.out_degree, 1);
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let future_expiry = now_millis() + 10_000;
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, valid_from: None, valid_to: Some(future_expiry), ..Default::default() })
.unwrap();
let entry_a = db.degree_cache_entry(a);
assert_eq!(entry_a.temporal_edge_count, 1);
assert_eq!(entry_a.out_degree, 2);
let entry_c = db.degree_cache_entry(c);
assert_eq!(entry_c.temporal_edge_count, 1);
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
let entry_b = db.degree_cache_entry(b);
assert_eq!(entry_b.temporal_edge_count, 0);
}
#[test]
fn test_degree_cache_temporal_delete_clears_count() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let future_expiry = now_millis() + 10_000;
let e = db
.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: None, valid_to: Some(future_expiry), ..Default::default() })
.unwrap();
assert_eq!(db.degree_cache_entry(a).temporal_edge_count, 1);
assert_eq!(db.degree_cache_entry(b).temporal_edge_count, 1);
db.delete_edge(e).unwrap();
assert_eq!(db.degree_cache_entry(a).temporal_edge_count, 0);
assert_eq!(db.degree_cache_entry(b).temporal_edge_count, 0);
}
#[test]
fn test_degree_cache_temporal_to_timeless_update() {
let dir = TempDir::new().unwrap();
let opts = DbOptions { edge_uniqueness: true, ..Default::default() };
let db = DatabaseEngine::open(&dir.path().join("db"), &opts).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let future_expiry = now_millis() + 10_000;
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: None, valid_to: Some(future_expiry), ..Default::default() })
.unwrap();
assert_eq!(db.degree_cache_entry(a).temporal_edge_count, 1);
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
assert_eq!(db.degree_cache_entry(a).temporal_edge_count, 0);
assert_eq!(db.degree_cache_entry(b).temporal_edge_count, 0);
}
#[test]
fn test_degree_cache_timeless_to_temporal_update() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let e = db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
assert_eq!(db.degree_cache_entry(a).temporal_edge_count, 0);
db.invalidate_edge(e, 1).unwrap();
assert_eq!(db.degree_cache_entry(a).temporal_edge_count, 1);
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 0);
}
#[test]
fn test_degree_cache_temporal_self_loop() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let future_expiry = now_millis() + 10_000;
let e = db
.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: None, valid_to: Some(future_expiry), ..Default::default() })
.unwrap();
assert_eq!(db.degree_cache_entry(a).temporal_edge_count, 1);
db.delete_edge(e).unwrap();
assert_eq!(db.degree_cache_entry(a).temporal_edge_count, 0);
}
#[test]
fn test_degree_cache_temporal_rebuild_after_flush() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
{
let db = open_imm(&path);
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let future = now_millis() + 100_000;
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions { weight: 2.0, valid_from: None, valid_to: Some(future), ..Default::default() })
.unwrap();
db.flush().unwrap();
db.close().unwrap();
}
{
let db = open_imm(&path);
let entry_a = db.degree_cache_entry(db.get_node_by_key("Person", "a").unwrap().unwrap().id);
assert_eq!(entry_a.temporal_edge_count, 1);
let entry_b = db.degree_cache_entry(db.get_node_by_key("Person", "b").unwrap().unwrap().id);
assert_eq!(entry_b.temporal_edge_count, 0);
let entry_c = db.degree_cache_entry(db.get_node_by_key("Person", "c").unwrap().unwrap().id);
assert_eq!(entry_c.temporal_edge_count, 1);
}
}
#[test]
fn test_degree_cache_expired_edge_not_counted_but_temporal() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: None, valid_to: Some(1), ..Default::default() }).unwrap();
let entry = db.degree_cache_entry(a);
assert_eq!(entry.out_degree, 0); assert_eq!(entry.temporal_edge_count, 1);
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 0);
}
#[test]
fn test_degree_cache_future_dated_edge_is_temporal() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let far_future = i64::MAX - 1_000_000;
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(far_future), valid_to: None, ..Default::default() }).unwrap();
let entry_a = db.degree_cache_entry(a);
let entry_b = db.degree_cache_entry(b);
assert_eq!(entry_a.out_degree, 0);
assert_eq!(entry_b.in_degree, 0);
assert_eq!(entry_a.temporal_edge_count, 1);
assert_eq!(entry_b.temporal_edge_count, 1);
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 0);
assert_eq!(db.degree(b, &DegreeOptions { direction: Direction::Incoming, ..Default::default() }).unwrap(), 0);
db.close().unwrap();
}
#[test]
fn test_degree_cache_future_dated_rebuild_after_flush() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
let b;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let far_future = i64::MAX - 1_000_000;
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(far_future), valid_to: None, ..Default::default() }).unwrap();
db.flush().unwrap();
db.close().unwrap();
}
{
let db = DatabaseEngine::open(&path, &DbOptions::default()).unwrap();
let entry_a = db.degree_cache_entry(a);
let entry_b = db.degree_cache_entry(b);
assert_eq!(entry_a.temporal_edge_count, 1);
assert_eq!(entry_b.temporal_edge_count, 1);
assert_eq!(entry_a.out_degree, 0);
assert_eq!(entry_b.in_degree, 0);
}
}
#[test]
fn test_degree_overlay_explicit_txn_publishes_only_on_commit() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let before = db.active_degree_overlay_for_test();
let mut txn = db.begin_write_txn().unwrap();
txn.upsert_edge(
TxnNodeRef::Id(a),
TxnNodeRef::Id(b),
"KNOWS",
UpsertEdgeOptions {
weight: 2.5,
..Default::default()
},
)
.unwrap();
assert!(std::sync::Arc::ptr_eq(
&before,
&db.active_degree_overlay_for_test()
));
assert_eq!(db.degree_cache_entry(a).out_degree, 0);
txn.commit().unwrap();
let after = db.active_degree_overlay_for_test();
assert!(!std::sync::Arc::ptr_eq(&before, &after));
let entry_a = db.degree_cache_entry(a);
assert_eq!(entry_a.out_degree, 1);
assert!((entry_a.out_weight_sum - 2.5).abs() < 1e-10);
db.close().unwrap();
}
#[test]
fn test_degree_overlay_txn_rollback_and_conflict_publish_no_state() {
let dir = TempDir::new().unwrap();
let opts = DbOptions {
edge_uniqueness: true,
..Default::default()
};
let db = DatabaseEngine::open(&dir.path().join("db"), &opts).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let before_rollback = db.active_degree_overlay_for_test();
let mut rollback_txn = db.begin_write_txn().unwrap();
rollback_txn
.upsert_edge(
TxnNodeRef::Id(a),
TxnNodeRef::Id(b),
"KNOWS",
UpsertEdgeOptions::default(),
)
.unwrap();
rollback_txn.rollback().unwrap();
assert!(std::sync::Arc::ptr_eq(
&before_rollback,
&db.active_degree_overlay_for_test()
));
assert_eq!(db.degree_cache_entry(a).out_degree, 0);
let mut conflicted_txn = db.begin_write_txn().unwrap();
conflicted_txn
.upsert_edge(
TxnNodeRef::Id(a),
TxnNodeRef::Id(b),
"KNOWS",
UpsertEdgeOptions {
weight: 2.0,
..Default::default()
},
)
.unwrap();
db.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 3.0,
..Default::default()
},
)
.unwrap();
let before_conflict = db.active_degree_overlay_for_test();
let err = match conflicted_txn.commit() {
Ok(_) => panic!("expected transaction conflict"),
Err(err) => err,
};
assert!(matches!(err, EngineError::TxnConflict(_)));
assert!(std::sync::Arc::ptr_eq(
&before_conflict,
&db.active_degree_overlay_for_test()
));
let entry_a = db.degree_cache_entry(a);
assert_eq!(entry_a.out_degree, 1);
assert!((entry_a.out_weight_sum - 3.0).abs() < 1e-10);
db.close().unwrap();
}
#[test]
fn test_degree_overlay_stale_read_view_and_node_only_no_overlay_edit() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let overlay_before_node_only = db.active_degree_overlay_for_test();
db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
assert!(std::sync::Arc::ptr_eq(
&overlay_before_node_only,
&db.active_degree_overlay_for_test()
));
let stale = db.published_read_view_for_test();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
assert_eq!(stale.degree_entry_for_test(a).out_degree, 0);
assert_eq!(db.degree_cache_entry(a).out_degree, 1);
db.close().unwrap();
}
#[test]
fn test_degree_overlay_repeated_edge_ids_in_one_wal_batch() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let now = now_millis();
let first = EdgeRecord {
id: 42,
from: a,
to: b,
label_id: 10,
props: std::collections::BTreeMap::new(),
created_at: now,
updated_at: now,
weight: 2.0,
valid_from: 0,
valid_to: i64::MAX,
last_write_seq: 0,
};
let mut second = first.clone();
second.updated_at = now + 1;
second.weight = 5.0;
write_internal_wal_op_batch(&db, &[WalOp::UpsertEdge(first), WalOp::UpsertEdge(second)])
.unwrap();
let entry_a = db.degree_cache_entry(a);
assert_eq!(entry_a.out_degree, 1);
assert!((entry_a.out_weight_sum - 5.0).abs() < 1e-10);
let entry_b = db.degree_cache_entry(b);
assert_eq!(entry_b.in_degree, 1);
assert!((entry_b.in_weight_sum - 5.0).abs() < 1e-10);
db.close().unwrap();
}
#[test]
fn test_degree_overlay_flush_writes_segment_sidecar() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() })
.unwrap();
db.flush().unwrap();
let segments = db.segments_for_test();
assert_eq!(segments.len(), 1);
assert!(segments[0].degree_delta_available());
assert_eq!(segments[0].degree_delta(a).unwrap().out_degree, 1);
assert_eq!(segments[0].degree_delta(b).unwrap().in_degree, 1);
let entry_a = db.degree_cache_entry(a);
assert_eq!(entry_a.out_degree, 1);
assert!((entry_a.out_weight_sum - 2.0).abs() < 1e-10);
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(
a,
c,
"KNOWS",
UpsertEdgeOptions {
weight: 3.0,
..Default::default()
},
)
.unwrap();
let entry_a = db.degree_cache_entry(a);
assert_eq!(entry_a.out_degree, 2);
assert!((entry_a.out_weight_sum - 5.0).abs() < 1e-10);
db.close().unwrap();
}
#[test]
fn test_degree_sidecar_compaction_folds_valid_inputs() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.compact().unwrap();
let segments = db.segments_for_test();
assert_eq!(segments.len(), 1);
assert!(segments[0].degree_delta_available());
assert_eq!(segments[0].degree_delta(a).unwrap().out_degree, 2);
assert_eq!(segments[0].degree_delta(b).unwrap().in_degree, 1);
assert_eq!(segments[0].degree_delta(c).unwrap().in_degree, 1);
db.close().unwrap();
}
#[test]
fn test_degree_sidecar_streaming_compaction_large_overlap_fast_path() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let hub = db
.upsert_node("Person", "hub", UpsertNodeOptions::default())
.unwrap();
let mut leaves = Vec::new();
for idx in 0..320 {
leaves.push(
db.upsert_node("Person", &format!("leaf-{idx}"), UpsertNodeOptions::default())
.unwrap(),
);
}
db.flush().unwrap();
for chunk in leaves.chunks(80) {
for &leaf in chunk {
db.upsert_edge(
hub,
leaf,
"KNOWS",
UpsertEdgeOptions {
weight: 1.5,
..Default::default()
},
)
.unwrap();
}
db.flush().unwrap();
}
db.compact().unwrap();
let segments = db.segments_for_test();
assert_eq!(segments.len(), 1);
assert!(segments[0].degree_delta_available());
assert_eq!(segments[0].degree_delta(hub).unwrap().out_degree, 320);
assert_scalar_degree_family_routes(
&db,
hub,
DegreeOptions::default(),
320,
480.0,
Some(1.5),
3,
0,
);
db.close().unwrap();
}
#[test]
fn test_degree_sidecar_compaction_folds_update_and_delete_deltas() {
let dir = TempDir::new().unwrap();
let opts = DbOptions {
edge_uniqueness: true,
..Default::default()
};
let db = DatabaseEngine::open(&dir.path().join("db"), &opts).unwrap();
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 2.0,
..Default::default()
},
)
.unwrap();
let ac = db
.upsert_edge(
a,
c,
"KNOWS",
UpsertEdgeOptions {
weight: 4.0,
..Default::default()
},
)
.unwrap();
db.flush().unwrap();
db.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 5.0,
..Default::default()
},
)
.unwrap();
db.delete_edge(ac).unwrap();
db.flush().unwrap();
db.compact().unwrap();
let segments = db.segments_for_test();
assert_eq!(segments.len(), 1);
assert!(segments[0].degree_delta_available());
let a_delta = segments[0].degree_delta(a).unwrap();
assert_eq!(a_delta.out_degree, 1);
assert!((a_delta.out_weight_sum - 5.0).abs() < 1e-10);
let b_delta = segments[0].degree_delta(b).unwrap();
assert_eq!(b_delta.in_degree, 1);
assert!((b_delta.in_weight_sum - 5.0).abs() < 1e-10);
assert_eq!(
segments[0].degree_delta(c).unwrap(),
crate::degree_cache::DegreeDelta::ZERO
);
db.close().unwrap();
}
#[test]
fn test_degree_sidecar_compaction_omits_when_input_unavailable() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
let b;
let c;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.close().unwrap();
}
std::fs::remove_file(segment_dir(&path, 1).join(crate::degree_cache::DEGREE_DELTA_FILENAME))
.unwrap();
let db = open_imm(&path);
assert!(db
.segments_for_test()
.iter()
.any(|segment| !segment.degree_delta_available()));
db.compact().unwrap();
let segments = db.segments_for_test();
assert_eq!(segments.len(), 1);
assert!(!segments[0].degree_delta_available());
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
db.close().unwrap();
}
#[test]
fn test_degree_sidecar_compaction_omits_when_prune_policy_active() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.set_prune_policy(
"active_but_no_match",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.0),
label: None,
},
)
.unwrap();
db.compact().unwrap();
let segments = db.segments_for_test();
assert_eq!(segments.len(), 1);
assert!(!segments[0].degree_delta_available());
assert_eq!(db.degree(a, &DegreeOptions::default()).unwrap(), 2);
db.close().unwrap();
}
#[allow(clippy::too_many_arguments)]
fn assert_scalar_degree_family_routes(
db: &DatabaseEngine,
node_id: u64,
options: DegreeOptions,
expected_degree: u64,
expected_sum: f64,
expected_avg: Option<f64>,
expected_fast: usize,
expected_walk: usize,
) {
db.reset_degree_query_routes();
assert_eq!(db.degree(node_id, &options).unwrap(), expected_degree);
assert!((db.sum_edge_weights(node_id, &options).unwrap() - expected_sum).abs() < 1e-10);
match (db.avg_edge_weight(node_id, &options).unwrap(), expected_avg) {
(Some(actual), Some(expected)) => assert!((actual - expected).abs() < 1e-10),
(None, None) => {}
(actual, expected) => panic!("avg mismatch: actual={actual:?}, expected={expected:?}"),
}
let routes = db.degree_query_route_snapshot();
assert_eq!(routes.fast_path, expected_fast);
assert_eq!(routes.walk_path, expected_walk);
}
fn assert_degrees_batch_routes(
db: &DatabaseEngine,
node_ids: &[u64],
options: DegreeOptions,
expected: &[(u64, u64)],
expected_fast: usize,
expected_walk: usize,
) {
db.reset_degree_query_routes();
let degrees = db.degrees(node_ids, &options).unwrap();
assert_eq!(degrees.len(), expected.len());
for &(node_id, degree) in expected {
assert_eq!(degrees.get(&node_id), Some(°ree));
}
let routes = db.degree_query_route_snapshot();
assert_eq!(routes.fast_path, expected_fast);
assert_eq!(routes.walk_path, expected_walk);
}
fn assert_degree_family_fast_matches_forced_walk(
db: &DatabaseEngine,
node_id: u64,
direction: Direction,
) {
db.reset_degree_query_routes();
let fast_options = DegreeOptions {
direction,
..Default::default()
};
let fast_degree = db.degree(node_id, &fast_options).unwrap();
let fast_sum = db.sum_edge_weights(node_id, &fast_options).unwrap();
let fast_avg = db.avg_edge_weight(node_id, &fast_options).unwrap();
let routes = db.degree_query_route_snapshot();
assert_eq!(routes.fast_path, 3);
assert_eq!(routes.walk_path, 0);
db.reset_degree_query_routes();
let walk_options = DegreeOptions {
direction,
at_epoch: Some(now_millis()),
..Default::default()
};
let walk_degree = db.degree(node_id, &walk_options).unwrap();
let walk_sum = db.sum_edge_weights(node_id, &walk_options).unwrap();
let walk_avg = db.avg_edge_weight(node_id, &walk_options).unwrap();
let routes = db.degree_query_route_snapshot();
assert_eq!(routes.fast_path, 0);
assert_eq!(routes.walk_path, 3);
assert_eq!(fast_degree, walk_degree);
assert!((fast_sum - walk_sum).abs() < 1e-10);
match (fast_avg, walk_avg) {
(Some(fast), Some(walk)) => assert!((fast - walk).abs() < 1e-10),
(None, None) => {}
(fast, walk) => panic!("avg mismatch: fast={fast:?}, walk={walk:?}"),
}
}
fn unique_node_count(node_ids: &[u64]) -> usize {
let mut unique = node_ids.to_vec();
unique.sort_unstable();
unique.dedup();
unique.len()
}
fn assert_degrees_batch_fast_matches_forced_walk(
db: &DatabaseEngine,
node_ids: &[u64],
direction: Direction,
) {
let unique_count = unique_node_count(node_ids);
let fast_options = DegreeOptions {
direction,
..Default::default()
};
db.reset_degree_query_routes();
let fast = db.degrees(node_ids, &fast_options).unwrap();
let routes = db.degree_query_route_snapshot();
assert_eq!(routes.fast_path, unique_count);
assert_eq!(routes.walk_path, 0);
let walk_options = DegreeOptions {
direction,
at_epoch: Some(now_millis()),
..Default::default()
};
db.reset_degree_query_routes();
let walk = db.degrees(node_ids, &walk_options).unwrap();
let routes = db.degree_query_route_snapshot();
assert_eq!(routes.fast_path, 0);
assert_eq!(routes.walk_path, unique_count);
assert_eq!(fast, walk);
}
fn assert_degree_family_all_directions_fast_match_walk(
db: &DatabaseEngine,
node_id: u64,
batch_node_ids: &[u64],
) {
for direction in [Direction::Outgoing, Direction::Incoming, Direction::Both] {
assert_degree_family_fast_matches_forced_walk(db, node_id, direction);
assert_degrees_batch_fast_matches_forced_walk(db, batch_node_ids, direction);
}
}
fn historical_temporal_edge(id: u64, from: u64, to: u64, weight: f32) -> EdgeRecord {
EdgeRecord {
id,
from,
to,
label_id: 10,
props: std::collections::BTreeMap::new(),
created_at: 1_000,
updated_at: 1_500,
weight,
valid_from: 1_000,
valid_to: 2_000,
last_write_seq: 0,
}
}
fn current_timeless_edge(id: u64, from: u64, to: u64, weight: f32) -> EdgeRecord {
let now = now_millis();
EdgeRecord {
id,
from,
to,
label_id: 10,
props: std::collections::BTreeMap::new(),
created_at: 1_000,
updated_at: now,
weight,
valid_from: 1_000,
valid_to: i64::MAX,
last_write_seq: 0,
}
}
#[test]
fn test_degree_fast_path_scalar_route_matrix() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 2.0,
..Default::default()
},
)
.unwrap();
db.upsert_edge(
a,
c,
"REPORTS_TO",
UpsertEdgeOptions {
weight: 3.0,
..Default::default()
},
)
.unwrap();
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
2,
5.0,
Some(2.5),
3,
0,
);
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions {
edge_label_filter: Some(vec!["KNOWS".to_string()]),
..Default::default()
},
1,
2.0,
Some(2.0),
0,
3,
);
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions {
at_epoch: Some(now_millis()),
..Default::default()
},
2,
5.0,
Some(2.5),
0,
3,
);
db.set_prune_policy(
"active_but_no_match",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.0),
label: None,
},
)
.unwrap();
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
2,
5.0,
Some(2.5),
0,
3,
);
db.close().unwrap();
}
#[test]
fn test_degree_fast_path_reads_frozen_overlay_while_flush_in_flight() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 2.0,
..Default::default()
},
)
.unwrap();
db.freeze_memtable().unwrap();
assert_eq!(db.immutable_epoch_count(), 1);
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
1,
2.0,
Some(2.0),
3,
0,
);
let (ready_rx, release_tx) = db.set_flush_pause();
db.enqueue_one_flush().unwrap();
ready_rx
.recv_timeout(std::time::Duration::from_secs(5))
.unwrap();
assert_eq!(db.in_flight_count(), 1);
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
1,
2.0,
Some(2.0),
3,
0,
);
release_tx.send(()).unwrap();
db.wait_one_flush().unwrap();
assert_eq!(db.immutable_epoch_count(), 0);
db.close().unwrap();
}
#[test]
fn test_degree_fast_path_walks_when_sidecar_unavailable_or_temporal() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 2.0,
..Default::default()
},
)
.unwrap();
db.flush().unwrap();
db.close().unwrap();
}
std::fs::remove_file(segment_dir(&path, 1).join(crate::degree_cache::DEGREE_DELTA_FILENAME))
.unwrap();
let db = open_imm(&path);
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
1,
2.0,
Some(2.0),
0,
3,
);
db.close().unwrap();
let temporal_dir = TempDir::new().unwrap();
let temporal = open_imm(&temporal_dir.path().join("db"));
let x = temporal
.upsert_node("Person", "x", UpsertNodeOptions::default())
.unwrap();
let y = temporal
.upsert_node("Person", "y", UpsertNodeOptions::default())
.unwrap();
temporal
.upsert_edge(
x,
y,
"KNOWS",
UpsertEdgeOptions {
weight: 4.0,
valid_to: Some(now_millis() + 10_000),
..Default::default()
},
)
.unwrap();
assert_scalar_degree_family_routes(
&temporal,
x,
DegreeOptions::default(),
1,
4.0,
Some(4.0),
0,
3,
);
temporal.close().unwrap();
}
#[test]
fn test_degree_fast_path_deleting_expired_temporal_edge_reverses_original_delta() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
write_internal_wal_op(&db, &WalOp::UpsertEdge(historical_temporal_edge(42, a, b, 2.0)))
.unwrap();
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
0,
0.0,
None,
0,
3,
);
db.flush().unwrap();
write_internal_wal_op(&db, &WalOp::DeleteEdge {
id: 42,
deleted_at: now_millis(),
})
.unwrap();
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
0,
0.0,
None,
3,
0,
);
db.flush().unwrap();
db.close().unwrap();
}
let reopened = open_imm(&path);
assert_scalar_degree_family_routes(
&reopened,
a,
DegreeOptions::default(),
0,
0.0,
None,
3,
0,
);
reopened.close().unwrap();
}
#[test]
fn test_degree_fast_path_updating_expired_temporal_edge_reverses_original_delta() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
write_internal_wal_op(&db, &WalOp::UpsertEdge(historical_temporal_edge(42, a, b, 2.0)))
.unwrap();
db.flush().unwrap();
write_internal_wal_op(&db, &WalOp::UpsertEdge(current_timeless_edge(42, a, b, 5.0)))
.unwrap();
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
1,
5.0,
Some(5.0),
3,
0,
);
db.flush().unwrap();
db.close().unwrap();
}
let reopened = open_imm(&path);
assert_scalar_degree_family_routes(
&reopened,
a,
DegreeOptions::default(),
1,
5.0,
Some(5.0),
3,
0,
);
reopened.close().unwrap();
}
#[test]
fn test_degrees_fast_path_batch_routes_all_fast_all_walk_and_mixed() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, c, "REPORTS_TO", UpsertEdgeOptions::default())
.unwrap();
db.reset_degree_query_routes();
let degrees = db
.degrees(&[c, a, b, a], &DegreeOptions::default())
.unwrap();
assert_eq!(degrees.get(&a), Some(&2));
assert!(!degrees.contains_key(&b));
assert!(!degrees.contains_key(&c));
let routes = db.degree_query_route_snapshot();
assert_eq!(routes.fast_path, 3);
assert_eq!(routes.walk_path, 0);
db.reset_degree_query_routes();
let degrees = db
.degrees(
&[a, b, c],
&DegreeOptions {
edge_label_filter: Some(vec!["KNOWS".to_string()]),
..Default::default()
},
)
.unwrap();
assert_eq!(degrees.get(&a), Some(&1));
let routes = db.degree_query_route_snapshot();
assert_eq!(routes.fast_path, 0);
assert_eq!(routes.walk_path, 3);
db.upsert_edge(
b,
c,
"KNOWS",
UpsertEdgeOptions {
valid_to: Some(now_millis() + 10_000),
..Default::default()
},
)
.unwrap();
db.reset_degree_query_routes();
let degrees = db.degrees(&[a, b, c], &DegreeOptions::default()).unwrap();
assert_eq!(degrees.get(&a), Some(&2));
assert_eq!(degrees.get(&b), Some(&1));
assert!(!degrees.contains_key(&c));
let routes = db.degree_query_route_snapshot();
assert_eq!(routes.fast_path, 1);
assert_eq!(routes.walk_path, 2);
db.close().unwrap();
}
#[test]
fn test_degree_fast_path_matches_forced_walk_across_source_states_and_directions() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
let b;
let c;
let d;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 2.0,
..Default::default()
},
)
.unwrap();
db.upsert_edge(
c,
a,
"KNOWS",
UpsertEdgeOptions {
weight: 6.0,
..Default::default()
},
)
.unwrap();
db.upsert_edge(
a,
a,
"KNOWS",
UpsertEdgeOptions {
weight: 4.0,
..Default::default()
},
)
.unwrap();
let initial_batch = [a, b, c, a];
assert_degree_family_all_directions_fast_match_walk(&db, a, &initial_batch);
db.freeze_memtable().unwrap();
assert_eq!(db.immutable_epoch_count(), 1);
assert_degree_family_all_directions_fast_match_walk(&db, a, &initial_batch);
db.flush().unwrap();
assert_eq!(db.segments_for_test().len(), 1);
assert_degree_family_all_directions_fast_match_walk(&db, a, &initial_batch);
d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(
a,
d,
"KNOWS",
UpsertEdgeOptions {
weight: 8.0,
..Default::default()
},
)
.unwrap();
let full_batch = [a, b, c, d, a];
assert_degree_family_all_directions_fast_match_walk(&db, a, &full_batch);
db.flush().unwrap();
assert_eq!(db.segments_for_test().len(), 2);
assert_degree_family_all_directions_fast_match_walk(&db, a, &full_batch);
db.compact().unwrap();
assert_eq!(db.segments_for_test().len(), 1);
assert_degree_family_all_directions_fast_match_walk(&db, a, &full_batch);
db.close().unwrap();
}
let reopened = open_imm(&path);
let full_batch = [a, b, c, d, a];
assert_degree_family_all_directions_fast_match_walk(&reopened, a, &full_batch);
reopened.close().unwrap();
}
#[test]
fn test_degree_fast_path_parity_across_sources_flush_compact_reopen() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 2.0,
..Default::default()
},
)
.unwrap();
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
1,
2.0,
Some(2.0),
3,
0,
);
db.flush().unwrap();
db.upsert_edge(
a,
c,
"KNOWS",
UpsertEdgeOptions {
weight: 3.0,
..Default::default()
},
)
.unwrap();
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
2,
5.0,
Some(2.5),
3,
0,
);
db.flush().unwrap();
db.upsert_edge(
a,
d,
"KNOWS",
UpsertEdgeOptions {
weight: 4.0,
..Default::default()
},
)
.unwrap();
db.flush().unwrap();
assert_eq!(db.segments_for_test().len(), 3);
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
3,
9.0,
Some(3.0),
3,
0,
);
db.compact().unwrap();
assert_eq!(db.segments_for_test().len(), 1);
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
3,
9.0,
Some(3.0),
3,
0,
);
db.close().unwrap();
}
let reopened = open_imm(&path);
assert_scalar_degree_family_routes(
&reopened,
a,
DegreeOptions::default(),
3,
9.0,
Some(3.0),
3,
0,
);
reopened.close().unwrap();
}
#[test]
fn test_degree_sidecar_missing_fallback_is_degree_only_and_no_backfill() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
let b;
let c;
let edge_ab;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
edge_ab = db
.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 2.0,
..Default::default()
},
)
.unwrap();
db.upsert_edge(
a,
c,
"KNOWS",
UpsertEdgeOptions {
weight: 3.0,
..Default::default()
},
)
.unwrap();
db.flush().unwrap();
db.close().unwrap();
}
let sidecar_path =
segment_dir(&path, 1).join(crate::degree_cache::DEGREE_DELTA_FILENAME);
assert!(sidecar_path.exists());
std::fs::remove_file(&sidecar_path).unwrap();
let db = open_imm(&path);
assert!(!sidecar_path.exists());
let segments = db.segments_for_test();
assert_eq!(segments.len(), 1);
assert!(!segments[0].degree_delta_available());
assert!(db.get_node(a).unwrap().is_some());
assert!(db.get_edge(edge_ab).unwrap().is_some());
assert_eq!(
db.neighbors(
a,
&NeighborOptions {
direction: Direction::Outgoing,
..Default::default()
},
)
.unwrap()
.len(),
2
);
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
2,
5.0,
Some(2.5),
0,
3,
);
assert_degrees_batch_routes(
&db,
&[c, a, b, a],
DegreeOptions::default(),
&[(a, 2)],
0,
3,
);
assert!(!sidecar_path.exists(), "open/query must not backfill a missing degree sidecar");
db.close().unwrap();
}
#[test]
fn test_degree_sidecar_corrupt_fallback_is_degree_only_and_no_repair() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
let b;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(
a,
b,
"KNOWS",
UpsertEdgeOptions {
weight: 4.0,
..Default::default()
},
)
.unwrap();
db.flush().unwrap();
db.close().unwrap();
}
let sidecar_path =
segment_dir(&path, 1).join(crate::degree_cache::DEGREE_DELTA_FILENAME);
std::fs::write(&sidecar_path, b"not a degree sidecar").unwrap();
let db = open_imm(&path);
assert!(sidecar_path.exists());
let segments = db.segments_for_test();
assert_eq!(segments.len(), 1);
assert!(!segments[0].degree_delta_available());
assert!(db.get_node(b).unwrap().is_some());
assert_eq!(
db.neighbors(
a,
&NeighborOptions {
direction: Direction::Outgoing,
..Default::default()
},
)
.unwrap()
.len(),
1
);
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
1,
4.0,
Some(4.0),
0,
3,
);
assert_degrees_batch_routes(
&db,
&[a, b, a],
DegreeOptions::default(),
&[(a, 1)],
0,
2,
);
assert_eq!(
std::fs::read(&sidecar_path).unwrap(),
b"not a degree sidecar",
"open/query must not repair a corrupt degree sidecar"
);
db.close().unwrap();
}
#[test]
fn test_degree_sidecar_compaction_omits_when_input_corrupt() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("db");
let a;
{
let db = open_imm(&path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.upsert_edge(a, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.close().unwrap();
}
std::fs::write(
segment_dir(&path, 1).join(crate::degree_cache::DEGREE_DELTA_FILENAME),
b"not a degree sidecar",
)
.unwrap();
let db = open_imm(&path);
assert!(db
.segments_for_test()
.iter()
.any(|segment| !segment.degree_delta_available()));
db.compact().unwrap();
let segments = db.segments_for_test();
assert_eq!(segments.len(), 1);
assert!(!segments[0].degree_delta_available());
assert_scalar_degree_family_routes(
&db,
a,
DegreeOptions::default(),
2,
2.0,
Some(1.0),
0,
3,
);
db.close().unwrap();
}
#[test]
fn test_degree_cache_batch_matches_individual() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let mut nodes = Vec::new();
for i in 0..5 {
nodes.push(db.upsert_node("Person", &format!("n{}", i), UpsertNodeOptions::default()).unwrap());
}
for i in 1..5 {
db.upsert_edge(nodes[0], nodes[i], "KNOWS", UpsertEdgeOptions::default()).unwrap();
}
db.upsert_edge(nodes[2], nodes[2], "KNOWS", UpsertEdgeOptions { weight: 2.0, ..Default::default() }).unwrap();
let batch = db.degrees(&nodes, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap();
for &nid in &nodes {
let individual = db.degree(nid, &DegreeOptions { direction: Direction::Both, ..Default::default() }).unwrap();
let batch_deg = batch.get(&nid).copied().unwrap_or(0);
assert_eq!(individual, batch_deg,
"batch vs individual mismatch for node {}", nid);
}
db.close().unwrap();
}
#[test]
fn test_wcc_single_component() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps.len(), 3);
let comp_id = comps[&a];
assert_eq!(comp_id, a);
assert_eq!(comps[&b], comp_id);
assert_eq!(comps[&c], comp_id);
db.close().unwrap();
}
#[test]
fn test_wcc_multiple_components() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
let e = db.upsert_node("Person", "e", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(d, e, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps.len(), 5);
assert_eq!(comps[&a], a); assert_eq!(comps[&b], a);
assert_eq!(comps[&c], c); assert_eq!(comps[&d], c);
assert_eq!(comps[&e], c);
let unique_comps: NodeIdSet = comps.values().copied().collect();
assert_eq!(unique_comps.len(), 2);
db.close().unwrap();
}
#[test]
fn test_wcc_isolated_nodes() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps.len(), 3);
assert_eq!(comps[&a], a);
assert_eq!(comps[&b], b);
assert_eq!(comps[&c], c);
let unique_comps: NodeIdSet = comps.values().copied().collect();
assert_eq!(unique_comps.len(), 3);
db.close().unwrap();
}
#[test]
fn test_wcc_self_loops() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps.len(), 2);
assert_eq!(comps[&a], a); assert_eq!(comps[&b], b);
db.close().unwrap();
}
#[test]
fn test_wcc_parallel_edges() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(a, b, "REPORTS_TO", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, a, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps.len(), 3);
assert_eq!(comps[&a], a);
assert_eq!(comps[&b], a); assert_eq!(comps[&c], c);
db.close().unwrap();
}
#[test]
fn test_wcc_deleted_nodes_and_edges() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let e1 = db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.delete_edge(e1).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps[&a], a); assert_ne!(comps[&a], comps[&b]); assert_eq!(comps[&b], comps[&c]);
db.delete_node(b).unwrap();
let comps2 = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps2.len(), 2); assert!(!comps2.contains_key(&b));
assert_eq!(comps2[&a], a);
assert_eq!(comps2[&c], c);
db.close().unwrap();
}
#[test]
fn test_wcc_edge_label_filter() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "REPORTS_TO", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions { edge_label_filter: Some(vec!["KNOWS".to_string()]), ..Default::default() }).unwrap();
assert_eq!(comps[&a], a);
assert_eq!(comps[&b], a);
assert_eq!(comps[&c], c);
let comps2 = db.connected_components(&ComponentOptions { edge_label_filter: Some(vec!["REPORTS_TO".to_string()]), ..Default::default() }).unwrap();
assert_eq!(comps2[&a], a); assert_eq!(comps2[&b], comps2[&c]);
db.close().unwrap();
}
#[test]
fn test_wcc_node_label_filter() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Company", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions { node_label_filter: Some(graph_node_label_filter(&["Person"], LabelMatchMode::Any)), ..Default::default() }).unwrap();
assert_eq!(comps.len(), 2);
assert_eq!(comps[&a], a);
assert_eq!(comps[&b], a);
assert!(!comps.contains_key(&c));
let comps2 = db.connected_components(&ComponentOptions { node_label_filter: Some(graph_node_label_filter(&["Company"], LabelMatchMode::Any)), ..Default::default() }).unwrap();
assert_eq!(comps2.len(), 1);
assert_eq!(comps2[&c], c);
db.close().unwrap();
}
#[test]
fn test_wcc_and_component_of_node_label_filter_support_any_all_multi_label() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db
.upsert_node(
&["Person", "Employee"],
"a",
UpsertNodeOptions::default(),
)
.unwrap();
let b = db
.upsert_node(
&["Person", "Employee"],
"b",
UpsertNodeOptions::default(),
)
.unwrap();
let c = db
.upsert_node("Person", "c", UpsertNodeOptions::default())
.unwrap();
let d = db
.upsert_node("Employee", "d", UpsertNodeOptions::default())
.unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
let all_filter = graph_node_label_filter(&["Person", "Employee"], LabelMatchMode::All);
let all_components = db
.connected_components(&ComponentOptions {
node_label_filter: Some(all_filter.clone()),
..Default::default()
})
.unwrap();
assert_eq!(all_components.len(), 2);
assert_eq!(all_components[&a], a);
assert_eq!(all_components[&b], a);
assert!(!all_components.contains_key(&c));
assert!(!all_components.contains_key(&d));
assert_eq!(
db.component_of(
a,
&ComponentOptions {
node_label_filter: Some(all_filter),
..Default::default()
},
)
.unwrap(),
vec![a, b]
);
let any_filter = graph_node_label_filter(&["Employee"], LabelMatchMode::Any);
let any_components = db
.connected_components(&ComponentOptions {
node_label_filter: Some(any_filter.clone()),
..Default::default()
})
.unwrap();
assert_eq!(any_components.len(), 3);
assert_eq!(any_components[&a], a);
assert_eq!(any_components[&b], a);
assert_eq!(any_components[&d], a);
assert!(!any_components.contains_key(&c));
assert_eq!(
db.component_of(
a,
&ComponentOptions {
node_label_filter: Some(any_filter),
..Default::default()
},
)
.unwrap(),
vec![a, b, d]
);
db.close().unwrap();
}
#[test]
fn test_wcc_after_flush() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps[&a], a);
assert_eq!(comps[&b], a);
assert_eq!(comps[&c], a);
db.close().unwrap();
}
#[test]
fn test_wcc_after_compaction() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.compact().unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps[&a], a);
assert_eq!(comps[&b], a);
assert_eq!(comps[&c], a);
db.close().unwrap();
}
#[test]
fn test_wcc_after_close_reopen() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("db");
{
let db = open_imm(&db_path);
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.close().unwrap();
}
let db = open_imm(&db_path);
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps.len(), 3);
let unique_comps: NodeIdSet = comps.values().copied().collect();
assert_eq!(unique_comps.len(), 1);
}
#[test]
fn test_wcc_empty_graph() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert!(comps.is_empty());
}
#[test]
fn test_wcc_direction_ignored() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(c, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
let comp = comps[&a];
assert_eq!(comps[&b], comp);
assert_eq!(comps[&c], comp);
db.close().unwrap();
}
#[test]
fn test_wcc_deterministic_component_ids() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(c, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, a, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps1 = db.connected_components(&ComponentOptions::default()).unwrap();
let comps2 = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps1, comps2);
assert_eq!(comps1[&a], a);
assert_eq!(comps1[&b], a);
assert_eq!(comps1[&c], a);
db.close().unwrap();
}
#[test]
fn test_wcc_prune_policy_hides_nodes() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.1, ..Default::default() }).unwrap(); let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps.len(), 2);
assert!(!comps.contains_key(&b));
assert_eq!(comps[&a], a);
assert_eq!(comps[&c], c);
db.close().unwrap();
}
#[test]
fn test_wcc_unconstrained_prune_policy_checks_unique_multi_label_nodes_once() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db
.upsert_node(
&["Person", "Employee"],
"a",
UpsertNodeOptions::default(),
)
.unwrap();
let b = db
.upsert_node(
&["Person", "Manager"],
"b",
UpsertNodeOptions {
weight: 0.1,
..Default::default()
},
)
.unwrap();
let c = db
.upsert_node(
&["Company", "Employee"],
"c",
UpsertNodeOptions::default(),
)
.unwrap();
let d = db
.upsert_node("Company", "d", UpsertNodeOptions::default())
.unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.upsert_edge(a, d, "KNOWS", UpsertEdgeOptions::default())
.unwrap();
db.flush().unwrap();
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
)
.unwrap();
db.reset_query_execution_counters_for_test();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
let counters = db.query_execution_counter_snapshot_for_test();
assert_eq!(comps.len(), 3);
assert!(!comps.contains_key(&b));
assert_eq!(comps[&a], a);
assert_eq!(comps[&d], a);
assert_eq!(comps[&c], c);
assert_eq!(
counters.node_visibility_meta_reads, 4,
"unconstrained WCC prune filtering should verify once per unique node, not once per label membership"
);
db.close().unwrap();
}
#[test]
fn test_wcc_memtable_only() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps.len(), 4);
assert_eq!(comps[&a], a);
assert_eq!(comps[&b], a);
assert_eq!(comps[&c], c);
assert_eq!(comps[&d], c);
db.close().unwrap();
}
#[test]
fn test_component_of_basic() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let mut members = db.component_of(a, &ComponentOptions::default()).unwrap();
members.sort_unstable();
assert_eq!(members, vec![a, b, c]);
let mut members_b = db.component_of(b, &ComponentOptions::default()).unwrap();
members_b.sort_unstable();
assert_eq!(members_b, vec![a, b, c]);
let members_d = db.component_of(d, &ComponentOptions::default()).unwrap();
assert_eq!(members_d, vec![d]);
db.close().unwrap();
}
#[test]
fn test_component_of_missing_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let members = db.component_of(99999, &ComponentOptions::default()).unwrap();
assert!(members.is_empty());
db.close().unwrap();
}
#[test]
fn test_component_of_deleted_node() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.delete_node(a).unwrap();
let members = db.component_of(a, &ComponentOptions::default()).unwrap();
assert!(members.is_empty());
let members_b = db.component_of(b, &ComponentOptions::default()).unwrap();
assert_eq!(members_b, vec![b]);
db.close().unwrap();
}
#[test]
fn test_component_of_node_label_filter() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Company", "b", UpsertNodeOptions::default()).unwrap(); let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let members = db.component_of(a, &ComponentOptions { node_label_filter: Some(graph_node_label_filter(&["Person"], LabelMatchMode::Any)), ..Default::default() }).unwrap();
assert_eq!(members, vec![a]);
let members_b = db.component_of(b, &ComponentOptions { node_label_filter: Some(graph_node_label_filter(&["Person"], LabelMatchMode::Any)), ..Default::default() }).unwrap();
assert!(members_b.is_empty());
db.close().unwrap();
}
#[test]
fn test_component_of_edge_label_filter() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "REPORTS_TO", UpsertEdgeOptions::default()).unwrap();
let members = db.component_of(a, &ComponentOptions { edge_label_filter: Some(vec!["KNOWS".to_string()]), ..Default::default() }).unwrap();
assert_eq!(members, vec![a, b]);
db.close().unwrap();
}
#[test]
fn test_component_of_after_flush() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let members = db.component_of(a, &ComponentOptions::default()).unwrap();
assert_eq!(members, vec![a, b, c]);
db.close().unwrap();
}
#[test]
fn test_component_of_after_compaction() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.compact().unwrap();
let members = db.component_of(c, &ComponentOptions::default()).unwrap();
assert_eq!(members, vec![a, b, c]);
db.close().unwrap();
}
#[test]
fn test_component_of_close_reopen() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("db");
let a;
let b;
let c;
{
let db = open_imm(&db_path);
a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.flush().unwrap();
db.close().unwrap();
}
let db = open_imm(&db_path);
let members = db.component_of(b, &ComponentOptions::default()).unwrap();
assert_eq!(members, vec![a, b, c]);
}
#[test]
fn test_component_of_prune_policy() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions { weight: 0.1, ..Default::default() }).unwrap(); let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.set_prune_policy(
"low_weight",
PrunePolicy {
max_age_ms: None,
max_weight: Some(0.5),
label: None,
},
).unwrap();
let members_a = db.component_of(a, &ComponentOptions::default()).unwrap();
assert_eq!(members_a, vec![a]);
let members_c = db.component_of(c, &ComponentOptions::default()).unwrap();
assert_eq!(members_c, vec![c]);
let members_b = db.component_of(b, &ComponentOptions::default()).unwrap();
assert!(members_b.is_empty());
db.close().unwrap();
}
#[test]
fn test_wcc_agrees_with_component_of() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
let e = db.upsert_node("Person", "e", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(d, e, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
for &node in &[a, b, c, d, e] {
let members = db.component_of(node, &ComponentOptions::default()).unwrap();
let comp_id = comps[&node];
for &member in &members {
assert_eq!(comps[&member], comp_id,
"component_of({}) member {} has different comp ID", node, member);
}
let wcc_count = comps.values().filter(|&&v| v == comp_id).count();
assert_eq!(wcc_count, members.len(),
"component_of({}) size mismatch with WCC", node);
}
db.close().unwrap();
}
#[test]
fn test_wcc_agrees_with_component_of_filtered() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Company", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(b, c, "REPORTS_TO", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions::default()).unwrap();
db.upsert_edge(a, d, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let ntf = Some(&["Person"][..]);
let etf = Some(&["KNOWS"][..]);
let comps = db.connected_components(&ComponentOptions { edge_label_filter: etf.map(graph_filter_names), node_label_filter: ntf.map(|labels| graph_node_label_filter(labels, LabelMatchMode::Any)), ..Default::default() }).unwrap();
for &node in &[a, c, d] {
let members = db.component_of(node, &ComponentOptions { edge_label_filter: etf.map(graph_filter_names), node_label_filter: ntf.map(|labels| graph_node_label_filter(labels, LabelMatchMode::Any)), ..Default::default() }).unwrap();
let comp_id = comps[&node];
for &member in &members {
assert_eq!(comps[&member], comp_id,
"filtered component_of({}) member {} disagrees with WCC", node, member);
}
let wcc_count = comps.values().filter(|&&v| v == comp_id).count();
assert_eq!(wcc_count, members.len(),
"filtered component_of({}) size mismatch with WCC", node);
}
db.close().unwrap();
}
#[test]
fn test_component_of_self_loop() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, a, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let members = db.component_of(a, &ComponentOptions::default()).unwrap();
assert_eq!(members, vec![a]);
db.close().unwrap();
}
#[test]
fn test_component_of_undirected_reachability() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let members = db.component_of(b, &ComponentOptions::default()).unwrap();
assert_eq!(members, vec![a, b]);
db.close().unwrap();
}
#[test]
fn test_wcc_at_epoch_temporal_filtering() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
let d = db.upsert_node("Person", "d", UpsertNodeOptions::default()).unwrap();
let now = now_millis();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(0), valid_to: Some(1000), ..Default::default() }).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions::default()).unwrap();
let future = now + 100_000_000;
db.upsert_edge(c, d, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(future), valid_to: None, ..Default::default() }).unwrap();
let comps = db.connected_components(&ComponentOptions::default()).unwrap();
assert_eq!(comps[&a], a, "A should be isolated (expired edge)");
assert_eq!(comps[&b], comps[&c], "B and C should be connected");
assert_ne!(comps[&a], comps[&b], "A should not be in B-C component");
assert_eq!(comps[&d], d, "D should be isolated (future edge)");
db.close().unwrap();
let dir2 = TempDir::new().unwrap();
let db2 = open_imm(&dir2.path().join("db"));
let a2 = db2.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b2 = db2.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c2 = db2.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db2.upsert_edge(a2, b2, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(0), valid_to: Some(1000), ..Default::default() }).unwrap();
db2.upsert_edge(b2, c2, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(0), valid_to: None, ..Default::default() }).unwrap();
let comps_t500 = db2.connected_components(&ComponentOptions { at_epoch: Some(500), ..Default::default() }).unwrap();
assert_eq!(comps_t500[&a2], comps_t500[&b2], "A-B connected at epoch 500");
assert_eq!(comps_t500[&b2], comps_t500[&c2], "B-C connected at epoch 500");
let comps_t2000 = db2.connected_components(&ComponentOptions { at_epoch: Some(2000), ..Default::default() }).unwrap();
assert_eq!(comps_t2000[&a2], a2, "A isolated at epoch 2000");
assert_eq!(comps_t2000[&b2], comps_t2000[&c2], "B-C connected at epoch 2000");
assert_ne!(comps_t2000[&a2], comps_t2000[&b2], "A not in B-C component at epoch 2000");
db2.close().unwrap();
}
#[test]
fn test_component_of_at_epoch_temporal_filtering() {
let dir = TempDir::new().unwrap();
let db = open_imm(&dir.path().join("db"));
let a = db.upsert_node("Person", "a", UpsertNodeOptions::default()).unwrap();
let b = db.upsert_node("Person", "b", UpsertNodeOptions::default()).unwrap();
let c = db.upsert_node("Person", "c", UpsertNodeOptions::default()).unwrap();
db.upsert_edge(a, b, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(0), valid_to: Some(1000), ..Default::default() }).unwrap();
db.upsert_edge(b, c, "KNOWS", UpsertEdgeOptions { weight: 1.0, valid_from: Some(0), valid_to: None, ..Default::default() }).unwrap();
let members_a = db.component_of(a, &ComponentOptions { at_epoch: Some(2000), ..Default::default() }).unwrap();
assert_eq!(members_a, vec![a], "A isolated at epoch 2000");
let mut members_b = db.component_of(b, &ComponentOptions { at_epoch: Some(2000), ..Default::default() }).unwrap();
members_b.sort_unstable();
assert_eq!(members_b, vec![b, c], "B-C connected at epoch 2000");
let mut members_a_t500 = db.component_of(a, &ComponentOptions { at_epoch: Some(500), ..Default::default() }).unwrap();
members_a_t500.sort_unstable();
assert_eq!(members_a_t500, vec![a, b, c], "A-B-C connected at epoch 500");
db.close().unwrap();
}