#![cfg(feature = "std")]
use graphitesql::{Connection, Value};
fn setup(sql: &str) -> Connection {
let mut c = Connection::open_memory().unwrap();
for stmt in sql.split(';') {
if !stmt.trim().is_empty() {
c.execute(stmt).unwrap();
}
}
c
}
fn plan(c: &mut Connection, sql: &str) -> String {
let r = c.query(sql).unwrap();
let row = r.rows.last().expect("a query-plan row");
match row.last().unwrap() {
Value::Text(s) => s.clone(),
other => panic!("plan detail not text: {other:?}"),
}
}
fn ints(c: &mut Connection, sql: &str) -> Vec<i64> {
let r = c.query(sql).unwrap();
let mut out: Vec<i64> = r
.rows
.iter()
.map(|row| match &row[0] {
Value::Integer(i) => *i,
other => panic!("not an integer: {other:?}"),
})
.collect();
out.sort_unstable();
out
}
#[test]
fn partial_index_used_when_predicate_is_a_conjunct() {
let mut c = setup(
"CREATE TABLE t(a, b, active);
CREATE INDEX ip ON t(a) WHERE active=1;
INSERT INTO t VALUES(5,1,1),(5,2,0),(6,3,1);",
);
let p = plan(
&mut c,
"EXPLAIN QUERY PLAN SELECT * FROM t WHERE a=5 AND active=1",
);
assert!(p.contains("USING INDEX ip"), "expected seek, got: {p}");
let p = plan(
&mut c,
"EXPLAIN QUERY PLAN SELECT * FROM t WHERE active=1 AND a=5",
);
assert!(p.contains("USING INDEX ip"), "expected seek, got: {p}");
assert_eq!(
ints(&mut c, "SELECT b FROM t WHERE a=5 AND active=1"),
vec![1]
);
}
#[test]
fn partial_index_not_used_when_predicate_absent() {
let mut c = setup(
"CREATE TABLE t(a, b, active);
CREATE INDEX ip ON t(a) WHERE active=1;
INSERT INTO t VALUES(5,1,1),(5,2,0),(6,3,1);",
);
let p = plan(&mut c, "EXPLAIN QUERY PLAN SELECT * FROM t WHERE a=5");
assert_eq!(p, "SCAN t", "partial index must not be used: {p}");
assert_eq!(ints(&mut c, "SELECT b FROM t WHERE a=5"), vec![1, 2]);
}
#[test]
fn expression_index_used_and_correct() {
let mut c = setup(
"CREATE TABLE t(name);
CREATE INDEX ie ON t(lower(name));
INSERT INTO t VALUES('Ann'),('BOB'),('bob');",
);
let p = plan(
&mut c,
"EXPLAIN QUERY PLAN SELECT * FROM t WHERE lower(name)='bob'",
);
assert!(p.contains("USING INDEX ie"), "expected seek, got: {p}");
let p = plan(
&mut c,
"EXPLAIN QUERY PLAN SELECT * FROM t WHERE 'bob'=lower(name)",
);
assert!(p.contains("USING INDEX ie"), "expected seek, got: {p}");
let r = c
.query("SELECT name FROM t WHERE lower(name)='bob'")
.unwrap();
let mut names: Vec<String> = r
.rows
.iter()
.map(|row| match &row[0] {
Value::Text(s) => s.clone(),
other => panic!("not text: {other:?}"),
})
.collect();
names.sort();
assert_eq!(names, vec!["BOB".to_string(), "bob".to_string()]);
}
#[test]
fn expression_index_not_used_for_plain_column_predicate() {
let mut c = setup(
"CREATE TABLE t(name);
CREATE INDEX ie ON t(lower(name));
INSERT INTO t VALUES('Ann'),('BOB');",
);
let p = plan(
&mut c,
"EXPLAIN QUERY PLAN SELECT * FROM t WHERE name='Ann'",
);
assert_eq!(p, "SCAN t", "expression index must not be used: {p}");
let r = c.query("SELECT name FROM t WHERE name='Ann'").unwrap();
assert_eq!(r.rows.len(), 1);
}
#[test]
fn plain_index_unaffected() {
let mut c = setup(
"CREATE TABLE t(a, b);
CREATE INDEX ia ON t(a);
INSERT INTO t VALUES(5,1),(5,2),(6,3);",
);
let p = plan(&mut c, "EXPLAIN QUERY PLAN SELECT * FROM t WHERE a=5");
assert!(p.contains("USING INDEX ia"), "expected seek, got: {p}");
assert_eq!(ints(&mut c, "SELECT b FROM t WHERE a=5"), vec![1, 2]);
}
#[test]
fn partial_expression_index_combined() {
let mut c = setup(
"CREATE TABLE t(name, live);
CREATE INDEX ipe ON t(lower(name)) WHERE live=1;
INSERT INTO t VALUES('Ann',1),('BOB',1),('bob',0);",
);
let p = plan(
&mut c,
"EXPLAIN QUERY PLAN SELECT * FROM t WHERE lower(name)='bob' AND live=1",
);
assert!(p.contains("USING INDEX ipe"), "expected seek, got: {p}");
let r = c
.query("SELECT name FROM t WHERE lower(name)='bob' AND live=1")
.unwrap();
assert_eq!(r.rows.len(), 1);
assert_eq!(r.rows[0][0], Value::Text("BOB".to_string()));
let p = plan(
&mut c,
"EXPLAIN QUERY PLAN SELECT * FROM t WHERE lower(name)='bob'",
);
assert_eq!(p, "SCAN t", "partial expr index must not be used: {p}");
let r = c
.query("SELECT name FROM t WHERE lower(name)='bob'")
.unwrap();
assert_eq!(r.rows.len(), 2);
}