use keyvaluedb::KeyValueDB;
use keyvaluedb_sqlite::{Database, DatabaseConfig, RepairCheck};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
fn wal_path(path: &Path) -> PathBuf {
let mut os = path.as_os_str().to_owned();
os.push("-wal");
PathBuf::from(os)
}
fn wal_len(path: &Path) -> u64 {
std::fs::metadata(wal_path(path))
.map(|m| m.len())
.unwrap_or(0)
}
async fn commit_rows(db: &Database, start: u32, n: u32) {
let value = vec![0xa5u8; 400];
for i in start..start + n {
let mut tx = db.transaction();
tx.put(0, format!("key_{i:06}").as_bytes(), &value);
db.write(tx).await.unwrap();
}
}
#[tokio::test]
async fn wal_stays_bounded_and_truncates() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("db");
let db = Database::open(&path, DatabaseConfig::new()).unwrap();
commit_rows(&db, 0, 2000).await;
let during = wal_len(&path);
assert!(
during < 2 * 1024 * 1024,
"WAL grew past the autocheckpoint watermark: {} bytes",
during
);
let result = db.checkpoint().await.unwrap();
assert!(
result.complete(),
"checkpoint did not complete: {:?}",
result
);
assert_eq!(wal_len(&path), 0, "truncate checkpoint left WAL bytes");
}
#[tokio::test]
async fn wal_config_overrides_apply() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("db");
let db = Database::open(
&path,
DatabaseConfig::new()
.with_wal_autocheckpoint_pages(1000)
.with_wal_journal_size_limit(6 * 1024 * 1024),
)
.unwrap();
commit_rows(&db, 0, 2000).await;
let during = wal_len(&path);
assert!(
during > 2 * 1024 * 1024,
"WAL stayed under the default watermark despite a raised override: {} bytes",
during
);
}
#[tokio::test]
async fn pinned_reader_grows_wal_and_checkpoint_reports_it() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("db");
let db = Database::open(&path, DatabaseConfig::new()).unwrap();
commit_rows(&db, 0, 10).await;
let raw = rusqlite::Connection::open(&path).unwrap();
raw.execute_batch("BEGIN").unwrap();
let _: i64 = raw
.query_row("SELECT count(*) FROM column_0", [], |r| r.get(0))
.unwrap();
commit_rows(&db, 10, 1000).await;
let pinned = wal_len(&path);
assert!(
pinned > 2 * 1024 * 1024,
"expected the pinned WAL to outgrow the watermark, got {} bytes",
pinned
);
let result = db.checkpoint().await.unwrap();
assert!(
!result.complete(),
"checkpoint claimed completion under a pinned reader: {:?}",
result
);
raw.execute_batch("COMMIT").unwrap();
drop(raw);
let result = db.checkpoint().await.unwrap();
assert!(
result.complete(),
"checkpoint did not complete: {:?}",
result
);
assert_eq!(wal_len(&path), 0);
}
#[tokio::test]
async fn open_replays_and_truncates_stale_wal() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("db");
let db = Database::open(&path, DatabaseConfig::new()).unwrap();
let raw = rusqlite::Connection::open(&path).unwrap();
raw.execute_batch("BEGIN").unwrap();
let _: i64 = raw
.query_row("SELECT count(*) FROM column_0", [], |r| r.get(0))
.unwrap();
commit_rows(&db, 0, 500).await;
let copy = dir.path().join("copy");
std::fs::copy(&path, ©).unwrap();
std::fs::copy(wal_path(&path), wal_path(©)).unwrap();
assert!(wal_len(©) > 1024 * 1024);
let db2 = Database::open(©, DatabaseConfig::new()).unwrap();
assert_eq!(wal_len(©), 0, "open did not truncate the stale WAL");
let val = db2.get(0, b"key_000499").await.unwrap();
assert_eq!(val.unwrap().len(), 400, "replayed row missing after open");
}
#[tokio::test(flavor = "multi_thread")]
async fn open_completes_under_concurrent_writer() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("db");
let db = Database::open(&path, DatabaseConfig::new()).unwrap();
commit_rows(&db, 0, 100).await;
let stop = Arc::new(AtomicBool::new(false));
let writer = {
let db = db.clone();
let stop = stop.clone();
tokio::spawn(async move {
let mut i = 100u32;
while !stop.load(Ordering::Relaxed) {
commit_rows(&db, i, 1).await;
i += 1;
tokio::time::sleep(Duration::from_millis(1)).await;
}
})
};
let cfg = DatabaseConfig::new()
.with_repair_on_corrupt(true)
.with_repair_check(RepairCheck::Full);
let path2 = path.clone();
let opened = tokio::time::timeout(
Duration::from_secs(30),
tokio::task::spawn_blocking(move || Database::open(&path2, cfg)),
)
.await
.expect("open wedged under a concurrent writer")
.unwrap()
.unwrap();
let val = opened.get(0, b"key_000050").await.unwrap();
assert!(val.is_some());
stop.store(true, Ordering::Relaxed);
writer.await.unwrap();
}