#![cfg(feature = "std")]
use graphitesql::Connection;
use std::process::Command;
fn norm(s: &str) -> String {
s.lines()
.filter(|l| !l.trim().eq_ignore_ascii_case("QUERY PLAN"))
.map(|l| l.trim_start_matches(|ch| "|`- ".contains(ch)).trim_end())
.collect::<Vec<_>>()
.join(" | ")
}
fn g_eqp(ddl: &str, q: &str) -> String {
let mut c = Connection::open_memory().unwrap();
for stmt in ddl.split_inclusive(';') {
if !stmt.trim().is_empty() {
c.execute(stmt).unwrap();
}
}
let rows = c.query(&format!("EXPLAIN QUERY PLAN {q}")).unwrap().rows;
let mut lines = Vec::new();
for r in &rows {
if let Some(graphitesql::Value::Text(s)) = r.last() {
lines.push(s.clone());
}
}
lines.join(" | ")
}
fn sqlite_eqp(ddl: &str, q: &str) -> String {
let o = Command::new("sqlite3")
.arg(":memory:")
.arg(format!("{ddl} EXPLAIN QUERY PLAN {q};"))
.output()
.unwrap();
norm(&String::from_utf8_lossy(&o.stdout))
}
fn g_rows(ddl: &str, q: &str) -> String {
let mut c = Connection::open_memory().unwrap();
for stmt in ddl.split_inclusive(';') {
if !stmt.trim().is_empty() {
c.execute(stmt).unwrap();
}
}
let r = c.query(q).unwrap();
r.rows
.iter()
.map(|row| {
row.iter()
.map(|v| match v {
graphitesql::Value::Null => "".to_string(),
graphitesql::Value::Integer(i) => i.to_string(),
graphitesql::Value::Real(f) => {
let s = format!("{f}");
if s.bytes().all(|b| b.is_ascii_digit() || b == b'-') {
format!("{s}.0")
} else {
s
}
}
graphitesql::Value::Text(s) => s.clone(),
graphitesql::Value::Blob(_) => "<blob>".to_string(),
})
.collect::<Vec<_>>()
.join("|")
})
.collect::<Vec<_>>()
.join("\n")
}
fn sqlite_rows(ddl: &str, q: &str) -> String {
let o = Command::new("sqlite3")
.arg(":memory:")
.arg(format!("{ddl} {q};"))
.output()
.unwrap();
String::from_utf8_lossy(&o.stdout).trim_end().to_string()
}
fn check(ddl: &str, q: &str) {
assert_eq!(g_eqp(ddl, q), sqlite_eqp(ddl, q), "EQP diverged for {q}");
assert_eq!(g_rows(ddl, q), sqlite_rows(ddl, q), "rows diverged for {q}");
}
fn have_sqlite() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
const SINGLE: &str = "CREATE TABLE w(a TEXT PRIMARY KEY, b) WITHOUT ROWID; \
INSERT INTO w VALUES('c',3),('a',1),('b',2);";
const COMPOSITE: &str = "CREATE TABLE w(x,y,z, PRIMARY KEY(x,y)) WITHOUT ROWID; \
INSERT INTO w VALUES(2,1,9),(1,2,8),(1,1,7),(2,2,6);";
#[test]
fn without_rowid_pk_prefix_elides_sorter() {
if !have_sqlite() {
return;
}
assert_eq!(g_eqp(SINGLE, "SELECT * FROM w ORDER BY a"), "SCAN w");
assert_eq!(g_eqp(SINGLE, "SELECT * FROM w ORDER BY a DESC"), "SCAN w");
assert_eq!(g_eqp(COMPOSITE, "SELECT * FROM w ORDER BY x, y"), "SCAN w");
for (ddl, q) in [
(SINGLE, "SELECT * FROM w ORDER BY a"),
(SINGLE, "SELECT a FROM w ORDER BY a"),
(SINGLE, "SELECT * FROM w ORDER BY a DESC"),
(SINGLE, "SELECT * FROM w ORDER BY a, b"),
(SINGLE, "SELECT * FROM w ORDER BY 1"),
(SINGLE, "SELECT * FROM w AS r ORDER BY 1"),
(COMPOSITE, "SELECT * FROM w ORDER BY x"),
(COMPOSITE, "SELECT * FROM w ORDER BY x, y"),
(COMPOSITE, "SELECT * FROM w ORDER BY x, y, z"),
(COMPOSITE, "SELECT * FROM w ORDER BY x DESC, y DESC"),
(COMPOSITE, "SELECT * FROM w ORDER BY 1, 2"),
] {
let plan = g_eqp(ddl, q);
assert!(
!plan.contains("ORDER BY"),
"expected no ORDER BY sorter for {q}, got {plan}"
);
check(ddl, q);
}
}
#[test]
fn without_rowid_desc_pk_keeps_sorter() {
if !have_sqlite() {
return;
}
for ddl in [
"CREATE TABLE w(a TEXT, b, PRIMARY KEY(a DESC)) WITHOUT ROWID; \
INSERT INTO w VALUES('c',3),('a',1),('b',2);",
"CREATE TABLE w(a TEXT PRIMARY KEY DESC, b) WITHOUT ROWID; \
INSERT INTO w VALUES('c',3),('a',1),('b',2);",
] {
assert_eq!(g_rows(ddl, "SELECT * FROM w ORDER BY a"), "a|1\nb|2\nc|3");
assert_eq!(
g_rows(ddl, "SELECT * FROM w ORDER BY a DESC"),
"c|3\nb|2\na|1"
);
assert_eq!(
g_rows(ddl, "SELECT * FROM w ORDER BY a"),
sqlite_rows(ddl, "SELECT * FROM w ORDER BY a")
);
}
}