#![cfg(not(target_arch = "wasm32"))]
use minigraf::QueryResult;
use minigraf::db::{Minigraf, OpenOptions};
const PAGE_SIZE: usize = 4096;
fn count_results(result: QueryResult) -> usize {
match result {
QueryResult::QueryResults { results, .. } => results.len(),
_ => 0,
}
}
fn wal_path_for(db_path: &std::path::Path) -> std::path::PathBuf {
let mut p = db_path.as_os_str().to_owned();
p.push(".wal");
std::path::PathBuf::from(p)
}
#[test]
fn test_file_backed_basic_persistence() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("basic.graph");
{
let db = Minigraf::open(&db_path).unwrap();
db.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
}
let db2 = Minigraf::open(&db_path).unwrap();
let n = count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(n, 1, "Alice must survive close/reopen");
}
#[test]
fn test_wal_recovery_after_simulated_crash() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("crash.graph");
let wal_path = wal_path_for(&db_path);
{
let db = Minigraf::open_with_options(
&db_path,
OpenOptions {
wal_checkpoint_threshold: usize::MAX,
..Default::default()
},
)
.unwrap();
db.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
std::mem::forget(db);
}
assert!(wal_path.exists(), "WAL must exist after simulated crash");
let db2 = Minigraf::open(&db_path).unwrap();
let n = count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(
n, 1,
"Alice must be recovered from WAL after simulated crash"
);
}
#[test]
fn test_no_duplicate_facts_after_post_checkpoint_crash() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("dedup.graph");
let wal_path = wal_path_for(&db_path);
{
let db = Minigraf::open_with_options(
&db_path,
OpenOptions {
wal_checkpoint_threshold: usize::MAX,
..Default::default()
},
)
.unwrap();
db.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
std::mem::forget(db);
}
let wal_backup = std::fs::read(&wal_path).unwrap();
{
let db = Minigraf::open(&db_path).unwrap();
let n = count_results(
db.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(n, 1, "Alice must be visible in session 2");
}
assert!(!wal_path.exists(), "WAL must be deleted after normal close");
std::fs::write(&wal_path, &wal_backup).unwrap();
let db3 = Minigraf::open(&db_path).unwrap();
let n = count_results(
db3.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(
n, 1,
"must have exactly 1 Alice — no duplicates after stale WAL replay"
);
}
#[test]
fn test_partial_wal_entry_discarded_earlier_entries_intact() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("partial.graph");
let wal_path = wal_path_for(&db_path);
{
let db = Minigraf::open_with_options(
&db_path,
OpenOptions {
wal_checkpoint_threshold: usize::MAX,
..Default::default()
},
)
.unwrap();
db.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
std::mem::forget(db);
}
{
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&wal_path)
.unwrap();
file.write_all(&[0xFF, 0xFF, 0xFF, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF])
.unwrap();
}
let db2 = Minigraf::open(&db_path).unwrap();
let n = count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(
n, 1,
"exactly 1 fact (Alice) must survive despite partial WAL entry"
);
}
#[test]
fn test_manual_checkpoint_deletes_wal() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("manual_cp.graph");
let wal_path = wal_path_for(&db_path);
let db = Minigraf::open_with_options(
&db_path,
OpenOptions {
wal_checkpoint_threshold: usize::MAX,
..Default::default()
},
)
.unwrap();
db.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
assert!(
wal_path.exists(),
"WAL must exist after write with high threshold"
);
db.checkpoint().unwrap();
assert!(
!wal_path.exists(),
"WAL must be deleted after manual checkpoint"
);
let n = count_results(
db.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(n, 1, "Alice must still be visible after checkpoint");
{
use std::io::Read;
let mut f = std::fs::File::open(&db_path).unwrap();
let mut page = vec![0u8; PAGE_SIZE];
f.read_exact(&mut page).unwrap();
let last_checkpointed_tx_count = u64::from_le_bytes(page[24..32].try_into().unwrap());
assert!(
last_checkpointed_tx_count > 0,
"last_checkpointed_tx_count must be set after checkpoint"
);
}
std::mem::forget(db);
let db2 = Minigraf::open(&db_path).unwrap();
let n2 = count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(
n2, 1,
"Alice must be present after crash-reopen when already checkpointed"
);
}
#[test]
fn test_auto_checkpoint_fires_at_threshold() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("autocheckpoint.graph");
let wal_path = wal_path_for(&db_path);
{
let db = Minigraf::open_with_options(
&db_path,
OpenOptions {
wal_checkpoint_threshold: 2,
..Default::default()
},
)
.unwrap();
db.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
db.execute(r#"(transact [[:bob :name "Bob"]])"#).unwrap();
assert!(
!wal_path.exists(),
"WAL must be deleted after auto-checkpoint at threshold=2"
);
std::mem::forget(db);
}
assert!(
!wal_path.exists(),
"WAL must not exist after auto-checkpoint crash"
);
let db2 = Minigraf::open(&db_path).unwrap();
let n = count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(
n, 2,
"both Alice and Bob must survive via main file after auto-checkpoint"
);
}
#[test]
fn test_explicit_tx_all_or_nothing_commit() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("explicit_commit.graph");
{
let db = Minigraf::open_with_options(
&db_path,
OpenOptions {
wal_checkpoint_threshold: usize::MAX,
..Default::default()
},
)
.unwrap();
let mut tx = db.begin_write().unwrap();
tx.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
tx.execute(r#"(transact [[:bob :name "Bob"]])"#).unwrap();
tx.commit().unwrap();
std::mem::forget(db);
}
let db2 = Minigraf::open(&db_path).unwrap();
let n = count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(
n, 2,
"both Alice and Bob must survive explicit commit + crash"
);
}
#[test]
fn test_explicit_tx_rollback_not_persisted() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("rollback.graph");
{
let db = Minigraf::open(&db_path).unwrap();
let mut tx = db.begin_write().unwrap();
tx.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
tx.rollback();
}
let db2 = Minigraf::open(&db_path).unwrap();
let n = count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(n, 0, "rolled-back facts must not survive reopen");
}
#[test]
fn test_explicit_tx_multiple_transacts_rollback_not_persisted() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("multi_rollback.graph");
let opts = OpenOptions {
wal_checkpoint_threshold: usize::MAX,
..Default::default()
};
{
let db = Minigraf::open_with_options(&db_path, opts).unwrap();
let mut tx = db.begin_write().unwrap();
tx.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
tx.execute(r#"(transact [[:bob :name "Bob"]])"#).unwrap();
tx.rollback();
}
let db2 = Minigraf::open(&db_path).unwrap();
let n = count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(n, 0, "both rolled-back facts must not persist after reopen");
}
#[test]
fn test_concurrent_reads_while_writer_holds_lock() {
use std::sync::{Arc, Barrier};
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("test.graph");
let db = Minigraf::open(&db_path).unwrap();
db.execute("(transact [[:alice :name \"Alice\"]])").unwrap();
db.checkpoint().unwrap();
let db2 = db.clone();
let barrier = Arc::new(Barrier::new(2));
let barrier2 = Arc::clone(&barrier);
let _tx = db.begin_write().unwrap();
let reader = std::thread::spawn(move || {
barrier2.wait(); count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
)
});
barrier.wait(); let n = reader.join().unwrap();
assert_eq!(
n, 1,
"reader must see committed state while writer holds the lock"
);
}
#[test]
fn test_implicit_tx_execute_survives_replay() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("implicit_tx.graph");
let wal_path = wal_path_for(&db_path);
{
let db = Minigraf::open_with_options(
&db_path,
OpenOptions {
wal_checkpoint_threshold: usize::MAX,
..Default::default()
},
)
.unwrap();
db.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
std::mem::forget(db);
}
assert!(wal_path.exists(), "WAL must exist after simulated crash");
let db2 = Minigraf::open(&db_path).unwrap();
let n = count_results(
db2.execute("(query [:find ?name :where [?e :name ?name]])")
.unwrap(),
);
assert_eq!(
n, 1,
"Alice must be recovered via WAL replay after implicit execute() crash"
);
}
#[test]
fn test_v2_file_opens_and_upgrades_to_v3_on_checkpoint() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("v2.graph");
{
let mut page = vec![0u8; PAGE_SIZE];
page[0..4].copy_from_slice(b"MGRF");
page[4..8].copy_from_slice(&2u32.to_le_bytes());
page[8..16].copy_from_slice(&1u64.to_le_bytes());
let mut file = std::fs::File::create(&db_path).unwrap();
file.write_all(&page).unwrap();
file.sync_all().unwrap();
}
{
let db = Minigraf::open(&db_path).unwrap();
db.execute(r#"(transact [[:alice :name "Alice"]])"#)
.unwrap();
db.checkpoint().unwrap();
}
let raw = std::fs::read(&db_path).unwrap();
assert!(
raw.len() >= PAGE_SIZE,
"file must be at least one page after checkpoint"
);
let magic = &raw[0..4];
let version = u32::from_le_bytes(raw[4..8].try_into().unwrap());
let last_checkpointed_tx_count = u64::from_le_bytes(raw[24..32].try_into().unwrap());
assert_eq!(version, 7, "file must be upgraded to v7 on checkpoint");
assert_eq!(magic, b"MGRF", "magic number must be preserved");
assert!(
last_checkpointed_tx_count > 0,
"last_checkpointed_tx_count must be set after checkpoint on v2→v6 upgrade"
);
}