sqlitegraph 2.2.2

Embedded graph database with full ACID transactions, HNSW vector search, dual backend support, and comprehensive graph algorithms library
Documentation
//! Snapshot isolation tests
//!
//! Tests for verifying that snapshots provide consistent, isolated views
//! of the database state at a point in time.
//!
//! **Hard Rule:** No API may observe state not bound to a committed snapshot_id.
//!
//! # Implementation Status
//!
//! - ✅ **Phase 38-02 COMPLETE**: SnapshotId type created
//! - ✅ **Phase 38-03 COMPLETE**: GraphBackend trait updated with snapshot_id parameters
//! - ⏳ **Phase 38-04 PENDING**: WAL filtering not yet implemented
//! - ❌ **Native/SQLite implementations**: Need to be updated to match trait signature

use sqlitegraph::snapshot::SnapshotId;

// ============================================================================
// SnapshotId Type Tests (Phase 38-02 - COMPLETE)
// ============================================================================

/// Verify SnapshotId values can be compared for ordering
///
/// Snapshot IDs must be monotonically increasing to support:
/// - Time-based snapshot queries
/// - Historical data navigation
/// - Consistent snapshot ordering
#[test]
fn test_snapshot_id_monotonic() {
    let s1 = SnapshotId::from_tx(100);
    let s2 = SnapshotId::from_tx(200);
    let s3 = SnapshotId::from_tx(150);

    assert!(s2.as_u64() > s1.as_u64(), "Higher tx_id should be greater");
    assert!(s3.as_u64() > s1.as_u64(), "Higher tx_id should be greater");
    assert!(s2.as_u64() > s3.as_u64(), "Higher tx_id should be greater");
}

// ============================================================================
// Snapshot Isolation Test Specifications (Blocked until 38-04 complete)
// ============================================================================

// Note: The following test specifications are documented but commented out
// because the GraphBackend implementations haven't been updated to match
// the new trait signature (with snapshot_id parameters).
//
// Once 38-04 is complete, uncomment these tests to verify snapshot isolation.

/*
/// **SNAPSHOT ISOLATION TEST**: Multiple snapshots see different, consistent views
///
/// # Expected Behavior
///
/// 1. Create snapshot_1 (empty graph)
/// 2. Insert 5 nodes → snapshot_2
/// 3. Create snapshot_3 (sees 5 nodes)
/// 4. Insert 5 more nodes → snapshot_4
/// 5. snapshot_2 should see 0 nodes
/// 6. snapshot_3 should see 5 nodes
/// 7. snapshot_4 should see 10 nodes
///
/// Each snapshot provides a consistent, isolated view of the database at that point in time.
///
/// # Implementation Required
///
/// - [ ] WAL filtering by tx_id
/// - [ ] Snapshot monotonicity tracking
/// - [ ] count_nodes() method with snapshot_id parameter
#[test]
fn test_multiple_snapshots_isolated() {
    use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
    use tempfile::NamedTempFile;

    let temp = NamedTempFile::new().unwrap();
    let cfg = GraphConfig::native();
    let graph = open_graph(temp.path(), &cfg).unwrap();

    // Insert first batch of nodes
    let snapshot_1 = SnapshotId::current();
    for i in 0..5 {
        graph.insert_node(NodeSpec {
            kind: "test".to_string(),
            name: format!("node_{}", i),
            data: None,
        }).unwrap();
    }

    let snapshot_2 = SnapshotId::current();

    // Insert second batch
    for i in 5..10 {
        graph.insert_node(NodeSpec {
            kind: "test".to_string(),
            name: format!("node_{}", i),
            data: None,
        }).unwrap();
    }

    let snapshot_3 = SnapshotId::current();

    // snapshot_1 should see 0 nodes (before any inserts)
    let count_1 = graph.count_nodes(snapshot_1).unwrap();
    assert_eq!(count_1, 0, "snapshot_1 should see 0 nodes");

    // snapshot_2 should see 5 nodes (first batch only)
    let count_2 = graph.count_nodes(snapshot_2).unwrap();
    assert_eq!(count_2, 5, "snapshot_2 should see 5 nodes");

    // snapshot_3 should see 10 nodes (both batches)
    let count_3 = graph.count_nodes(snapshot_3).unwrap();
    assert_eq!(count_3, 10, "snapshot_3 should see 10 nodes");
}

/// **WAL FILTERING TEST**: WAL records filtered by snapshot_id
///
/// # Expected Behavior
///
/// This test verifies the core WAL filtering mechanism:
/// 1. Perform 10 commits (each creates new snapshot)
/// 2. Snapshot at commit 5 should see exactly 5 nodes
/// 3. Snapshot at commit 10 should see exactly 10 nodes
/// 4. Each snapshot filters WAL records by tx_id <= snapshot_id
///
/// # Implementation Required
///
/// - [ ] WAL stores tx_id with each record
/// - [ ] Read path filters records by tx_id <= snapshot_id
/// - [ ] SnapshotId tracking returns max committed tx_id
#[test]
fn test_wal_records_filtered_by_snapshot() {
    use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
    use tempfile::NamedTempFile;

    let temp = NamedTempFile::new().unwrap();
    let cfg = GraphConfig::native();
    let graph = open_graph(temp.path(), &cfg).unwrap();

    let mut snapshot_ids = Vec::new();

    // Write 10 nodes (each commit creates new snapshot)
    for i in 0..10 {
        let current = SnapshotId::current();
        graph.insert_node(NodeSpec {
            kind: "test".to_string(),
            name: format!("node_{}", i),
            data: None,
        }).unwrap();
        snapshot_ids.push(SnapshotId::current());
    }

    // Snapshot at commit 5 should see exactly 5 nodes
    let snapshot_at_5 = snapshot_ids[4]; // 0-indexed, so 4 = 5th commit
    let count_5 = graph.count_nodes(snapshot_at_5).unwrap();
    assert_eq!(count_5, 5, "Snapshot at commit 5 should see 5 nodes");

    // Snapshot at commit 10 should see exactly 10 nodes
    let snapshot_at_10 = snapshot_ids[9];
    let count_10 = graph.count_nodes(snapshot_at_10).unwrap();
    assert_eq!(count_10, 10, "Snapshot at commit 10 should see 10 nodes");

    // Verify earlier snapshots don't see later data
    let node_10 = graph.get_node(snapshot_at_5, 10);
    assert!(node_10.is_err(), "Snapshot at 5 should not see node 10");

    let node_10_current = graph.get_node(snapshot_at_10, 10);
    assert!(node_10_current.is_ok(), "Snapshot at 10 should see node 10");
}

/// **CONCURRENT READER TEST**: Multiple readers see consistent state
///
/// # Expected Behavior
///
/// Simulates concurrent readers and writers:
/// 1. Reader 1 starts with snapshot (sees N nodes)
/// 2. Writer adds M nodes, commits
/// 3. Reader 1 should STILL see N nodes (snapshot isolated)
/// 4. Reader 2 (new snapshot) should see N+M nodes
///
/// This proves that snapshots provide true isolation - readers don't see
/// writes that occur after their snapshot was taken.
///
/// # Implementation Required
///
/// - [ ] Snapshot isolation in read path
/// - [ ] WAL filtering by snapshot_id
/// - [ ] No cross-snapshot pollution
#[test]
fn test_concurrent_readers_see_consistent_state() {
    use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
    use tempfile::NamedTempFile;

    let temp = NamedTempFile::new().unwrap();
    let cfg = GraphConfig::native();
    let graph = open_graph(temp.path(), &cfg).unwrap();

    // Reader 1 starts, sees N nodes
    let reader1_snapshot = SnapshotId::current();

    // Insert initial nodes
    for i in 0..5 {
        graph.insert_node(NodeSpec {
            kind: "test".to_string(),
            name: format!("node_{}", i),
            data: None,
        }).unwrap();
    }

    // Reader 1 should see 5 nodes
    let count1 = graph.count_nodes(reader1_snapshot).unwrap();
    assert_eq!(count1, 0, "Reader 1 snapshot should see 0 nodes (taken before inserts)");

    // Writer adds more nodes, commits
    let writer_snapshot = SnapshotId::current();
    for i in 5..10 {
        graph.insert_node(NodeSpec {
            kind: "test".to_string(),
            name: format!("node_{}", i),
            data: None,
        }).unwrap();
    }

    // Reader 1 should STILL see 0 nodes (its snapshot is isolated)
    let count1_again = graph.count_nodes(reader1_snapshot).unwrap();
    assert_eq!(count1_again, 0, "Reader 1 snapshot should not see later commits");

    // Reader 2 (new snapshot) should see 10 nodes
    let reader2_snapshot = SnapshotId::current();
    let count2 = graph.count_nodes(reader2_snapshot).unwrap();
    assert_eq!(count2, 10, "Reader 2 should see all committed nodes");
}

/// **TRANSACTION INTEGRATION TEST**: Commit returns usable SnapshotId
///
/// # Expected Behavior
///
/// Verify that commit operations create usable snapshots:
/// 1. Insert node (returns snapshot_id after commit)
/// 2. Read with that snapshot_id should see the node
/// 3. SnapshotId should be monotonically increasing
///
/// # Implementation Required
///
/// - [ ] insert_node() returns node_id AND new snapshot_id
/// - [ ] commit_transaction() tracks and returns SnapshotId
/// - [ ] SnapshotId reflects committed transaction ID
#[test]
fn test_commit_returns_snapshot_id() {
    use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
    use tempfile::NamedTempFile;

    let temp = NamedTempFile::new().unwrap();
    let cfg = GraphConfig::native();
    let graph = open_graph(temp.path(), &cfg).unwrap();

    // Insert should commit and create new snapshot
    let snapshot_before = SnapshotId::current();
    let node_id = graph.insert_node(NodeSpec {
        kind: "test".to_string(),
        name: "test_node".to_string(),
        data: None,
    }).unwrap();
    let snapshot_after = SnapshotId::current();

    // Verify the new snapshot is greater than before
    assert!(snapshot_after.as_u64() > snapshot_before.as_u64());

    // Verify the new snapshot is usable
    let node = graph.get_node(snapshot_after, node_id).unwrap();
    assert_eq!(node.name, "test_node");
}

/// **FULL ACID WORKFLOW TEST**: Complete ACID compliance verification
///
/// # Expected Behavior
///
/// End-to-end test of ACID properties:
///
/// **Atomicity**: Node A and C are atomic (either both visible or neither)
/// **Consistency**: All invariants maintained (no orphan nodes)
/// **Isolation**: snapshot_1 doesn't see snapshot_2's writes
/// **Durability**: Committed nodes survive (after 38-04 with WAL checkpoint)
///
/// # Test Scenario
///
/// 1. Create graph
/// 2. Insert node A (commit) → snapshot_1
/// 3. Insert node B (uncommitted, will be rolled back)
/// 4. Insert node C (commit) → snapshot_2
/// 5. Read with snapshot_1 → sees A only
/// 6. Read with snapshot_2 → sees A, C
/// 7. Read with current → sees A, C (B rolled back)
///
/// # Implementation Required
///
/// - [ ] Transaction API with explicit begin/commit/rollback
/// - [ ] Snapshot isolation for all reads
/// - [ ] WAL filtering by tx_id
/// - [ ] Uncommitted data not visible in any snapshot
#[test]
fn test_full_acid_workflow() {
    use sqlitegraph::{open_graph, GraphConfig, BackendKind, snapshot::SnapshotId};
    use tempfile::NamedTempFile;

    let temp = NamedTempFile::new().unwrap();
    let cfg = GraphConfig::native();
    let graph = open_graph(temp.path(), &cfg).unwrap();

    // 1. Create graph - done
    // 2. Insert node A (commit) → snapshot_1
    let snapshot_0 = SnapshotId::current();
    let node_a = graph.insert_node(NodeSpec {
        kind: "test".to_string(),
        name: "A".to_string(),
        data: None,
    }).unwrap();
    let snapshot_1 = SnapshotId::current();

    // 3. Insert node B (uncommitted)
    // Note: Need explicit transaction API for uncommitted writes
    // For now, skip this test scenario

    // 4. Insert node C (commit) → snapshot_2
    let node_c = graph.insert_node(NodeSpec {
        kind: "test".to_string(),
        name: "C".to_string(),
        data: None,
    }).unwrap();
    let snapshot_2 = SnapshotId::current();

    // 5. Read with snapshot_0 → sees 0 nodes (before any writes)
    let count_0 = graph.count_nodes(snapshot_0).unwrap();
    assert_eq!(count_0, 0, "snapshot_0 should see 0 nodes");

    // 6. Read with snapshot_1 → sees A only
    let count_1 = graph.count_nodes(snapshot_1).unwrap();
    assert_eq!(count_1, 1, "snapshot_1 should see only A");

    // 7. Read with snapshot_2 → sees A, C
    let count_2 = graph.count_nodes(snapshot_2).unwrap();
    assert_eq!(count_2, 2, "snapshot_2 should see A and C");

    // 8. Verify specific nodes visible in correct snapshots
    let node_a_in_s0 = graph.get_node(snapshot_0, node_a);
    assert!(node_a_in_s0.is_err(), "A should NOT be visible in snapshot_0");

    let node_a_in_s1 = graph.get_node(snapshot_1, node_a);
    assert!(node_a_in_s1.is_ok(), "A should be visible in snapshot_1");

    let node_c_in_s1 = graph.get_node(snapshot_1, node_c);
    assert!(node_c_in_s1.is_err(), "C should NOT be visible in snapshot_1");

    let node_c_in_s2 = graph.get_node(snapshot_2, node_c);
    assert!(node_c_in_s2.is_ok(), "C should be visible in snapshot_2");
}
*/