use sqlitegraph::{
GraphEdgeCreate, GraphEntity, GraphEntityCreate, SqliteGraph, SqliteGraphError,
backend::native::{
NativeNodeId, edge_store::EdgeStore, graph_file::GraphFile, graph_ops::native_bfs,
node_store::NodeStore,
},
};
use tempfile::TempDir;
fn create_simple_native_graph() -> (GraphFile, Vec<NativeNodeId>, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test_graph.db");
let mut graph_file = GraphFile::create(&db_path).expect("Failed to create graph file");
let mut node_ids = Vec::new();
for i in 1..=3 {
let mut node_store = NodeStore::new(&mut graph_file);
let node_id = node_store
.allocate_node_id()
.expect("Failed to allocate node ID");
let record = sqlitegraph::backend::native::NodeRecord::new(
node_id,
"test".to_string(),
format!("node_{}", i),
serde_json::json!({}),
);
node_store
.write_node(&record)
.expect("Failed to write node");
node_ids.push(node_id);
}
let mut edge_store = EdgeStore::new(&mut graph_file);
let edge1 = sqlitegraph::backend::native::EdgeRecord::new(
1, node_ids[0], node_ids[1], "connects".to_string(),
serde_json::json!({}),
);
edge_store
.write_edge(&edge1)
.expect("Failed to write edge A->B");
let edge2 = sqlitegraph::backend::native::EdgeRecord::new(
2, node_ids[1], node_ids[2], "connects".to_string(),
serde_json::json!({}),
);
edge_store
.write_edge(&edge2)
.expect("Failed to write edge B->C");
(graph_file, node_ids, temp_dir)
}
fn run_bfs_traversal(
graph_file: &mut GraphFile,
start: NativeNodeId,
depth: u32,
) -> Vec<NativeNodeId> {
native_bfs(graph_file, start, depth).expect("BFS should succeed")
}
#[test]
fn test_cache_evaporation_on_function_return() {
let (mut graph_file, node_ids, _temp_dir) = create_simple_native_graph();
let start_node = node_ids[0];
let result1 = run_bfs_traversal(&mut graph_file, start_node, 2);
assert!(!result1.is_empty(), "First traversal should find nodes");
let result2 = run_bfs_traversal(&mut graph_file, start_node, 2);
assert!(!result2.is_empty(), "Second traversal should find nodes");
assert_eq!(
result1, result2,
"Both traversals should return same results"
);
assert!(result1.contains(&node_ids[1]), "Should reach node B from A");
assert!(result1.contains(&node_ids[2]), "Should reach node C from A");
}
#[test]
fn test_cache_evaporation_multiple_sequential_traversals() {
let (mut graph_file, node_ids, _temp_dir) = create_simple_native_graph();
let start_node = node_ids[0];
let expected_result = run_bfs_traversal(&mut graph_file, start_node, 2);
for i in 1..10 {
let result = run_bfs_traversal(&mut graph_file, start_node, 2);
assert_eq!(
result, expected_result,
"Traversal {} should match first result",
i
);
}
}
#[test]
fn test_cache_evaporation_different_start_nodes() {
let (mut graph_file, node_ids, _temp_dir) = create_simple_native_graph();
let result_a = run_bfs_traversal(&mut graph_file, node_ids[0], 2);
assert!(result_a.contains(&node_ids[1]));
assert!(result_a.contains(&node_ids[2]));
let result_b = run_bfs_traversal(&mut graph_file, node_ids[1], 1);
assert!(result_b.contains(&node_ids[2]));
assert!(!result_b.contains(&node_ids[0]));
let result_a2 = run_bfs_traversal(&mut graph_file, node_ids[0], 2);
assert_eq!(result_a, result_a2, "Second BFS from A should match first");
}
#[test]
fn test_sequential_traversals_separate_caches() {
let (mut graph_file, node_ids, _temp_dir) = create_simple_native_graph();
let result1 = run_bfs_traversal(&mut graph_file, node_ids[0], 2);
assert!(
result1.contains(&node_ids[1]),
"First traversal should reach node B"
);
assert!(
result1.contains(&node_ids[2]),
"First traversal should reach node C"
);
let result2 = run_bfs_traversal(&mut graph_file, node_ids[1], 1);
assert!(
result2.contains(&node_ids[2]),
"Second traversal should reach node C"
);
assert!(
!result2.contains(&node_ids[0]),
"Second traversal should not see node A"
);
let result3 = run_bfs_traversal(&mut graph_file, node_ids[0], 2);
assert_eq!(result1, result3, "Third traversal should match first");
}
#[test]
fn test_multiple_start_nodes_isolated_caches() {
let (mut graph_file, node_ids, _temp_dir) = create_simple_native_graph();
let result_a = run_bfs_traversal(&mut graph_file, node_ids[0], 2);
assert_eq!(result_a.len(), 2, "From A should reach 2 nodes");
let result_b = run_bfs_traversal(&mut graph_file, node_ids[1], 1);
assert_eq!(result_b.len(), 1, "From B should reach 1 node");
assert!(result_b.contains(&node_ids[2]), "From B should reach C");
let result_c = run_bfs_traversal(&mut graph_file, node_ids[2], 1);
assert_eq!(result_c.len(), 0, "From C should reach 0 nodes");
let result_a2 = run_bfs_traversal(&mut graph_file, node_ids[0], 2);
assert_eq!(result_a, result_a2, "Second A traversal should match first");
}
#[test]
fn test_alternating_directions_isolated_caches() {
let (mut graph_file, node_ids, _temp_dir) = create_simple_native_graph();
for i in 0..10 {
let start_idx = i % 3;
let depth = if i % 2 == 0 { 1 } else { 2 };
let result = run_bfs_traversal(&mut graph_file, node_ids[start_idx], depth);
if start_idx == 0 {
if depth == 1 {
assert_eq!(result.len(), 1, "A depth 1: should reach 1 node (B)");
assert!(result.contains(&node_ids[1]), "A depth 1: should reach B");
} else {
assert_eq!(result.len(), 2, "A depth 2: should reach 2 nodes (B, C)");
assert!(result.contains(&node_ids[2]), "A depth 2: should reach C");
}
} else if start_idx == 1 {
if depth == 1 {
assert_eq!(result.len(), 1, "B depth 1: should reach 1 node (C)");
assert!(result.contains(&node_ids[2]), "B depth 1: should reach C");
} else {
assert_eq!(result.len(), 1, "B depth 2: should reach 1 node (C)");
}
} else {
assert_eq!(result.len(), 0, "C: should reach 0 nodes");
}
}
}
fn create_sqlite_graph_for_snapshot() -> Result<(SqliteGraph, i64, i64), SqliteGraphError> {
let graph = SqliteGraph::open_in_memory()?;
let entity_a = GraphEntityCreate {
kind: "function".to_string(),
name: "node_a".to_string(),
file_path: Some("a.rs".to_string()),
data: serde_json::json!({}),
};
let id_a = graph.insert_entity(&GraphEntity {
id: 0,
kind: entity_a.kind,
name: entity_a.name,
file_path: entity_a.file_path,
data: entity_a.data,
})?;
let entity_b = GraphEntityCreate {
kind: "function".to_string(),
name: "node_b".to_string(),
file_path: Some("b.rs".to_string()),
data: serde_json::json!({}),
};
let id_b = graph.insert_entity(&GraphEntity {
id: 0,
kind: entity_b.kind,
name: entity_b.name,
file_path: entity_b.file_path,
data: entity_b.data,
})?;
let edge = GraphEdgeCreate {
from_id: id_a,
to_id: id_b,
edge_type: "calls".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&sqlitegraph::GraphEdge {
id: 0,
from_id: edge.from_id,
to_id: edge.to_id,
edge_type: edge.edge_type,
data: edge.data,
})?;
Ok((graph, id_a, id_b))
}
#[test]
fn test_cache_no_cross_transaction_staleness() -> Result<(), SqliteGraphError> {
let (graph, id_a, _id_b) = create_sqlite_graph_for_snapshot()?;
let neighbors_t1 = graph.query().outgoing(id_a)?;
let entity_d = GraphEntityCreate {
kind: "function".to_string(),
name: "node_d".to_string(),
file_path: Some("d.rs".to_string()),
data: serde_json::json!({}),
};
let id_d = graph.insert_entity(&GraphEntity {
id: 0,
kind: entity_d.kind,
name: entity_d.name,
file_path: entity_d.file_path,
data: entity_d.data,
})?;
let edge_d = GraphEdgeCreate {
from_id: id_a,
to_id: id_d,
edge_type: "calls".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&sqlitegraph::GraphEdge {
id: 0,
from_id: edge_d.from_id,
to_id: edge_d.to_id,
edge_type: edge_d.edge_type,
data: edge_d.data,
})?;
let neighbors_t1_updated = graph.query().outgoing(id_a)?;
assert!(
neighbors_t1_updated.len() > neighbors_t1.len(),
"After adding edge, should have more neighbors"
);
assert!(
neighbors_t1_updated.contains(&id_d),
"New node D should be visible"
);
Ok(())
}
#[test]
fn test_snapshot_with_traversal_isolation() -> Result<(), SqliteGraphError> {
let (graph, id_a, id_b) = create_sqlite_graph_for_snapshot()?;
let entity_ids = graph.list_entity_ids()?;
for &id in &entity_ids {
let _ = graph.query().outgoing(id)?;
let _ = graph.query().incoming(id)?;
}
let snapshot = graph.acquire_snapshot()?;
let snapshot_neighbors = snapshot.get_outgoing(id_a);
assert_eq!(
snapshot_neighbors,
Some(&vec![id_b]),
"Snapshot should see A->B edge"
);
let entity_c = GraphEntityCreate {
kind: "function".to_string(),
name: "node_c".to_string(),
file_path: Some("c.rs".to_string()),
data: serde_json::json!({}),
};
let id_c = graph.insert_entity(&GraphEntity {
id: 0,
kind: entity_c.kind,
name: entity_c.name,
file_path: entity_c.file_path,
data: entity_c.data,
})?;
let edge_c = GraphEdgeCreate {
from_id: id_a,
to_id: id_c,
edge_type: "calls".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&sqlitegraph::GraphEdge {
id: 0,
from_id: edge_c.from_id,
to_id: edge_c.to_id,
edge_type: edge_c.edge_type,
data: edge_c.data,
})?;
let snapshot_neighbors_after = snapshot.get_outgoing(id_a);
assert_eq!(
snapshot_neighbors_after,
Some(&vec![id_b]),
"Snapshot should still see only A->B edge"
);
let current_neighbors = graph.query().outgoing(id_a)?;
assert!(
current_neighbors.contains(&id_c),
"Current traversal should see new edge A->C"
);
Ok(())
}
#[test]
fn test_multiple_traversals_consistent_results() {
let (mut graph_file, node_ids, _temp_dir) = create_simple_native_graph();
let start_node = node_ids[0];
let result_d1 = run_bfs_traversal(&mut graph_file, start_node, 1);
assert_eq!(result_d1.len(), 1, "Depth 1 should reach 1 node");
assert!(
result_d1.contains(&node_ids[1]),
"Depth 1 should reach node B"
);
let result_d2 = run_bfs_traversal(&mut graph_file, start_node, 2);
assert_eq!(result_d2.len(), 2, "Depth 2 should reach 2 nodes");
assert!(
result_d2.contains(&node_ids[1]),
"Depth 2 should reach node B"
);
assert!(
result_d2.contains(&node_ids[2]),
"Depth 2 should reach node C"
);
let result_d1_again = run_bfs_traversal(&mut graph_file, start_node, 1);
assert_eq!(result_d1_again, result_d1, "Depth 1 should be consistent");
}
#[test]
fn test_cache_with_graph_modifications() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("modification_test.db");
let mut graph_file = GraphFile::create(&db_path).expect("Failed to create graph file");
let mut node_ids = Vec::new();
for i in 1..=3 {
let mut node_store = NodeStore::new(&mut graph_file);
let node_id = node_store
.allocate_node_id()
.expect("Failed to allocate node ID");
let record = sqlitegraph::backend::native::NodeRecord::new(
node_id,
"test".to_string(),
format!("node_{}", i),
serde_json::json!({}),
);
node_store
.write_node(&record)
.expect("Failed to write node");
node_ids.push(node_id);
}
{
let mut edge_store = EdgeStore::new(&mut graph_file);
let edge1 = sqlitegraph::backend::native::EdgeRecord::new(
1,
node_ids[0],
node_ids[1],
"connects".to_string(),
serde_json::json!({}),
);
edge_store.write_edge(&edge1).expect("Failed to write edge");
}
let result1 = run_bfs_traversal(&mut graph_file, node_ids[0], 2);
assert_eq!(result1.len(), 1, "First BFS should reach 1 node");
{
let mut edge_store = EdgeStore::new(&mut graph_file);
let edge2 = sqlitegraph::backend::native::EdgeRecord::new(
2,
node_ids[1],
node_ids[2],
"connects".to_string(),
serde_json::json!({}),
);
edge_store.write_edge(&edge2).expect("Failed to write edge");
}
let result2 = run_bfs_traversal(&mut graph_file, node_ids[0], 2);
assert_eq!(
result2.len(),
2,
"Second BFS should reach 2 nodes after modification"
);
assert!(
result2.contains(&node_ids[2]),
"Second BFS should reach node 3"
);
}
#[test]
fn test_rapid_sequential_traversals() {
let (mut graph_file, node_ids, _temp_dir) = create_simple_native_graph();
let start_node = node_ids[0];
let expected = run_bfs_traversal(&mut graph_file, start_node, 2);
for i in 0..100 {
let result = run_bfs_traversal(&mut graph_file, start_node, 2);
assert_eq!(
result, expected,
"Traversal {} should produce consistent result",
i
);
}
}
#[test]
fn test_interleaved_traversals_different_depths() {
let (mut graph_file, node_ids, _temp_dir) = create_simple_native_graph();
let start_node = node_ids[0];
for i in 0..20 {
let depth = (i % 3) + 1; let result = run_bfs_traversal(&mut graph_file, start_node, depth);
if depth == 1 {
assert_eq!(result.len(), 1, "Depth 1 should reach 1 node");
} else if depth == 2 {
assert_eq!(result.len(), 2, "Depth 2 should reach 2 nodes");
}
}
}