#![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-range-stat4-{}-{}-{}.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,
"range SCAN/SEARCH mismatch for `{query}`\nsetup: {setup}"
);
}
const SETUP: &str = "CREATE TABLE t(id INTEGER PRIMARY KEY, a, b, pad); \
WITH RECURSIVE c(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM c WHERE i<1000) \
INSERT INTO t(a,b,pad) SELECT i, CASE WHEN i<=900 THEN 0 ELSE i END, i FROM c; \
CREATE INDEX ia ON t(a); CREATE INDEX ib ON t(b);";
#[test]
fn range_open_ended_scan_vs_search() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
check(&orc, SETUP, "SELECT * FROM t WHERE a>100;"); check(&orc, SETUP, "SELECT * FROM t WHERE a>990;"); check(&orc, SETUP, "SELECT * FROM t WHERE a>=500;");
check(&orc, SETUP, "SELECT * FROM t WHERE a<200;"); check(&orc, SETUP, "SELECT * FROM t WHERE a<=990;");
check(&orc, SETUP, "SELECT * FROM t WHERE a<10;"); }
#[test]
fn range_two_sided_scan_vs_search() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
check(&orc, SETUP, "SELECT * FROM t WHERE a>5 AND a<900;"); check(&orc, SETUP, "SELECT * FROM t WHERE a>5 AND a<10;"); check(&orc, SETUP, "SELECT * FROM t WHERE a BETWEEN 10 AND 800;"); check(&orc, SETUP, "SELECT * FROM t WHERE a BETWEEN 490 AND 500;"); }
#[test]
fn range_skewed_selectivity() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
check(&orc, SETUP, "SELECT * FROM t WHERE b>0;");
check(&orc, SETUP, "SELECT * FROM t WHERE b>950;");
check(&orc, SETUP, "SELECT * FROM t WHERE b BETWEEN 901 AND 999;");
check(&orc, SETUP, "SELECT * FROM t WHERE b BETWEEN 0 AND 999;");
}
#[test]
fn range_boundary_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 n in [100, 300, 500, 540, 550, 560, 600, 700, 850, 900, 950, 990] {
check(&orc, SETUP, &format!("SELECT * FROM t WHERE a>{n};"));
}
}
#[test]
fn range_text_and_real_bounds() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
let text = "CREATE TABLE t(id INTEGER PRIMARY KEY, s TEXT, pad); \
WITH RECURSIVE c(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM c WHERE i<1000) \
INSERT INTO t(s,pad) SELECT printf('%04d', i), i FROM c; CREATE INDEX is1 ON t(s);";
check(&orc, text, "SELECT * FROM t WHERE s>'0100';"); check(&orc, text, "SELECT * FROM t WHERE s>'0990';"); check(
&orc,
text,
"SELECT * FROM t WHERE s BETWEEN '0100' AND '0800';",
);
let real = "CREATE TABLE t(id INTEGER PRIMARY KEY, x REAL, pad); \
WITH RECURSIVE c(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM c WHERE i<1000) \
INSERT INTO t(x,pad) SELECT i*1.5, i FROM c; CREATE INDEX ix ON t(x);";
check(&orc, real, "SELECT * FROM t WHERE x>150.0;"); check(&orc, real, "SELECT * FROM t WHERE x>1450.0;"); }
#[test]
fn range_desc_index() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
let desc = "CREATE TABLE t(id INTEGER PRIMARY KEY, a, pad); \
WITH RECURSIVE c(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM c WHERE i<1000) \
INSERT INTO t(a,pad) SELECT i, i FROM c; CREATE INDEX ia ON t(a DESC);";
check(&orc, desc, "SELECT * FROM t WHERE a>100;");
check(&orc, desc, "SELECT * FROM t WHERE a>990;");
check(&orc, desc, "SELECT * FROM t WHERE a BETWEEN 100 AND 800;");
}
#[test]
fn range_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 g = graphite_access(SETUP, "SELECT * FROM t WHERE a>100;");
let odb = tmp("ns");
let o = orc_query(
&orc,
&odb,
&format!("{SETUP} EXPLAIN QUERY PLAN SELECT * FROM t WHERE a>100;"),
);
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 range plan should match (both SEARCH)");
assert!(
g.contains("SEARCH"),
"un-analyzed range should SEARCH, got {g}"
);
}
#[test]
fn range_indexed_by_hint_forces_seek() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
check(&orc, SETUP, "SELECT * FROM t INDEXED BY ia WHERE a>100;");
check(&orc, SETUP, "SELECT * FROM t INDEXED BY ia WHERE a>990;");
}