use crate::btree::CellValue;
#[derive(Debug, Clone, PartialEq)]
pub enum Comparator {
Eq,
Ne,
Lt,
Lte,
Gt,
Gte,
Like,
NotLike,
IsNull,
IsNotNull,
}
#[derive(Debug, Clone)]
pub enum FilterOperand {
Column(usize),
Literal(CellValue),
}
#[derive(Debug, Clone)]
pub struct Predicate {
pub left: FilterOperand,
pub op: Comparator,
pub right: Option<FilterOperand>,
}
#[derive(Debug, Clone)]
pub enum FilterExpr {
Pred(Predicate),
And(Box<FilterExpr>, Box<FilterExpr>),
Or(Box<FilterExpr>, Box<FilterExpr>),
Not(Box<FilterExpr>),
}
impl FilterExpr {
pub fn col_eq(col_idx: usize, val: CellValue) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::Eq,
right: Some(FilterOperand::Literal(val)),
})
}
pub fn col_ne(col_idx: usize, val: CellValue) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::Ne,
right: Some(FilterOperand::Literal(val)),
})
}
pub fn col_lt(col_idx: usize, val: CellValue) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::Lt,
right: Some(FilterOperand::Literal(val)),
})
}
pub fn col_lte(col_idx: usize, val: CellValue) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::Lte,
right: Some(FilterOperand::Literal(val)),
})
}
pub fn col_gt(col_idx: usize, val: CellValue) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::Gt,
right: Some(FilterOperand::Literal(val)),
})
}
pub fn col_gte(col_idx: usize, val: CellValue) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::Gte,
right: Some(FilterOperand::Literal(val)),
})
}
pub fn col_like(col_idx: usize, pattern: &str) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::Like,
right: Some(FilterOperand::Literal(CellValue::Text(pattern.to_owned()))),
})
}
pub fn col_not_like(col_idx: usize, pattern: &str) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::NotLike,
right: Some(FilterOperand::Literal(CellValue::Text(pattern.to_owned()))),
})
}
pub fn col_is_null(col_idx: usize) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::IsNull,
right: None,
})
}
pub fn col_is_not_null(col_idx: usize) -> Self {
Self::Pred(Predicate {
left: FilterOperand::Column(col_idx),
op: Comparator::IsNotNull,
right: None,
})
}
pub fn and(a: FilterExpr, b: FilterExpr) -> Self {
Self::And(Box::new(a), Box::new(b))
}
pub fn or(a: FilterExpr, b: FilterExpr) -> Self {
Self::Or(Box::new(a), Box::new(b))
}
#[allow(clippy::should_implement_trait)]
pub fn not(e: FilterExpr) -> Self {
Self::Not(Box::new(e))
}
}
#[must_use]
pub fn evaluate(expr: &FilterExpr, row: &[CellValue]) -> bool {
match expr {
FilterExpr::Pred(pred) => eval_predicate(pred, row),
FilterExpr::And(a, b) => evaluate(a, row) && evaluate(b, row),
FilterExpr::Or(a, b) => evaluate(a, row) || evaluate(b, row),
FilterExpr::Not(e) => !evaluate(e, row),
}
}
fn resolve_operand<'row>(
operand: &'row FilterOperand,
row: &'row [CellValue],
) -> Option<&'row CellValue> {
match operand {
FilterOperand::Column(idx) => row.get(*idx),
FilterOperand::Literal(v) => Some(v),
}
}
fn eval_predicate(pred: &Predicate, row: &[CellValue]) -> bool {
match pred.op {
Comparator::IsNull => {
let left = resolve_operand(&pred.left, row);
matches!(left, Some(CellValue::Null) | None)
}
Comparator::IsNotNull => {
let left = resolve_operand(&pred.left, row);
!matches!(left, Some(CellValue::Null) | None)
}
_ => {
let left = resolve_operand(&pred.left, row);
let right = pred.right.as_ref().and_then(|r| resolve_operand(r, row));
match (left, right) {
(Some(l), Some(r)) => compare(l, &pred.op, r),
_ => false,
}
}
}
}
fn compare(left: &CellValue, op: &Comparator, right: &CellValue) -> bool {
match op {
Comparator::Eq => cell_eq(left, right),
Comparator::Ne => !cell_eq(left, right),
Comparator::Lt => numeric_compare(left, right)
.map(|o| o.is_lt())
.unwrap_or(false),
Comparator::Lte => numeric_compare(left, right)
.map(|o| o.is_le())
.unwrap_or(false),
Comparator::Gt => numeric_compare(left, right)
.map(|o| o.is_gt())
.unwrap_or(false),
Comparator::Gte => numeric_compare(left, right)
.map(|o| o.is_ge())
.unwrap_or(false),
Comparator::Like => like_match(left, right),
Comparator::NotLike => !like_match(left, right),
Comparator::IsNull | Comparator::IsNotNull => {
unreachable!("IsNull / IsNotNull must be handled in eval_predicate, not compare")
}
}
}
fn cell_eq(a: &CellValue, b: &CellValue) -> bool {
match (a, b) {
(CellValue::Integer(x), CellValue::Integer(y)) => x == y,
(CellValue::Float(x), CellValue::Float(y)) => x == y,
(CellValue::Text(x), CellValue::Text(y)) => x == y,
(CellValue::Blob(x), CellValue::Blob(y)) => x == y,
_ => false,
}
}
fn cell_as_f64(v: &CellValue) -> Option<f64> {
match v {
CellValue::Integer(i) => Some(*i as f64),
CellValue::Float(f) => Some(*f),
_ => None,
}
}
fn numeric_compare(a: &CellValue, b: &CellValue) -> Option<std::cmp::Ordering> {
if let (Some(x), Some(y)) = (cell_as_f64(a), cell_as_f64(b)) {
return x.partial_cmp(&y);
}
if let (CellValue::Text(x), CellValue::Text(y)) = (a, b) {
return Some(x.cmp(y));
}
None
}
fn like_match(value: &CellValue, pattern: &CellValue) -> bool {
match (value, pattern) {
(CellValue::Text(v), CellValue::Text(p)) => like_matches_bytes(v.as_bytes(), p.as_bytes()),
_ => false,
}
}
fn like_matches_bytes(s: &[u8], pat: &[u8]) -> bool {
match pat {
[] => s.is_empty(),
[b'%', rest @ ..] => {
for i in 0..=s.len() {
if like_matches_bytes(&s[i..], rest) {
return true;
}
}
false
}
[b'_', rest @ ..] => !s.is_empty() && like_matches_bytes(&s[1..], rest),
[b'\\', c, rest @ ..] if matches!(c, b'%' | b'_' | b'\\') => {
!s.is_empty() && s[0] == *c && like_matches_bytes(&s[1..], rest)
}
[c, rest @ ..] => !s.is_empty() && s[0] == *c && like_matches_bytes(&s[1..], rest),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_eq_integer_same() {
assert!(cell_eq(&CellValue::Integer(42), &CellValue::Integer(42)));
}
#[test]
fn test_cell_eq_integer_different() {
assert!(!cell_eq(&CellValue::Integer(42), &CellValue::Integer(43)));
}
#[test]
fn test_cell_eq_null_null_is_false() {
assert!(!cell_eq(&CellValue::Null, &CellValue::Null));
}
#[test]
fn test_cell_eq_integer_float_no_coercion() {
assert!(!cell_eq(&CellValue::Integer(5), &CellValue::Float(5.0)));
}
#[test]
fn test_numeric_compare_integer_cross_float() {
let ord = numeric_compare(&CellValue::Integer(3), &CellValue::Float(4.5));
assert_eq!(ord, Some(std::cmp::Ordering::Less));
}
#[test]
fn test_numeric_compare_text_lexicographic() {
let ord = numeric_compare(
&CellValue::Text("abc".into()),
&CellValue::Text("abd".into()),
);
assert_eq!(ord, Some(std::cmp::Ordering::Less));
}
#[test]
fn test_numeric_compare_incompatible_types_is_none() {
let ord = numeric_compare(&CellValue::Text("abc".into()), &CellValue::Integer(1));
assert_eq!(ord, None);
}
#[test]
fn test_like_percent_matches_empty() {
assert!(like_matches_bytes(b"", b"%"));
}
#[test]
fn test_like_percent_matches_any() {
assert!(like_matches_bytes(b"hello world", b"hello%"));
assert!(like_matches_bytes(b"hello world", b"%world"));
assert!(like_matches_bytes(b"hello world", b"%llo%"));
}
#[test]
fn test_like_underscore_matches_one_char() {
assert!(like_matches_bytes(b"abc", b"a_c"));
assert!(!like_matches_bytes(b"ac", b"a_c"));
assert!(!like_matches_bytes(b"abbc", b"a_c"));
}
#[test]
fn test_like_escape_literal_percent() {
assert!(like_matches_bytes(b"50%", b"50\\%"));
assert!(!like_matches_bytes(b"50x", b"50\\%"));
}
#[test]
fn test_like_escape_literal_underscore() {
assert!(like_matches_bytes(b"a_b", b"a\\_b"));
assert!(!like_matches_bytes(b"axb", b"a\\_b"));
}
#[test]
fn test_like_exact_match() {
assert!(like_matches_bytes(b"exact", b"exact"));
assert!(!like_matches_bytes(b"exact", b"eXact"));
}
#[test]
fn test_like_non_text_returns_false() {
assert!(!like_match(
&CellValue::Integer(42),
&CellValue::Text("%".into())
));
}
}