use std::collections::HashMap;
use super::filter::{Filter, Predicate};
use reddb_types::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompileError {
UnknownColumn(String),
}
impl std::fmt::Display for CompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompileError::UnknownColumn(c) => {
write!(f, "compiled filter: unknown column '{c}'")
}
}
}
}
impl std::error::Error for CompileError {}
#[derive(Debug, Clone)]
pub enum CompiledOp {
Predicate {
col_idx: usize,
predicate: Predicate,
},
AndN(usize),
OrN(usize),
Not,
}
#[derive(Debug, Clone)]
pub struct CompiledFilter {
ops: Vec<CompiledOp>,
}
impl CompiledFilter {
pub fn compile(filter: &Filter, schema: &HashMap<String, usize>) -> Result<Self, CompileError> {
let mut ops = Vec::new();
compile_into(filter, schema, &mut ops)?;
Ok(Self { ops })
}
pub fn op_count(&self) -> usize {
self.ops.len()
}
pub fn evaluate(&self, slot: &[Value]) -> bool {
let mut stack: Vec<bool> = Vec::with_capacity(8);
for op in &self.ops {
match op {
CompiledOp::Predicate { col_idx, predicate } => {
let value = slot.get(*col_idx).unwrap_or(&Value::Null);
let result = predicate.evaluate(value);
stack.push(result);
}
CompiledOp::AndN(n) => {
let take = (*n).min(stack.len());
let new_len = stack.len() - take;
let result = stack[new_len..].iter().all(|b| *b);
stack.truncate(new_len);
stack.push(result);
}
CompiledOp::OrN(n) => {
let take = (*n).min(stack.len());
let new_len = stack.len() - take;
let result = stack[new_len..].iter().any(|b| *b);
stack.truncate(new_len);
stack.push(result);
}
CompiledOp::Not => {
let v = stack.pop().unwrap_or(true);
stack.push(!v);
}
}
}
stack.pop().unwrap_or(true)
}
}
fn compile_into(
filter: &Filter,
schema: &HashMap<String, usize>,
ops: &mut Vec<CompiledOp>,
) -> Result<(), CompileError> {
match filter {
Filter::Predicate(p) => {
let col_idx = schema
.get(&p.column)
.copied()
.ok_or_else(|| CompileError::UnknownColumn(p.column.clone()))?;
ops.push(CompiledOp::Predicate {
col_idx,
predicate: p.clone(),
});
}
Filter::And(filters) => {
for f in filters {
compile_into(f, schema, ops)?;
}
ops.push(CompiledOp::AndN(filters.len()));
}
Filter::Or(filters) => {
for f in filters {
compile_into(f, schema, ops)?;
}
ops.push(CompiledOp::OrN(filters.len()));
}
Filter::Not(inner) => {
compile_into(inner, schema, ops)?;
ops.push(CompiledOp::Not);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::filter::Predicate;
fn schema(cols: &[&str]) -> HashMap<String, usize> {
cols.iter()
.enumerate()
.map(|(i, c)| (c.to_string(), i))
.collect()
}
fn slot(values: Vec<Value>) -> Vec<Value> {
values
}
#[test]
fn compile_simple_eq() {
let s = schema(&["a"]);
let f = Filter::Predicate(Predicate::eq("a", Value::Integer(5)));
let c = CompiledFilter::compile(&f, &s).unwrap();
assert_eq!(c.op_count(), 1);
}
#[test]
fn compile_and_two() {
let s = schema(&["a", "b"]);
let f = Filter::And(vec![
Filter::Predicate(Predicate::eq("a", Value::Integer(1))),
Filter::Predicate(Predicate::eq("b", Value::Integer(2))),
]);
let c = CompiledFilter::compile(&f, &s).unwrap();
assert_eq!(c.op_count(), 3);
}
#[test]
fn compile_or_three() {
let s = schema(&["a"]);
let f = Filter::Or(vec![
Filter::Predicate(Predicate::eq("a", Value::Integer(1))),
Filter::Predicate(Predicate::eq("a", Value::Integer(2))),
Filter::Predicate(Predicate::eq("a", Value::Integer(3))),
]);
let c = CompiledFilter::compile(&f, &s).unwrap();
assert_eq!(c.op_count(), 4); }
#[test]
fn compile_not_wraps_inner() {
let s = schema(&["a"]);
let f = Filter::Not(Box::new(Filter::Predicate(Predicate::eq(
"a",
Value::Integer(1),
))));
let c = CompiledFilter::compile(&f, &s).unwrap();
assert_eq!(c.op_count(), 2); }
#[test]
fn compile_unknown_column_errors() {
let s = schema(&["a"]);
let f = Filter::Predicate(Predicate::eq("missing", Value::Integer(1)));
let err = CompiledFilter::compile(&f, &s).unwrap_err();
assert_eq!(err, CompileError::UnknownColumn("missing".to_string()));
}
#[test]
fn eval_eq_true() {
let s = schema(&["age"]);
let c = CompiledFilter::compile(
&Filter::Predicate(Predicate::eq("age", Value::Integer(42))),
&s,
)
.unwrap();
assert!(c.evaluate(&slot(vec![Value::Integer(42)])));
}
#[test]
fn eval_eq_false() {
let s = schema(&["age"]);
let c = CompiledFilter::compile(
&Filter::Predicate(Predicate::eq("age", Value::Integer(42))),
&s,
)
.unwrap();
assert!(!c.evaluate(&slot(vec![Value::Integer(7)])));
}
#[test]
fn eval_and_true_when_all_true() {
let s = schema(&["a", "b"]);
let f = Filter::And(vec![
Filter::Predicate(Predicate::eq("a", Value::Integer(1))),
Filter::Predicate(Predicate::eq("b", Value::Integer(2))),
]);
let c = CompiledFilter::compile(&f, &s).unwrap();
assert!(c.evaluate(&slot(vec![Value::Integer(1), Value::Integer(2)])));
}
#[test]
fn eval_and_false_when_one_false() {
let s = schema(&["a", "b"]);
let f = Filter::And(vec![
Filter::Predicate(Predicate::eq("a", Value::Integer(1))),
Filter::Predicate(Predicate::eq("b", Value::Integer(2))),
]);
let c = CompiledFilter::compile(&f, &s).unwrap();
assert!(!c.evaluate(&slot(vec![Value::Integer(1), Value::Integer(99)])));
}
#[test]
fn eval_or_true_when_one_true() {
let s = schema(&["a"]);
let f = Filter::Or(vec![
Filter::Predicate(Predicate::eq("a", Value::Integer(1))),
Filter::Predicate(Predicate::eq("a", Value::Integer(2))),
]);
let c = CompiledFilter::compile(&f, &s).unwrap();
assert!(c.evaluate(&slot(vec![Value::Integer(2)])));
}
#[test]
fn eval_not_flips_result() {
let s = schema(&["a"]);
let inner = Filter::Predicate(Predicate::eq("a", Value::Integer(1)));
let f = Filter::Not(Box::new(inner));
let c = CompiledFilter::compile(&f, &s).unwrap();
assert!(!c.evaluate(&slot(vec![Value::Integer(1)])));
assert!(c.evaluate(&slot(vec![Value::Integer(2)])));
}
#[test]
fn eval_is_null_on_null_value() {
let s = schema(&["a"]);
let c = CompiledFilter::compile(&Filter::Predicate(Predicate::is_null("a")), &s).unwrap();
assert!(c.evaluate(&slot(vec![Value::Null])));
assert!(!c.evaluate(&slot(vec![Value::Integer(1)])));
}
#[test]
fn eval_range_predicate() {
let s = schema(&["age"]);
let c = CompiledFilter::compile(
&Filter::Predicate(Predicate::between(
"age",
Value::Integer(18),
Value::Integer(65),
)),
&s,
)
.unwrap();
assert!(c.evaluate(&slot(vec![Value::Integer(30)])));
assert!(!c.evaluate(&slot(vec![Value::Integer(10)])));
assert!(!c.evaluate(&slot(vec![Value::Integer(70)])));
}
#[test]
fn eval_nested_and_or_not() {
let s = schema(&["a", "b", "c"]);
let f = Filter::Or(vec![
Filter::And(vec![
Filter::Predicate(Predicate::eq("a", Value::Integer(1))),
Filter::Predicate(Predicate::eq("b", Value::Integer(2))),
]),
Filter::Not(Box::new(Filter::Predicate(Predicate::eq(
"c",
Value::Integer(3),
)))),
]);
let c = CompiledFilter::compile(&f, &s).unwrap();
assert!(c.evaluate(&slot(vec![
Value::Integer(1),
Value::Integer(2),
Value::Integer(99),
])));
assert!(c.evaluate(&slot(vec![
Value::Integer(99),
Value::Integer(99),
Value::Integer(99),
])));
assert!(!c.evaluate(&slot(vec![
Value::Integer(99),
Value::Integer(99),
Value::Integer(3),
])));
}
#[test]
fn compiled_matches_legacy_for_simple_filters() {
let s = schema(&["a", "b"]);
let f = Filter::Or(vec![
Filter::And(vec![
Filter::Predicate(Predicate::eq("a", Value::Integer(1))),
Filter::Predicate(Predicate::gt("b", Value::Integer(5))),
]),
Filter::Predicate(Predicate::is_null("a")),
]);
let compiled = CompiledFilter::compile(&f, &s).unwrap();
let cases: Vec<(Vec<Value>, bool)> = vec![
(vec![Value::Integer(1), Value::Integer(10)], true),
(vec![Value::Integer(1), Value::Integer(5)], false),
(vec![Value::Null, Value::Integer(0)], true),
(vec![Value::Integer(2), Value::Integer(10)], false),
];
for (slot_vals, expected) in cases {
let got_compiled = compiled.evaluate(&slot_vals);
let row: HashMap<String, Value> = vec!["a", "b"]
.into_iter()
.zip(slot_vals.iter().cloned())
.map(|(k, v)| (k.to_string(), v))
.collect();
let got_legacy = f.evaluate(&|c| row.get(c).cloned());
assert_eq!(
got_compiled, got_legacy,
"compiled and legacy disagree on row {:?}",
slot_vals
);
assert_eq!(got_compiled, expected, "wrong answer for {:?}", slot_vals);
}
}
#[test]
fn compiled_matches_legacy_for_random_filters() {
let s = schema(&["a", "b", "c"]);
let mut seed: u64 = 0x9E3779B97F4A7C15;
let mut next = || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
seed
};
for _ in 0..1000 {
let n = (next() % 3) as usize;
let filter = make_random_filter(&mut next, n);
let compiled = match CompiledFilter::compile(&filter, &s) {
Ok(c) => c,
Err(_) => continue,
};
let row_vals = vec![
Value::Integer((next() % 10) as i64),
Value::Integer((next() % 10) as i64),
Value::Integer((next() % 10) as i64),
];
let row: HashMap<String, Value> = vec!["a", "b", "c"]
.into_iter()
.zip(row_vals.iter().cloned())
.map(|(k, v)| (k.to_string(), v))
.collect();
let got_compiled = compiled.evaluate(&row_vals);
let got_legacy = filter.evaluate(&|c| row.get(c).cloned());
assert_eq!(
got_compiled, got_legacy,
"fuzz disagreement: filter={:?} row={:?}",
filter, row_vals
);
}
}
fn make_random_filter(next: &mut impl FnMut() -> u64, depth: usize) -> Filter {
if depth == 0 {
let col = ["a", "b", "c"][(next() % 3) as usize];
let lit = Value::Integer((next() % 10) as i64);
let op = next() % 5;
return match op {
0 => Filter::Predicate(Predicate::eq(col, lit)),
1 => Filter::Predicate(Predicate::ne(col, lit)),
2 => Filter::Predicate(Predicate::lt(col, lit)),
3 => Filter::Predicate(Predicate::gt(col, lit)),
_ => Filter::Predicate(Predicate::ge(col, lit)),
};
}
match next() % 3 {
0 => Filter::And(vec![
make_random_filter(next, depth - 1),
make_random_filter(next, depth - 1),
]),
1 => Filter::Or(vec![
make_random_filter(next, depth - 1),
make_random_filter(next, depth - 1),
]),
_ => Filter::Not(Box::new(make_random_filter(next, depth - 1))),
}
}
#[test]
fn evaluate_handles_oversized_index_as_null() {
let s = schema(&["a", "b", "out_of_range"]);
let f = Filter::Predicate(Predicate::eq("out_of_range", Value::Integer(1)));
let c = CompiledFilter::compile(&f, &s).unwrap();
let short_slot = vec![Value::Integer(0), Value::Integer(0)];
assert!(!c.evaluate(&short_slot));
}
#[test]
fn empty_filter_returns_true() {
let s = schema(&["a"]);
let c = CompiledFilter::compile(&Filter::And(vec![]), &s).unwrap();
assert!(c.evaluate(&slot(vec![Value::Integer(1)])));
}
}