#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use sqlite_core::row_history::ViewState;
use sqlite_core::{Database, Value};
fn sqlite3_bin() -> Option<String> {
let bin = std::env::var("SQLITE3_BIN").unwrap_or_else(|_| "sqlite3".to_string());
Command::new(&bin)
.arg("--version")
.output()
.ok()
.filter(|o| o.status.success())
.map(|_| bin)
}
fn scratch(tag: &str) -> PathBuf {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let mut p = std::env::temp_dir();
p.push(format!("sqlite4n6_rowhist_{tag}_{nonce}"));
std::fs::create_dir_all(&p).unwrap();
p
}
fn writer_sql(bin: &str, db: &Path, sql: &str) {
let out = Command::new(bin).arg(db).arg(sql).output().unwrap();
assert!(
out.status.success(),
"sqlite3 writer failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
fn build_fixture(bin: &str, dir: &Path) -> (std::process::Child, PathBuf, PathBuf) {
let db = dir.join("ev.db");
let wal = dir.join("ev.db-wal");
let mut reader = Command::new(bin)
.arg(&db)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
let mut rin = reader.stdin.take().unwrap();
writeln!(
rin,
"PRAGMA journal_mode=WAL;\nPRAGMA wal_autocheckpoint=0;\nPRAGMA secure_delete=OFF;\n\
CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT);"
)
.unwrap();
rin.flush().unwrap();
std::thread::sleep(std::time::Duration::from_millis(400));
writeln!(rin, "BEGIN;\nSELECT count(*) FROM t;").unwrap();
rin.flush().unwrap();
std::thread::sleep(std::time::Duration::from_millis(400));
reader.stdin = Some(rin);
writer_sql(
bin,
&db,
"PRAGMA wal_autocheckpoint=0; \
INSERT INTO t VALUES(1,'a'); INSERT INTO t VALUES(2,'b'); INSERT INTO t VALUES(4,'x');",
);
writer_sql(
bin,
&db,
"PRAGMA wal_autocheckpoint=0; \
UPDATE t SET name='A' WHERE id=1; DELETE FROM t WHERE id=2; DELETE FROM t WHERE id=4;",
);
writer_sql(
bin,
&db,
"PRAGMA wal_autocheckpoint=0; \
INSERT INTO t VALUES(3,'c'); INSERT INTO t VALUES(4,'y');",
);
(reader, db, wal)
}
fn teardown(mut reader: std::process::Child, dir: &Path) {
if let Some(mut rin) = reader.stdin.take() {
let _ = writeln!(rin, "COMMIT;\n.quit");
}
let _ = reader.wait();
let _ = std::fs::remove_dir_all(dir);
}
fn name_of(v: &sqlite_core::row_history::RowVersion) -> String {
match v.values.get(1) {
Some(Value::Text(s)) => s.clone(),
other => panic!("expected TEXT name, got {other:?}"),
}
}
#[test]
fn row_histories_reconstruct_the_known_mutation_sequence() {
let Some(bin) = sqlite3_bin() else {
eprintln!("SKIP row_histories_reconstruct_the_known_mutation_sequence: no sqlite3");
return;
};
let dir = scratch("oracle");
let (reader, db_path, wal_path) = build_fixture(&bin, &dir);
let main = std::fs::read(&db_path).unwrap();
let walb = std::fs::read(&wal_path).expect("snapshot -wal present");
assert!(!walb.is_empty(), "the -wal must be RETAINED (non-empty)");
let db = Database::open_with_wal(main, &walb).expect("open with wal");
let histories = db.row_histories();
let t = histories
.iter()
.find(|h| h.table == "t")
.expect("table t present");
assert!(!t.without_rowid, "t is an ordinary rowid table");
let by_rowid = |id: i64| -> Vec<&sqlite_core::row_history::RowVersion> {
t.versions
.iter()
.filter(|v| v.rowid == Some(id))
.collect::<Vec<_>>()
};
let r1 = by_rowid(1);
assert_eq!(r1.len(), 2, "rowid 1 has two versions");
assert_eq!(name_of(r1[0]), "a");
assert_eq!(r1[0].view_state, ViewState::ValueChangedLater);
assert_eq!(name_of(r1[1]), "A");
assert_eq!(r1[1].view_state, ViewState::PresentInFinalView);
assert!(
r1[0].commit_seq.unwrap() < r1[1].commit_seq.unwrap(),
"commit_seq is per-epoch monotonic"
);
let r2 = by_rowid(2);
assert_eq!(r2.len(), 1, "rowid 2 has one (deleted) version");
assert_eq!(name_of(r2[0]), "b");
assert_eq!(r2[0].view_state, ViewState::AbsentInFinalView);
assert!(r2[0].is_deleted);
let r3 = by_rowid(3);
assert_eq!(r3.len(), 1, "rowid 3 has one (live) version");
assert_eq!(name_of(r3[0]), "c");
assert_eq!(r3[0].view_state, ViewState::PresentInFinalView);
let r4 = by_rowid(4);
assert_eq!(r4.len(), 2, "rowid 4 has two distinct entities");
assert_eq!(name_of(r4[0]), "x");
assert_eq!(r4[0].view_state, ViewState::AbsentInFinalView);
assert!(r4[0].is_deleted);
assert_eq!(name_of(r4[1]), "y");
assert_eq!(r4[1].view_state, ViewState::PresentInFinalView);
assert!(
r4.iter().all(|v| v.rowid_reused),
"every version of a reused rowid is flagged rowid_reused"
);
teardown(reader, &dir);
}