1use crate::{
7 db::predicate::coercion::{CoercionId, CoercionSpec},
8 value::Value,
9};
10use std::ops::{BitAnd, BitOr};
11
12#[cfg_attr(doc, doc = "Predicate")]
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum Predicate {
15 True,
16 False,
17 And(Vec<Self>),
18 Or(Vec<Self>),
19 Not(Box<Self>),
20 Compare(ComparePredicate),
21 CompareFields(CompareFieldsPredicate),
22 IsNull { field: String },
23 IsNotNull { field: String },
24 IsMissing { field: String },
25 IsEmpty { field: String },
26 IsNotEmpty { field: String },
27 TextContains { field: String, value: Value },
28 TextContainsCi { field: String, value: Value },
29}
30
31impl Predicate {
32 #[must_use]
34 pub const fn and(preds: Vec<Self>) -> Self {
35 Self::And(preds)
36 }
37
38 #[must_use]
40 pub const fn or(preds: Vec<Self>) -> Self {
41 Self::Or(preds)
42 }
43
44 #[must_use]
46 #[expect(clippy::should_implement_trait)]
47 pub fn not(pred: Self) -> Self {
48 Self::Not(Box::new(pred))
49 }
50
51 #[must_use]
53 pub fn eq(field: String, value: Value) -> Self {
54 Self::Compare(ComparePredicate::eq(field, value))
55 }
56
57 #[must_use]
59 pub fn ne(field: String, value: Value) -> Self {
60 Self::Compare(ComparePredicate::ne(field, value))
61 }
62
63 #[must_use]
65 pub fn lt(field: String, value: Value) -> Self {
66 Self::Compare(ComparePredicate::lt(field, value))
67 }
68
69 #[must_use]
71 pub fn lte(field: String, value: Value) -> Self {
72 Self::Compare(ComparePredicate::lte(field, value))
73 }
74
75 #[must_use]
77 pub fn gt(field: String, value: Value) -> Self {
78 Self::Compare(ComparePredicate::gt(field, value))
79 }
80
81 #[must_use]
83 pub fn gte(field: String, value: Value) -> Self {
84 Self::Compare(ComparePredicate::gte(field, value))
85 }
86
87 #[must_use]
89 pub fn in_(field: String, values: Vec<Value>) -> Self {
90 Self::Compare(ComparePredicate::in_(field, values))
91 }
92
93 #[must_use]
95 pub fn not_in(field: String, values: Vec<Value>) -> Self {
96 Self::Compare(ComparePredicate::not_in(field, values))
97 }
98
99 #[must_use]
101 pub const fn is_not_null(field: String) -> Self {
102 Self::IsNotNull { field }
103 }
104
105 #[must_use]
107 pub fn between(field: String, lower: Value, upper: Value) -> Self {
108 Self::And(vec![
109 Self::gte(field.clone(), lower),
110 Self::lte(field, upper),
111 ])
112 }
113
114 #[must_use]
116 pub fn not_between(field: String, lower: Value, upper: Value) -> Self {
117 Self::Or(vec![Self::lt(field.clone(), lower), Self::gt(field, upper)])
118 }
119}
120
121impl BitAnd for Predicate {
122 type Output = Self;
123
124 fn bitand(self, rhs: Self) -> Self::Output {
125 Self::And(vec![self, rhs])
126 }
127}
128
129impl BitAnd for &Predicate {
130 type Output = Predicate;
131
132 fn bitand(self, rhs: Self) -> Self::Output {
133 Predicate::And(vec![self.clone(), rhs.clone()])
134 }
135}
136
137impl BitOr for Predicate {
138 type Output = Self;
139
140 fn bitor(self, rhs: Self) -> Self::Output {
141 Self::Or(vec![self, rhs])
142 }
143}
144
145impl BitOr for &Predicate {
146 type Output = Predicate;
147
148 fn bitor(self, rhs: Self) -> Self::Output {
149 Predicate::Or(vec![self.clone(), rhs.clone()])
150 }
151}
152
153#[cfg_attr(doc, doc = "CompareOp")]
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155#[repr(u8)]
156pub enum CompareOp {
157 Eq = 0x01,
158 Ne = 0x02,
159 Lt = 0x03,
160 Lte = 0x04,
161 Gt = 0x05,
162 Gte = 0x06,
163 In = 0x07,
164 NotIn = 0x08,
165 Contains = 0x09,
166 StartsWith = 0x0a,
167 EndsWith = 0x0b,
168}
169
170impl CompareOp {
171 #[must_use]
173 pub const fn tag(self) -> u8 {
174 self as u8
175 }
176
177 #[must_use]
179 pub const fn is_equality_family(self) -> bool {
180 matches!(self, Self::Eq | Self::Ne)
181 }
182
183 #[must_use]
185 pub const fn is_ordering_family(self) -> bool {
186 matches!(self, Self::Lt | Self::Lte | Self::Gt | Self::Gte)
187 }
188
189 #[must_use]
191 pub const fn is_membership_family(self) -> bool {
192 matches!(self, Self::In | Self::NotIn)
193 }
194
195 #[must_use]
197 pub const fn is_contains_family(self) -> bool {
198 matches!(self, Self::Contains)
199 }
200
201 #[must_use]
203 pub const fn is_text_pattern_family(self) -> bool {
204 matches!(self, Self::StartsWith | Self::EndsWith)
205 }
206
207 #[must_use]
209 pub const fn supports_field_compare(self) -> bool {
210 self.is_equality_family() || self.is_ordering_family()
211 }
212
213 #[must_use]
216 pub const fn lower_bound_inclusive(self) -> Option<bool> {
217 match self {
218 Self::Gt => Some(false),
219 Self::Gte => Some(true),
220 Self::Eq
221 | Self::Ne
222 | Self::Lt
223 | Self::Lte
224 | Self::In
225 | Self::NotIn
226 | Self::Contains
227 | Self::StartsWith
228 | Self::EndsWith => None,
229 }
230 }
231
232 #[must_use]
235 pub const fn upper_bound_inclusive(self) -> Option<bool> {
236 match self {
237 Self::Lt => Some(false),
238 Self::Lte => Some(true),
239 Self::Eq
240 | Self::Ne
241 | Self::Gt
242 | Self::Gte
243 | Self::In
244 | Self::NotIn
245 | Self::Contains
246 | Self::StartsWith
247 | Self::EndsWith => None,
248 }
249 }
250
251 #[must_use]
253 pub const fn flipped(self) -> Self {
254 match self {
255 Self::Eq => Self::Eq,
256 Self::Ne => Self::Ne,
257 Self::Lt => Self::Gt,
258 Self::Lte => Self::Gte,
259 Self::Gt => Self::Lt,
260 Self::Gte => Self::Lte,
261 Self::In => Self::In,
262 Self::NotIn => Self::NotIn,
263 Self::Contains => Self::Contains,
264 Self::StartsWith => Self::StartsWith,
265 Self::EndsWith => Self::EndsWith,
266 }
267 }
268}
269
270#[cfg_attr(doc, doc = "ComparePredicate")]
271#[derive(Clone, Debug, Eq, PartialEq)]
272pub struct ComparePredicate {
273 pub(crate) field: String,
274 pub(crate) op: CompareOp,
275 pub(crate) value: Value,
276 pub(crate) coercion: CoercionSpec,
277}
278
279impl ComparePredicate {
280 fn new(field: String, op: CompareOp, value: Value) -> Self {
281 Self {
282 field,
283 op,
284 value,
285 coercion: CoercionSpec::default(),
286 }
287 }
288
289 #[must_use]
297 pub fn with_coercion(
298 field: impl Into<String>,
299 op: CompareOp,
300 value: Value,
301 coercion: CoercionId,
302 ) -> Self {
303 Self {
304 field: field.into(),
305 op,
306 value,
307 coercion: CoercionSpec::new(coercion),
308 }
309 }
310
311 #[must_use]
313 pub fn eq(field: String, value: Value) -> Self {
314 Self::new(field, CompareOp::Eq, value)
315 }
316
317 #[must_use]
319 pub fn ne(field: String, value: Value) -> Self {
320 Self::new(field, CompareOp::Ne, value)
321 }
322
323 #[must_use]
325 pub fn lt(field: String, value: Value) -> Self {
326 Self::new(field, CompareOp::Lt, value)
327 }
328
329 #[must_use]
331 pub fn lte(field: String, value: Value) -> Self {
332 Self::new(field, CompareOp::Lte, value)
333 }
334
335 #[must_use]
337 pub fn gt(field: String, value: Value) -> Self {
338 Self::new(field, CompareOp::Gt, value)
339 }
340
341 #[must_use]
343 pub fn gte(field: String, value: Value) -> Self {
344 Self::new(field, CompareOp::Gte, value)
345 }
346
347 #[must_use]
349 pub fn in_(field: String, values: Vec<Value>) -> Self {
350 Self::new(field, CompareOp::In, Value::List(values))
351 }
352
353 #[must_use]
355 pub fn not_in(field: String, values: Vec<Value>) -> Self {
356 Self::new(field, CompareOp::NotIn, Value::List(values))
357 }
358
359 #[must_use]
361 pub fn field(&self) -> &str {
362 &self.field
363 }
364
365 #[must_use]
367 pub const fn op(&self) -> CompareOp {
368 self.op
369 }
370
371 #[must_use]
373 pub const fn value(&self) -> &Value {
374 &self.value
375 }
376
377 #[must_use]
379 pub const fn coercion(&self) -> &CoercionSpec {
380 &self.coercion
381 }
382}
383
384#[derive(Clone, Debug, Eq, PartialEq)]
393pub struct CompareFieldsPredicate {
394 pub(crate) left_field: String,
395 pub(crate) op: CompareOp,
396 pub(crate) right_field: String,
397 pub(crate) coercion: CoercionSpec,
398}
399
400impl CompareFieldsPredicate {
401 fn canonicalize_symmetric_fields(
402 op: CompareOp,
403 left_field: String,
404 right_field: String,
405 ) -> (String, String) {
406 if op.is_equality_family() && left_field < right_field {
407 (right_field, left_field)
408 } else {
409 (left_field, right_field)
410 }
411 }
412
413 #[must_use]
421 pub fn with_coercion(
422 left_field: impl Into<String>,
423 op: CompareOp,
424 right_field: impl Into<String>,
425 coercion: CoercionId,
426 ) -> Self {
427 let (left_field, right_field) =
428 Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
429
430 Self {
431 left_field,
432 op,
433 right_field,
434 coercion: CoercionSpec::new(coercion),
435 }
436 }
437
438 #[must_use]
440 pub fn left_field(&self) -> &str {
441 &self.left_field
442 }
443
444 #[must_use]
446 pub const fn op(&self) -> CompareOp {
447 self.op
448 }
449
450 #[must_use]
452 pub fn right_field(&self) -> &str {
453 &self.right_field
454 }
455
456 #[must_use]
458 pub const fn coercion(&self) -> &CoercionSpec {
459 &self.coercion
460 }
461}
462
463#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn compare_predicate_builders_preserve_operator_shape() {
473 assert_eq!(
474 Predicate::gt("age".to_string(), Value::Nat64(7)),
475 Predicate::Compare(ComparePredicate::gt("age".to_string(), Value::Nat64(7))),
476 );
477 }
478}