sqlitegraph 2.0.9

Embedded graph database with full ACID transactions, HNSW vector search, dual backend support, and comprehensive graph algorithms library
Documentation
//! Snapshot Isolation Tests for GraphBackend
//!
//! These tests verify that concurrent transactions properly isolate reads
//! from uncommitted writes, ensuring ACID compliance.
//!
//! Phase 38-04: MVCC Filtering Implementation

use sqlitegraph::backend::{GraphBackend, NodeSpec, EdgeSpec};
use sqlitegraph::snapshot::SnapshotId;
use std::sync::Arc;

/// Test helper: Create a test node spec
fn test_node(name: &str, kind: &str) -> NodeSpec {
    NodeSpec {
        name: name.to_string(),
        kind: kind.to_string(),
        file_path: None,
        data: serde_json::json!({}),
    }
}

/// Test 1: Uncommitted writes should not be visible to other transactions
#[test]
fn test_uncommitted_writes_not_visible() {
    let temp_dir = tempfile::tempdir().unwrap();
    let backend = sqlitegraph::backend::native::NativeGraphBackend::create(
        temp_dir.path().join("test.graph")
    ).unwrap();
    let backend = Arc::new(backend);

    // Get initial snapshot before any writes
    let initial_snapshot = SnapshotId::current();

    // Insert a node
    let node_id = backend.insert_node(test_node("node1", "test")).unwrap();

    // Get current snapshot after insert
    let after_insert_snapshot = SnapshotId::current();

    // Read with initial snapshot - should NOT see the new node
    let result = backend.get_node(initial_snapshot, node_id);
    println!("Initial snapshot read result: {:?}", result);

    // Read with current snapshot - should see the new node
    let result_current = backend.get_node(after_insert_snapshot, node_id);
    println!("Current snapshot read result: {:?}", result_current);
    assert!(result_current.is_ok(), "Current snapshot should see committed data");
}

/// Test 2: BFS should respect snapshot isolation
#[test]
fn test_bfs_snapshot_isolation() {
    let temp_dir = tempfile::tempdir().unwrap();
    let backend = Arc::new(
        sqlitegraph::backend::native::NativeGraphBackend::create(
            temp_dir.path().join("test.graph")
        ).unwrap()
    );

    // Create a chain: A -> B -> C
    let node_a = backend.insert_node(test_node("A", "chain")).unwrap();
    let node_b = backend.insert_node(test_node("B", "chain")).unwrap();

    backend.insert_edge(EdgeSpec {
        from: node_a,
        to: node_b,
        edge_type: "links".to_string(),
        data: serde_json::json!({}),
    }).unwrap();

    let snapshot_before_c = SnapshotId::current();

    let node_c = backend.insert_node(test_node("C", "chain")).unwrap();
    backend.insert_edge(EdgeSpec {
        from: node_b,
        to: node_c,
        edge_type: "links".to_string(),
        data: serde_json::json!({}),
    }).unwrap();

    let result = backend.bfs(snapshot_before_c, node_a, 2);
    println!("BFS with old snapshot (depth 2 from A): {:?}", result);
}

/// Test 3: Shortest path should respect snapshot isolation
#[test]
fn test_shortest_path_snapshot_isolation() {
    let temp_dir = tempfile::tempdir().unwrap();
    let backend = Arc::new(
        sqlitegraph::backend::native::NativeGraphBackend::create(
            temp_dir.path().join("test.graph")
        ).unwrap()
    );

    let node_a = backend.insert_node(test_node("A", "path")).unwrap();
    let node_b = backend.insert_node(test_node("B", "path")).unwrap();

    backend.insert_edge(EdgeSpec {
        from: node_a,
        to: node_b,
        edge_type: "connects".to_string(),
        data: serde_json::json!({}),
    }).unwrap();

    let snapshot_before_c = SnapshotId::current();

    let node_c = backend.insert_node(test_node("C", "path")).unwrap();
    backend.insert_edge(EdgeSpec {
        from: node_b,
        to: node_c,
        edge_type: "connects".to_string(),
        data: serde_json::json!({}),
    }).unwrap();

    let result = backend.shortest_path(snapshot_before_c, node_a, node_c);
    println!("Shortest path with old snapshot (A to C): {:?}", result);
}