#![cfg(feature = "std")]
use graphitesql::{Connection, Value};
use std::path::PathBuf;
use std::process::Command;
fn oracle() -> Option<PathBuf> {
let candidates = [
std::env::var("SQLITE3_ORACLE").ok().map(PathBuf::from),
std::env::var("TMPDIR").ok().map(|t| {
PathBuf::from(t).join("sqlite-src/sqlite-amalgamation-3500400/sqlite3-oracle")
}),
];
candidates.into_iter().flatten().find(|c| c.exists())
}
fn oracle_run(bin: &PathBuf, script: &str) -> String {
let o = Command::new(bin)
.arg(":memory:")
.arg(script)
.output()
.unwrap();
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
}
fn oracle_plan(bin: &PathBuf, setup: &str, sql: &str) -> Vec<String> {
let out = oracle_run(bin, &format!("{setup}\nEXPLAIN QUERY PLAN {sql};"));
out.lines()
.filter(|l| *l != "QUERY PLAN")
.map(|l| l.trim_start_matches(['|', '-', '`', ' ']).to_string())
.collect()
}
fn fmt(v: &Value) -> String {
match v {
Value::Null => String::new(),
Value::Integer(i) => i.to_string(),
Value::Real(r) => {
if *r == (*r as i64) as f64 {
format!("{r:.1}")
} else {
format!("{r}")
}
}
Value::Text(s) => String::from(s.as_str()),
Value::Blob(b) => String::from_utf8_lossy(b).into_owned(),
}
}
fn graphite(setup: &str, use_vdbe: bool) -> Connection {
let mut c = Connection::open_memory().unwrap();
c.set_use_vdbe(use_vdbe);
for stmt in setup.split(';') {
if !stmt.trim().is_empty() {
c.execute(stmt).unwrap();
}
}
c
}
fn graphite_plan(c: &Connection, sql: &str) -> Vec<String> {
c.query(&format!("EXPLAIN QUERY PLAN {sql}"))
.unwrap()
.rows
.iter()
.map(|row| match row.last() {
Some(Value::Text(s)) => String::from(s.as_str()),
other => panic!("plan detail not text: {other:?}"),
})
.collect()
}
fn graphite_rows(c: &Connection, sql: &str) -> String {
c.query(sql)
.unwrap()
.rows
.iter()
.map(|row| row.iter().map(fmt).collect::<Vec<_>>().join("|"))
.collect::<Vec<_>>()
.join("\n")
}
fn assert_plan(bin: &PathBuf, setup: &str, sql: &str) {
let want = oracle_plan(bin, setup, sql);
let c = graphite(setup, true);
let got = graphite_plan(&c, sql);
assert_eq!(got, want, "plan diverged for `{sql}`");
}
fn assert_rows(bin: &PathBuf, setup: &str, sql: &str) {
let want = oracle_run(bin, &format!("{setup}\n{sql};"));
for &vdbe in &[true, false] {
let c = graphite(setup, vdbe);
let got = graphite_rows(&c, sql);
assert_eq!(got, want, "rows diverged (use_vdbe={vdbe}) for `{sql}`");
}
}
const IDX: &str = "CREATE TABLE big(id INTEGER PRIMARY KEY,k,v);\
CREATE TABLE small(id INTEGER PRIMARY KEY,k,v);\
CREATE INDEX ibk ON big(k);\
CREATE INDEX isk ON small(k);\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<1000)\
INSERT INTO big SELECT i,i%50,i FROM c;\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<5)\
INSERT INTO small SELECT i,i,i FROM c;\
ANALYZE;";
#[test]
fn both_secondary_index_seekable_drives_smaller() {
let Some(bin) = oracle() else {
eprintln!("STAT4 oracle not found; skipping");
return;
};
let sql = "SELECT * FROM big JOIN small ON big.k=small.k";
assert_plan(&bin, IDX, sql);
let c = graphite(IDX, true);
let plan = graphite_plan(&c, sql);
assert_eq!(plan[0], "SCAN small", "drives the smaller table: {plan:?}");
assert!(
plan[1].starts_with("SEARCH big USING INDEX ibk"),
"big is the index inner: {plan:?}"
);
assert_rows(
&bin,
IDX,
"SELECT big.v,small.v FROM big JOIN small ON big.k=small.k",
);
}
#[test]
fn already_optimal_declaration_order_not_flipped() {
let Some(bin) = oracle() else {
eprintln!("STAT4 oracle not found; skipping");
return;
};
let sql = "SELECT * FROM small JOIN big ON big.k=small.k";
assert_plan(&bin, IDX, sql);
let c = graphite(IDX, true);
assert_eq!(graphite_plan(&c, sql)[0], "SCAN small");
}
#[test]
fn both_rowid_seekable_drives_smaller() {
let Some(bin) = oracle() else {
eprintln!("STAT4 oracle not found; skipping");
return;
};
let sql = "SELECT * FROM big JOIN small ON big.id=small.id";
assert_plan(&bin, IDX, sql);
let c = graphite(IDX, true);
let plan = graphite_plan(&c, sql);
assert_eq!(plan[0], "SCAN small", "drives the smaller table: {plan:?}");
assert_eq!(plan[1], "SEARCH big USING INTEGER PRIMARY KEY (rowid=?)");
assert_rows(
&bin,
IDX,
"SELECT big.v,small.v FROM big JOIN small ON big.id=small.id",
);
}
#[test]
fn comma_join_form_flips_identically() {
let Some(bin) = oracle() else {
eprintln!("STAT4 oracle not found; skipping");
return;
};
let sql = "SELECT * FROM big,small WHERE big.k=small.k";
assert_plan(&bin, IDX, sql);
assert_rows(
&bin,
IDX,
"SELECT big.v,small.v FROM big,small WHERE big.k=small.k",
);
}
#[test]
fn no_analyze_keeps_declaration_order() {
let Some(bin) = oracle() else {
eprintln!("STAT4 oracle not found; skipping");
return;
};
let setup = "CREATE TABLE big(id INTEGER PRIMARY KEY,k,v);\
CREATE TABLE small(id INTEGER PRIMARY KEY,k,v);\
CREATE INDEX ibk ON big(k);\
CREATE INDEX isk ON small(k);\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<1000)\
INSERT INTO big SELECT i,i%50,i FROM c;\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<5)\
INSERT INTO small SELECT i,i,i FROM c;";
let sql = "SELECT * FROM big JOIN small ON big.k=small.k";
assert_plan(&bin, setup, sql);
let c = graphite(setup, true);
assert_eq!(
graphite_plan(&c, sql)[0],
"SCAN big",
"no stats ⇒ declaration order drives"
);
}
const HUB3: &str = "CREATE TABLE u(x INTEGER PRIMARY KEY,y);\
CREATE TABLE v(p INTEGER PRIMARY KEY,q);\
CREATE TABLE w(r INTEGER PRIMARY KEY,s);\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<1000)\
INSERT INTO u SELECT i,i FROM c;\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<10)\
INSERT INTO v SELECT i,i FROM c;\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<500)\
INSERT INTO w SELECT i,i FROM c;\
ANALYZE;";
#[test]
fn three_table_drives_smallest_by_cost() {
let Some(bin) = oracle() else {
eprintln!("STAT4 oracle not found; skipping");
return;
};
let sql = "SELECT * FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r";
assert_plan(&bin, HUB3, sql);
let c = graphite(HUB3, true);
assert_eq!(
graphite_plan(&c, sql)[0],
"SCAN v",
"drives the smallest table"
);
assert_rows(
&bin,
HUB3,
"SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r",
);
}
#[test]
fn three_table_no_analyze_declaration_order() {
let Some(bin) = oracle() else {
eprintln!("STAT4 oracle not found; skipping");
return;
};
let setup = "CREATE TABLE u(x INTEGER PRIMARY KEY,y);\
CREATE TABLE v(p INTEGER PRIMARY KEY,q);\
CREATE TABLE w(r INTEGER PRIMARY KEY,s);\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<1000)\
INSERT INTO u SELECT i,i FROM c;\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<10)\
INSERT INTO v SELECT i,i FROM c;\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<500)\
INSERT INTO w SELECT i,i FROM c;";
let sql = "SELECT u.x,v.q,w.s FROM u JOIN v ON u.x=v.p JOIN w ON u.x=w.r";
let mut want: Vec<String> = oracle_run(&bin, &format!("{setup}\n{sql};"))
.lines()
.map(str::to_string)
.collect();
want.sort();
for &vdbe in &[true, false] {
let c = graphite(setup, vdbe);
let mut got: Vec<String> = graphite_rows(&c, sql).lines().map(str::to_string).collect();
got.sort();
assert_eq!(got, want, "row set diverged (use_vdbe={vdbe})");
}
}
#[test]
fn one_side_only_seekable_unchanged() {
let Some(bin) = oracle() else {
eprintln!("STAT4 oracle not found; skipping");
return;
};
let setup = "CREATE TABLE big(id INTEGER PRIMARY KEY,k,v);\
CREATE TABLE small(id INTEGER PRIMARY KEY,k,v);\
CREATE INDEX ibk ON big(k);\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<1000)\
INSERT INTO big SELECT i,i%50,i FROM c;\
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<5)\
INSERT INTO small SELECT i,i,i FROM c;\
ANALYZE;";
assert_plan(&bin, setup, "SELECT * FROM big JOIN small ON big.k=small.k");
assert_plan(&bin, setup, "SELECT * FROM small JOIN big ON big.k=small.k");
}