#![cfg(feature = "std")]
use graphitesql::pager::{CheckpointMode, PageSource, WritePager};
use graphitesql::vfs::std_file::StdVfs;
use graphitesql::vfs::{OpenFlags, Vfs};
use std::process::Command;
fn temp_path(name: &str) -> String {
let mut p = std::env::temp_dir();
p.push(format!("graphitesql-ckptm-{}-{name}", std::process::id()));
p.to_string_lossy().into_owned()
}
fn cleanup(path: &str) {
for suffix in ["", "-journal", "-wal", "-shm"] {
let _ = std::fs::remove_file(format!("{path}{suffix}"));
}
}
fn sqlite3_available() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
fn sqlite3_run(path: &str, sql: &str) -> String {
let out = Command::new("sqlite3").arg(path).arg(sql).output().unwrap();
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn wal_pager(path: &str, create: bool) -> WritePager {
let vfs = StdVfs::new();
let main = vfs.open(path, OpenFlags::READ_WRITE_CREATE).unwrap();
let journal = vfs
.open(&format!("{path}-journal"), OpenFlags::READ_WRITE_CREATE)
.unwrap();
let wal = vfs
.open(&format!("{path}-wal"), OpenFlags::READ_WRITE_CREATE)
.unwrap();
let mut wp = if create {
let mut wp = WritePager::create_wal(main, Some(journal), Some(wal), 4096).unwrap();
wp.commit().unwrap();
wp
} else {
WritePager::open_wal(main, Some(journal), Some(wal)).unwrap()
};
assert!(wp.set_wal_mode().unwrap());
wp
}
fn commit_uv(wp: &mut WritePager, v: u32) {
wp.header_mut().user_version = v;
wp.commit().unwrap();
}
fn wal_size(path: &str) -> u64 {
std::fs::metadata(format!("{path}-wal")).unwrap().len()
}
fn wal_hdr_seq_salt1(path: &str) -> (u32, u32) {
let bytes = std::fs::read(format!("{path}-wal")).unwrap();
let be32 =
|at: usize| u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]);
(be32(12), be32(16))
}
const FRAME: u64 = 24 + 4096;
#[test]
fn passive_backfills_all_and_keeps_wal() {
let path = temp_path("passive.db");
cleanup(&path);
let mut wp = wal_pager(&path, true);
for v in 1..=3 {
commit_uv(&mut wp, v);
}
let before = wal_size(&path);
assert_eq!(before, 32 + 3 * FRAME);
assert_eq!(
wp.checkpoint_mode(CheckpointMode::Passive).unwrap(),
(0, 3, 3)
);
assert_eq!(wal_size(&path), before, "PASSIVE must not touch the -wal");
assert_eq!(
wp.checkpoint_mode(CheckpointMode::Passive).unwrap(),
(0, 3, 3)
);
drop(wp);
if sqlite3_available() {
assert_eq!(sqlite3_run(&path, "PRAGMA integrity_check;"), "ok");
assert_eq!(sqlite3_run(&path, "PRAGMA user_version;"), "3");
}
cleanup(&path);
}
#[test]
fn truncate_zeroes_wal() {
let path = temp_path("truncate.db");
cleanup(&path);
let mut wp = wal_pager(&path, true);
for v in 1..=2 {
commit_uv(&mut wp, v);
}
assert_eq!(
wp.checkpoint_mode(CheckpointMode::Truncate).unwrap(),
(0, 0, 0)
);
assert_eq!(wal_size(&path), 0, "TRUNCATE zeroes the -wal file");
drop(wp);
if sqlite3_available() {
assert_eq!(sqlite3_run(&path, "PRAGMA integrity_check;"), "ok");
assert_eq!(sqlite3_run(&path, "PRAGMA user_version;"), "2");
}
cleanup(&path);
}
#[test]
fn pinned_reader_limits_backfill_and_flags_busy() {
let path = temp_path("pinned.db");
cleanup(&path);
let mut writer = wal_pager(&path, true);
for v in 1..=2 {
commit_uv(&mut writer, v);
}
let reader = wal_pager(&path, false);
reader.begin_read_txn().unwrap();
commit_uv(&mut writer, 3);
let before = wal_size(&path);
assert_eq!(
writer.checkpoint_mode(CheckpointMode::Passive).unwrap(),
(0, 3, 2),
"PASSIVE: backfill stops at the pinned mark, no busy"
);
assert_eq!(
writer.checkpoint_mode(CheckpointMode::Full).unwrap(),
(1, 3, 2),
"FULL: the pinned reader makes the checkpoint busy"
);
assert_eq!(
writer.checkpoint_mode(CheckpointMode::Truncate).unwrap(),
(1, 3, 2),
"TRUNCATE: blocked the same way"
);
assert_eq!(wal_size(&path), before, "a blocked TRUNCATE keeps the -wal");
reader.end_read_txn();
assert_eq!(
writer.checkpoint_mode(CheckpointMode::Full).unwrap(),
(0, 3, 3),
"reader gone: FULL completes"
);
drop(reader);
drop(writer);
cleanup(&path);
}
#[test]
fn restart_then_next_commit_overwrites_in_place() {
let path = temp_path("restart.db");
cleanup(&path);
let mut wp = wal_pager(&path, true);
for v in 1..=3 {
commit_uv(&mut wp, v);
}
let size3 = wal_size(&path);
let (seq0, salt1_0) = wal_hdr_seq_salt1(&path);
assert_eq!(
wp.checkpoint_mode(CheckpointMode::Restart).unwrap(),
(0, 3, 3)
);
assert_eq!(
wal_size(&path),
size3,
"RESTART leaves the -wal bytes alone"
);
assert_eq!(wal_hdr_seq_salt1(&path), (seq0, salt1_0));
commit_uv(&mut wp, 4);
assert_eq!(
wal_size(&path),
size3,
"the restarted log overwrites in place — no truncate, no append"
);
let (seq1, salt1_1) = wal_hdr_seq_salt1(&path);
assert_eq!(seq1, seq0 + 1, "checkpoint sequence increments on restart");
assert_eq!(salt1_1, salt1_0.wrapping_add(1), "salt-1 increments by one");
drop(wp);
let wp = wal_pager(&path, false);
assert_eq!(wp.header().user_version, 4);
drop(wp);
if sqlite3_available() {
assert_eq!(sqlite3_run(&path, "PRAGMA integrity_check;"), "ok");
assert_eq!(sqlite3_run(&path, "PRAGMA user_version;"), "4");
}
cleanup(&path);
}
#[test]
fn non_wal_database_reports_minus_one() {
let path = temp_path("nonwal.db");
cleanup(&path);
let vfs = StdVfs::new();
let main = vfs.open(&path, OpenFlags::READ_WRITE_CREATE).unwrap();
let mut wp = WritePager::create(main, None, 4096).unwrap();
wp.commit().unwrap();
assert_eq!(
wp.checkpoint_mode(CheckpointMode::Passive).unwrap(),
(0, -1, -1)
);
assert_eq!(
wp.checkpoint_mode(CheckpointMode::Truncate).unwrap(),
(0, -1, -1)
);
drop(wp);
cleanup(&path);
}
#[test]
fn mode_names_parse_like_sqlite() {
assert_eq!(CheckpointMode::from_name("FULL"), CheckpointMode::Full);
assert_eq!(CheckpointMode::from_name("full"), CheckpointMode::Full);
assert_eq!(
CheckpointMode::from_name("Restart"),
CheckpointMode::Restart
);
assert_eq!(
CheckpointMode::from_name("truncate"),
CheckpointMode::Truncate
);
assert_eq!(
CheckpointMode::from_name("passive"),
CheckpointMode::Passive
);
assert_eq!(CheckpointMode::from_name("bogus"), CheckpointMode::Passive);
assert_eq!(CheckpointMode::from_name("0"), CheckpointMode::Passive);
}