Skip to main content

uqa_core/
predicate.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Filter predicates evaluated against [`Value`] field contents.
8//!
9//! `Predicate` is an enum (rather than a trait) so callers can pattern
10//! match on the special `IsNull` / `IsNotNull` cases that need to see the
11//! `None` field value, while everything else short-circuits on missing
12//! fields. `Like` / `ILike` regex variants land alongside the SQL
13//! compiler.
14
15use std::collections::BTreeSet;
16
17use crate::types::Value;
18
19#[derive(Debug, Clone, PartialEq)]
20pub enum Predicate {
21    Equals(Value),
22    NotEquals(Value),
23    GreaterThan(Value),
24    GreaterThanOrEqual(Value),
25    LessThan(Value),
26    LessThanOrEqual(Value),
27    InSet(BTreeSet<Value>),
28    Between { low: Value, high: Value },
29    IsNull,
30    IsNotNull,
31}
32
33impl Predicate {
34    /// Returns `true` if the predicate must inspect `None` field values
35    /// (the `IsNull` / `IsNotNull` case). Filter operators short-circuit
36    /// missing fields for every other variant.
37    pub fn is_null_aware(&self) -> bool {
38        matches!(self, Predicate::IsNull | Predicate::IsNotNull)
39    }
40
41    /// Evaluate against an optional field value. The two null-aware
42    /// variants treat both an absent field (`None`) and an explicit
43    /// `Value::Null` as null; the rest reject either form.
44    pub fn evaluate(&self, value: Option<&Value>) -> bool {
45        let is_null = matches!(value, None | Some(Value::Null));
46        match self {
47            Predicate::IsNull => is_null,
48            Predicate::IsNotNull => !is_null,
49            Predicate::Equals(target) => !is_null && value.is_some_and(|v| values_equal(v, target)),
50            Predicate::NotEquals(target) => {
51                !is_null && value.is_some_and(|v| !values_equal(v, target))
52            }
53            Predicate::GreaterThan(target) => {
54                !is_null && value.is_some_and(|v| compare_values(v, target).is_gt())
55            }
56            Predicate::GreaterThanOrEqual(target) => {
57                !is_null && value.is_some_and(|v| compare_values(v, target).is_ge())
58            }
59            Predicate::LessThan(target) => {
60                !is_null && value.is_some_and(|v| compare_values(v, target).is_lt())
61            }
62            Predicate::LessThanOrEqual(target) => {
63                !is_null && value.is_some_and(|v| compare_values(v, target).is_le())
64            }
65            Predicate::InSet(values) => {
66                !is_null
67                    && value.is_some_and(|v| values.iter().any(|target| values_equal(v, target)))
68            }
69            Predicate::Between { low, high } => {
70                !is_null
71                    && value.is_some_and(|v| {
72                        compare_values(v, low).is_ge() && compare_values(v, high).is_le()
73                    })
74            }
75        }
76    }
77}
78
79fn values_equal(a: &Value, b: &Value) -> bool {
80    match (a, b) {
81        (Value::Temporal(x), Value::Temporal(y)) => x == y,
82        (Value::Temporal(x), Value::Str(y)) | (Value::Str(y), Value::Temporal(x)) => {
83            x.parse_same_kind(y).is_some_and(|parsed| parsed == *x)
84        }
85        (Value::FixedChar(x) | Value::Str(x), Value::FixedChar(y))
86        | (Value::FixedChar(x), Value::Str(y)) => {
87            x.trim_end_matches(' ') == y.trim_end_matches(' ')
88        }
89        _ => a == b,
90    }
91}
92
93fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
94    match (a, b) {
95        (Value::Temporal(x), Value::Temporal(y)) => x.cmp(y),
96        (Value::Temporal(x), Value::Str(y)) => x
97            .parse_same_kind(y)
98            .map_or_else(|| a.cmp(b), |parsed| x.cmp(&parsed)),
99        (Value::Str(x), Value::Temporal(y)) => y
100            .parse_same_kind(x)
101            .map_or_else(|| a.cmp(b), |parsed| parsed.cmp(y)),
102        (Value::FixedChar(x) | Value::Str(x), Value::FixedChar(y))
103        | (Value::FixedChar(x), Value::Str(y)) => {
104            x.trim_end_matches(' ').cmp(y.trim_end_matches(' '))
105        }
106        _ => a.cmp(b),
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    fn iv(n: i64) -> Value {
115        Value::Int(n)
116    }
117
118    #[test]
119    fn equals_matches_exact() {
120        let p = Predicate::Equals(iv(42));
121        assert!(p.evaluate(Some(&iv(42))));
122        assert!(!p.evaluate(Some(&iv(43))));
123        assert!(!p.evaluate(None));
124    }
125
126    #[test]
127    fn comparators_respect_ordering() {
128        let p = Predicate::GreaterThan(iv(10));
129        assert!(p.evaluate(Some(&iv(11))));
130        assert!(!p.evaluate(Some(&iv(10))));
131        assert!(!p.evaluate(Some(&iv(9))));
132    }
133
134    #[test]
135    fn between_inclusive_bounds() {
136        let p = Predicate::Between {
137            low: iv(1),
138            high: iv(3),
139        };
140        assert!(p.evaluate(Some(&iv(1))));
141        assert!(p.evaluate(Some(&iv(3))));
142        assert!(!p.evaluate(Some(&iv(0))));
143        assert!(!p.evaluate(Some(&iv(4))));
144    }
145
146    #[test]
147    fn in_set_membership() {
148        let s: BTreeSet<Value> = [iv(1), iv(2), iv(5)].into_iter().collect();
149        let p = Predicate::InSet(s);
150        assert!(p.evaluate(Some(&iv(2))));
151        assert!(!p.evaluate(Some(&iv(3))));
152    }
153
154    #[test]
155    fn null_aware_predicates_see_none() {
156        assert!(Predicate::IsNull.evaluate(None));
157        assert!(!Predicate::IsNull.evaluate(Some(&iv(0))));
158        assert!(!Predicate::IsNotNull.evaluate(None));
159        assert!(Predicate::IsNotNull.evaluate(Some(&iv(0))));
160    }
161
162    #[test]
163    fn fixed_character_predicates_ignore_blank_padding() {
164        let fixed = Value::FixedChar("x   ".into());
165        assert!(Predicate::Equals(Value::Str("x".into())).evaluate(Some(&fixed)));
166        assert!(Predicate::Equals(Value::Str("x  ".into())).evaluate(Some(&fixed)));
167        assert!(Predicate::LessThan(Value::Str("y".into())).evaluate(Some(&fixed)));
168    }
169}