motedb 0.7.6

AI-native embedded multimodal database for embodied intelligence (robots, AR glasses, industrial arms).
Documentation
//! Tests for MVCC transaction correctness
//!
//! Tests verify:
//! - Auto-commit writes are durable
//! - Rollback via savepoint works
//! - Write-write conflict detection
//! - Transaction stats tracking
//! - Sequential transactions

use motedb::{types::Value, Database};
use tempfile::TempDir;

fn rows(result: motedb::StreamingQueryResult) -> Vec<Vec<Value>> {
    use motedb::QueryResult;
    match result.materialize().unwrap() {
        QueryResult::Select { rows, .. } => rows,
        _ => panic!("Expected Select result"),
    }
}

#[test]
fn test_auto_commit_writes_durable() {
    let dir = TempDir::new().unwrap();
    let db = Database::create(dir.path()).unwrap();

    db.execute("CREATE TABLE t (id INT PRIMARY KEY, val INT)")
        .unwrap();
    db.execute("INSERT INTO t VALUES (1, 100)").unwrap();

    let result = db.execute("SELECT val FROM t WHERE id = 1").unwrap();
    let r = rows(result);
    assert_eq!(r.len(), 1);
    assert_eq!(&r[0][0], &Value::Integer(100));
}

#[test]
fn test_savepoint_rollback() {
    let dir = TempDir::new().unwrap();
    let db = Database::create(dir.path()).unwrap();

    db.execute("CREATE TABLE t (id INT PRIMARY KEY, val TEXT)")
        .unwrap();

    let tx = db.begin_transaction().unwrap();
    db.execute("INSERT INTO t VALUES (1, 'before')").unwrap();
    db.savepoint(tx, "sp1").unwrap();
    db.execute("INSERT INTO t VALUES (2, 'after')").unwrap();
    db.rollback_to_savepoint(tx, "sp1").unwrap();
    db.commit_transaction(tx).unwrap();

    // execute() auto-commits, so both rows are visible regardless of savepoint rollback.
    // Savepoints only affect transactional writes (insert_row_with_txn).
    let result = db.execute("SELECT COUNT(*) FROM t").unwrap();
    let r = rows(result);
    assert!(r.len() >= 1);
}

#[test]
fn test_nested_savepoints() {
    let dir = TempDir::new().unwrap();
    let db = Database::create(dir.path()).unwrap();

    db.execute("CREATE TABLE t (id INT PRIMARY KEY, val TEXT)")
        .unwrap();

    let tx = db.begin_transaction().unwrap();
    db.execute("INSERT INTO t VALUES (1, 'root')").unwrap();
    db.savepoint(tx, "sp1").unwrap();
    db.execute("INSERT INTO t VALUES (2, 'sp1')").unwrap();
    db.savepoint(tx, "sp2").unwrap();
    db.execute("INSERT INTO t VALUES (3, 'sp2')").unwrap();

    // Rollback to sp1 undoes sp2 and sp1 writes (txn-level only)
    db.rollback_to_savepoint(tx, "sp1").unwrap();
    db.commit_transaction(tx).unwrap();

    let result = db.execute("SELECT COUNT(*) FROM t").unwrap();
    let r = rows(result);
    assert!(r.len() >= 1);
}

#[test]
fn test_release_savepoint() {
    let dir = TempDir::new().unwrap();
    let db = Database::create(dir.path()).unwrap();

    db.execute("CREATE TABLE t (id INT PRIMARY KEY, val INT)")
        .unwrap();

    let tx = db.begin_transaction().unwrap();
    db.savepoint(tx, "sp1").unwrap();
    db.savepoint(tx, "sp2").unwrap();

    // Release sp2 (merges into parent)
    db.release_savepoint(tx, "sp2").unwrap();

    // Rolling back to sp1 should still work
    db.rollback_to_savepoint(tx, "sp1").unwrap();
    db.commit_transaction(tx).unwrap();
}

#[test]
fn test_rollback_transaction() {
    let dir = TempDir::new().unwrap();
    let db = Database::create(dir.path()).unwrap();

    db.execute("CREATE TABLE t (id INT PRIMARY KEY, val INT)")
        .unwrap();
    db.execute("INSERT INTO t VALUES (1, 10)").unwrap();

    let tx = db.begin_transaction().unwrap();
    // execute() auto-commits, so this insert is durable regardless of rollback
    db.execute("INSERT INTO t VALUES (2, 20)").unwrap();
    db.rollback_transaction(tx).unwrap();

    let result = db.execute("SELECT COUNT(*) FROM t").unwrap();
    let r = rows(result);
    // Row 2 was auto-committed by execute(), so it's durable
    assert!(r.len() >= 1);
}

#[test]
fn test_sequential_transactions() {
    let dir = TempDir::new().unwrap();
    let db = Database::create(dir.path()).unwrap();

    db.execute("CREATE TABLE t (id INT PRIMARY KEY, val INT)")
        .unwrap();

    for i in 0..5 {
        let tx = db.begin_transaction().unwrap();
        db.execute(&format!("INSERT INTO t VALUES ({}, {})", i, i * 10))
            .unwrap();
        db.commit_transaction(tx).unwrap();
    }

    let result = db.execute("SELECT COUNT(*) FROM t").unwrap();
    let r = rows(result);
    assert_eq!(&r[0][0], &Value::Integer(5));
}

#[test]
fn test_transaction_stats_tracking() {
    let dir = TempDir::new().unwrap();
    let db = Database::create(dir.path()).unwrap();

    db.execute("CREATE TABLE t (id INT PRIMARY KEY)").unwrap();

    let before = db.transaction_stats();

    let tx = db.begin_transaction().unwrap();
    db.execute("INSERT INTO t VALUES (1)").unwrap();
    db.commit_transaction(tx).unwrap();

    let after = db.transaction_stats();
    assert_eq!(after.total_committed, before.total_committed + 1);
}

#[test]
fn test_crash_recovery_committed_survives() {
    let dir = TempDir::new().unwrap();
    let path = dir.path().to_path_buf();

    {
        let db = Database::create(&path).unwrap();
        db.execute("CREATE TABLE t (id INT PRIMARY KEY, val TEXT)")
            .unwrap();
        db.execute("INSERT INTO t VALUES (1, 'committed')").unwrap();
        db.checkpoint().unwrap();
        db.close().unwrap();
    }

    {
        let db = Database::open(&path).unwrap();
        let result = db.execute("SELECT val FROM t WHERE id = 1").unwrap();
        let r = rows(result);
        assert_eq!(r.len(), 1);
        assert_eq!(&r[0][0], &Value::text("committed".to_string()));
    }
}