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