#![cfg(feature = "std")]
use graphitesql::{Connection, Error};
fn parent_child() -> Connection {
let mut c = Connection::open_memory().unwrap();
c.execute("PRAGMA foreign_keys = ON").unwrap();
c.execute("CREATE TABLE parent(id INTEGER PRIMARY KEY, name TEXT)")
.unwrap();
c.execute("CREATE TABLE child(id INTEGER PRIMARY KEY, pid INT REFERENCES parent(id), v INT)")
.unwrap();
c.execute("INSERT INTO parent(id,name) VALUES (1,'a'),(2,'b'),(3,'c')")
.unwrap();
c
}
#[test]
fn pragma_toggle() {
let mut c = Connection::open_memory().unwrap();
assert_eq!(
c.query("PRAGMA foreign_keys").unwrap().rows[0][0],
graphitesql::Value::Integer(0)
);
c.execute("PRAGMA foreign_keys = ON").unwrap();
assert_eq!(
c.query("PRAGMA foreign_keys").unwrap().rows[0][0],
graphitesql::Value::Integer(1)
);
c.execute("PRAGMA foreign_keys = OFF").unwrap();
assert_eq!(
c.query("PRAGMA foreign_keys").unwrap().rows[0][0],
graphitesql::Value::Integer(0)
);
}
#[test]
fn insert_requires_parent() {
let mut c = parent_child();
c.execute("INSERT INTO child(id,pid,v) VALUES (1,1,10)")
.unwrap();
c.execute("INSERT INTO child(id,pid,v) VALUES (2,NULL,20)")
.unwrap();
let err = c.execute("INSERT INTO child(id,pid,v) VALUES (3,99,30)");
assert!(matches!(err, Err(Error::Constraint(_))), "got {err:?}");
}
#[test]
fn disabled_allows_orphan() {
let mut c = parent_child();
c.execute("PRAGMA foreign_keys = OFF").unwrap();
c.execute("INSERT INTO child(id,pid,v) VALUES (3,99,30)")
.unwrap();
}
#[test]
fn delete_restrict_default() {
let mut c = parent_child();
c.execute("INSERT INTO child(id,pid,v) VALUES (1,1,10)")
.unwrap();
let err = c.execute("DELETE FROM parent WHERE id = 1");
assert!(matches!(err, Err(Error::Constraint(_))), "got {err:?}");
c.execute("DELETE FROM parent WHERE id = 2").unwrap();
}
#[test]
fn delete_cascade() {
let mut c = Connection::open_memory().unwrap();
c.execute("PRAGMA foreign_keys = ON").unwrap();
c.execute("CREATE TABLE parent(id INTEGER PRIMARY KEY)")
.unwrap();
c.execute(
"CREATE TABLE child(id INTEGER PRIMARY KEY, pid INT REFERENCES parent(id) ON DELETE CASCADE)",
)
.unwrap();
c.execute("INSERT INTO parent VALUES (1),(2)").unwrap();
c.execute("INSERT INTO child VALUES (10,1),(11,1),(12,2)")
.unwrap();
c.execute("DELETE FROM parent WHERE id = 1").unwrap();
let r = c.query("SELECT id FROM child ORDER BY id").unwrap();
assert_eq!(r.rows.len(), 1);
assert_eq!(r.rows[0][0], graphitesql::Value::Integer(12));
}
#[test]
fn delete_set_null() {
let mut c = Connection::open_memory().unwrap();
c.execute("PRAGMA foreign_keys = ON").unwrap();
c.execute("CREATE TABLE parent(id INTEGER PRIMARY KEY)")
.unwrap();
c.execute(
"CREATE TABLE child(id INTEGER PRIMARY KEY, pid INT REFERENCES parent(id) ON DELETE SET NULL)",
)
.unwrap();
c.execute("INSERT INTO parent VALUES (1)").unwrap();
c.execute("INSERT INTO child VALUES (10,1)").unwrap();
c.execute("DELETE FROM parent WHERE id = 1").unwrap();
let r = c.query("SELECT pid FROM child WHERE id = 10").unwrap();
assert_eq!(r.rows[0][0], graphitesql::Value::Null);
}
#[test]
fn update_cascade() {
let mut c = Connection::open_memory().unwrap();
c.execute("PRAGMA foreign_keys = ON").unwrap();
c.execute("CREATE TABLE parent(id INTEGER PRIMARY KEY)")
.unwrap();
c.execute(
"CREATE TABLE child(id INTEGER PRIMARY KEY, pid INT REFERENCES parent(id) ON UPDATE CASCADE)",
)
.unwrap();
c.execute("INSERT INTO parent VALUES (1)").unwrap();
c.execute("INSERT INTO child VALUES (10,1)").unwrap();
c.execute("UPDATE parent SET id = 5 WHERE id = 1").unwrap();
let r = c.query("SELECT pid FROM child WHERE id = 10").unwrap();
assert_eq!(r.rows[0][0], graphitesql::Value::Integer(5));
}
#[test]
fn foreign_keys_against_sqlite3() {
use std::process::Command;
if Command::new("sqlite3").arg("--version").output().is_err() {
eprintln!("sqlite3 not found; skipping");
return;
}
let schema = "PRAGMA foreign_keys=ON;\
CREATE TABLE p(id INTEGER PRIMARY KEY, n TEXT);\
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) ON DELETE CASCADE ON UPDATE CASCADE, v INT);\
INSERT INTO p VALUES (1,'x'),(2,'y'),(3,'z');\
INSERT INTO c VALUES (10,1,100),(11,1,101),(12,2,102),(13,NULL,103);";
let run_sqlite = |ops: &str, q: &str| -> String {
let path = std::env::temp_dir().join(format!("gsql-fk-{}.db", std::process::id()));
let _ = std::fs::remove_file(&path);
let full = format!("{schema}{ops}");
let o = Command::new("sqlite3")
.arg(&path)
.arg(&full)
.output()
.unwrap();
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
let o = Command::new("sqlite3")
.arg(&path)
.arg(format!("PRAGMA foreign_keys=ON;{q}"))
.output()
.unwrap();
let _ = std::fs::remove_file(&path);
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
};
let run_graphite = |ops: &str, q: &str| -> String {
let mut g = Connection::open_memory().unwrap();
for s in schema.split(';') {
if !s.trim().is_empty() {
g.execute(s).unwrap();
}
}
for s in ops.split(';') {
if !s.trim().is_empty() {
g.execute(s).unwrap();
}
}
let r = g.query(q).unwrap();
r.rows
.iter()
.map(|row| {
row.iter()
.map(|v| match v {
graphitesql::Value::Null => String::new(),
graphitesql::Value::Integer(i) => i.to_string(),
graphitesql::Value::Text(s) => s.clone(),
graphitesql::Value::Real(r) => graphitesql::exec::eval::format_real(*r),
graphitesql::Value::Blob(b) => {
b.iter().map(|x| format!("{x:02x}")).collect()
}
})
.collect::<Vec<_>>()
.join("|")
})
.collect::<Vec<_>>()
.join("\n")
};
let cases = [
(
"DELETE FROM p WHERE id=1;",
"SELECT id,pid FROM c ORDER BY id",
),
(
"UPDATE p SET id=9 WHERE id=2;",
"SELECT id,pid FROM c ORDER BY id",
),
("DELETE FROM p WHERE id=3;", "SELECT id FROM p ORDER BY id"),
];
for (ops, q) in cases {
assert_eq!(run_graphite(ops, q), run_sqlite(ops, q), "ops: {ops}");
}
}