Skip to main content

icydb_core/db/query/predicate/
eval.rs

1use super::{
2    ast::{CompareOp, ComparePredicate, Predicate},
3    coercion::{CoercionSpec, TextOp, compare_eq, compare_order, compare_text},
4};
5use crate::{
6    traits::FieldValues,
7    value::{TextMode, Value},
8};
9use std::cmp::Ordering;
10
11///
12/// FieldPresence
13///
14/// Result of attempting to read a field from a row during predicate
15/// evaluation. This distinguishes between a missing field and a
16/// present field whose value may be `None`.
17///
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub enum FieldPresence {
20    /// Field exists and has a value (including `Value::None`).
21    Present(Value),
22    /// Field is not present on the row.
23    Missing,
24}
25
26///
27/// Row
28///
29/// Abstraction over a row-like value that can expose fields by name.
30/// This decouples predicate evaluation from concrete entity types.
31///
32pub trait Row {
33    fn field(&self, name: &str) -> FieldPresence;
34}
35
36///
37/// Default `Row` implementation for any type that exposes
38/// `FieldValues`, which is the standard runtime entity interface.
39///
40impl<T: FieldValues> Row for T {
41    fn field(&self, name: &str) -> FieldPresence {
42        match self.get_value(name) {
43            Some(value) => FieldPresence::Present(value),
44            None => FieldPresence::Missing,
45        }
46    }
47}
48
49///
50/// Evaluate a predicate against a single row.
51///
52/// This function performs **pure runtime evaluation**:
53/// - no schema access
54/// - no planning or index logic
55/// - no validation
56///
57/// Any unsupported comparison simply evaluates to `false`.
58///
59#[must_use]
60#[expect(clippy::match_like_matches_macro)]
61pub fn eval<R: Row + ?Sized>(row: &R, predicate: &Predicate) -> bool {
62    match predicate {
63        Predicate::True => true,
64        Predicate::False => false,
65
66        Predicate::And(children) => children.iter().all(|child| eval(row, child)),
67        Predicate::Or(children) => children.iter().any(|child| eval(row, child)),
68        Predicate::Not(inner) => !eval(row, inner),
69
70        Predicate::Compare(cmp) => eval_compare(row, cmp),
71
72        Predicate::IsNull { field } => match row.field(field) {
73            FieldPresence::Present(Value::None) => true,
74            _ => false,
75        },
76
77        Predicate::IsMissing { field } => matches!(row.field(field), FieldPresence::Missing),
78
79        Predicate::IsEmpty { field } => match row.field(field) {
80            FieldPresence::Present(value) => is_empty_value(&value),
81            FieldPresence::Missing => false,
82        },
83
84        Predicate::IsNotEmpty { field } => match row.field(field) {
85            FieldPresence::Present(value) => !is_empty_value(&value),
86            FieldPresence::Missing => false,
87        },
88
89        Predicate::MapContainsKey {
90            field,
91            key,
92            coercion,
93        } => match row.field(field) {
94            FieldPresence::Present(value) => map_contains_key(&value, key, coercion),
95            FieldPresence::Missing => false,
96        },
97
98        Predicate::MapContainsValue {
99            field,
100            value,
101            coercion,
102        } => match row.field(field) {
103            FieldPresence::Present(actual) => map_contains_value(&actual, value, coercion),
104            FieldPresence::Missing => false,
105        },
106
107        Predicate::MapContainsEntry {
108            field,
109            key,
110            value,
111            coercion,
112        } => match row.field(field) {
113            FieldPresence::Present(actual) => map_contains_entry(&actual, key, value, coercion),
114            FieldPresence::Missing => false,
115        },
116        Predicate::TextContains { field, value } => match row.field(field) {
117            FieldPresence::Present(actual) => {
118                actual.text_contains(value, TextMode::Cs).unwrap_or(false)
119            }
120            FieldPresence::Missing => false,
121        },
122        Predicate::TextContainsCi { field, value } => match row.field(field) {
123            FieldPresence::Present(actual) => {
124                actual.text_contains(value, TextMode::Ci).unwrap_or(false)
125            }
126            FieldPresence::Missing => false,
127        },
128    }
129}
130
131///
132/// Evaluate a single comparison predicate against a row.
133///
134/// Returns `false` if:
135/// - the field is missing
136/// - the comparison is not defined under the given coercion
137///
138fn eval_compare<R: Row + ?Sized>(row: &R, cmp: &ComparePredicate) -> bool {
139    let ComparePredicate {
140        field,
141        op,
142        value,
143        coercion,
144    } = cmp;
145
146    let FieldPresence::Present(actual) = row.field(field) else {
147        return false;
148    };
149
150    match op {
151        CompareOp::Eq => compare_eq(&actual, value, coercion).unwrap_or(false),
152        CompareOp::Ne => compare_eq(&actual, value, coercion).is_some_and(|v| !v),
153
154        CompareOp::Lt => compare_order(&actual, value, coercion).is_some_and(Ordering::is_lt),
155        CompareOp::Lte => compare_order(&actual, value, coercion).is_some_and(Ordering::is_le),
156        CompareOp::Gt => compare_order(&actual, value, coercion).is_some_and(Ordering::is_gt),
157        CompareOp::Gte => compare_order(&actual, value, coercion).is_some_and(Ordering::is_ge),
158
159        CompareOp::In => in_list(&actual, value, coercion),
160        CompareOp::NotIn => !in_list(&actual, value, coercion),
161
162        CompareOp::AnyIn => any_in(&actual, value, coercion),
163        CompareOp::AllIn => all_in(&actual, value, coercion),
164
165        CompareOp::Contains => contains(&actual, value, coercion),
166
167        CompareOp::StartsWith => {
168            compare_text(&actual, value, coercion, TextOp::StartsWith).unwrap_or(false)
169        }
170        CompareOp::EndsWith => {
171            compare_text(&actual, value, coercion, TextOp::EndsWith).unwrap_or(false)
172        }
173    }
174}
175
176///
177/// Determine whether a value is considered empty for `IsEmpty` checks.
178///
179const fn is_empty_value(value: &Value) -> bool {
180    match value {
181        Value::Text(text) => text.is_empty(),
182        Value::List(items) => items.is_empty(),
183        _ => false,
184    }
185}
186
187///
188/// Check whether a value equals any element in a list.
189///
190fn in_list(actual: &Value, list: &Value, coercion: &CoercionSpec) -> bool {
191    let Value::List(items) = list else {
192        return false;
193    };
194
195    items
196        .iter()
197        .any(|item| compare_eq(actual, item, coercion).unwrap_or(false))
198}
199
200///
201/// Check whether any element of `actual` exists in `list`.
202///
203fn any_in(actual: &Value, list: &Value, coercion: &CoercionSpec) -> bool {
204    let Value::List(actual_items) = actual else {
205        return false;
206    };
207    let Value::List(needles) = list else {
208        return false;
209    };
210
211    actual_items.iter().any(|item| {
212        needles
213            .iter()
214            .any(|needle| compare_eq(item, needle, coercion).unwrap_or(false))
215    })
216}
217
218///
219/// Check whether all elements of `actual` exist in `list`.
220///
221fn all_in(actual: &Value, list: &Value, coercion: &CoercionSpec) -> bool {
222    let Value::List(actual_items) = actual else {
223        return false;
224    };
225    let Value::List(needles) = list else {
226        return false;
227    };
228
229    actual_items.iter().all(|item| {
230        needles
231            .iter()
232            .any(|needle| compare_eq(item, needle, coercion).unwrap_or(false))
233    })
234}
235
236///
237/// Check whether a value contains another value.
238///
239/// For textual values, this defers to text comparison semantics.
240/// For collections, this performs element-wise equality checks.
241///
242fn contains(actual: &Value, needle: &Value, coercion: &CoercionSpec) -> bool {
243    if let Some(res) = compare_text(actual, needle, coercion, TextOp::Contains) {
244        return res;
245    }
246
247    let Value::List(items) = actual else {
248        return false;
249    };
250
251    items
252        .iter()
253        .any(|item| compare_eq(item, needle, coercion).unwrap_or(false))
254}
255
256///
257/// Check whether a map-like value contains a given key.
258///
259/// Maps are represented as lists of 2-element lists `[key, value]`.
260///
261fn map_contains_key(map: &Value, key: &Value, coercion: &CoercionSpec) -> bool {
262    let Value::List(entries) = map else {
263        return false;
264    };
265
266    for entry in entries {
267        let Value::List(pair) = entry else {
268            return false;
269        };
270        if pair.len() != 2 {
271            return false;
272        }
273        if compare_eq(&pair[0], key, coercion).unwrap_or(false) {
274            return true;
275        }
276    }
277
278    false
279}
280
281///
282/// Check whether a map-like value contains a given value.
283///
284fn map_contains_value(map: &Value, value: &Value, coercion: &CoercionSpec) -> bool {
285    let Value::List(entries) = map else {
286        return false;
287    };
288
289    for entry in entries {
290        let Value::List(pair) = entry else {
291            return false;
292        };
293        if pair.len() != 2 {
294            return false;
295        }
296        if compare_eq(&pair[1], value, coercion).unwrap_or(false) {
297            return true;
298        }
299    }
300
301    false
302}
303
304///
305/// Check whether a map-like value contains an exact key/value pair.
306///
307fn map_contains_entry(map: &Value, key: &Value, value: &Value, coercion: &CoercionSpec) -> bool {
308    let Value::List(entries) = map else {
309        return false;
310    };
311
312    for entry in entries {
313        let Value::List(pair) = entry else {
314            return false;
315        };
316        if pair.len() != 2 {
317            return false;
318        }
319
320        let key_match = compare_eq(&pair[0], key, coercion).unwrap_or(false);
321        let value_match = compare_eq(&pair[1], value, coercion).unwrap_or(false);
322
323        if key_match && value_match {
324            return true;
325        }
326    }
327
328    false
329}