#![cfg(feature = "std")]
use graphitesql::{Connection, Value};
use std::process::Command;
fn sqlite3(script: &str) -> String {
let o = Command::new("sqlite3")
.arg(":memory:")
.arg(script)
.output()
.unwrap();
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
}
fn have_sqlite3() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
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!("{:.1}", r)
} else {
format!("{r}")
}
}
Value::Text(s) => s.clone(),
Value::Blob(b) => String::from_utf8_lossy(b).into_owned(),
}
}
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 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)) => s.clone(),
other => panic!("plan detail not text: {other:?}"),
})
.collect()
}
fn sqlite_plan(setup: &str, sql: &str) -> Vec<String> {
let out = sqlite3(&format!("{setup}\nEXPLAIN QUERY PLAN {sql};"));
out.lines()
.filter(|l| *l != "QUERY PLAN")
.map(|l| l.trim_start_matches(['|', '-', '`', ' ']).to_string())
.collect()
}
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 assert_rows(setup: &str, sql: &str) {
let want = sqlite3(&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}`");
}
}
fn assert_plan(setup: &str, sql: &str) {
let want = sqlite_plan(setup, sql);
let c = graphite(setup, true);
let got = graphite_plan(&c, sql);
assert_eq!(got, want, "plan diverged for `{sql}`");
}
fn assert_plan_is(setup: &str, sql: &str, want: &[&str]) {
let c = graphite(setup, true);
let got = graphite_plan(&c, sql);
assert_eq!(got, want, "plan mismatch for `{sql}`");
}
const SETUP: &str = "CREATE TABLE u(x INTEGER PRIMARY KEY, y);\
CREATE TABLE v(p, q TEXT);\
CREATE INDEX iv ON v(p);\
INSERT INTO u VALUES(3,30),(1,10),(2,20);\
INSERT INTO v VALUES(2,'two'),(1,'one'),(3,'three'),(1,'uno');";
#[test]
fn cross_join_covered_inner_bug_case() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
assert_plan(SETUP, "SELECT u.x, v.p FROM u CROSS JOIN v");
assert_rows(SETUP, "SELECT u.x, v.p FROM u CROSS JOIN v");
assert_plan_is(
SETUP,
"SELECT u.x, v.p FROM u CROSS JOIN v",
&["SCAN u", "SCAN v USING COVERING INDEX iv"],
);
}
#[test]
fn inner_join_covered_inner_via_rowid_swap() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
assert_plan(SETUP, "SELECT u.x, v.p FROM u JOIN v ON u.x=v.p");
assert_rows(SETUP, "SELECT u.x, v.p FROM u JOIN v ON u.x=v.p");
}
#[test]
fn covered_driver_with_rowid_seek_inner() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
assert_plan(SETUP, "SELECT v.p, u.y FROM v JOIN u ON u.x=v.p");
assert_rows(SETUP, "SELECT v.p, u.y FROM v JOIN u ON u.x=v.p");
}
#[test]
fn distinct_over_covered_join() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
assert_rows(SETUP, "SELECT DISTINCT v.p FROM u CROSS JOIN v");
assert_rows(SETUP, "SELECT DISTINCT u.y FROM u JOIN v ON u.x=v.p");
}
#[test]
fn group_by_over_covered_join() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
assert_rows(
SETUP,
"SELECT v.p, count(*) FROM u CROSS JOIN v GROUP BY v.p",
);
}
#[test]
fn order_by_over_covered_join_is_invariant() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
assert_rows(
SETUP,
"SELECT u.x, v.p FROM u CROSS JOIN v ORDER BY u.x, v.p",
);
}
#[test]
fn left_join_covered_inner_rows_and_order() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
let setup = "CREATE TABLE u(x INTEGER PRIMARY KEY, y);\
CREATE TABLE v(p, q TEXT);\
CREATE INDEX iv ON v(p);\
INSERT INTO u VALUES(3,30),(5,50),(1,10);\
INSERT INTO v VALUES(2,'two'),(1,'one'),(3,'three');";
assert_plan(setup, "SELECT u.x, v.p FROM u LEFT JOIN v ON 1=1");
assert_rows(setup, "SELECT u.x, v.p FROM u LEFT JOIN v ON 1=1");
}
#[test]
fn no_covering_index_stays_plain_scan() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
let setup = "CREATE TABLE u(x INTEGER PRIMARY KEY, y);\
CREATE TABLE v(p, q);\
INSERT INTO u VALUES(3,30),(1,10);\
INSERT INTO v VALUES(2,20),(1,10);";
assert_plan(setup, "SELECT u.x, v.p FROM u CROSS JOIN v");
assert_rows(setup, "SELECT u.x, v.p FROM u CROSS JOIN v");
assert_plan_is(
setup,
"SELECT u.x, v.p FROM u CROSS JOIN v",
&["SCAN u", "SCAN v"],
);
}
#[test]
fn index_not_narrower_stays_plain_scan() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
let setup = "CREATE TABLE u(x INTEGER PRIMARY KEY, y);\
CREATE TABLE v(p TEXT);\
CREATE INDEX iv ON v(p);\
INSERT INTO u VALUES(1,10),(2,20);\
INSERT INTO v VALUES('b'),('a');";
assert_plan(setup, "SELECT u.x, v.p FROM u CROSS JOIN v");
assert_rows(setup, "SELECT u.x, v.p FROM u CROSS JOIN v");
assert_plan_is(
setup,
"SELECT u.x, v.p FROM u CROSS JOIN v",
&["SCAN u", "SCAN v"],
);
}
#[test]
fn referenced_column_outside_index_stays_plain_scan() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
assert_plan(SETUP, "SELECT u.x, v.p, v.q FROM u CROSS JOIN v");
assert_rows(SETUP, "SELECT u.x, v.p, v.q FROM u CROSS JOIN v");
let c = graphite(SETUP, true);
let plan = graphite_plan(&c, "SELECT u.x, v.p, v.q FROM u CROSS JOIN v");
assert_eq!(plan, vec!["SCAN u", "SCAN v"]);
}
#[test]
fn wildcard_over_uncovered_table_stays_plain_scan() {
if !have_sqlite3() {
eprintln!("sqlite3 not found; skipping");
return;
}
assert_plan(SETUP, "SELECT * FROM u CROSS JOIN v");
assert_rows(SETUP, "SELECT * FROM u CROSS JOIN v");
}