use sqlitegraph::{
SqliteGraphError,
backend::native::v3::V3Backend,
backend::{EdgeSpec, GraphBackend, NodeSpec},
};
use tempfile::TempDir;
fn create_v3_backend() -> (V3Backend, TempDir) {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test_v3.graph");
let backend = V3Backend::create(&db_path).unwrap();
(backend, temp_dir)
}
fn build_chain_graph(backend: &V3Backend) -> Vec<i64> {
let mut nodes = Vec::new();
for i in 1..=4 {
let id = backend
.insert_node(NodeSpec {
kind: "Node".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
nodes.push(id);
}
for i in 0..nodes.len() - 1 {
backend
.insert_edge(EdgeSpec {
from: nodes[i],
to: nodes[i + 1],
edge_type: "links_to".to_string(),
data: serde_json::json!({}),
})
.unwrap();
}
nodes
}
fn build_star_graph(backend: &V3Backend) -> (i64, Vec<i64>) {
let center = backend
.insert_node(NodeSpec {
kind: "Center".to_string(),
name: "center".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let mut leaves = Vec::new();
for i in 1..=3 {
let leaf = backend
.insert_node(NodeSpec {
kind: "Leaf".to_string(),
name: format!("leaf_{}", i),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: leaf,
edge_type: "links_to".to_string(),
data: serde_json::json!({}),
})
.unwrap();
leaves.push(leaf);
}
(center, leaves)
}
fn build_cycle_graph(backend: &V3Backend) -> Vec<i64> {
let mut nodes = Vec::new();
for i in 1..=3 {
let id = backend
.insert_node(NodeSpec {
kind: "Node".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
nodes.push(id);
}
for i in 0..nodes.len() {
let next = (i + 1) % nodes.len();
backend
.insert_edge(EdgeSpec {
from: nodes[i],
to: nodes[next],
edge_type: "links_to".to_string(),
data: serde_json::json!({}),
})
.unwrap();
}
nodes
}
#[test]
fn test_v3_entity_ids_basic() {
let (backend, _temp) = create_v3_backend();
let ids = backend.entity_ids().unwrap();
assert!(ids.is_empty(), "New database should have no entities");
let node1 = backend
.insert_node(NodeSpec {
kind: "Test".to_string(),
name: "A".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let node2 = backend
.insert_node(NodeSpec {
kind: "Test".to_string(),
name: "B".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let ids = backend.entity_ids().unwrap();
assert_eq!(ids.len(), 2, "Should have 2 entities");
assert!(ids.contains(&node1), "Should contain node1");
assert!(ids.contains(&node2), "Should contain node2");
}
#[test]
fn test_v3_fetch_outgoing() {
let (backend, _temp) = create_v3_backend();
let nodes = build_chain_graph(&backend);
let out_0 = backend.fetch_outgoing(nodes[0]).unwrap();
assert_eq!(out_0.len(), 1, "Node 0 should have 1 outgoing edge");
assert!(out_0.contains(&nodes[1]), "Node 0 should point to node 1");
let out_1 = backend.fetch_outgoing(nodes[1]).unwrap();
assert_eq!(out_1.len(), 1, "Node 1 should have 1 outgoing edge");
assert!(out_1.contains(&nodes[2]), "Node 1 should point to node 2");
let out_3 = backend.fetch_outgoing(nodes[3]).unwrap();
assert!(out_3.is_empty(), "Node 3 should have no outgoing edges");
}
#[test]
fn test_v3_fetch_incoming() {
let (backend, _temp) = create_v3_backend();
let nodes = build_chain_graph(&backend);
let in_0 = backend.fetch_incoming(nodes[0]).unwrap();
assert!(in_0.is_empty(), "Node 0 should have no incoming edges");
let in_1 = backend.fetch_incoming(nodes[1]).unwrap();
assert_eq!(in_1.len(), 1, "Node 1 should have 1 incoming edge");
assert!(
in_1.contains(&nodes[0]),
"Node 1 should be pointed to by node 0"
);
let in_3 = backend.fetch_incoming(nodes[3]).unwrap();
assert_eq!(in_3.len(), 1, "Node 3 should have 1 incoming edge");
assert!(
in_3.contains(&nodes[2]),
"Node 3 should be pointed to by node 2"
);
}
#[test]
fn test_v3_pagerank_via_trait() {
let (backend, _temp) = create_v3_backend();
let nodes = build_chain_graph(&backend);
let all_ids = backend.entity_ids().unwrap();
let n = all_ids.len();
assert_eq!(n, 4, "Should have 4 nodes");
let mut scores: std::collections::HashMap<i64, f64> =
all_ids.iter().map(|&id| (id, 1.0 / n as f64)).collect();
let mut outgoing_counts: std::collections::HashMap<i64, usize> =
std::collections::HashMap::new();
for &id in &all_ids {
let count = backend.fetch_outgoing(id).unwrap().len();
outgoing_counts.insert(id, count);
}
let damping = 0.85;
for _ in 0..20 {
let mut new_scores: std::collections::HashMap<i64, f64> = std::collections::HashMap::new();
let base_score = (1.0 - damping) / n as f64;
for &id in &all_ids {
new_scores.insert(id, base_score);
}
let mut dangling_score = 0.0;
for &id in &all_ids {
let score = scores[&id];
let out_count = outgoing_counts[&id];
if out_count == 0 {
dangling_score += score;
} else {
let share = score / out_count as f64;
for &neighbor in &backend.fetch_outgoing(id).unwrap() {
*new_scores.get_mut(&neighbor).unwrap() += damping * share;
}
}
}
let dangling_share = damping * dangling_score / n as f64;
for (_, score) in new_scores.iter_mut() {
*score += dangling_share;
}
scores = new_scores;
}
let total: f64 = scores.values().sum();
assert!(
(total - 1.0).abs() < 0.01,
"Scores should sum to ~1.0, got {}",
total
);
assert!(
scores[&nodes[3]] > scores[&nodes[0]],
"End of chain should have higher score than start"
);
}
#[test]
fn test_v3_bfs_via_trait() {
let (backend, _temp) = create_v3_backend();
let nodes = build_chain_graph(&backend);
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
let mut result = Vec::new();
queue.push_back(nodes[0]);
visited.insert(nodes[0]);
while let Some(node) = queue.pop_front() {
result.push(node);
for neighbor in backend.fetch_outgoing(node).unwrap() {
if visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
assert_eq!(result.len(), 4, "BFS should visit all 4 nodes");
assert_eq!(result[0], nodes[0]);
assert_eq!(result[1], nodes[1]);
assert_eq!(result[2], nodes[2]);
assert_eq!(result[3], nodes[3]);
}
#[test]
fn test_v3_scc_cycle_via_trait() {
let (backend, _temp) = create_v3_backend();
let nodes = build_cycle_graph(&backend);
for i in 0..nodes.len() {
let outgoing = backend.fetch_outgoing(nodes[i]).unwrap();
assert_eq!(outgoing.len(), 1, "Each node should have 1 outgoing");
let next = (i + 1) % nodes.len();
assert_eq!(
outgoing[0], nodes[next],
"Should point to next node in cycle"
);
}
for node in &nodes {
let incoming = backend.fetch_incoming(*node).unwrap();
assert_eq!(
incoming.len(),
1,
"Each node should have 1 incoming in cycle"
);
}
}
#[test]
fn test_v3_star_topology() {
let (backend, _temp) = create_v3_backend();
let (center, leaves) = build_star_graph(&backend);
let center_out = backend.fetch_outgoing(center).unwrap();
assert_eq!(center_out.len(), 3, "Center should have 3 outgoing edges");
for leaf in &leaves {
assert!(
center_out.contains(leaf),
"Center should point to all leaves"
);
}
let center_in = backend.fetch_incoming(center).unwrap();
assert!(center_in.is_empty(), "Center should have no incoming edges");
for leaf in &leaves {
let leaf_out = backend.fetch_outgoing(*leaf).unwrap();
assert!(leaf_out.is_empty(), "Leaf should have no outgoing edges");
let leaf_in = backend.fetch_incoming(*leaf).unwrap();
assert_eq!(leaf_in.len(), 1, "Leaf should have 1 incoming edge");
assert_eq!(leaf_in[0], center, "Leaf should be pointed to by center");
}
}
#[test]
fn test_v3_shortest_path_via_trait() {
let (backend, _temp) = create_v3_backend();
let nodes = build_chain_graph(&backend);
let start = nodes[0];
let end = nodes[3];
let mut distances: std::collections::HashMap<i64, usize> = std::collections::HashMap::new();
let mut predecessors: std::collections::HashMap<i64, i64> = std::collections::HashMap::new();
let mut queue = std::collections::VecDeque::new();
distances.insert(start, 0);
queue.push_back(start);
while let Some(node) = queue.pop_front() {
let current_dist = distances[&node];
for neighbor in backend.fetch_outgoing(node).unwrap() {
if !distances.contains_key(&neighbor) {
distances.insert(neighbor, current_dist + 1);
predecessors.insert(neighbor, node);
queue.push_back(neighbor);
}
}
}
assert!(
distances.contains_key(&end),
"Should have found path to end node"
);
assert_eq!(distances[&end], 3, "Path length should be 3 edges");
let mut path = vec![end];
let mut current = end;
while let Some(&pred) = predecessors.get(¤t) {
path.push(pred);
current = pred;
}
path.reverse();
assert_eq!(path.len(), 4, "Path should have 4 nodes");
assert_eq!(path[0], nodes[0]);
assert_eq!(path[1], nodes[1]);
assert_eq!(path[2], nodes[2]);
assert_eq!(path[3], nodes[3]);
}
#[test]
fn test_v3_edge_type_filtering() {
use sqlitegraph::{
backend::{BackendDirection, NeighborQuery},
snapshot::SnapshotId,
};
let (backend, _temp) = create_v3_backend();
let center = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "main".to_string(),
file_path: Some("/src/main.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
let helper1 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "helper1".to_string(),
file_path: Some("/src/helper1.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
let helper2 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "helper2".to_string(),
file_path: Some("/src/helper2.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
let util1 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "util1".to_string(),
file_path: Some("/src/util1.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper1,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper2,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: util1,
edge_type: "USES".to_string(),
data: serde_json::json!({}),
})
.unwrap();
let all_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
},
)
.unwrap();
assert_eq!(
all_neighbors.len(),
3,
"Should have 3 neighbors without filter"
);
assert!(all_neighbors.contains(&helper1), "Should contain helper1");
assert!(all_neighbors.contains(&helper2), "Should contain helper2");
assert!(all_neighbors.contains(&util1), "Should contain util1");
let calls_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("CALLS".to_string()),
},
)
.unwrap();
assert_eq!(calls_neighbors.len(), 2, "Should have 2 CALLS neighbors");
assert!(
calls_neighbors.contains(&helper1),
"Should contain helper1 (CALLS)"
);
assert!(
calls_neighbors.contains(&helper2),
"Should contain helper2 (CALLS)"
);
assert!(
!calls_neighbors.contains(&util1),
"Should NOT contain util1 (not CALLS)"
);
let uses_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("USES".to_string()),
},
)
.unwrap();
assert_eq!(uses_neighbors.len(), 1, "Should have 1 USES neighbor");
assert!(
uses_neighbors.contains(&util1),
"Should contain util1 (USES)"
);
assert!(
!uses_neighbors.contains(&helper1),
"Should NOT contain helper1 (not USES)"
);
assert!(
!uses_neighbors.contains(&helper2),
"Should NOT contain helper2 (not USES)"
);
let empty_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("NONEXISTENT".to_string()),
},
)
.unwrap();
assert_eq!(
empty_neighbors.len(),
0,
"Should have 0 neighbors for non-existent edge type"
);
let incoming_calls = backend
.neighbors(
SnapshotId::current(),
helper1,
NeighborQuery {
direction: BackendDirection::Incoming,
edge_type: Some("CALLS".to_string()),
},
)
.unwrap();
assert_eq!(
incoming_calls.len(),
1,
"helper1 should have 1 incoming CALLS"
);
assert!(
incoming_calls.contains(¢er),
"helper1 should be called by center"
);
let incoming_uses = backend
.neighbors(
SnapshotId::current(),
helper1,
NeighborQuery {
direction: BackendDirection::Incoming,
edge_type: Some("USES".to_string()),
},
)
.unwrap();
assert_eq!(
incoming_uses.len(),
0,
"helper1 should have 0 incoming USES"
);
}
#[test]
fn test_v3_edge_type_durability_across_reopen() {
use sqlitegraph::{
backend::{BackendDirection, NeighborQuery},
snapshot::SnapshotId,
};
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test_v3_durable.graph");
{
let backend = V3Backend::create(&db_path).unwrap();
let center = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "main".to_string(),
file_path: Some("/src/main.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
let helper1 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "helper1".to_string(),
file_path: Some("/src/helper1.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
let helper2 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "helper2".to_string(),
file_path: Some("/src/helper2.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
let util1 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "util1".to_string(),
file_path: Some("/src/util1.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper1,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper2,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: util1,
edge_type: "USES".to_string(),
data: serde_json::json!({}),
})
.unwrap();
let calls_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("CALLS".to_string()),
},
)
.unwrap();
assert_eq!(
calls_neighbors.len(),
2,
"Before reopen: should have 2 CALLS neighbors"
);
let uses_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("USES".to_string()),
},
)
.unwrap();
assert_eq!(
uses_neighbors.len(),
1,
"Before reopen: should have 1 USES neighbor"
);
}
{
let backend = V3Backend::open(&db_path).unwrap();
let all_ids = backend.entity_ids().unwrap();
assert_eq!(all_ids.len(), 4, "Should have 4 nodes after reopen");
let center = all_ids
.iter()
.find(|&&id| match backend.get_node(SnapshotId::current(), id) {
Ok(entity) => entity.name == "main",
Err(_) => false,
})
.copied()
.unwrap();
println!("After reopen, all_ids: {:?}", all_ids);
for &id in &all_ids {
if let Ok(entity) = backend.get_node(SnapshotId::current(), id) {
println!(" Node {}: name={}", id, entity.name);
}
}
let all_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
},
)
.unwrap();
println!("Unfiltered neighbors after reopen: {:?}", all_neighbors);
let calls_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("CALLS".to_string()),
},
)
.unwrap();
assert_eq!(
calls_neighbors.len(),
2,
"After reopen: should have 2 CALLS neighbors (edge_type survived recovery)"
);
let uses_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("USES".to_string()),
},
)
.unwrap();
assert_eq!(
uses_neighbors.len(),
1,
"After reopen: should have 1 USES neighbor (edge_type survived recovery)"
);
let all_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
},
)
.unwrap();
assert_eq!(
all_neighbors.len(),
3,
"After reopen: should have 3 neighbors total (unfiltered)"
);
let empty_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("NONEXISTENT".to_string()),
},
)
.unwrap();
assert_eq!(
empty_neighbors.len(),
0,
"After reopen: non-existent edge type should return empty"
);
}
}
#[test]
fn test_v3_edge_type_mixed_queries_after_reopen() {
use sqlitegraph::{
backend::{BackendDirection, NeighborQuery},
snapshot::SnapshotId,
};
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test_v3_mixed.graph");
let (center, _helper1, _helper2, _util1, _util2) = {
let backend = V3Backend::create(&db_path).unwrap();
let center = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "center".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let helper1 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "h1".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let helper2 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "h2".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let util1 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "u1".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let util2 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "u2".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper1,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper2,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: util1,
edge_type: "USES".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: util2,
edge_type: "USES".to_string(),
data: serde_json::json!({}),
})
.unwrap();
(center, helper1, helper2, util1, util2)
};
let backend = V3Backend::open(&db_path).unwrap();
let calls_only = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("CALLS".to_string()),
},
)
.unwrap();
assert_eq!(
calls_only.len(),
2,
"CALLS filter should return 2 neighbors"
);
let uses_only = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("USES".to_string()),
},
)
.unwrap();
assert_eq!(uses_only.len(), 2, "USES filter should return 2 neighbors");
let all_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
},
)
.unwrap();
assert_eq!(
all_neighbors.len(),
4,
"Unfiltered query should return all 4 neighbors"
);
let calls_set: std::collections::HashSet<_> = calls_only.into_iter().collect();
let uses_set: std::collections::HashSet<_> = uses_only.into_iter().collect();
let intersection = calls_set.intersection(&uses_set).collect::<Vec<_>>();
assert_eq!(
intersection.len(),
0,
"CALLS and USES sets should be disjoint"
);
}
#[test]
fn test_v3_edge_type_incoming_after_reopen() {
use sqlitegraph::{
backend::{BackendDirection, NeighborQuery},
snapshot::SnapshotId,
};
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test_v3_incoming.graph");
let (center, helper, _util) = {
let backend = V3Backend::create(&db_path).unwrap();
let center = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "center".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let helper = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "helper".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let util = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "util".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: util,
edge_type: "USES".to_string(),
data: serde_json::json!({}),
})
.unwrap();
(center, helper, util)
};
let backend = V3Backend::open(&db_path).unwrap();
let helper_incoming_calls = backend
.neighbors(
SnapshotId::current(),
helper,
NeighborQuery {
direction: BackendDirection::Incoming,
edge_type: Some("CALLS".to_string()),
},
)
.unwrap();
assert_eq!(
helper_incoming_calls.len(),
1,
"helper should have 1 incoming CALLS"
);
assert!(
helper_incoming_calls.contains(¢er),
"helper should be called by center"
);
let helper_incoming_uses = backend
.neighbors(
SnapshotId::current(),
helper,
NeighborQuery {
direction: BackendDirection::Incoming,
edge_type: Some("USES".to_string()),
},
)
.unwrap();
assert_eq!(
helper_incoming_uses.len(),
0,
"helper should have 0 incoming USES"
);
}
#[test]
fn test_v3_diagnostic_edge_disk_write() {
use sqlitegraph::{
backend::{BackendDirection, NeighborQuery},
snapshot::SnapshotId,
};
use std::fs::File;
use std::io::Read;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test_disk_write.graph");
let center = {
let backend = V3Backend::create(&db_path).unwrap();
let center = backend
.insert_node(NodeSpec {
kind: "F".to_string(),
name: "center".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let helper1 = backend
.insert_node(NodeSpec {
kind: "F".to_string(),
name: "helper1".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let helper2 = backend
.insert_node(NodeSpec {
kind: "F".to_string(),
name: "helper2".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper1,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper2,
edge_type: "USES".to_string(),
data: serde_json::json!({}),
})
.unwrap();
drop(backend);
center
};
{
let mut file = File::open(&db_path).unwrap();
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).unwrap();
println!("Database file size: {} bytes", buffer.len());
let mut found_clusters = Vec::new();
for i in 0..buffer.len().saturating_sub(10) {
if buffer[i] == 1 {
let count = u32::from_be_bytes([
buffer[i + 1],
buffer[i + 2],
buffer[i + 3],
buffer[i + 4],
]);
if count > 0 && count <= 100 {
found_clusters.push((i, count));
}
}
}
println!("Found {} potential edge clusters:", found_clusters.len());
for (offset, count) in &found_clusters {
println!(" Offset {}: {} edges", offset, count);
}
if found_clusters.is_empty() {
println!("WARNING: No edge clusters found in database file!");
}
}
let backend = V3Backend::open(&db_path).unwrap();
let all_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
},
)
.unwrap();
println!("Unfiltered neighbors after reopen: {:?}", all_neighbors);
assert_eq!(
all_neighbors.len(),
2,
"Should have 2 neighbors after reopen"
);
}
#[test]
fn test_v3_edge_type_aliasing_known_limitation() {
use sqlitegraph::{
backend::{BackendDirection, NeighborQuery},
snapshot::SnapshotId,
};
let (backend, _temp) = create_v3_backend();
let center = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "center".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let helper = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "helper".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
let calls_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("CALLS".to_string()),
},
)
.unwrap();
assert_eq!(calls_neighbors.len(), 1, "Should have 1 CALLS neighbor");
assert!(calls_neighbors.contains(&helper), "Should be helper");
backend
.insert_edge(EdgeSpec {
from: center,
to: helper,
edge_type: "USES".to_string(),
data: serde_json::json!({}),
})
.unwrap();
let uses_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("USES".to_string()),
},
)
.unwrap();
assert_eq!(
uses_neighbors.len(),
1,
"Should have 1 USES neighbor (overwrites CALLS)"
);
let calls_neighbors_after = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("CALLS".to_string()),
},
)
.unwrap();
assert_eq!(
calls_neighbors_after.len(),
0,
"KNOWN LIMITATION: CALLS was overwritten by USES"
);
let all_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
},
)
.unwrap();
assert_eq!(
all_neighbors.len(),
1,
"Should have 1 neighbor total (not duplicated)"
);
assert!(all_neighbors.contains(&helper), "Should be helper");
}
#[test]
#[ignore]
fn test_v3_edge_type_aliasing_across_reopen() {
use sqlitegraph::{
backend::{BackendDirection, NeighborQuery},
snapshot::SnapshotId,
};
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test_aliasing.graph");
let (center, _helper) = {
let backend = V3Backend::create(&db_path).unwrap();
let center = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "center".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let helper = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "helper".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: center,
to: helper,
edge_type: "USES".to_string(),
data: serde_json::json!({}),
})
.unwrap();
(center, helper)
};
let backend = V3Backend::open(&db_path).unwrap();
let uses_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("USES".to_string()),
},
)
.unwrap();
assert_eq!(
uses_neighbors.len(),
1,
"After reopen: USES should be the type"
);
let calls_neighbors = backend
.neighbors(
SnapshotId::current(),
center,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: Some("CALLS".to_string()),
},
)
.unwrap();
assert_eq!(
calls_neighbors.len(),
0,
"After reopen: CALLS was overwritten"
);
}
#[test]
fn test_v3_snapshot_all_accepted() {
use sqlitegraph::{
backend::{BackendDirection, NeighborQuery},
snapshot::SnapshotId,
};
let (backend, _temp) = create_v3_backend();
let node1 = backend
.insert_node(NodeSpec {
kind: "Node".to_string(),
name: "node1".to_string(),
file_path: None,
data: serde_json::json!({"initial": "state"}),
})
.unwrap();
let current = SnapshotId::current();
let entity = backend.get_node(current, node1).unwrap();
assert_eq!(entity.name, "node1");
assert_eq!(entity.data["initial"], "state");
let arbitrary = SnapshotId::from_lsn(12345);
let result = backend.get_node(arbitrary, node1);
assert!(result.is_ok(), "V3 accepts all snapshots (no MVCC)");
assert_eq!(result.unwrap().name, "node1");
let neighbors_result = backend.neighbors(
arbitrary,
node1,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
},
);
assert!(
neighbors_result.is_ok(),
"V3 neighbors accepts all snapshots"
);
}
#[test]
fn test_v3_snapshot_current_works() {
use sqlitegraph::{
backend::{BackendDirection, NeighborQuery},
snapshot::SnapshotId,
};
let (backend, _temp) = create_v3_backend();
let node1 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "main".to_string(),
file_path: Some("/src/main.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
let node2 = backend
.insert_node(NodeSpec {
kind: "Function".to_string(),
name: "helper".to_string(),
file_path: Some("/src/helper.rs".to_string()),
data: serde_json::json!({}),
})
.unwrap();
backend
.insert_edge(EdgeSpec {
from: node1,
to: node2,
edge_type: "CALLS".to_string(),
data: serde_json::json!({}),
})
.unwrap();
let current = SnapshotId::current();
let entity = backend.get_node(current, node1).unwrap();
assert_eq!(entity.name, "main");
let neighbors = backend
.neighbors(
current,
node1,
NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
},
)
.unwrap();
assert_eq!(neighbors.len(), 1);
assert!(neighbors.contains(&node2));
let outgoing = backend.fetch_outgoing(node1).unwrap();
assert_eq!(outgoing.len(), 1);
assert!(outgoing.contains(&node2));
let bfs_result = backend.bfs(current, node1, 1).unwrap();
assert_eq!(bfs_result.len(), 2);
let path = backend.shortest_path(current, node1, node2).unwrap();
assert!(path.is_some());
assert_eq!(path.unwrap().len(), 2);
let (out, inc) = backend.node_degree(current, node1).unwrap();
assert_eq!(out, 1);
assert_eq!(inc, 0);
let all_ids = backend.entity_ids().unwrap();
assert_eq!(all_ids.len(), 2);
}
#[test]
fn test_v3_pattern_search_unsupported() {
use sqlitegraph::snapshot::SnapshotId;
let (backend, _temp) = create_v3_backend();
let node1 = backend
.insert_node(NodeSpec {
kind: "Node".to_string(),
name: "node1".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let current = SnapshotId::current();
let result = backend.pattern_search(current, node1, &Default::default());
assert!(
result.is_err(),
"pattern_search should return Unsupported (not yet implemented)"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("does not support pattern_search"),
"Error message should explain limitation: {}",
err_msg
);
}
#[test]
fn test_v3_query_nodes_by_kind_accepts_any_snapshot() {
use sqlitegraph::snapshot::SnapshotId;
let (backend, _temp) = create_v3_backend();
backend
.insert_node(NodeSpec {
kind: "TestKind".to_string(),
name: "node1".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let arbitrary = SnapshotId::from_lsn(888);
let result = backend.query_nodes_by_kind(arbitrary, "TestKind");
assert!(
result.is_ok(),
"query_nodes_by_kind should accept any snapshot"
);
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn test_v3_query_nodes_by_name_pattern_accepts_any_snapshot() {
use sqlitegraph::snapshot::SnapshotId;
let (backend, _temp) = create_v3_backend();
backend
.insert_node(NodeSpec {
kind: "Node".to_string(),
name: "test_node".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let arbitrary = SnapshotId::from_lsn(777);
let result = backend.query_nodes_by_name_pattern(arbitrary, "test");
assert!(
result.is_ok(),
"query_nodes_by_name_pattern should accept any snapshot"
);
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn test_v3_fixed_methods_work_with_current_snapshot() {
use sqlitegraph::snapshot::SnapshotId;
let (backend, _temp) = create_v3_backend();
let node1 = backend
.insert_node(NodeSpec {
kind: "TestKind".to_string(),
name: "test_node".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let current = SnapshotId::current();
let pattern_result = backend.pattern_search(current, node1, &Default::default());
assert!(
matches!(pattern_result, Err(SqliteGraphError::Unsupported(_))),
"pattern_search should return Unsupported error (not yet implemented for V3)"
);
let kind_result = backend.query_nodes_by_kind(current, "TestKind");
assert!(
kind_result.is_ok(),
"query_nodes_by_kind should work with current snapshot"
);
assert_eq!(kind_result.unwrap().len(), 1);
let pattern_result = backend.query_nodes_by_name_pattern(current, "test");
assert!(
pattern_result.is_ok(),
"query_nodes_by_name_pattern should work with current snapshot"
);
assert_eq!(pattern_result.unwrap().len(), 1);
}