#![cfg(feature = "std")]
use graphitesql::Connection;
use std::path::PathBuf;
use std::process::Command;
fn oracle() -> Option<String> {
if let Ok(p) = std::env::var("GRAPHITE_STAT4_ORACLE")
&& Command::new(&p)
.arg(":memory:")
.arg("SELECT 1")
.output()
.is_ok()
{
return Some(p);
}
let default = "/tmp/claude-1000/-home-magicaltux-projects-graphitesql/\
faf0a91b-ae7e-4ff2-9c4e-2e8b1eed5c39/scratchpad/sqlite-src/\
sqlite-amalgamation-3500400/sqlite3-oracle";
if Command::new(default)
.arg(":memory:")
.arg("SELECT 1")
.output()
.is_ok()
{
return Some(default.to_string());
}
None
}
fn oracle_has_stat4(orc: &str) -> bool {
let out = Command::new(orc)
.arg(":memory:")
.arg(
"CREATE TABLE t(a); INSERT INTO t VALUES(1),(2),(3); CREATE INDEX i ON t(a); \
ANALYZE; SELECT count(*) FROM sqlite_stat4;",
)
.output();
matches!(out, Ok(o) if String::from_utf8_lossy(&o.stdout).trim().parse::<i64>().unwrap_or(0) > 0)
}
fn orc_query(orc: &str, db: &str, sql: &str) -> String {
let o = Command::new(orc).arg(db).arg(sql).output().unwrap();
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
}
fn tmp(name: &str) -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let mut p: PathBuf = std::env::temp_dir();
p.push(format!(
"gsql-scanseek-{}-{}-{}.db",
std::process::id(),
name,
n
));
let s = p.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&s);
s
}
fn graphite_access(setup: &str, query: &str) -> String {
let mut conn = Connection::open_memory().unwrap();
for stmt in setup.split(';') {
let s = stmt.trim();
if s.is_empty() {
continue;
}
conn.execute(s).unwrap();
}
let res = conn.query(&format!("EXPLAIN QUERY PLAN {query}")).unwrap();
res.rows
.iter()
.filter_map(|r| match r.last() {
Some(graphitesql::Value::Text(s)) if s.contains("SCAN") || s.contains("SEARCH") => {
Some(String::from(s.as_str()))
}
_ => None,
})
.next()
.unwrap_or_default()
}
fn oracle_access(orc: &str, setup: &str, query: &str) -> String {
let odb = tmp("o");
let script = format!("{setup} ANALYZE;");
let out = orc_query(orc, &odb, &format!("{script} EXPLAIN QUERY PLAN {query}"));
let _ = std::fs::remove_file(&odb);
out.lines()
.find(|l| l.contains("SCAN") || l.contains("SEARCH"))
.map(|l| l.trim_start_matches(['`', '-', ' ']).to_string())
.unwrap_or_default()
}
fn check(orc: &str, setup: &str, query: &str) {
let o = oracle_access(orc, setup, query);
let g = graphite_access(&format!("{setup} ANALYZE;"), query);
assert_eq!(o, g, "SCAN/SEARCH mismatch for `{query}`\nsetup: {setup}");
}
fn skew_setup(cut: usize) -> String {
format!(
"CREATE TABLE t(a,b); \
WITH RECURSIVE c(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM c WHERE i<1000) \
INSERT INTO t(a,b) SELECT i, CASE WHEN i<={cut} THEN 0 ELSE i END FROM c; \
CREATE INDEX ib ON t(b);"
)
}
#[test]
fn scan_vs_search_common_value_full_scans() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
let s = skew_setup(900);
check(&orc, &s, "SELECT * FROM t WHERE b=0;");
check(&orc, &s, "SELECT * FROM t WHERE b=950;");
check(&orc, &s, "SELECT a FROM t WHERE b=0;");
check(&orc, &s, "SELECT b FROM t WHERE b=0;");
}
#[test]
fn scan_vs_search_threshold_sweep() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
for cut in [100, 300, 440, 450, 460, 480, 500, 550, 700, 900, 990] {
let s = skew_setup(cut);
check(&orc, &s, "SELECT * FROM t WHERE b=0;");
check(&orc, &s, "SELECT * FROM t WHERE b=999;");
}
}
#[test]
fn scan_vs_search_no_stats_unchanged() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
let setup = skew_setup(900);
let g = graphite_access(&setup, "SELECT * FROM t WHERE b=0;");
let odb = tmp("ns");
let o = orc_query(
&orc,
&odb,
&format!("{setup} EXPLAIN QUERY PLAN SELECT * FROM t WHERE b=0;"),
);
let _ = std::fs::remove_file(&odb);
let o = o
.lines()
.find(|l| l.contains("SCAN") || l.contains("SEARCH"))
.map(|l| l.trim_start_matches(['`', '-', ' ']).to_string())
.unwrap_or_default();
assert_eq!(o, g, "un-analyzed plan should match (both SEARCH)");
assert!(g.contains("SEARCH"), "un-analyzed should SEARCH, got {g}");
}