use grounddb::system_db::SystemDb;
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
fn doc(title: &str) -> serde_yaml::Value {
serde_yaml::from_str(&format!("title: {title}")).unwrap()
}
#[test]
fn an_open_reader_does_not_block_a_writer() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("_system.db");
let writer = SystemDb::open(&path).unwrap();
writer
.upsert_document("seed", "posts", "posts/seed.md", &doc("seed"), None, None, None)
.unwrap();
let reader = rusqlite::Connection::open(&path).unwrap();
reader.execute_batch("BEGIN DEFERRED").unwrap();
let seen: i64 = reader
.query_row("SELECT count(*) FROM documents", [], |row| row.get(0))
.unwrap();
assert_eq!(seen, 1);
let started = Instant::now();
let result = writer.upsert_document(
"written",
"posts",
"posts/written.md",
&doc("written"),
None,
None,
None,
);
let elapsed = started.elapsed();
result.expect("an open reader must not be able to fail a write");
assert!(
elapsed < Duration::from_secs(1),
"write took {elapsed:?} — it contended with the reader instead of \
proceeding, which means WAL is not in effect"
);
reader.execute_batch("COMMIT").unwrap();
}
#[test]
fn second_connection_waits_for_a_held_write_transaction() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("_system.db");
let writer = SystemDb::open(&path).unwrap();
let contender = SystemDb::open(&path).unwrap();
writer.begin_transaction().unwrap();
writer
.upsert_document("held", "posts", "posts/held.md", &doc("held"), None, None, None)
.unwrap();
let (tx, rx) = mpsc::channel();
let contender_thread = thread::spawn(move || {
tx.send(()).unwrap();
let result = contender.upsert_document(
"other",
"posts",
"posts/other.md",
&doc("other"),
None,
None,
None,
);
(contender, result)
});
rx.recv().unwrap();
thread::sleep(Duration::from_millis(300));
writer.commit_transaction().unwrap();
let (contender, result) = contender_thread.join().unwrap();
result.expect("contending write should have waited for the commit, not returned SQLITE_BUSY");
let reader = SystemDb::open(&path).unwrap();
assert!(reader.get_document("posts", "held").unwrap().is_some());
assert!(reader.get_document("posts", "other").unwrap().is_some());
drop(contender);
}
#[test]
fn reader_is_not_blocked_by_an_uncommitted_writer() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("_system.db");
let writer = SystemDb::open(&path).unwrap();
writer
.upsert_document(
"before",
"posts",
"posts/before.md",
&doc("before"),
None,
None,
None,
)
.unwrap();
let reader = SystemDb::open(&path).unwrap();
writer.begin_transaction().unwrap();
writer
.upsert_document(
"during",
"posts",
"posts/during.md",
&doc("during"),
None,
None,
None,
)
.unwrap();
assert!(reader.get_document("posts", "before").unwrap().is_some());
assert!(reader.get_document("posts", "during").unwrap().is_none());
writer.commit_transaction().unwrap();
assert!(reader.get_document("posts", "during").unwrap().is_some());
}
#[test]
fn transaction_that_reads_before_writing_survives_a_concurrent_commit() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("_system.db");
let a = SystemDb::open(&path).unwrap();
let b = SystemDb::open(&path).unwrap();
a.upsert_document("seed", "posts", "posts/seed.md", &doc("seed"), None, None, None)
.unwrap();
a.begin_transaction().unwrap();
assert!(a.get_document("posts", "seed").unwrap().is_some());
let (tx, rx) = mpsc::channel();
let contender = thread::spawn(move || {
tx.send(()).unwrap();
let r = b.upsert_document("theirs", "posts", "posts/t.md", &doc("t"), None, None, None);
(b, r)
});
rx.recv().unwrap();
thread::sleep(Duration::from_millis(300));
a.upsert_document("mine", "posts", "posts/m.md", &doc("mine"), None, None, None)
.expect("write after read inside a transaction must not hit BUSY_SNAPSHOT");
a.commit_transaction().unwrap();
let (b, contender_result) = contender.join().unwrap();
contender_result.expect("contending write should have waited for A's commit");
let reader = SystemDb::open(&path).unwrap();
assert!(reader.get_document("posts", "mine").unwrap().is_some());
assert!(reader.get_document("posts", "theirs").unwrap().is_some());
drop(b);
}
#[test]
fn on_disk_db_uses_wal_and_a_busy_timeout() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("_system.db");
let db = SystemDb::open(&path).unwrap();
assert_eq!(db.journal_mode().unwrap().to_lowercase(), "wal");
assert_eq!(db.busy_timeout_ms().unwrap(), 5000);
}
#[test]
fn in_memory_db_does_not_claim_wal() {
let db = SystemDb::open_in_memory().unwrap();
assert_ne!(db.journal_mode().unwrap().to_lowercase(), "wal");
assert_eq!(db.busy_timeout_ms().unwrap(), 5000);
}