use fsqlite::Connection;
use fsqlite_types::SqliteValue;
fn render(v: &SqliteValue) -> String {
match v {
SqliteValue::Null => "NULL".to_owned(),
SqliteValue::Integer(n) => n.to_string(),
SqliteValue::Float(f) => format!("{f:?}"),
SqliteValue::Text(s) => format!("'{s}'"),
SqliteValue::Blob(b) => format!(
"X'{}'",
b.iter().map(|x| format!("{x:02X}")).collect::<String>()
),
}
}
fn frank_rows(conn: &Connection, sql: &str) -> Vec<Vec<String>> {
conn.query(sql)
.unwrap_or_else(|e| panic!("frank `{sql}`: {e}"))
.iter()
.map(|row| row.values().iter().map(render).collect())
.collect()
}
fn sqlite_rows(conn: &rusqlite::Connection, sql: &str) -> Vec<Vec<String>> {
let mut stmt = conn.prepare(sql).unwrap();
let n = stmt.column_count();
stmt.query_map([], |row| {
let mut out = Vec::with_capacity(n);
for i in 0..n {
out.push(match row.get_unwrap::<_, rusqlite::types::Value>(i) {
rusqlite::types::Value::Null => "NULL".to_owned(),
rusqlite::types::Value::Integer(x) => x.to_string(),
rusqlite::types::Value::Real(f) => format!("{f:?}"),
rusqlite::types::Value::Text(s) => format!("'{s}'"),
rusqlite::types::Value::Blob(b) => format!(
"X'{}'",
b.iter().map(|x| format!("{x:02X}")).collect::<String>()
),
});
}
Ok(out)
})
.unwrap()
.map(Result::unwrap)
.collect()
}
fn has_op(conn: &Connection, sql: &str, want: &str) -> bool {
conn.query(&format!("EXPLAIN {sql}")).unwrap().iter().any(
|row| matches!(row.values().get(1), Some(SqliteValue::Text(op)) if op.to_string() == want),
)
}
#[test]
fn minmax_range_seek_matches_sqlite() {
let f = Connection::open(":memory:").expect("frank");
let r = rusqlite::Connection::open_in_memory().expect("sqlite");
for stmt in [
"CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER);",
"CREATE INDEX idx_a ON t(a);",
] {
f.execute(stmt).unwrap();
r.execute_batch(stmt).unwrap();
}
for i in 1..=500_i64 {
let a = if i <= 4 {
"NULL".to_owned()
} else {
format!("{}", ((i.wrapping_mul(7)) % 1000) - 400)
};
let stmt = format!("INSERT INTO t VALUES ({i}, {a});");
f.execute(&stmt).unwrap();
r.execute_batch(&stmt).unwrap();
}
for (id, val) in [
(9001, "7.5"),
(9002, "250.25"),
(9003, "'apple'"),
(9004, "'zzz'"),
] {
let stmt = format!("INSERT INTO t VALUES ({id}, {val});");
f.execute(&stmt).unwrap();
r.execute_batch(&stmt).unwrap();
}
let cmp = |sql: &str| {
assert_eq!(
frank_rows(&f, sql),
sqlite_rows(&r, sql),
"diverged: `{sql}`"
);
};
for sql in [
"SELECT MIN(a) FROM t WHERE a > 100",
"SELECT MIN(a) FROM t WHERE a >= 100",
"SELECT MAX(a) FROM t WHERE a < 100",
"SELECT MAX(a) FROM t WHERE a <= 100",
"SELECT MIN(a) FROM t WHERE 100 < a",
"SELECT MAX(a) FROM t WHERE 100 > a",
"SELECT MIN(a) FROM t WHERE a > 100000",
"SELECT MAX(a) FROM t WHERE a < -100000",
"SELECT COALESCE(MIN(a), -1) FROM t WHERE a > 100000",
"SELECT COALESCE(MAX(a), -1) FROM t WHERE a < -100000",
"SELECT MIN(a) FROM t WHERE a > 249",
"SELECT MAX(a) FROM t WHERE a < 8",
"SELECT MAX(a) FROM t WHERE a <= 600",
"SELECT MIN(a) FROM t WHERE a < 100",
"SELECT MAX(a) FROM t WHERE a > 100",
"SELECT MIN(a) FROM t WHERE a > 3.5",
] {
cmp(sql);
}
assert!(
has_op(&f, "SELECT MIN(a) FROM t WHERE a > 100", "SeekGT"),
"MIN(a) WHERE a>c must seek the bound (SeekGT)"
);
assert!(
has_op(&f, "SELECT MAX(a) FROM t WHERE a < 100", "SeekLT"),
"MAX(a) WHERE a<c must seek the bound (SeekLT)"
);
assert!(
!has_op(&f, "SELECT MIN(a) FROM t WHERE a < 100", "SeekLT")
&& !has_op(&f, "SELECT MIN(a) FROM t WHERE a < 100", "SeekLE"),
"MIN(a) WHERE a<c (unnatural pairing) must decline the bound seek"
);
}