use iqdb_types::{Filter, IqdbError, Metadata, Result};
use crate::eval;
pub const MAX_FILTER_DEPTH: usize = 64;
pub const MAX_IN_VALUES: usize = 1024;
#[derive(Debug, Clone)]
pub struct FilterEvaluator {
filter: Filter,
}
impl FilterEvaluator {
pub fn new(filter: Filter) -> Result<Self> {
validate(&filter)?;
Ok(Self { filter })
}
#[must_use]
pub fn evaluate(&self, metadata: Option<&Metadata>) -> bool {
eval::eval(&self.filter, metadata)
}
pub fn prefilter<'a, K, I>(&'a self, candidates: I) -> impl Iterator<Item = K> + 'a
where
I: IntoIterator<Item = (K, Option<&'a Metadata>)>,
I::IntoIter: 'a,
K: 'a,
{
candidates
.into_iter()
.filter_map(move |(key, metadata)| self.evaluate(metadata).then_some(key))
}
pub fn postfilter<'a, H, I>(&'a self, scored: I) -> impl Iterator<Item = H> + 'a
where
I: IntoIterator<Item = (H, Option<&'a Metadata>)>,
I::IntoIter: 'a,
H: 'a,
{
scored
.into_iter()
.filter_map(move |(hit, metadata)| self.evaluate(metadata).then_some(hit))
}
#[must_use]
pub fn filter(&self) -> &Filter {
&self.filter
}
}
struct Frame<'a> {
node: &'a Filter,
parent_depth: usize,
}
fn validate(root: &Filter) -> Result<()> {
let mut stack: Vec<Frame<'_>> = Vec::new();
stack.push(Frame {
node: root,
parent_depth: 0,
});
while let Some(Frame { node, parent_depth }) = stack.pop() {
match node {
Filter::In { values, .. } => {
if values.len() > MAX_IN_VALUES {
return Err(IqdbError::InvalidFilter);
}
}
Filter::Eq { .. }
| Filter::Neq { .. }
| Filter::Lt { .. }
| Filter::Lte { .. }
| Filter::Gt { .. }
| Filter::Gte { .. } => {}
Filter::And(children) | Filter::Or(children) => {
let depth = parent_depth.saturating_add(1);
if depth > MAX_FILTER_DEPTH {
return Err(IqdbError::InvalidFilter);
}
for child in children {
stack.push(Frame {
node: child,
parent_depth: depth,
});
}
}
Filter::Not(inner) => {
let depth = parent_depth.saturating_add(1);
if depth > MAX_FILTER_DEPTH {
return Err(IqdbError::InvalidFilter);
}
stack.push(Frame {
node: inner,
parent_depth: depth,
});
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use iqdb_types::Value;
fn nested_not(depth: usize) -> Filter {
let mut current = Filter::eq("k", Value::Int(0));
for _ in 0..depth {
current = Filter::not(current);
}
current
}
#[test]
fn new_accepts_a_leaf() {
let f = Filter::eq("k", Value::Int(1));
assert!(FilterEvaluator::new(f).is_ok());
}
#[test]
fn new_accepts_at_max_depth() {
let f = nested_not(MAX_FILTER_DEPTH);
assert!(FilterEvaluator::new(f).is_ok());
}
#[test]
fn new_rejects_just_over_max_depth() {
let f = nested_not(MAX_FILTER_DEPTH + 1);
let err = FilterEvaluator::new(f).unwrap_err();
assert_eq!(err, IqdbError::InvalidFilter);
}
#[test]
fn new_accepts_in_at_cap() {
let f = Filter::is_in("tag", vec![Value::Int(0); MAX_IN_VALUES]);
assert!(FilterEvaluator::new(f).is_ok());
}
#[test]
fn new_rejects_in_just_over_cap() {
let f = Filter::is_in("tag", vec![Value::Int(0); MAX_IN_VALUES + 1]);
let err = FilterEvaluator::new(f).unwrap_err();
assert_eq!(err, IqdbError::InvalidFilter);
}
#[test]
fn evaluate_matches_validated_filter() {
let evaluator = FilterEvaluator::new(Filter::eq("k", Value::Int(1))).unwrap();
let meta: Metadata = [("k".to_string(), Value::Int(1))].into_iter().collect();
assert!(evaluator.evaluate(Some(&meta)));
assert!(!evaluator.evaluate(None));
}
#[test]
fn evaluator_clone_preserves_filter() {
let evaluator = FilterEvaluator::new(Filter::eq("k", Value::Int(1))).unwrap();
let copy = evaluator.clone();
let meta: Metadata = [("k".to_string(), Value::Int(1))].into_iter().collect();
assert_eq!(evaluator.evaluate(Some(&meta)), copy.evaluate(Some(&meta)));
}
fn meta(field: &str, value: Value) -> Metadata {
[(field.to_string(), value)].into_iter().collect()
}
#[test]
fn prefilter_keeps_only_matching_keys() {
let evaluator = FilterEvaluator::new(Filter::gt("year", Value::Int(2000))).unwrap();
let m2026 = meta("year", Value::Int(2026));
let m1999 = meta("year", Value::Int(1999));
let rows = [(10_usize, Some(&m2026)), (20, Some(&m1999)), (30, None)];
let kept: Vec<usize> = evaluator.prefilter(rows).collect();
assert_eq!(kept, [10]);
}
#[test]
fn postfilter_keeps_only_matching_hits_and_is_lazy() {
let evaluator =
FilterEvaluator::new(Filter::eq("lang", Value::String("rust".into()))).unwrap();
let rust = meta("lang", Value::String("rust".into()));
let go = meta("lang", Value::String("go".into()));
let scored = [("a", Some(&go)), ("b", Some(&rust)), ("c", Some(&rust))];
let first: Vec<&str> = evaluator.postfilter(scored).take(1).collect();
assert_eq!(first, ["b"]);
}
#[test]
fn prefilter_empty_input_yields_nothing() {
let evaluator = FilterEvaluator::new(Filter::eq("k", Value::Int(1))).unwrap();
let rows: [(usize, Option<&Metadata>); 0] = [];
assert_eq!(evaluator.prefilter(rows).count(), 0);
}
}