#![cfg(feature = "std")]
use graphitesql::Connection;
use std::process::Command;
fn norm(s: &str) -> String {
s.lines()
.filter(|l| !l.trim().eq_ignore_ascii_case("QUERY PLAN"))
.map(|l| l.trim_start_matches(|ch| "|`- ".contains(ch)).trim_end())
.collect::<Vec<_>>()
.join(" | ")
}
fn g_eqp(ddl: &str, q: &str) -> String {
let mut c = Connection::open_memory().unwrap();
for stmt in ddl.split_inclusive(';') {
if !stmt.trim().is_empty() {
c.execute(stmt).unwrap();
}
}
let rows = c.query(&format!("EXPLAIN QUERY PLAN {q}")).unwrap().rows;
let mut lines = Vec::new();
for r in &rows {
if let Some(graphitesql::Value::Text(s)) = r.last() {
lines.push(s.clone());
}
}
lines.join(" | ")
}
fn sqlite_eqp(ddl: &str, q: &str) -> String {
let o = Command::new("sqlite3")
.arg(":memory:")
.arg(format!("{ddl} EXPLAIN QUERY PLAN {q};"))
.output()
.unwrap();
norm(&String::from_utf8_lossy(&o.stdout))
}
fn g_rows(ddl: &str, q: &str) -> String {
let mut c = Connection::open_memory().unwrap();
for stmt in ddl.split_inclusive(';') {
if !stmt.trim().is_empty() {
c.execute(stmt).unwrap();
}
}
let r = c.query(q).unwrap();
r.rows
.iter()
.map(|row| {
row.iter()
.map(|v| match v {
graphitesql::Value::Null => "".to_string(),
graphitesql::Value::Integer(i) => i.to_string(),
graphitesql::Value::Real(f) => format!("{f}"),
graphitesql::Value::Text(s) => s.clone(),
graphitesql::Value::Blob(_) => "<blob>".to_string(),
})
.collect::<Vec<_>>()
.join("|")
})
.collect::<Vec<_>>()
.join("\n")
}
fn sqlite_rows(ddl: &str, q: &str) -> String {
let o = Command::new("sqlite3")
.arg(":memory:")
.arg(format!("{ddl} {q};"))
.output()
.unwrap();
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
}
fn check(ddl: &str, q: &str) {
assert_eq!(g_eqp(ddl, q), sqlite_eqp(ddl, q), "EQP diverged for {q}");
assert_eq!(g_rows(ddl, q), sqlite_rows(ddl, q), "rows diverged for {q}");
}
#[test]
fn single_column_isnull_seeks_the_index() {
if Command::new("sqlite3").arg("--version").output().is_err() {
return;
}
let d = "CREATE TABLE t(a, b, c); CREATE INDEX ia ON t(a); \
INSERT INTO t VALUES(1,2,3),(NULL,3,4),(1,5,6),(NULL,7,8);";
for q in [
"SELECT * FROM t WHERE a IS NULL",
"SELECT b FROM t WHERE a IS NULL",
"SELECT count(*) FROM t WHERE a IS NULL",
"SELECT sum(a) FROM t WHERE a IS NULL",
"SELECT a, count(*) FROM t WHERE a IS NULL GROUP BY a",
"SELECT * FROM t WHERE a IS NOT NULL",
] {
check(d, q);
}
}
#[test]
fn composite_index_isnull_prefix() {
if Command::new("sqlite3").arg("--version").output().is_err() {
return;
}
let d = "CREATE TABLE t(a, b); CREATE INDEX iab ON t(a, b); \
INSERT INTO t VALUES(1,2),(NULL,3),(NULL,8),(NULL,NULL),(1,NULL);";
for q in [
"SELECT * FROM t WHERE a IS NULL AND b IS NULL",
"SELECT * FROM t WHERE a IS NULL AND b = 3",
"SELECT * FROM t WHERE a = 1 AND b IS NULL",
"SELECT * FROM t WHERE a IS NULL AND b > 2",
"SELECT * FROM t WHERE a IS NULL AND b BETWEEN 2 AND 8",
"SELECT count(*) FROM t WHERE a IS NULL AND b IS NULL",
] {
check(d, q);
}
}
#[test]
fn rowid_isnull_still_scans() {
if Command::new("sqlite3").arg("--version").output().is_err() {
return;
}
let d = "CREATE TABLE t(id INTEGER PRIMARY KEY, a); CREATE INDEX ia ON t(a); \
INSERT INTO t VALUES(1,NULL),(2,5),(3,NULL);";
check(d, "SELECT * FROM t WHERE id IS NULL");
check(d, "SELECT * FROM t WHERE a IS NULL");
}
#[test]
fn without_rowid_secondary_isnull_seeks() {
if Command::new("sqlite3").arg("--version").output().is_err() {
return;
}
let d1 = "CREATE TABLE w(k TEXT PRIMARY KEY, v, u) WITHOUT ROWID; \
CREATE INDEX iv ON w(v); \
INSERT INTO w VALUES('a',NULL,1),('b',2,2),('c',NULL,3);";
check(d1, "SELECT * FROM w WHERE v IS NULL");
check(d1, "SELECT count(*) FROM w WHERE v IS NULL");
check(d1, "SELECT k FROM w WHERE v IS NULL");
check(d1, "SELECT * FROM w WHERE k IS NULL");
let d2 = "CREATE TABLE w(k TEXT PRIMARY KEY, v, u) WITHOUT ROWID; \
CREATE INDEX ivu ON w(v, u); \
INSERT INTO w VALUES('a',NULL,1),('b',2,2),('c',NULL,NULL);";
check(d2, "SELECT k FROM w WHERE v IS NULL AND u IS NULL");
check(d2, "SELECT k, u FROM w WHERE v IS NULL AND u = 1");
}