#![cfg(feature = "std")]
use graphitesql::Connection;
use std::process::Command;
fn sqlite3_available() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
fn sqlite3_ok(sql: &str) -> bool {
Command::new("sqlite3")
.arg(":memory:")
.arg(sql)
.output()
.unwrap()
.status
.success()
}
fn graphite_ok(script: &[&str]) -> bool {
let mut c = Connection::open_memory().unwrap();
for s in script {
if c.execute(s).is_err() {
return false;
}
}
true
}
fn agree(script: &[&str]) {
let g = graphite_ok(script);
if sqlite3_available() {
let s = sqlite3_ok(&script.join(";"));
assert_eq!(g, s, "graphite/sqlite disagree on: {script:?}");
}
}
#[test]
fn standalone_unique_index_enforced() {
agree(&[
"CREATE TABLE t(a INT)",
"CREATE UNIQUE INDEX ux ON t(a)",
"INSERT INTO t VALUES(1)",
"INSERT INTO t VALUES(1)", ]);
agree(&[
"CREATE TABLE t(a INT)",
"CREATE UNIQUE INDEX ux ON t(a)",
"INSERT INTO t VALUES(1)",
"INSERT INTO t VALUES(2)", ]);
agree(&[
"CREATE TABLE t(a INT)",
"CREATE UNIQUE INDEX ux ON t(a)",
"INSERT INTO t VALUES(NULL)",
"INSERT INTO t VALUES(NULL)",
]);
}
#[test]
fn unique_index_collation_and_expression() {
agree(&[
"CREATE TABLE t(x TEXT)",
"CREATE UNIQUE INDEX ux ON t(lower(x))",
"INSERT INTO t VALUES('Hello')",
"INSERT INTO t VALUES('HELLO')",
]);
agree(&[
"CREATE TABLE t(x TEXT)",
"CREATE UNIQUE INDEX ux ON t(x COLLATE NOCASE)",
"INSERT INTO t VALUES('abc')",
"INSERT INTO t VALUES('ABC')",
]);
}
#[test]
fn unique_index_partial_and_multicolumn() {
agree(&[
"CREATE TABLE t(a INT, b INT)",
"CREATE UNIQUE INDEX ux ON t(a) WHERE b > 0",
"INSERT INTO t VALUES(1, 1)",
"INSERT INTO t VALUES(1, 5)", ]);
agree(&[
"CREATE TABLE t(a INT, b INT)",
"CREATE UNIQUE INDEX ux ON t(a) WHERE b > 0",
"INSERT INTO t VALUES(1, 1)",
"INSERT INTO t VALUES(1, -5)", ]);
agree(&[
"CREATE TABLE t(a INT, b INT)",
"CREATE UNIQUE INDEX ux ON t(a, b)",
"INSERT INTO t VALUES(1, 2)",
"INSERT INTO t VALUES(1, 2)", ]);
agree(&[
"CREATE TABLE t(a INT, b INT)",
"CREATE UNIQUE INDEX ux ON t(a, b)",
"INSERT INTO t VALUES(1, NULL)",
"INSERT INTO t VALUES(1, NULL)", ]);
}
#[test]
fn unique_index_update_conflict() {
agree(&[
"CREATE TABLE t(a INT)",
"CREATE UNIQUE INDEX ux ON t(a)",
"INSERT INTO t VALUES(1)",
"INSERT INTO t VALUES(2)",
"UPDATE t SET a = 1 WHERE a = 2", ]);
}
#[test]
fn unique_index_without_rowid() {
agree(&[
"CREATE TABLE t(k INT PRIMARY KEY, a INT) WITHOUT ROWID",
"CREATE UNIQUE INDEX ux ON t(a)",
"INSERT INTO t VALUES(1, 10)",
"INSERT INTO t VALUES(2, 10)", ]);
agree(&[
"CREATE TABLE t(k INT PRIMARY KEY, a INT) WITHOUT ROWID",
"CREATE UNIQUE INDEX ux ON t(a)",
"INSERT INTO t VALUES(1, 10)",
"INSERT INTO t VALUES(2, 20)", "UPDATE t SET a = 10 WHERE k = 2", ]);
}
#[test]
fn or_ignore_and_replace_with_unique_index() {
if !sqlite3_available() {
eprintln!("sqlite3 CLI not found; skipping");
return;
}
let mut p = std::env::temp_dir();
p.push(format!("graphitesql-uqidx-{}.db", std::process::id()));
let path = p.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&path);
{
let mut c = Connection::create(&path).unwrap();
for s in [
"CREATE TABLE t(a INT, v TEXT)",
"CREATE UNIQUE INDEX ux ON t(a)",
"INSERT INTO t VALUES(1, 'x')",
"INSERT OR IGNORE INTO t VALUES(1, 'y')", "INSERT OR REPLACE INTO t VALUES(2, 'p')",
"INSERT OR REPLACE INTO t VALUES(2, 'q')", ] {
c.execute(s).unwrap();
}
let r = c.query("SELECT a, v FROM t ORDER BY a").unwrap();
assert_eq!(r.rows.len(), 2);
}
let check = Command::new("sqlite3")
.arg(&path)
.arg("PRAGMA integrity_check;")
.output()
.unwrap();
assert_eq!(
String::from_utf8_lossy(&check.stdout).trim(),
"ok",
"integrity_check failed"
);
let rows = Command::new("sqlite3")
.arg(&path)
.arg("SELECT a||':'||v FROM t ORDER BY a;")
.output()
.unwrap();
assert_eq!(String::from_utf8_lossy(&rows.stdout).trim(), "1:x\n2:q");
let _ = std::fs::remove_file(&path);
}