#![cfg(feature = "std")]
use graphitesql::{Connection, Value};
use std::process::Command;
fn temp_path(name: &str) -> String {
let mut p = std::env::temp_dir();
p.push(format!("graphitesql-pm-{}-{name}", std::process::id()));
p.to_string_lossy().into_owned()
}
fn cleanup(path: &str) {
for s in ["", "-journal", "-wal"] {
let _ = std::fs::remove_file(format!("{path}{s}"));
}
}
fn sqlite3_available() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
fn integrity(path: &str) -> String {
Command::new("sqlite3")
.arg(path)
.arg("PRAGMA integrity_check;")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap()
}
fn count(c: &Connection, sql: &str) -> i64 {
match c.query(sql).unwrap().rows[0][0] {
Value::Integer(n) => n,
ref o => panic!("{o:?}"),
}
}
#[test]
fn heavy_delete_reclaims_and_stays_valid() {
if !sqlite3_available() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = temp_path("heavy.db");
cleanup(&path);
let page_count_after_first;
{
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, s TEXT)")
.unwrap();
for i in 1..=3000 {
c.execute(&format!("INSERT INTO t(s) VALUES ('value-number-{i}')"))
.unwrap();
}
page_count_after_first = count(&c, "PRAGMA page_count");
c.execute("DELETE FROM t WHERE id > 100").unwrap();
assert_eq!(count(&c, "SELECT count(*) FROM t"), 100);
for i in 1..=2000 {
c.execute(&format!("INSERT INTO t(s) VALUES ('again-{i}')"))
.unwrap();
}
let after = count(&c, "PRAGMA page_count");
assert!(
after <= page_count_after_first + 5,
"freed pages should be reused: first={page_count_after_first} after={after}"
);
assert_eq!(count(&c, "SELECT count(*) FROM t"), 2100);
}
assert_eq!(integrity(&path), "ok");
cleanup(&path);
}
#[test]
fn scattered_and_full_deletes_valid() {
if !sqlite3_available() {
eprintln!("sqlite3 not found; skipping");
return;
}
let path = temp_path("scatter.db");
cleanup(&path);
{
let mut c = Connection::create(&path).unwrap();
c.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, a INT, b TEXT)")
.unwrap();
c.execute("CREATE INDEX ia ON t(a)").unwrap();
for i in 1..=1500 {
c.execute(&format!("INSERT INTO t(a,b) VALUES ({}, 'x{i}')", i % 30))
.unwrap();
}
c.execute("DELETE FROM t WHERE id % 3 = 0").unwrap();
assert_eq!(integrity(&path), "ok");
c.execute("DELETE FROM t WHERE id BETWEEN 500 AND 1400")
.unwrap();
assert_eq!(integrity(&path), "ok");
let want = count(&c, "SELECT count(*) FROM t WHERE a = 5");
assert!(want >= 0);
c.execute("DELETE FROM t").unwrap();
assert_eq!(count(&c, "SELECT count(*) FROM t"), 0);
}
assert_eq!(integrity(&path), "ok");
cleanup(&path);
}
#[test]
fn delete_then_query_matches_sqlite() {
if !sqlite3_available() {
eprintln!("sqlite3 not found; skipping");
return;
}
let setup: Vec<String> = {
let mut v = vec!["CREATE TABLE t(id INTEGER PRIMARY KEY, v INT)".to_string()];
for i in 1..=800 {
v.push(format!("INSERT INTO t(v) VALUES ({})", i * 7 % 101));
}
v.push("DELETE FROM t WHERE id % 5 <> 0".to_string());
v
};
let path = temp_path("cmp.db");
cleanup(&path);
let mut c = Connection::create(&path).unwrap();
for s in &setup {
c.execute(s).unwrap();
}
drop(c);
let want = Command::new("sqlite3")
.arg(":memory:")
.arg(format!(
"{};SELECT count(*), sum(v) FROM t;",
setup.join(";")
))
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap();
let c = Connection::open_readonly(&path).unwrap();
let got = format!(
"{}|{}",
count(&c, "SELECT count(*) FROM t"),
count(&c, "SELECT sum(v) FROM t"),
);
assert_eq!(got, want);
assert_eq!(integrity(&path), "ok");
cleanup(&path);
}