use serde::{Deserialize, Serialize};
use super::value::LogicalValue;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum LogicalFilter {
And(Vec<LogicalFilter>),
Or(Vec<LogicalFilter>),
Not(Box<LogicalFilter>),
Comparison {
field: String,
op: ComparisonOp,
value: LogicalValue,
},
IsNull(String),
InList {
field: String,
values: Vec<LogicalValue>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComparisonOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
Like,
ILike,
Contains,
StartsWith,
EndsWith,
}
impl LogicalFilter {
pub fn always() -> Self {
Self::And(Vec::new())
}
pub fn never() -> Self {
Self::Or(Vec::new())
}
pub fn and(self, other: Self) -> Self {
match self {
Self::And(mut clauses) => {
clauses.push(other);
Self::And(clauses)
}
existing => Self::And(vec![existing, other]),
}
}
pub fn referenced_fields<'a>(&'a self, out: &mut Vec<&'a str>) {
match self {
Self::And(clauses) | Self::Or(clauses) => {
for c in clauses {
c.referenced_fields(out);
}
}
Self::Not(inner) => inner.referenced_fields(out),
Self::Comparison { field, .. } | Self::IsNull(field) | Self::InList { field, .. } => {
out.push(field.as_str());
}
}
}
}
impl ComparisonOp {
pub fn token(self) -> &'static str {
match self {
Self::Eq => "eq",
Self::Ne => "ne",
Self::Lt => "lt",
Self::Le => "le",
Self::Gt => "gt",
Self::Ge => "ge",
Self::Like => "like",
Self::ILike => "ilike",
Self::Contains => "contains",
Self::StartsWith => "starts_with",
Self::EndsWith => "ends_with",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_constructors_are_neutral() {
let p = LogicalFilter::Comparison {
field: "x".into(),
op: ComparisonOp::Eq,
value: LogicalValue::Int(1),
};
let combined = LogicalFilter::always().and(p.clone());
match combined {
LogicalFilter::And(clauses) => assert_eq!(clauses, vec![p]),
other => panic!("expected And, got {other:?}"),
}
}
#[test]
fn and_chains_flatten() {
let p1 = LogicalFilter::Comparison {
field: "a".into(),
op: ComparisonOp::Eq,
value: LogicalValue::Int(1),
};
let p2 = LogicalFilter::Comparison {
field: "b".into(),
op: ComparisonOp::Eq,
value: LogicalValue::Int(2),
};
let p3 = LogicalFilter::Comparison {
field: "c".into(),
op: ComparisonOp::Eq,
value: LogicalValue::Int(3),
};
let combined = p1.clone().and(p2.clone()).and(p3.clone());
match combined {
LogicalFilter::And(clauses) => assert_eq!(clauses, vec![p1, p2, p3]),
other => panic!("expected flat And, got {other:?}"),
}
}
#[test]
fn referenced_fields_walks_all_branches() {
let f = LogicalFilter::And(vec![
LogicalFilter::Comparison {
field: "a".into(),
op: ComparisonOp::Eq,
value: LogicalValue::Int(1),
},
LogicalFilter::Or(vec![
LogicalFilter::IsNull("b".into()),
LogicalFilter::InList {
field: "c".into(),
values: vec![LogicalValue::Int(2)],
},
]),
LogicalFilter::Not(Box::new(LogicalFilter::Comparison {
field: "d".into(),
op: ComparisonOp::Gt,
value: LogicalValue::Int(3),
})),
]);
let mut out: Vec<&str> = Vec::new();
f.referenced_fields(&mut out);
out.sort();
assert_eq!(out, vec!["a", "b", "c", "d"]);
}
#[test]
fn op_tokens_are_pinned() {
assert_eq!(ComparisonOp::Eq.token(), "eq");
assert_eq!(ComparisonOp::ILike.token(), "ilike");
assert_eq!(ComparisonOp::StartsWith.token(), "starts_with");
}
}