use vibesql_ast::Expression;
use vibesql_storage::Database;
use crate::evaluator::expression_hash::ExpressionHasher;
fn collect_conjuncts<'a>(expr: &'a Expression, out: &mut Vec<&'a Expression>) {
match expr {
Expression::BinaryOp { left, op: vibesql_ast::BinaryOperator::And, right } => {
collect_conjuncts(left, out);
collect_conjuncts(right, out);
}
Expression::Conjunction(exprs) => {
for e in exprs {
collect_conjuncts(e, out);
}
}
other => out.push(other),
}
}
pub(crate) fn query_implies_index_predicate(
query_where: &Expression,
index_where: &Expression,
) -> bool {
let mut query_conjuncts = Vec::new();
collect_conjuncts(query_where, &mut query_conjuncts);
let query_hashes: Vec<u64> =
query_conjuncts.iter().map(|e| ExpressionHasher::hash(e)).collect();
let mut index_conjuncts = Vec::new();
collect_conjuncts(index_where, &mut index_conjuncts);
index_conjuncts.iter().all(|conjunct| {
let conjunct_hash = ExpressionHasher::hash(conjunct);
query_conjuncts
.iter()
.zip(query_hashes.iter())
.any(|(q, q_hash)| *q_hash == conjunct_hash && *q == *conjunct)
})
}
pub(crate) fn partial_index_usable(
database: &Database,
index_name: &str,
query_where: Option<&Expression>,
) -> bool {
let Some(metadata) = database.catalog.find_index_by_name(index_name) else {
return true;
};
let Some(index_where) = metadata.where_clause.as_deref() else {
return true;
};
match query_where {
Some(query_where) => query_implies_index_predicate(query_where, index_where),
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use vibesql_ast::Statement;
fn parse_where(expr_sql: &str) -> Expression {
let sql = format!("SELECT 1 FROM t WHERE {}", expr_sql);
let stmt = vibesql_parser::Parser::parse_sql(&sql).expect("parse failed");
match stmt {
Statement::Select(select) => select.where_clause.expect("expected WHERE clause"),
other => panic!("expected SELECT, got {:?}", other),
}
}
#[test]
fn exact_conjunct_match_implies() {
let query =
parse_where("typeof(b)='real' AND datetime(b) BETWEEN '2017-07-04' AND '2017-07-08'");
let index = parse_where("typeof(b)='real'");
assert!(query_implies_index_predicate(&query, &index));
}
#[test]
fn identical_predicate_implies_itself() {
let query = parse_where("typeof(b)='real'");
let index = parse_where("typeof(b)='real'");
assert!(query_implies_index_predicate(&query, &index));
}
#[test]
fn hash_equality_for_predicate_parsed_twice() {
let a = parse_where("typeof(b)='real'");
let b = parse_where("typeof(b)='real'");
assert_eq!(ExpressionHasher::hash(&a), ExpressionHasher::hash(&b));
}
#[test]
fn like_escape_does_not_imply_escapeless_like() {
let with_escape = parse_where("name LIKE 'x!%y' ESCAPE '!'");
let without_escape = parse_where("name LIKE 'x!%y'");
assert!(!query_implies_index_predicate(&with_escape, &without_escape));
assert!(!query_implies_index_predicate(&without_escape, &with_escape));
let with_escape_2 = parse_where("name LIKE 'x!%y' ESCAPE '!'");
assert!(query_implies_index_predicate(&with_escape, &with_escape_2));
}
#[test]
fn hash_collision_does_not_imply() {
let with_escape = parse_where("name LIKE 'x!%y' ESCAPE '!'");
let without_escape = parse_where("name LIKE 'x!%y'");
assert_eq!(
ExpressionHasher::hash(&with_escape),
ExpressionHasher::hash(&without_escape),
"expected a hash collision (lossy escape arm); update this test if the hasher changed"
);
assert!(!query_implies_index_predicate(&with_escape, &without_escape));
let query = parse_where("name LIKE 'x!%y' AND name = 'x!zzy'");
let index = parse_where("name LIKE 'x!%y' ESCAPE '!'");
assert!(!query_implies_index_predicate(&query, &index));
}
#[test]
fn missing_conjunct_does_not_imply() {
let query = parse_where("datetime(b) BETWEEN '2017-07-04' AND '2017-07-08'");
let index = parse_where("typeof(b)='real'");
assert!(!query_implies_index_predicate(&query, &index));
}
#[test]
fn different_literal_does_not_imply() {
let query = parse_where("typeof(b)='text' AND a = 1");
let index = parse_where("typeof(b)='real'");
assert!(!query_implies_index_predicate(&query, &index));
}
#[test]
fn top_level_or_does_not_imply() {
let query = parse_where("typeof(b)='real' OR a = 1");
let index = parse_where("typeof(b)='real'");
assert!(!query_implies_index_predicate(&query, &index));
}
#[test]
fn structurally_identical_or_implies() {
let query = parse_where("(a = 1 OR a = 2) AND b > 0");
let index = parse_where("a = 1 OR a = 2");
assert!(query_implies_index_predicate(&query, &index));
}
#[test]
fn index_predicate_with_and_requires_all_conjuncts() {
let query = parse_where("a = 1 AND b > 0 AND c < 5");
let index_ok = parse_where("a = 1 AND c < 5");
let index_missing = parse_where("a = 1 AND d = 9");
assert!(query_implies_index_predicate(&query, &index_ok));
assert!(!query_implies_index_predicate(&query, &index_missing));
}
#[test]
fn nested_binary_and_conjuncts_are_split() {
let a = parse_where("a = 1");
let b = parse_where("b > 0");
let c = parse_where("c < 5");
let nested = Expression::BinaryOp {
left: Box::new(Expression::BinaryOp {
left: Box::new(a),
op: vibesql_ast::BinaryOperator::And,
right: Box::new(b),
}),
op: vibesql_ast::BinaryOperator::And,
right: Box::new(c),
};
let index = parse_where("b > 0");
assert!(query_implies_index_predicate(&nested, &index));
}
#[test]
fn flat_conjunction_conjuncts_are_split() {
let flat = Expression::Conjunction(vec![
parse_where("a = 1"),
parse_where("b > 0"),
parse_where("c < 5"),
]);
let index = parse_where("c < 5");
assert!(query_implies_index_predicate(&flat, &index));
let index_missing = parse_where("d = 2");
assert!(!query_implies_index_predicate(&flat, &index_missing));
}
#[test]
fn partial_index_usable_requires_query_where() {
use vibesql_storage::Database;
let mut db = Database::new();
let create_table =
vibesql_parser::Parser::parse_sql("CREATE TABLE t (a INTEGER, b INTEGER)").unwrap();
if let Statement::CreateTable(stmt) = create_table {
crate::CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
let create_index =
vibesql_parser::Parser::parse_sql("CREATE INDEX tb1 ON t(a) WHERE b = 1").unwrap();
if let Statement::CreateIndex(stmt) = create_index {
crate::CreateIndexExecutor::execute(&stmt, &mut db).unwrap();
}
assert!(!partial_index_usable(&db, "tb1", None));
let implying = parse_where("b = 1 AND a > 5");
assert!(partial_index_usable(&db, "tb1", Some(&implying)));
let non_implying = parse_where("a > 5");
assert!(!partial_index_usable(&db, "tb1", Some(&non_implying)));
}
#[test]
fn partial_expression_index_usable_when_implied() {
use vibesql_storage::Database;
let mut db = Database::new();
let create_table =
vibesql_parser::Parser::parse_sql("CREATE TABLE t3 (a INTEGER, b REAL)").unwrap();
if let Statement::CreateTable(stmt) = create_table {
crate::CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
let create_index = vibesql_parser::Parser::parse_sql(
"CREATE INDEX t3b1 ON t3(datetime(b)) WHERE typeof(b)='real'",
)
.unwrap();
if let Statement::CreateIndex(stmt) = create_index {
crate::CreateIndexExecutor::execute(&stmt, &mut db).unwrap();
}
let implying =
parse_where("typeof(b)='real' AND datetime(b) BETWEEN '2017-07-04' AND '2017-07-08'");
assert!(partial_index_usable(&db, "t3b1", Some(&implying)));
let non_implying = parse_where("datetime(b) > '2017-07-04'");
assert!(!partial_index_usable(&db, "t3b1", Some(&non_implying)));
assert!(!partial_index_usable(&db, "t3b1", None));
}
#[test]
fn partial_index_usable_true_for_non_partial_index() {
use vibesql_storage::Database;
let mut db = Database::new();
let create_table =
vibesql_parser::Parser::parse_sql("CREATE TABLE t (a INTEGER, b INTEGER)").unwrap();
if let Statement::CreateTable(stmt) = create_table {
crate::CreateTableExecutor::execute(&stmt, &mut db).unwrap();
}
let create_index = vibesql_parser::Parser::parse_sql("CREATE INDEX ta ON t(a)").unwrap();
if let Statement::CreateIndex(stmt) = create_index {
crate::CreateIndexExecutor::execute(&stmt, &mut db).unwrap();
}
assert!(partial_index_usable(&db, "ta", None));
let any_where = parse_where("b = 1");
assert!(partial_index_usable(&db, "ta", Some(&any_where)));
}
}