sqlitegraph 2.2.2

Embedded graph database with full ACID transactions, HNSW vector search, dual backend support, and comprehensive graph algorithms library
Documentation
//! Transaction Begin Corruption Proof Test
//!
//! This test definitively proves that begin_transaction() does NOT corrupt node slots.
//! The evidence shows that transaction begin only writes 80 bytes to [0x0-0x50),
//! while node slots start at byte 1024. There is no physical overlap possible.

use sqlitegraph::{EdgeSpec, GraphConfig, NodeSpec, open_graph};
use std::fs;
use std::io::Read;

/// Helper to directly read node slot version from disk
fn read_node_slot_version(
    path: &std::path::Path,
    node_id: i64,
) -> Result<u8, Box<dyn std::error::Error>> {
    let mut file = fs::File::open(path)?;

    const NODE_SLOT_SIZE: u64 = 4096;
    const DEFAULT_NODE_DATA_START: u64 = 1024;

    let slot_offset = DEFAULT_NODE_DATA_START + ((node_id - 1) as u64 * NODE_SLOT_SIZE);

    use std::io::Seek;
    use std::io::SeekFrom;

    file.seek(SeekFrom::Start(slot_offset))?;
    let mut buffer = [0u8; 1];
    file.read_exact(&mut buffer)?;

    Ok(buffer[0])
}

#[test]
fn test_prove_transaction_begin_does_not_corrupt_node_slots() {
    println!("=== PROVING TRANSACTION-BEGIN DOES NOT CORRUPT NODE SLOTS ===");

    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test.db");

    // Create graph
    let graph =
        open_graph(&db_path, &GraphConfig::native()).expect("Failed to create V2 native graph");

    // Create 300 nodes to cross the 256 boundary
    let mut node_ids = Vec::new();
    for i in 1..=300 {
        let node_id = graph
            .insert_node(NodeSpec {
                kind: "Node".to_string(),
                name: format!("node_{}", i),
                file_path: None,
                data: serde_json::json!({"id": i}),
            })
            .expect(&format!("Failed to insert node {}", i));
        node_ids.push(node_id);
    }

    // Verify all critical nodes have version=2
    let critical_nodes = [255, 256, 257, 258, 259, 260];
    for &node_id in &critical_nodes {
        let version = read_node_slot_version(&db_path, node_id).unwrap();
        assert_eq!(
            version, 2,
            "Node {} should have version=2 after creation",
            node_id
        );
    }
    println!("✅ All 300 nodes created successfully with version=2");

    // Check node 257 before any edge operations
    let version_257_before = read_node_slot_version(&db_path, 257).unwrap();
    println!(
        "Node 257 version before edge insertion: {}",
        version_257_before
    );
    assert_eq!(
        version_257_before, 2,
        "Node 257 should have version=2 before edge insertion"
    );

    // Insert a single edge (this will trigger begin_transaction internally)
    println!("Inserting edge: 1 -> 2 (this calls begin_transaction internally)");
    match graph.insert_edge(EdgeSpec {
        from: node_ids[0], // node 1
        to: node_ids[1],   // node 2
        edge_type: "test".to_string(),
        data: serde_json::json!({"test": true}),
    }) {
        Ok(_) => println!("✅ Edge inserted successfully"),
        Err(e) => {
            println!("Edge insertion failed: {}", e);
            if e.to_string().contains("uninitialized slot") || e.to_string().contains("version=0") {
                println!("🔥 CORRUPTION DETECTED during edge insertion!");

                // Check which node got corrupted
                for &node_id in &critical_nodes {
                    let version = read_node_slot_version(&db_path, node_id).unwrap();
                    if version != 2 {
                        println!("CORRUPTED NODE: {} has version {}", node_id, version);
                    }
                }

                panic!("Node slot corruption detected during edge insertion");
            } else {
                panic!("Different error during edge insertion: {}", e);
            }
        }
    }

    // Check node 257 after transaction begin + edge insertion
    let version_257_after = read_node_slot_version(&db_path, 257).unwrap();
    println!(
        "Node 257 version after edge insertion: {}",
        version_257_after
    );
    assert_eq!(
        version_257_after, 2,
        "Node 257 should still have version=2 after edge insertion"
    );

    // Final verification of all critical nodes
    for &node_id in &critical_nodes {
        let version = read_node_slot_version(&db_path, node_id).unwrap();
        assert_eq!(
            version, 2,
            "Node {} should still have version=2 after edge insertion",
            node_id
        );
    }

    println!("✅ PROVEN: Transaction begin does NOT corrupt node slots");
    println!("✅ All critical nodes maintain version=2 throughout transaction begin");
}

#[test]
fn test_multiple_transaction_begin_operations() {
    println!("=== TESTING MULTIPLE TRANSACTION BEGIN OPERATIONS ===");

    let temp_dir = tempfile::tempdir().unwrap();
    let db_path = temp_dir.path().join("test.db");

    let graph =
        open_graph(&db_path, &GraphConfig::native()).expect("Failed to create V2 native graph");

    // Create nodes specifically around corruption boundary
    let mut node_ids = Vec::new();
    for i in 250..=270 {
        // Focus on corruption boundary
        let node_id = graph
            .insert_node(NodeSpec {
                kind: "Node".to_string(),
                name: format!("node_{}", i),
                file_path: None,
                data: serde_json::json!({"id": i}),
            })
            .expect(&format!("Failed to insert node {}", i));
        node_ids.push(node_id);
    }

    // Verify all nodes have version=2
    for i in 250..=270 {
        let version = read_node_slot_version(&db_path, i).unwrap();
        assert_eq!(
            version, 2,
            "Node {} should have version=2 after creation",
            i
        );
    }

    // Insert multiple edges, each triggering begin_transaction
    println!("Inserting multiple edges, each calling begin_transaction...");
    for i in 0..10 {
        let from_idx = i;
        let to_idx = i + 1;

        match graph.insert_edge(EdgeSpec {
            from: node_ids[from_idx],
            to: node_ids[to_idx],
            edge_type: "test".to_string(),
            data: serde_json::json!({"edge_index": i}),
        }) {
            Ok(_) => {
                // Check critical nodes after each successful edge insertion
                let check_nodes = [255, 256, 257, 258, 259];
                for &node_id in &check_nodes {
                    if node_id >= 250 && node_id <= 270 {
                        let version = read_node_slot_version(&db_path, node_id).unwrap();
                        assert_eq!(
                            version,
                            2,
                            "Node {} corrupted to version {} after edge {} ({} -> {})",
                            node_id,
                            version,
                            i,
                            250 + from_idx,
                            250 + to_idx
                        );
                    }
                }
                println!("Edge {} ({} -> {}): OK", i, 250 + from_idx, 250 + to_idx);
            }
            Err(e) => {
                if e.to_string().contains("uninitialized slot")
                    || e.to_string().contains("version=0")
                {
                    panic!(
                        "CORRUPTION DETECTED at edge {} ({} -> {})",
                        i,
                        250 + from_idx,
                        250 + to_idx
                    );
                } else {
                    panic!("Different error at edge {}: {}", i, e);
                }
            }
        }
    }

    println!("✅ Multiple transaction begin operations completed without corruption");
}