use sqlitegraph::{
BackendDirection, EdgeSpec, GraphConfig, NeighborQuery, NodeSpec, SnapshotId, open_graph,
};
use std::time::Instant;
use tempfile::tempdir;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("═══════════════════════════════════════════════════════════════════");
println!(" SQLiteGraph Backend Performance Comparison");
println!("═══════════════════════════════════════════════════════════════════");
println!("");
println!(" This test measures three distinct operations:");
println!(" 1. POINT LOOKUP: Single node lookup by ID");
println!(" 2. ADJACENCY FETCH: Getting neighbors (warm cache)");
println!(" 3. TRAVERSAL: BFS through graph (bulk operation)");
println!("═══════════════════════════════════════════════════════════════════\n");
test_point_lookup()?;
test_adjacency_fetch()?;
test_traversal()?;
println!("\n═══════════════════════════════════════════════════════════════════");
println!(" SUMMARY: When to use which backend");
println!("═══════════════════════════════════════════════════════════════════");
println!(" Use SQLITE when:");
println!(" • Primary workload is single-node lookups");
println!(" • Need mature, battle-tested storage");
println!(" • Debuggability with SQL is important");
println!("");
println!(" Use V3 when:");
println!(" • Primary workload is graph traversal (BFS/DFS)");
println!(" • Need high-throughput adjacency queries");
println!(" • Working with large graphs (unlimited scale)");
println!("═══════════════════════════════════════════════════════════════════");
Ok(())
}
fn test_point_lookup() -> Result<(), Box<dyn std::error::Error>> {
println!("───────────────────────────────────────────────────────────────────");
println!(" TEST 1: POINT LOOKUP (Single node by ID)");
println!("───────────────────────────────────────────────────────────────────");
let sqlite_time;
let v3_time;
{
let temp_dir = tempdir()?;
let graph = open_graph(&temp_dir.path().join("test.db"), &GraphConfig::sqlite())?;
let mut node_ids = Vec::new();
for i in 0..1000 {
let id = graph.insert_node(NodeSpec {
kind: "Test".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({"id": i}),
})?;
node_ids.push(id);
}
let snapshot = SnapshotId::current();
let target = node_ids[500];
for _ in 0..100 {
let _ = graph.get_node(snapshot, target)?;
}
let start = Instant::now();
for _ in 0..10000 {
let _ = graph.get_node(snapshot, target)?;
}
sqlite_time = start.elapsed().as_nanos() / 10000;
println!(" SQLite: {} ns/lookup (B-tree optimized)", sqlite_time);
}
{
let temp_dir = tempdir()?;
let graph = open_graph(&temp_dir.path().join("test.db"), &GraphConfig::native())?;
let mut node_ids = Vec::new();
for i in 0..1000 {
let id = graph.insert_node(NodeSpec {
kind: "Test".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({"id": i}),
})?;
node_ids.push(id);
}
let snapshot = SnapshotId::current();
let target = node_ids[500];
for _ in 0..100 {
let _ = graph.get_node(snapshot, target)?;
}
let start = Instant::now();
for _ in 0..10000 {
let _ = graph.get_node(snapshot, target)?;
}
v3_time = start.elapsed().as_nanos() / 10000;
println!(" V3: {} ns/lookup (B+tree + page decode)", v3_time);
}
let ratio = v3_time as f64 / sqlite_time as f64;
println!(
"\n Result: SQLite is {:.1}× faster for point lookups",
ratio
);
println!(" Why: SQLite's B-tree has decades of optimization\n");
Ok(())
}
fn test_adjacency_fetch() -> Result<(), Box<dyn std::error::Error>> {
println!("───────────────────────────────────────────────────────────────────");
println!(" TEST 2: ADJACENCY FETCH (Get neighbors - warm cache)");
println!("───────────────────────────────────────────────────────────────────");
let sqlite_time;
let v3_time;
{
let temp_dir = tempdir()?;
let graph = open_graph(&temp_dir.path().join("test.db"), &GraphConfig::sqlite())?;
let mut node_ids = Vec::new();
for i in 0..100 {
let id = graph.insert_node(NodeSpec {
kind: "Test".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})?;
node_ids.push(id);
}
for j in 1..=20 {
graph.insert_edge(EdgeSpec {
from: node_ids[0],
to: node_ids[j],
edge_type: "test".to_string(),
data: serde_json::Value::Null,
})?;
}
let snapshot = SnapshotId::current();
let query = NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
};
for _ in 0..100 {
let _ = graph.neighbors(snapshot, node_ids[0], query.clone())?;
}
let start = Instant::now();
for _ in 0..10000 {
let _ = graph.neighbors(snapshot, node_ids[0], query.clone())?;
}
sqlite_time = start.elapsed().as_nanos() / 10000;
println!(
" SQLite: {} ns/fetch (prepared statement + index)",
sqlite_time
);
}
{
let temp_dir = tempdir()?;
let graph = open_graph(&temp_dir.path().join("test.db"), &GraphConfig::native())?;
let mut node_ids = Vec::new();
for i in 0..100 {
let id = graph.insert_node(NodeSpec {
kind: "Test".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})?;
node_ids.push(id);
}
for j in 1..=20 {
graph.insert_edge(EdgeSpec {
from: node_ids[0],
to: node_ids[j],
edge_type: "test".to_string(),
data: serde_json::Value::Null,
})?;
}
let snapshot = SnapshotId::current();
let query = NeighborQuery {
direction: BackendDirection::Outgoing,
edge_type: None,
};
for _ in 0..100 {
let _ = graph.neighbors(snapshot, node_ids[0], query.clone())?;
}
let start = Instant::now();
for _ in 0..10000 {
let _ = graph.neighbors(snapshot, node_ids[0], query.clone())?;
}
v3_time = start.elapsed().as_nanos() / 10000;
println!(" V3: {} ns/fetch (HashMap + Arc::clone)", v3_time);
}
let ratio = sqlite_time as f64 / v3_time as f64;
if ratio > 1.0 {
println!("\n Result: V3 is {:.1}× faster for adjacency fetch", ratio);
} else {
println!(
"\n Result: SQLite is {:.1}× faster for adjacency fetch",
1.0 / ratio
);
}
println!(" Note: Both are fast; difference is in API overhead\n");
Ok(())
}
fn test_traversal() -> Result<(), Box<dyn std::error::Error>> {
println!("───────────────────────────────────────────────────────────────────");
println!(" TEST 3: TRAVERSAL (BFS - 3 hops from start)");
println!("───────────────────────────────────────────────────────────────────");
println!(" This is where V3 shines due to contiguous adjacency storage\n");
let sqlite_time;
let v3_time;
{
let temp_dir = tempdir()?;
let graph = open_graph(&temp_dir.path().join("test.db"), &GraphConfig::sqlite())?;
let mut node_ids = Vec::new();
for i in 0..100 {
let id = graph.insert_node(NodeSpec {
kind: "Test".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})?;
node_ids.push(id);
}
for i in 0..99 {
graph.insert_edge(EdgeSpec {
from: node_ids[i],
to: node_ids[i + 1],
edge_type: "next".to_string(),
data: serde_json::Value::Null,
})?;
}
let snapshot = SnapshotId::current();
let start = Instant::now();
for _ in 0..1000 {
let _ = graph.bfs(snapshot, node_ids[0], 3)?;
}
sqlite_time = start.elapsed().as_millis() as f64 / 1000.0;
println!(" SQLite: {:.3} ms/BFS (3 hops, 100 nodes)", sqlite_time);
}
{
let temp_dir = tempdir()?;
let graph = open_graph(&temp_dir.path().join("test.db"), &GraphConfig::native())?;
let mut node_ids = Vec::new();
for i in 0..100 {
let id = graph.insert_node(NodeSpec {
kind: "Test".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})?;
node_ids.push(id);
}
for i in 0..99 {
graph.insert_edge(EdgeSpec {
from: node_ids[i],
to: node_ids[i + 1],
edge_type: "next".to_string(),
data: serde_json::Value::Null,
})?;
}
let snapshot = SnapshotId::current();
let start = Instant::now();
for _ in 0..1000 {
let _ = graph.bfs(snapshot, node_ids[0], 3)?;
}
v3_time = start.elapsed().as_millis() as f64 / 1000.0;
println!(" V3: {:.3} ms/BFS (3 hops, 100 nodes)", v3_time);
}
let ratio = sqlite_time / v3_time;
println!("\n Result: V3 is {:.1}× faster for traversal", ratio);
println!(" Why: Contiguous adjacency storage reduces I/O\n");
Ok(())
}