use std::sync::Arc;
use limbo_core::{Connection, Database, StepResult, Value};
const WAL_HEADER_SIZE: u64 = 32;
fn new_io() -> Arc<dyn limbo_core::IO> {
Arc::new(limbo_core::SyscallIO::new().unwrap())
}
fn temp_db_path(tag: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"oxisqlite_durability_{}_{}_{}.db",
tag,
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
fn cleanup(path: &std::path::Path) {
let _ = std::fs::remove_file(path);
let _ = std::fs::remove_file(format!("{}-wal", path.display()));
let _ = std::fs::remove_file(format!("{}-shm", path.display()));
}
fn read_int(io: &Arc<dyn limbo_core::IO>, conn: &Arc<Connection>, sql: &str) -> i64 {
let mut stmt = conn.query(sql).unwrap().expect("statement");
loop {
match stmt.step().unwrap() {
StepResult::Row => {
let row = stmt.row().expect("row");
return match row.get_value(0) {
Value::Integer(i) => *i,
other => panic!("expected integer, got {other:?}"),
};
}
StepResult::IO => io.run_once().unwrap(),
StepResult::Done => panic!("no row produced for {sql}"),
other => panic!("unexpected step result: {other:?}"),
}
}
}
fn wal_len(path: &std::path::Path) -> Option<u64> {
std::fs::metadata(format!("{}-wal", path.display()))
.ok()
.map(|m| m.len())
}
fn wal_frame_bytes_present(path: &std::path::Path) -> bool {
let wal = format!("{}-wal", path.display());
match std::fs::read(&wal) {
Ok(bytes) => {
if bytes.len() > 512 {
return true;
}
bytes
.get(WAL_HEADER_SIZE as usize..)
.map(|tail| tail.iter().any(|b| *b != 0))
.unwrap_or(false)
}
Err(_) => false,
}
}
#[test]
fn pragma_synchronous_roundtrip() {
let path = temp_db_path("sync_roundtrip");
cleanup(&path);
let io = new_io();
let db = Database::open_file(io.clone(), path.to_str().unwrap(), false).unwrap();
let conn = db.connect().unwrap();
for (name, expected) in [("OFF", 0), ("NORMAL", 1), ("FULL", 2), ("EXTRA", 3)] {
conn.execute(format!("PRAGMA synchronous = {name}"))
.unwrap();
let got = read_int(&io, &conn, "PRAGMA synchronous");
assert_eq!(
got, expected,
"name form {name} should read back {expected}"
);
}
for n in [0_i64, 1, 2, 3] {
conn.execute(format!("PRAGMA synchronous = {n}")).unwrap();
let got = read_int(&io, &conn, "PRAGMA synchronous");
assert_eq!(got, n, "numeric form {n} should read back {n}");
}
cleanup(&path);
}
#[test]
fn synchronous_off_still_durable_within_process() {
let path = temp_db_path("sync_off");
cleanup(&path);
let io = new_io();
let db = Database::open_file(io.clone(), path.to_str().unwrap(), false).unwrap();
let conn = db.connect().unwrap();
conn.execute("PRAGMA synchronous = OFF").unwrap();
conn.execute("CREATE TABLE t(x)").unwrap();
for i in 0..10 {
conn.execute(format!("INSERT INTO t VALUES ({i})")).unwrap();
}
let count = read_int(&io, &conn, "SELECT count(*) FROM t");
assert_eq!(
count, 10,
"rows visible within process even with synchronous=OFF"
);
conn.checkpoint().unwrap();
let count_after = read_int(&io, &conn, "SELECT count(*) FROM t");
assert_eq!(count_after, 10);
cleanup(&path);
}
#[test]
fn wal_truncated_on_close() {
let path = temp_db_path("close_truncate");
cleanup(&path);
let io = new_io();
{
let db = Database::open_file(io.clone(), path.to_str().unwrap(), false).unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t(x)").unwrap();
for i in 0..5 {
conn.execute(format!("INSERT INTO t VALUES ({i})")).unwrap();
}
conn.close().unwrap();
}
assert!(
!wal_frame_bytes_present(&path),
"post-close -wal should hold no frames, was {:?} bytes",
wal_len(&path)
);
let io2 = new_io();
let db2 = Database::open_file(io2.clone(), path.to_str().unwrap(), false).unwrap();
let conn2 = db2.connect().unwrap();
let count = read_int(&io2, &conn2, "SELECT count(*) FROM t");
assert_eq!(count, 5, "fresh reader sees all committed rows after close");
cleanup(&path);
}
#[test]
fn wal_checkpoint_truncate_mode() {
let path = temp_db_path("ckpt_truncate");
cleanup(&path);
let io = new_io();
let db = Database::open_file(io.clone(), path.to_str().unwrap(), false).unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t(x)").unwrap();
for i in 0..8 {
conn.execute(format!("INSERT INTO t VALUES ({i})")).unwrap();
}
conn.checkpoint_truncate().unwrap();
assert!(
!wal_frame_bytes_present(&path),
"post-TRUNCATE -wal should hold no frames, was {:?} bytes",
wal_len(&path)
);
let count = read_int(&io, &conn, "SELECT count(*) FROM t");
assert_eq!(count, 8);
cleanup(&path);
}
#[test]
fn drop_checkpoints_without_explicit_close() {
let path = temp_db_path("drop_ckpt");
cleanup(&path);
let io = new_io();
{
let db = Database::open_file(io.clone(), path.to_str().unwrap(), false).unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t(x)").unwrap();
for i in 0..6 {
conn.execute(format!("INSERT INTO t VALUES ({i})")).unwrap();
}
}
assert!(
!wal_frame_bytes_present(&path),
"post-Drop -wal should hold no frames, was {:?} bytes",
wal_len(&path)
);
let io2 = new_io();
let db2 = Database::open_file(io2.clone(), path.to_str().unwrap(), false).unwrap();
let conn2 = db2.connect().unwrap();
let count = read_int(&io2, &conn2, "SELECT count(*) FROM t");
assert_eq!(
count, 6,
"Drop ran the checkpoint; fresh reader sees all rows"
);
cleanup(&path);
}
#[test]
fn malformed_wal_returns_err_not_panic() {
let base = temp_db_path("malformed");
cleanup(&base);
{
let io = new_io();
let db = Database::open_file(io.clone(), base.to_str().unwrap(), false).unwrap();
let conn = db.connect().unwrap();
conn.execute("CREATE TABLE t(x)").unwrap();
conn.execute("INSERT INTO t VALUES (1)").unwrap();
conn.close().unwrap();
}
let _ = std::fs::remove_file(format!("{}-wal", base.display()));
let _ = std::fs::remove_file(format!("{}-shm", base.display()));
let wal_path = format!("{}-wal", base.display());
std::fs::write(&wal_path, vec![0u8; 10]).unwrap();
let res_a = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let io = new_io();
Database::open_file(io.clone(), base.to_str().unwrap(), false)
.and_then(|db| db.connect().map(|_| ()))
}));
assert!(res_a.is_ok(), "truncated-header WAL must not panic");
assert!(
res_a.unwrap().is_err(),
"truncated-header WAL must surface an error"
);
let _ = std::fs::remove_file(&wal_path);
let _ = std::fs::remove_file(format!("{}-shm", base.display()));
let mut bogus = vec![0u8; 32];
bogus[0] = 0x37;
bogus[1] = 0x7f;
bogus[2] = 0x06;
bogus[3] = 0x82;
bogus[8] = 0x00;
bogus[9] = 0x00;
bogus[10] = 0x10;
bogus[11] = 0x00;
std::fs::write(&wal_path, &bogus).unwrap();
let res_b = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let io = new_io();
Database::open_file(io.clone(), base.to_str().unwrap(), false)
.and_then(|db| db.connect().map(|_| ()))
}));
assert!(res_b.is_ok(), "bogus-checksum WAL must not panic");
assert!(
res_b.unwrap().is_err(),
"bogus-checksum WAL must surface an error"
);
cleanup(&base);
}