use crate::HeerId;
use crate::RanjId;
use std::ops::{BitAnd, BitOr, Not};
#[derive(Debug, Clone)]
pub enum Condition {
True,
Leaf(Leaf),
And(Vec<Condition>),
Or(Vec<Condition>),
Not(Box<Condition>),
Expr(crate::expr::Expr<bool>),
ArrayContains(crate::array::ArrayContainsLeaf),
ArrayContainedBy(crate::array::ArrayContainedByLeaf),
ArrayOverlap(crate::array::ArrayOverlapLeaf),
RangePredicate(crate::range::RangePredicateLeaf),
JsonbPath(crate::jsonb::path::JsonbPathLeaf),
RawSql(RawSqlFragment),
}
#[derive(Debug, Clone)]
pub struct RawSqlFragment(pub(crate) &'static str);
impl RawSqlFragment {
pub(crate) fn as_str(&self) -> &'static str {
self.0
}
}
impl Condition {
#[doc(hidden)]
#[must_use]
pub fn __from_raw_sql_fragment(sql: &'static str) -> Condition {
Condition::RawSql(RawSqlFragment(sql))
}
}
#[allow(clippy::derivable_impls)]
impl Default for Condition {
fn default() -> Self {
Condition::True
}
}
impl Condition {
pub(crate) fn is_vacuously_true(&self) -> bool {
match self {
Condition::True => true,
Condition::And(xs) => xs.iter().all(Condition::is_vacuously_true),
Condition::Not(inner) => {
matches!(inner.as_ref(), Condition::Or(xs) if xs.is_empty())
}
_ => false,
}
}
}
impl Condition {
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn and(a: Condition, b: Condition) -> Condition {
match (a, b) {
(Condition::True, c) | (c, Condition::True) => c,
(Condition::And(mut va), Condition::And(vb)) => {
va.extend(vb);
Condition::And(va)
}
(Condition::And(mut va), other) => {
va.push(other);
Condition::And(va)
}
(other, Condition::And(vb)) => {
let mut v = vec![other];
v.extend(vb);
Condition::And(v)
}
(a, b) => Condition::And(vec![a, b]),
}
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn or(a: Condition, b: Condition) -> Condition {
match (a, b) {
(Condition::Or(mut va), Condition::Or(vb)) => {
va.extend(vb);
Condition::Or(va)
}
(Condition::Or(mut va), other) => {
va.push(other);
Condition::Or(va)
}
(other, Condition::Or(vb)) => {
let mut v = vec![other];
v.extend(vb);
Condition::Or(v)
}
(a, b) => Condition::Or(vec![a, b]),
}
}
#[allow(clippy::should_implement_trait)]
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn not(inner: Condition) -> Condition {
Condition::Not(Box::new(inner))
}
}
pub trait ConditionExt {
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
fn and(self, other: Condition) -> Condition;
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
fn or(self, other: Condition) -> Condition;
}
impl ConditionExt for Condition {
fn and(self, other: Condition) -> Condition {
Condition::and(self, other)
}
fn or(self, other: Condition) -> Condition {
Condition::or(self, other)
}
}
impl BitAnd for Condition {
type Output = Condition;
fn bitand(self, rhs: Condition) -> Self::Output {
Condition::and(self, rhs)
}
}
impl BitOr for Condition {
type Output = Condition;
fn bitor(self, rhs: Condition) -> Self::Output {
Condition::or(self, rhs)
}
}
impl Not for Condition {
type Output = Condition;
fn not(self) -> Self::Output {
Condition::not(self)
}
}
#[derive(Debug, Clone)]
pub struct Leaf {
pub(crate) column: &'static str,
pub(crate) op: LookupOp,
pub(crate) value: FilterValue,
}
impl Leaf {
pub(crate) fn new(column: &'static str, op: LookupOp, value: FilterValue) -> Self {
Leaf { column, op, value }
}
#[cfg(test)]
pub(crate) fn eq_raw(column: &'static str, value: FilterValue) -> Leaf {
Leaf::new(column, LookupOp::Eq, value)
}
pub fn column(&self) -> &'static str {
self.column
}
pub fn op(&self) -> LookupOp {
self.op
}
pub fn value(&self) -> &FilterValue {
&self.value
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LookupOp {
Eq,
Neq,
Gt,
Gte,
Lt,
Lte,
In,
NotIn,
IsNull,
IsNotNull,
IContains,
IStartsWith,
IEndsWith,
Between,
IExact,
Regex,
IRegex,
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LookupOpSourceClass {
RustEvaluable,
SqlOnly,
}
impl LookupOp {
#[allow(dead_code)]
pub(crate) fn source_class(self) -> LookupOpSourceClass {
match self {
LookupOp::Eq
| LookupOp::Neq
| LookupOp::Gt
| LookupOp::Gte
| LookupOp::Lt
| LookupOp::Lte
| LookupOp::In
| LookupOp::NotIn
| LookupOp::IsNull
| LookupOp::IsNotNull
| LookupOp::Between
| LookupOp::IContains
| LookupOp::IStartsWith
| LookupOp::IEndsWith
| LookupOp::IExact => LookupOpSourceClass::RustEvaluable,
LookupOp::Regex | LookupOp::IRegex => LookupOpSourceClass::SqlOnly,
}
}
}
impl LookupOp {
pub(crate) fn binary_op_token(self) -> Option<&'static str> {
match self {
LookupOp::Eq => Some(" = "),
LookupOp::Neq => Some(" <> "),
LookupOp::Gt => Some(" > "),
LookupOp::Gte => Some(" >= "),
LookupOp::Lt => Some(" < "),
LookupOp::Lte => Some(" <= "),
LookupOp::Regex => Some(" ~ "),
LookupOp::IRegex => Some(" ~* "),
_ => None,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum FilterValue {
String(String),
I16(i16),
I32(i32),
I64(i64),
F32(f32),
F64(f64),
Bool(bool),
Timestamp(time::PrimitiveDateTime),
DateTime(time::OffsetDateTime),
Date(time::Date),
Uuid(uuid::Uuid),
HeerId(HeerId),
RanjId(RanjId),
HeerIdDesc(crate::types::HeerIdDesc),
RanjIdDesc(crate::types::RanjIdDesc),
Null,
List(Vec<FilterValue>),
Pair(Box<FilterValue>, Box<FilterValue>),
Decimal(rust_decimal::Decimal),
Interval(crate::Interval),
#[cfg(feature = "network")]
Inet(std::net::IpAddr),
#[cfg(feature = "network")]
Cidr(crate::CidrAddr),
#[cfg(feature = "network")]
Macaddr(crate::MacAddr),
RangeI32(crate::Range<i32>),
RangeI64(crate::Range<i64>),
RangeDecimal(crate::Range<rust_decimal::Decimal>),
RangeTimestamp(crate::Range<time::PrimitiveDateTime>),
RangeDateTime(crate::Range<time::OffsetDateTime>),
RangeDate(crate::Range<time::Date>),
ArrayString(Vec<String>),
ArrayI32(Vec<i32>),
ArrayI64(Vec<i64>),
ArrayBool(Vec<bool>),
ArrayI16(Vec<i16>),
ArrayF32(Vec<f32>),
ArrayF64(Vec<f64>),
ArrayDateTime(Vec<time::OffsetDateTime>),
ArrayDate(Vec<time::Date>),
ArrayUuid(Vec<uuid::Uuid>),
ArrayDecimal(Vec<rust_decimal::Decimal>),
ArrayHeerId(Vec<crate::types::HeerId>),
ArrayRanjId(Vec<crate::types::RanjId>),
ArrayHeerIdDesc(Vec<crate::types::HeerIdDesc>),
ArrayRanjIdDesc(Vec<crate::types::RanjIdDesc>),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn condition_true_is_default() {
let c = Condition::default();
assert!(matches!(c, Condition::True));
}
#[test]
fn and_flattens_nested_ands() {
let a = Condition::Leaf(Leaf::eq_raw("a", FilterValue::Bool(true)));
let b = Condition::Leaf(Leaf::eq_raw("b", FilterValue::Bool(false)));
let c = Condition::Leaf(Leaf::eq_raw("c", FilterValue::Bool(true)));
let combined = Condition::and(Condition::and(a, b), c);
if let Condition::And(parts) = combined {
assert_eq!(
parts.len(),
3,
"nested And should flatten to 3 leaves, got {parts:?}"
);
} else {
panic!("expected And, got {combined:?}");
}
}
#[test]
fn and_with_true_is_identity() {
let leaf = Condition::Leaf(Leaf::eq_raw("a", FilterValue::Bool(true)));
let combined = Condition::and(Condition::True, leaf.clone());
assert!(matches!(combined, Condition::Leaf(_)));
}
#[test]
fn or_flattens_nested_ors() {
let a = Condition::Leaf(Leaf::eq_raw("a", FilterValue::Bool(true)));
let b = Condition::Leaf(Leaf::eq_raw("b", FilterValue::Bool(false)));
let c = Condition::Leaf(Leaf::eq_raw("c", FilterValue::Bool(true)));
let combined = Condition::or(Condition::or(a, b), c);
if let Condition::Or(parts) = combined {
assert_eq!(parts.len(), 3);
} else {
panic!("expected Or");
}
}
#[test]
fn and_flattens_three_levels_of_nesting() {
let a = Condition::Leaf(Leaf::eq_raw("a", FilterValue::Bool(true)));
let b = Condition::Leaf(Leaf::eq_raw("b", FilterValue::Bool(false)));
let c = Condition::Leaf(Leaf::eq_raw("c", FilterValue::Bool(true)));
let d = Condition::Leaf(Leaf::eq_raw("d", FilterValue::Bool(false)));
let combined = Condition::and(Condition::and(Condition::and(a, b), c), d);
if let Condition::And(parts) = combined {
assert_eq!(parts.len(), 4, "3-deep nesting should flatten to 4 leaves");
} else {
panic!("expected And, got {combined:?}");
}
}
#[test]
fn and_preserves_order_with_rhs_container() {
let x = Condition::Leaf(Leaf::eq_raw("x", FilterValue::Bool(true)));
let a = Condition::Leaf(Leaf::eq_raw("a", FilterValue::Bool(true)));
let b = Condition::Leaf(Leaf::eq_raw("b", FilterValue::Bool(false)));
let combined = Condition::and(x, Condition::And(vec![a, b]));
if let Condition::And(parts) = combined {
assert_eq!(parts.len(), 3);
let names: Vec<&'static str> = parts
.iter()
.filter_map(|p| {
if let Condition::Leaf(l) = p {
Some(l.column)
} else {
None
}
})
.collect();
assert_eq!(names, vec!["x", "a", "b"]);
} else {
panic!("expected And, got {combined:?}");
}
}
#[test]
fn condition_ext_methods_compose_left_to_right() {
let a = Condition::Leaf(Leaf::eq_raw("a", FilterValue::Bool(true)));
let b = Condition::Leaf(Leaf::eq_raw("b", FilterValue::Bool(false)));
let c = Condition::Leaf(Leaf::eq_raw("c", FilterValue::Bool(true)));
let combined = a.and(b).or(c);
assert!(matches!(combined, Condition::Or(parts) if parts.len() == 2));
}
#[test]
fn condition_operators_delegate_to_existing_constructors() {
let a = Condition::Leaf(Leaf::eq_raw("a", FilterValue::Bool(true)));
let b = Condition::Leaf(Leaf::eq_raw("b", FilterValue::Bool(false)));
assert!(matches!(a.clone() & b.clone(), Condition::And(parts) if parts.len() == 2));
assert!(matches!(a.clone() | b.clone(), Condition::Or(parts) if parts.len() == 2));
assert!(matches!(!a, Condition::Not(_)));
}
#[test]
fn or_empty_vec_is_not_auto_replaced() {
let empty = Condition::Or(Vec::new());
assert!(matches!(empty, Condition::Or(ref v) if v.is_empty()));
}
#[test]
fn lookup_op_partitions_cleanly_into_15_lift_2_sql_only() {
let rust_evaluable = [
LookupOp::Eq,
LookupOp::Neq,
LookupOp::Gt,
LookupOp::Gte,
LookupOp::Lt,
LookupOp::Lte,
LookupOp::In,
LookupOp::NotIn,
LookupOp::IsNull,
LookupOp::IsNotNull,
LookupOp::Between,
LookupOp::IContains,
LookupOp::IStartsWith,
LookupOp::IEndsWith,
LookupOp::IExact,
];
assert_eq!(
rust_evaluable.len(),
15,
"expected 15 Rust-evaluable variants per §660"
);
for op in rust_evaluable {
assert_eq!(
op.source_class(),
LookupOpSourceClass::RustEvaluable,
"variant {op:?} must be Rust-evaluable per the §660 split"
);
}
let sql_only = [LookupOp::Regex, LookupOp::IRegex];
assert_eq!(
sql_only.len(),
2,
"expected exactly 2 SQL-only variants — Regex + IRegex per decisions.md row 108"
);
for op in sql_only {
assert_eq!(
op.source_class(),
LookupOpSourceClass::SqlOnly,
"variant {op:?} must be SQL-only — lifting it to sassi requires a Rust regex engine"
);
}
}
#[test]
fn regex_variants_are_sql_only() {
assert_eq!(LookupOp::Regex.source_class(), LookupOpSourceClass::SqlOnly);
assert_eq!(
LookupOp::IRegex.source_class(),
LookupOpSourceClass::SqlOnly
);
}
}