#![cfg(feature = "std")]
use std::process::Command;
fn sqlite3_available() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
fn load(bin: &str, db: &str, sql: &str) {
let _ = std::fs::remove_file(db);
let out = Command::new(bin).arg(db).arg(sql).output().expect("run");
assert!(
out.status.success(),
"load failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
fn query(bin: &str, db: &str, sql: &str) -> String {
let out = Command::new(bin).arg(db).arg(sql).output().expect("query");
String::from_utf8_lossy(&out.stdout).trim_end().to_owned()
}
fn run_full(bin: &str, db: &str, sql: &str) -> String {
let _ = std::fs::remove_file(db);
let out = Command::new(bin).arg(db).arg(sql).output().expect("run");
let mut s = String::from_utf8_lossy(&out.stdout).into_owned();
s.push_str(&String::from_utf8_lossy(&out.stderr));
s.trim_end().to_owned()
}
fn tmp(name: &str) -> String {
let mut p = std::env::temp_dir();
p.push(format!("graphite_fkwr_{}_{}.db", std::process::id(), name));
p.to_string_lossy().into_owned()
}
fn assert_valid_and_matching(name: &str, sql: &str, probes: &[&str]) {
let g = env!("CARGO_BIN_EXE_graphitesql");
let gdb = tmp(&format!("g_{name}"));
load(g, &gdb, sql);
assert_eq!(
query(g, &gdb, "PRAGMA integrity_check"),
"ok",
"[{name}] graphite integrity_check"
);
if sqlite3_available() {
assert_eq!(
query("sqlite3", &gdb, "PRAGMA quick_check"),
"ok",
"[{name}] sqlite3 quick_check on graphite file"
);
let sdb = tmp(&format!("s_{name}"));
load("sqlite3", &sdb, sql);
for probe in probes {
assert_eq!(
query(g, &gdb, probe),
query("sqlite3", &sdb, probe),
"[{name}] row set diverged from sqlite3 for `{probe}`"
);
}
let _ = std::fs::remove_file(&sdb);
}
let _ = std::fs::remove_file(&gdb);
}
fn assert_full_matching(name: &str, sql: &str) {
let g = env!("CARGO_BIN_EXE_graphitesql");
let gdb = tmp(&format!("gf_{name}"));
let gout = run_full(g, &gdb, sql);
if sqlite3_available() {
let sdb = tmp(&format!("sf_{name}"));
let sout = run_full("sqlite3", &sdb, sql);
assert_eq!(gout, sout, "[{name}] output diverged from sqlite3");
if !sout.to_lowercase().contains("error") {
assert_eq!(
query("sqlite3", &gdb, "PRAGMA quick_check"),
"ok",
"[{name}] sqlite3 quick_check on graphite file"
);
}
let _ = std::fs::remove_file(&sdb);
}
let _ = std::fs::remove_file(&gdb);
}
#[test]
fn on_delete_cascade_into_wr_child() {
let sql = "PRAGMA foreign_keys=ON;\
PRAGMA page_size=4096;\
CREATE TABLE p(id INTEGER PRIMARY KEY);\
CREATE TABLE ch(k TEXT, pid INT REFERENCES p(id) ON DELETE CASCADE, data BLOB, PRIMARY KEY(k,pid)) WITHOUT ROWID;\
WITH RECURSIVE c(x) AS(SELECT 1 UNION ALL SELECT x+1 FROM c WHERE x<50) INSERT INTO p SELECT x FROM c;\
WITH RECURSIVE c(x) AS(SELECT 1 UNION ALL SELECT x+1 FROM c WHERE x<400)\
INSERT INTO ch SELECT 'k'||x,(x%49)+1,zeroblob(1500) FROM c;\
DELETE FROM p WHERE id<25;";
assert_valid_and_matching(
"wr_cascade_del",
sql,
&[
"SELECT count(*) FROM p",
"SELECT count(*), coalesce(sum(length(data)),0) FROM ch",
"SELECT k,pid FROM ch ORDER BY k,pid",
],
);
}
#[test]
fn on_delete_set_null_into_wr_child() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(id INTEGER PRIMARY KEY);\
CREATE TABLE ch(k TEXT PRIMARY KEY, pid INT REFERENCES p(id) ON DELETE SET NULL, tag TEXT) WITHOUT ROWID;\
INSERT INTO p VALUES(1),(2),(3);\
INSERT INTO ch VALUES('a',1,'x'),('b',1,'y'),('c',2,'z');\
DELETE FROM p WHERE id=1;";
assert_valid_and_matching(
"wr_set_null",
sql,
&[
"SELECT id FROM p ORDER BY id",
"SELECT k,pid,tag FROM ch ORDER BY k",
],
);
}
#[test]
fn on_delete_set_default_into_wr_child() {
let ok = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(id INTEGER PRIMARY KEY);\
CREATE TABLE ch(k TEXT PRIMARY KEY, pid INT DEFAULT 3 REFERENCES p(id) ON DELETE SET DEFAULT) WITHOUT ROWID;\
INSERT INTO p VALUES(1),(2),(3);\
INSERT INTO ch VALUES('a',1),('b',1),('c',2);\
DELETE FROM p WHERE id=1;";
assert_valid_and_matching(
"wr_set_default",
ok,
&[
"SELECT id FROM p ORDER BY id",
"SELECT k,pid FROM ch ORDER BY k",
],
);
let bad = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(id INTEGER PRIMARY KEY);\
CREATE TABLE ch(k TEXT PRIMARY KEY, pid INT DEFAULT 9 REFERENCES p(id) ON DELETE SET DEFAULT) WITHOUT ROWID;\
INSERT INTO p VALUES(1),(2);\
INSERT INTO ch VALUES('a',1);\
DELETE FROM p WHERE id=1;\
SELECT k,pid FROM ch ORDER BY k;";
assert_full_matching("wr_set_default_reject", bad);
}
#[test]
fn on_update_cascade_into_wr_child_pk_recluster() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(id INTEGER PRIMARY KEY);\
CREATE TABLE ch(k TEXT, pid INT REFERENCES p(id) ON UPDATE CASCADE, PRIMARY KEY(k,pid)) WITHOUT ROWID;\
INSERT INTO p VALUES(1),(2);\
INSERT INTO ch VALUES('a',1),('b',1),('c',2);\
UPDATE p SET id=100 WHERE id=1;";
assert_valid_and_matching(
"wr_update_cascade",
sql,
&[
"SELECT id FROM p ORDER BY id",
"SELECT k,pid FROM ch ORDER BY k,pid",
],
);
}
#[test]
fn cascade_keeps_wr_child_secondary_indexes() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(id INTEGER PRIMARY KEY);\
CREATE TABLE ch(k TEXT PRIMARY KEY, pid INT REFERENCES p(id) ON DELETE CASCADE, tag TEXT) WITHOUT ROWID;\
CREATE INDEX ich_tag ON ch(tag);\
CREATE INDEX ich_pid ON ch(pid);\
INSERT INTO p VALUES(1),(2),(3);\
INSERT INTO ch VALUES('a',1,'red'),('b',1,'green'),('c',2,'blue'),('d',3,'red');\
DELETE FROM p WHERE id=1;";
assert_valid_and_matching(
"wr_secondary_idx",
sql,
&[
"SELECT k,pid,tag FROM ch ORDER BY k",
"SELECT k FROM ch WHERE tag='red' ORDER BY k",
"SELECT k FROM ch WHERE pid=2",
"SELECT count(*) FROM ch WHERE tag='green'",
],
);
}
#[test]
fn recursive_cascade_through_wr_tables() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(id INTEGER PRIMARY KEY);\
CREATE TABLE ch(k TEXT, pid INT REFERENCES p(id) ON DELETE CASCADE, PRIMARY KEY(k)) WITHOUT ROWID;\
CREATE TABLE gc(g TEXT, ck TEXT REFERENCES ch(k) ON DELETE CASCADE, PRIMARY KEY(g)) WITHOUT ROWID;\
INSERT INTO p VALUES(1),(2);\
INSERT INTO ch VALUES('a',1),('b',1),('c',2);\
INSERT INTO gc VALUES('g1','a'),('g2','a'),('g3','b'),('g4','c');\
DELETE FROM p WHERE id=1;";
assert_valid_and_matching(
"wr_recursive",
sql,
&[
"SELECT k,pid FROM ch ORDER BY k",
"SELECT g,ck FROM gc ORDER BY g",
],
);
}
#[test]
fn self_referential_wr_cascade() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE n(id TEXT PRIMARY KEY, parent TEXT REFERENCES n(id) ON DELETE CASCADE) WITHOUT ROWID;\
INSERT INTO n VALUES('root',NULL),('a','root'),('b','root'),('a1','a'),('a2','a'),('b1','b');\
DELETE FROM n WHERE id='a';";
assert_valid_and_matching("wr_self_ref", sql, &["SELECT id,parent FROM n ORDER BY id"]);
}
#[test]
fn insert_existence_check_scans_wr_parent() {
let ok = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(k TEXT PRIMARY KEY) WITHOUT ROWID;\
CREATE TABLE c(id INTEGER PRIMARY KEY, pk TEXT REFERENCES p(k));\
INSERT INTO p VALUES('a'),('b'),('c');\
INSERT INTO c VALUES(1,'a'),(2,'b'),(3,'a');\
SELECT id,pk FROM c ORDER BY id;";
assert_full_matching("wr_parent_exists_ok", ok);
let bad = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(k TEXT PRIMARY KEY) WITHOUT ROWID;\
CREATE TABLE c(id INTEGER PRIMARY KEY, pk TEXT REFERENCES p(k));\
INSERT INTO p VALUES('a'),('b');\
INSERT INTO c VALUES(1,'a');\
INSERT INTO c VALUES(2,'zzz');\
SELECT id,pk FROM c ORDER BY id;";
assert_full_matching("wr_parent_exists_reject", bad);
}
#[test]
fn on_delete_cascade_from_wr_parent_to_rowid_child() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(k TEXT PRIMARY KEY) WITHOUT ROWID;\
CREATE TABLE c(id INTEGER PRIMARY KEY, pk TEXT REFERENCES p(k) ON DELETE CASCADE);\
INSERT INTO p VALUES('a'),('b'),('c');\
INSERT INTO c VALUES(1,'a'),(2,'a'),(3,'b');\
DELETE FROM p WHERE k='a';";
assert_valid_and_matching(
"wr_parent_cascade",
sql,
&[
"SELECT k FROM p ORDER BY k",
"SELECT id,pk FROM c ORDER BY id",
],
);
}
#[test]
fn wr_parent_delete_restrict_blocks() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(k TEXT PRIMARY KEY) WITHOUT ROWID;\
CREATE TABLE c(id INTEGER PRIMARY KEY, pk TEXT REFERENCES p(k) ON DELETE RESTRICT);\
INSERT INTO p VALUES('a'),('b');\
INSERT INTO c VALUES(1,'a');\
DELETE FROM p WHERE k='a';\
SELECT (SELECT count(*) FROM p), (SELECT count(*) FROM c);";
assert_full_matching("wr_parent_restrict", sql);
}
#[test]
fn on_update_cascade_from_wr_parent() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(k TEXT PRIMARY KEY) WITHOUT ROWID;\
CREATE TABLE c(id INTEGER PRIMARY KEY, pk TEXT REFERENCES p(k) ON UPDATE CASCADE);\
INSERT INTO p VALUES('a'),('b');\
INSERT INTO c VALUES(1,'a'),(2,'b'),(3,'a');\
UPDATE p SET k='z' WHERE k='a';";
assert_valid_and_matching(
"wr_parent_update_cascade",
sql,
&[
"SELECT k FROM p ORDER BY k",
"SELECT id,pk FROM c ORDER BY id",
],
);
}
#[test]
fn cascade_wr_parent_and_wr_child() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(k TEXT PRIMARY KEY, v INT) WITHOUT ROWID;\
CREATE TABLE c(ck TEXT, pk TEXT REFERENCES p(k) ON DELETE CASCADE, PRIMARY KEY(ck)) WITHOUT ROWID;\
INSERT INTO p VALUES('a',1),('b',2),('c',3);\
INSERT INTO c VALUES('x','a'),('y','a'),('z','b');\
DELETE FROM p WHERE k='a';";
assert_valid_and_matching(
"wr_both_sides",
sql,
&[
"SELECT k,v FROM p ORDER BY k",
"SELECT ck,pk FROM c ORDER BY ck",
],
);
}
#[test]
fn composite_fk_into_composite_pk_wr_child() {
let sql = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(a INT, b INT, PRIMARY KEY(a,b));\
CREATE TABLE ch(k TEXT, fa INT, fb INT, PRIMARY KEY(k), FOREIGN KEY(fa,fb) REFERENCES p(a,b) ON DELETE CASCADE) WITHOUT ROWID;\
INSERT INTO p VALUES(1,1),(1,2),(2,1);\
INSERT INTO ch VALUES('x',1,1),('y',1,1),('z',1,2);\
DELETE FROM p WHERE a=1 AND b=1;";
assert_valid_and_matching("wr_composite", sql, &["SELECT k,fa,fb FROM ch ORDER BY k"]);
}