sqlitegraph 2.0.7

Embedded graph database with full ACID transactions, HNSW vector search, dual backend support, and comprehensive graph algorithms library
Documentation
//! SQLite vs V3 Backend Performance Comparison (Defensible)
//!
//! This example provides accurate, apples-to-apples performance comparisons
//! between SQLite and V3 backends, clearly distinguishing:
//!
//! 1. POINT LOOKUP: Single node lookup by ID (B+tree traversal)
//! 2. ADJACENCY FETCH: Getting neighbors of a node (cached)
//! 3. TRAVERSAL: BFS/DFS through multiple hops (bulk operation)
//!
//! KEY INSIGHT:
//! - SQLite wins on POINT LOOKUP (mature B-tree optimization)
//! - V3 wins on TRAVERSAL (contiguous adjacency storage)
//! - ADJACENCY FETCH depends on cache state and API overhead

use std::time::Instant;
use sqlitegraph::{GraphConfig, NodeSpec, EdgeSpec, open_graph, SnapshotId, NeighborQuery, BackendDirection};
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;

    // SQLite
    {
        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);
    }

    // V3
    {
        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;

    // SQLite
    {
        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);
    }

    // V3
    {
        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;

    // SQLite
    {
        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);
    }

    // V3
    {
        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(())
}