icydb_core/db/query/predicate/
eval.rs1use 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#[derive(Clone, Debug, Eq, PartialEq)]
19pub enum FieldPresence {
20 Present(Value),
22 Missing,
24}
25
26pub trait Row {
33 fn field(&self, name: &str) -> FieldPresence;
34}
35
36impl<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#[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
131fn 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
176const 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
187fn 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
200fn 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
218fn 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
236fn 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
256fn 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
281fn 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
304fn 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}