#![cfg(feature = "std")]
use graphitesql::{Connection, Value};
use std::path::PathBuf;
use std::process::Command;
fn render(v: &Value) -> String {
match v {
Value::Null => String::new(),
Value::Integer(i) => i.to_string(),
Value::Text(s) => String::from(s.as_str()),
Value::Real(r) => graphitesql::exec::eval::format_real(*r),
Value::Blob(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
}
}
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-pstat4-{}-{}-{}.db",
std::process::id(),
name,
n
));
let s = p.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&s);
s
}
fn graphite_eqp(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()
.map(|r| r.last().map(render).unwrap_or_default())
.collect::<Vec<_>>()
.join("\n")
}
fn check_index_choice(orc: &str, setup: &str, query: &str) {
let odb = tmp("o");
let script = format!("{setup} ANALYZE;");
let oracle_eqp = orc_query(orc, &odb, &format!("{script} EXPLAIN QUERY PLAN {query}"));
let _ = std::fs::remove_file(&odb);
let graphite = graphite_eqp(&script, query);
let idx_tok = |s: &str| -> Option<String> {
s.lines().find_map(|l| {
l.find("USING INDEX ").map(|p| {
l[p + "USING INDEX ".len()..]
.split_whitespace()
.next()
.unwrap_or("")
.to_string()
})
})
};
assert_eq!(
idx_tok(&oracle_eqp),
idx_tok(&graphite),
"index choice mismatch for `{query}`\noracle:\n{oracle_eqp}\ngraphite:\n{graphite}",
);
}
const SKEW_SETUP: &str = "CREATE TABLE t(id INTEGER PRIMARY KEY, 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<=900 THEN 0 ELSE i END FROM c; \
CREATE INDEX ia ON t(a); CREATE INDEX ib ON t(b);";
#[test]
fn stat4_equality_index_choice_skewed() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
check_index_choice(&orc, SKEW_SETUP, "SELECT * FROM t WHERE b=0 AND a=5;");
check_index_choice(&orc, SKEW_SETUP, "SELECT * FROM t WHERE b=950 AND a=950;");
}
const TWO_COL_SETUP: &str = "CREATE TABLE p(id INTEGER PRIMARY KEY, hot, uniq); \
WITH RECURSIVE c(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM c WHERE i<600) \
INSERT INTO p(hot,uniq) SELECT CASE WHEN i<=500 THEN 7 ELSE i END, i FROM c; \
CREATE INDEX ph ON p(hot); CREATE INDEX pu ON p(uniq);";
#[test]
fn stat4_equality_two_candidate_indexes() {
let Some(orc) = oracle() else {
eprintln!("skipping: no STAT4 oracle available");
return;
};
if !oracle_has_stat4(&orc) {
eprintln!("skipping: oracle lacks STAT4");
return;
}
check_index_choice(
&orc,
TWO_COL_SETUP,
"SELECT * FROM p WHERE hot=7 AND uniq=3;",
);
check_index_choice(
&orc,
TWO_COL_SETUP,
"SELECT * FROM p WHERE hot=550 AND uniq=550;",
);
}