use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::document::{Document, Value};
use crate::error::TalaDbError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Filter {
All,
Eq(String, Value),
Ne(String, Value),
Gt(String, Value),
Gte(String, Value),
Lt(String, Value),
Lte(String, Value),
In(String, Vec<Value>),
Nin(String, Vec<Value>),
Exists(String, bool),
And(Vec<Self>),
Or(Vec<Self>),
Not(Box<Self>),
Contains(String, String),
Regex(String, String),
}
fn any_element<F: Fn(&Value) -> bool>(value: &Value, pred: F) -> bool {
if pred(value) {
return true;
}
match value {
Value::Array(items) => items.iter().any(pred),
_ => false,
}
}
const REGEX_SIZE_LIMIT: usize = 1 << 20;
pub(crate) fn build_regex(pattern: &str) -> Result<regex::Regex, TalaDbError> {
regex::RegexBuilder::new(pattern)
.size_limit(REGEX_SIZE_LIMIT)
.dfa_size_limit(REGEX_SIZE_LIMIT)
.build()
.map_err(|e| TalaDbError::InvalidFilter(format!("regex: {e}")))
}
enum RegexSource<'a> {
Compile,
Cached(&'a HashMap<String, regex::Regex>),
}
impl Filter {
pub(super) fn matches_index_value(&self, value: &Value) -> bool {
use std::cmp::Ordering::{Equal, Greater, Less};
match self {
Self::Eq(_, v) => value == v,
Self::In(_, values) => values.contains(value),
Self::Gt(_, v) => value.partial_cmp_numeric(v) == Some(Greater),
Self::Gte(_, v) => matches!(value.partial_cmp_numeric(v), Some(Greater | Equal)),
Self::Lt(_, v) => value.partial_cmp_numeric(v) == Some(Less),
Self::Lte(_, v) => matches!(value.partial_cmp_numeric(v), Some(Less | Equal)),
_ => false,
}
}
fn matches_leaf(&self, doc: &Document) -> bool {
match self {
Self::All => true,
Self::Eq(field, val) => {
if field == "_id" {
return matches!(val, Value::Str(s) if s == &doc.id.to_string());
}
doc.get(field)
.is_some_and(|v| any_element(v, |e| self.matches_index_value(e)))
}
Self::Ne(field, val) => {
if field == "_id" {
return !matches!(val, Value::Str(s) if s == &doc.id.to_string());
}
doc.get(field).is_none_or(|v| !any_element(v, |e| e == val))
}
Self::Gt(field, _) | Self::Gte(field, _) | Self::Lt(field, _) | Self::Lte(field, _) => {
doc.get(field)
.is_some_and(|v| any_element(v, |e| self.matches_index_value(e)))
}
Self::In(field, vals) => {
if field == "_id" {
let id_str = doc.id.to_string();
return vals
.iter()
.any(|v| matches!(v, Value::Str(s) if *s == id_str));
}
doc.get(field)
.is_some_and(|v| any_element(v, |e| self.matches_index_value(e)))
}
Self::Nin(field, vals) => {
if field == "_id" {
let id_str = doc.id.to_string();
return !vals
.iter()
.any(|v| matches!(v, Value::Str(s) if *s == id_str));
}
doc.get(field)
.is_none_or(|v| !any_element(v, |e| vals.contains(e)))
}
Self::Exists(field, should_exist) => {
if field == "_id" {
return *should_exist;
}
doc.contains_key(field) == *should_exist
}
Self::And(_) | Self::Or(_) | Self::Not(_) | Self::Regex(..) => false,
Self::Contains(field, query) => {
use crate::fts::tokenize;
let query_tokens = tokenize(query);
if query_tokens.is_empty() {
return true;
}
if let Some(Value::Str(text)) = doc.get(field) {
let doc_tokens = tokenize(text);
query_tokens
.iter()
.all(|qt| doc_tokens.iter().any(|dt| dt == qt))
} else {
false
}
}
}
}
fn eval(&self, doc: &Document, regexes: &RegexSource<'_>) -> Result<bool, TalaDbError> {
match self {
Self::And(filters) => {
for f in filters {
if !f.eval(doc, regexes)? {
return Ok(false);
}
}
Ok(true)
}
Self::Or(filters) => {
for f in filters {
if f.eval(doc, regexes)? {
return Ok(true);
}
}
Ok(false)
}
Self::Not(inner) => Ok(!inner.eval(doc, regexes)?),
Self::Regex(field, pattern) => {
let compiled;
let re = match regexes {
RegexSource::Compile => {
compiled = build_regex(pattern)?;
&compiled
}
RegexSource::Cached(cache) => cache.get(pattern).ok_or_else(|| {
TalaDbError::InvalidFilter(format!(
"internal: no compiled regex for pattern {pattern:?} — the cache was built from a different filter"
))
})?,
};
let Some(value) = doc.get(field) else {
return Ok(false);
};
Ok(any_element(
value,
|e| matches!(e, Value::Str(t) if re.is_match(t)),
))
}
leaf => Ok(leaf.matches_leaf(doc)),
}
}
pub fn matches(&self, doc: &Document) -> Result<bool, TalaDbError> {
self.eval(doc, &RegexSource::Compile)
}
pub fn collect_regex_patterns(&self) -> Vec<String> {
let mut patterns = Vec::new();
self.collect_regex_patterns_into(&mut patterns);
patterns.sort();
patterns.dedup();
patterns
}
fn collect_regex_patterns_into(&self, out: &mut Vec<String>) {
match self {
Self::Regex(_, pattern) => out.push(pattern.clone()),
Self::And(filters) | Self::Or(filters) => {
for f in filters {
f.collect_regex_patterns_into(out);
}
}
Self::Not(inner) => inner.collect_regex_patterns_into(out),
_ => {}
}
}
pub(crate) fn compile_regex_cache(&self) -> Result<HashMap<String, regex::Regex>, TalaDbError> {
let patterns = self.collect_regex_patterns();
let mut map = HashMap::with_capacity(patterns.len());
for pat in patterns {
let re = build_regex(&pat)?;
map.insert(pat, re);
}
Ok(map)
}
pub fn matches_with_cache(
&self,
doc: &Document,
regex_cache: &HashMap<String, regex::Regex>,
) -> Result<bool, TalaDbError> {
self.eval(doc, &RegexSource::Cached(regex_cache))
}
pub fn primary_field(&self) -> Option<&str> {
match self {
Self::Eq(f, _)
| Self::Ne(f, _)
| Self::Gt(f, _)
| Self::Gte(f, _)
| Self::Lt(f, _)
| Self::Lte(f, _)
| Self::In(f, _)
| Self::Nin(f, _)
| Self::Exists(f, _) => Some(f.as_str()),
Self::And(filters) => filters.iter().find_map(|f| f.primary_field()),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ulid::Ulid;
fn doc(fields: Vec<(&str, Value)>) -> Document {
Document::with_id(
Ulid::nil(),
fields
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
)
}
#[test]
fn basic_eq() {
let d = doc(vec![("age", Value::Int(30))]);
assert!(
Filter::Eq("age".into(), Value::Int(30))
.matches(&d)
.expect("regex-free filter")
);
assert!(
!Filter::Eq("age".into(), Value::Int(31))
.matches(&d)
.expect("regex-free filter")
);
}
#[test]
fn range_ops() {
let d = doc(vec![("score", Value::Float(7.5))]);
assert!(
Filter::Gt("score".into(), Value::Float(7.0))
.matches(&d)
.expect("regex-free filter")
);
assert!(
!Filter::Gt("score".into(), Value::Float(8.0))
.matches(&d)
.expect("regex-free filter")
);
assert!(
Filter::Lte("score".into(), Value::Float(7.5))
.matches(&d)
.expect("regex-free filter")
);
}
#[test]
fn and_or() {
let d = doc(vec![("a", Value::Int(1)), ("b", Value::Int(2))]);
let f = Filter::And(vec![
Filter::Eq("a".into(), Value::Int(1)),
Filter::Eq("b".into(), Value::Int(2)),
]);
assert!(f.matches(&d).expect("regex-free filter"));
let f2 = Filter::Or(vec![
Filter::Eq("a".into(), Value::Int(99)),
Filter::Eq("b".into(), Value::Int(2)),
]);
assert!(f2.matches(&d).expect("regex-free filter"));
}
#[test]
fn exists() {
let d = doc(vec![("x", Value::Null)]);
assert!(
Filter::Exists("x".into(), true)
.matches(&d)
.expect("regex-free filter")
);
assert!(
!Filter::Exists("y".into(), true)
.matches(&d)
.expect("regex-free filter")
);
assert!(
Filter::Exists("y".into(), false)
.matches(&d)
.expect("regex-free filter")
);
}
}