graph_d 1.3.2

A native graph database implementation in Rust with built-in JSON support and SQLite-like simplicity
Documentation
//! Transaction Integration Tests
//!
//! Validates: GAP-9 - Full transaction flow integration
//! Satisfies: RT-2 (ACID transactions), B4 (ACID compliance)
//! Tests: TransactionManager + MVCC integration
//!
//! These tests verify the complete transaction lifecycle from
//! begin through commit/rollback, ensuring all components work together.

use graph_d::graph::{Node, Relationship};
use graph_d::transaction::{IsolationLevel, TransactionManager};
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;

/// Helper to create a test node
fn make_node(id: u64, label: &str) -> Node {
    let mut props = HashMap::new();
    props.insert("label".to_string(), serde_json::json!(label));
    Node::new(id, props)
}

/// Helper to create a test relationship
fn make_relationship(id: u64, from: u64, to: u64, rel_type: &str) -> Relationship {
    let mut props = HashMap::new();
    props.insert("type".to_string(), serde_json::json!(rel_type));
    Relationship::new(id, from, to, rel_type.to_string(), props)
}

/// Test: Basic transaction lifecycle without MVCC
/// Validates: B4 (basic transaction support)
#[test]
fn test_basic_transaction_lifecycle() {
    let tm = TransactionManager::new(IsolationLevel::ReadCommitted);

    // Begin transaction
    let mut tx = tm.begin();
    assert!(tx.id() > 0);

    // Commit transaction
    let result = tx.commit();
    assert!(result.is_ok());
}

/// Test: Transaction rollback
/// Validates: RT-2 (atomicity - rollback support)
#[test]
fn test_transaction_rollback() {
    let tm = TransactionManager::new(IsolationLevel::ReadCommitted);

    let mut tx = tm.begin();
    let result = tx.rollback();
    assert!(result.is_ok());
}

/// Test: MVCC-enabled transaction manager
/// Validates: RT-2 (MVCC integration with TransactionManager)
#[test]
fn test_mvcc_enabled_transaction_manager() {
    let tm = TransactionManager::new_with_mvcc(IsolationLevel::RepeatableRead);

    // Verify MVCC is enabled
    assert!(tm.has_mvcc());
    assert!(tm.mvcc().is_some());

    // Begin MVCC transaction and write
    let tx_id = tm.begin_mvcc().unwrap();
    assert!(tx_id > 0);

    // Write a node
    let node = make_node(1, "Person");
    let result = tm.mvcc_write_node(tx_id, node);
    assert!(result.is_ok());

    // Commit first
    let commit_result = tm.mvcc_commit(tx_id);
    assert!(commit_result.is_ok());

    // Now read in a new transaction (after commit, data is visible)
    let read_tx = tm.begin_mvcc().unwrap();
    let read_result = tm.mvcc_read_node(read_tx, 1);
    assert!(read_result.is_ok());
    assert!(read_result.unwrap().is_some());
    tm.mvcc_commit(read_tx).unwrap();
}

/// Test: MVCC isolation between concurrent transactions
/// Validates: RT-2 (isolation), TN4 (optimistic concurrency)
#[test]
fn test_mvcc_isolation() {
    let tm = TransactionManager::new_with_mvcc(IsolationLevel::RepeatableRead);

    // TX1: Create initial node
    let tx1 = tm.begin_mvcc().unwrap();
    tm.mvcc_write_node(tx1, make_node(1, "Original")).unwrap();
    tm.mvcc_commit(tx1).unwrap();

    // TX2: Start reading
    let tx2 = tm.begin_mvcc().unwrap();
    let node_v1 = tm.mvcc_read_node(tx2, 1).unwrap().unwrap();
    assert_eq!(node_v1.get_property("label").unwrap(), "Original");

    // TX3: Modify the node
    let tx3 = tm.begin_mvcc().unwrap();
    tm.mvcc_write_node(tx3, make_node(1, "Modified")).unwrap();
    tm.mvcc_commit(tx3).unwrap();

    // TX2 should still see original (repeatable read)
    let node_still_v1 = tm.mvcc_read_node(tx2, 1).unwrap().unwrap();
    assert_eq!(node_still_v1.get_property("label").unwrap(), "Original");

    tm.mvcc_commit(tx2).unwrap();
}

/// Test: MVCC write-write conflict detection
/// Validates: TN4 (conflict detection at commit)
#[test]
fn test_mvcc_write_conflict() {
    let tm = TransactionManager::new_with_mvcc(IsolationLevel::RepeatableRead);

    // Setup: Create node
    let setup = tm.begin_mvcc().unwrap();
    tm.mvcc_write_node(setup, make_node(1, "Initial")).unwrap();
    tm.mvcc_commit(setup).unwrap();

    // Two concurrent transactions
    let tx1 = tm.begin_mvcc().unwrap();
    let tx2 = tm.begin_mvcc().unwrap();

    // Both read the same node
    tm.mvcc_read_node(tx1, 1).unwrap();
    tm.mvcc_read_node(tx2, 1).unwrap();

    // TX1 modifies and commits first
    tm.mvcc_write_node(tx1, make_node(1, "TX1")).unwrap();
    tm.mvcc_commit(tx1).unwrap();

    // TX2 tries to modify the same node
    tm.mvcc_write_node(tx2, make_node(1, "TX2")).unwrap();

    // TX2 commit should fail due to conflict
    let result = tm.mvcc_commit(tx2);
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("conflict"));
}

/// Test: MVCC rollback clears uncommitted writes
/// Validates: RT-2 (atomicity)
#[test]
fn test_mvcc_rollback_isolation() {
    let tm = TransactionManager::new_with_mvcc(IsolationLevel::RepeatableRead);

    // TX1: Write and rollback
    let tx1 = tm.begin_mvcc().unwrap();
    tm.mvcc_write_node(tx1, make_node(1, "ShouldNotExist"))
        .unwrap();
    tm.mvcc_rollback(tx1).unwrap();

    // TX2: Should not see rolled back data
    let tx2 = tm.begin_mvcc().unwrap();
    let node = tm.mvcc_read_node(tx2, 1).unwrap();
    assert!(node.is_none());
    tm.mvcc_commit(tx2).unwrap();
}

/// Test: Relationship transactions with MVCC
/// Validates: B4 (relationships are also ACID compliant)
#[test]
fn test_mvcc_relationship_transaction() {
    let tm = TransactionManager::new_with_mvcc(IsolationLevel::RepeatableRead);

    // Create nodes and relationship
    let tx = tm.begin_mvcc().unwrap();
    tm.mvcc_write_node(tx, make_node(1, "Person")).unwrap();
    tm.mvcc_write_node(tx, make_node(2, "Company")).unwrap();
    tm.mvcc_write_relationship(tx, make_relationship(1, 1, 2, "WORKS_AT"))
        .unwrap();
    tm.mvcc_commit(tx).unwrap();

    // Verify
    let verify_tx = tm.begin_mvcc().unwrap();
    let node1 = tm.mvcc_read_node(verify_tx, 1).unwrap();
    let node2 = tm.mvcc_read_node(verify_tx, 2).unwrap();
    let rel = tm.mvcc_read_relationship(verify_tx, 1).unwrap();

    assert!(node1.is_some());
    assert!(node2.is_some());
    assert!(rel.is_some());

    let rel = rel.unwrap();
    assert_eq!(rel.from_id, 1);
    assert_eq!(rel.to_id, 2);

    tm.mvcc_commit(verify_tx).unwrap();
}

/// Test: MVCC stats reporting
/// Validates: Observability of transaction system
#[test]
fn test_mvcc_stats() {
    let tm = TransactionManager::new_with_mvcc(IsolationLevel::ReadCommitted);

    // Start some transactions
    let tx1 = tm.begin_mvcc().unwrap();
    let tx2 = tm.begin_mvcc().unwrap();

    let stats = tm.mvcc_stats();
    assert!(stats.is_some());
    let stats = stats.unwrap();
    assert!(stats.active_transactions >= 2);

    // Commit and check stats update
    tm.mvcc_commit(tx1).unwrap();
    tm.mvcc_commit(tx2).unwrap();

    let stats_after = tm.mvcc_stats().unwrap();
    assert_eq!(stats_after.active_transactions, 0);
}

/// Test: Garbage collection
/// Validates: Memory efficiency (don't keep infinite history)
#[test]
fn test_mvcc_garbage_collection() {
    let tm = TransactionManager::new_with_mvcc(IsolationLevel::ReadCommitted);

    // Create many versions
    for i in 0..10 {
        let tx = tm.begin_mvcc().unwrap();
        tm.mvcc_write_node(tx, make_node(1, &format!("Version{i}")))
            .unwrap();
        tm.mvcc_commit(tx).unwrap();
    }

    // Run GC
    tm.mvcc_gc();

    // Should still be able to read latest version
    let tx = tm.begin_mvcc().unwrap();
    let node = tm.mvcc_read_node(tx, 1).unwrap();
    assert!(node.is_some());
    tm.mvcc_commit(tx).unwrap();
}

/// Test: Transaction manager without MVCC doesn't panic
/// Validates: Graceful handling of non-MVCC operations
#[test]
fn test_non_mvcc_transaction_manager() {
    let tm = TransactionManager::new(IsolationLevel::ReadCommitted);

    // MVCC should not be enabled
    assert!(!tm.has_mvcc());
    assert!(tm.mvcc().is_none());

    // Regular transactions should still work
    let mut tx = tm.begin();
    tx.commit().unwrap();
}

/// Test: Concurrent transactions from multiple threads
/// Validates: T6 (thread-safe concurrent operations)
#[test]
fn test_concurrent_transactions() {
    let tm = Arc::new(TransactionManager::new_with_mvcc(
        IsolationLevel::RepeatableRead,
    ));

    // Setup initial data
    let setup = tm.begin_mvcc().unwrap();
    for i in 0..10 {
        tm.mvcc_write_node(setup, make_node(i, "Initial")).unwrap();
    }
    tm.mvcc_commit(setup).unwrap();

    // Spawn multiple reader threads
    let mut handles = vec![];

    for _thread_id in 0..4 {
        let tm_clone = Arc::clone(&tm);
        let handle = thread::spawn(move || {
            for _ in 0..10 {
                let tx = tm_clone.begin_mvcc().unwrap();

                // Read all nodes
                for i in 0..10 {
                    let node = tm_clone.mvcc_read_node(tx, i).unwrap();
                    assert!(node.is_some());
                }

                tm_clone.mvcc_commit(tx).unwrap();
            }
        });
        handles.push(handle);
    }

    // Wait for all threads
    for handle in handles {
        handle.join().unwrap();
    }
}

/// Test: Serializable isolation level prevents phantom reads
/// Validates: Full ACID compliance at Serializable level
#[test]
fn test_serializable_isolation() {
    let tm = TransactionManager::new_with_mvcc(IsolationLevel::Serializable);

    // Setup
    let setup = tm
        .begin_mvcc_with_isolation(IsolationLevel::Serializable)
        .unwrap();
    tm.mvcc_write_node(setup, make_node(1, "Node1")).unwrap();
    tm.mvcc_commit(setup).unwrap();

    // TX1: Read node 1 at serializable level
    let tx1 = tm
        .begin_mvcc_with_isolation(IsolationLevel::Serializable)
        .unwrap();
    tm.mvcc_read_node(tx1, 1).unwrap();

    // TX2: Modify node 1 and commit
    let tx2 = tm
        .begin_mvcc_with_isolation(IsolationLevel::Serializable)
        .unwrap();
    tm.mvcc_write_node(tx2, make_node(1, "Modified")).unwrap();
    tm.mvcc_commit(tx2).unwrap();

    // TX1: Try to write something else
    tm.mvcc_write_node(tx1, make_node(2, "NewNode")).unwrap();

    // TX1 commit should fail (serialization failure)
    let result = tm.mvcc_commit(tx1);
    assert!(result.is_err());
}

/// Test: Mixed isolation levels
/// Validates: Configurable isolation per transaction (TN4)
#[test]
fn test_mixed_isolation_levels() {
    let tm = TransactionManager::new_with_mvcc(IsolationLevel::ReadCommitted);

    // Setup
    let setup = tm.begin_mvcc().unwrap();
    tm.mvcc_write_node(setup, make_node(1, "Initial")).unwrap();
    tm.mvcc_commit(setup).unwrap();

    // TX1: RepeatableRead (stricter)
    let tx1 = tm
        .begin_mvcc_with_isolation(IsolationLevel::RepeatableRead)
        .unwrap();
    let v1 = tm.mvcc_read_node(tx1, 1).unwrap().unwrap();

    // TX2: ReadCommitted, modify and commit
    let tx2 = tm
        .begin_mvcc_with_isolation(IsolationLevel::ReadCommitted)
        .unwrap();
    tm.mvcc_write_node(tx2, make_node(1, "Changed")).unwrap();
    tm.mvcc_commit(tx2).unwrap();

    // TX1 still sees old value (repeatable read)
    let v1_again = tm.mvcc_read_node(tx1, 1).unwrap().unwrap();
    assert_eq!(v1.get_property("label"), v1_again.get_property("label"));

    tm.mvcc_commit(tx1).unwrap();
}