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 eq_fields(left_field: String, right_field: String) -> Self {
90 Self::CompareFields(CompareFieldsPredicate::eq(left_field, right_field))
91 }
92
93 #[must_use]
95 pub fn ne_fields(left_field: String, right_field: String) -> Self {
96 Self::CompareFields(CompareFieldsPredicate::ne(left_field, right_field))
97 }
98
99 #[must_use]
101 pub fn lt_fields(left_field: String, right_field: String) -> Self {
102 Self::CompareFields(CompareFieldsPredicate::with_coercion(
103 left_field,
104 CompareOp::Lt,
105 right_field,
106 CoercionId::NumericWiden,
107 ))
108 }
109
110 #[must_use]
112 pub fn lte_fields(left_field: String, right_field: String) -> Self {
113 Self::CompareFields(CompareFieldsPredicate::with_coercion(
114 left_field,
115 CompareOp::Lte,
116 right_field,
117 CoercionId::NumericWiden,
118 ))
119 }
120
121 #[must_use]
123 pub fn gt_fields(left_field: String, right_field: String) -> Self {
124 Self::CompareFields(CompareFieldsPredicate::with_coercion(
125 left_field,
126 CompareOp::Gt,
127 right_field,
128 CoercionId::NumericWiden,
129 ))
130 }
131
132 #[must_use]
134 pub fn gte_fields(left_field: String, right_field: String) -> Self {
135 Self::CompareFields(CompareFieldsPredicate::with_coercion(
136 left_field,
137 CompareOp::Gte,
138 right_field,
139 CoercionId::NumericWiden,
140 ))
141 }
142
143 #[must_use]
145 pub fn in_(field: String, values: Vec<Value>) -> Self {
146 Self::Compare(ComparePredicate::in_(field, values))
147 }
148
149 #[must_use]
151 pub fn not_in(field: String, values: Vec<Value>) -> Self {
152 Self::Compare(ComparePredicate::not_in(field, values))
153 }
154
155 #[must_use]
157 pub const fn is_not_null(field: String) -> Self {
158 Self::IsNotNull { field }
159 }
160
161 #[must_use]
163 pub fn between(field: String, lower: Value, upper: Value) -> Self {
164 Self::And(vec![
165 Self::gte(field.clone(), lower),
166 Self::lte(field, upper),
167 ])
168 }
169
170 #[must_use]
172 pub fn not_between(field: String, lower: Value, upper: Value) -> Self {
173 Self::Or(vec![Self::lt(field.clone(), lower), Self::gt(field, upper)])
174 }
175}
176
177impl BitAnd for Predicate {
178 type Output = Self;
179
180 fn bitand(self, rhs: Self) -> Self::Output {
181 Self::And(vec![self, rhs])
182 }
183}
184
185impl BitAnd for &Predicate {
186 type Output = Predicate;
187
188 fn bitand(self, rhs: Self) -> Self::Output {
189 Predicate::And(vec![self.clone(), rhs.clone()])
190 }
191}
192
193impl BitOr for Predicate {
194 type Output = Self;
195
196 fn bitor(self, rhs: Self) -> Self::Output {
197 Self::Or(vec![self, rhs])
198 }
199}
200
201impl BitOr for &Predicate {
202 type Output = Predicate;
203
204 fn bitor(self, rhs: Self) -> Self::Output {
205 Predicate::Or(vec![self.clone(), rhs.clone()])
206 }
207}
208
209#[cfg_attr(doc, doc = "CompareOp")]
210#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211#[repr(u8)]
212pub enum CompareOp {
213 Eq = 0x01,
214 Ne = 0x02,
215 Lt = 0x03,
216 Lte = 0x04,
217 Gt = 0x05,
218 Gte = 0x06,
219 In = 0x07,
220 NotIn = 0x08,
221 Contains = 0x09,
222 StartsWith = 0x0a,
223 EndsWith = 0x0b,
224}
225
226impl CompareOp {
227 #[must_use]
229 pub const fn tag(self) -> u8 {
230 self as u8
231 }
232
233 #[must_use]
235 pub const fn is_equality_family(self) -> bool {
236 matches!(self, Self::Eq | Self::Ne)
237 }
238
239 #[must_use]
241 pub const fn is_ordering_family(self) -> bool {
242 matches!(self, Self::Lt | Self::Lte | Self::Gt | Self::Gte)
243 }
244
245 #[must_use]
247 pub const fn is_membership_family(self) -> bool {
248 matches!(self, Self::In | Self::NotIn)
249 }
250
251 #[must_use]
253 pub const fn is_contains_family(self) -> bool {
254 matches!(self, Self::Contains)
255 }
256
257 #[must_use]
259 pub const fn is_text_pattern_family(self) -> bool {
260 matches!(self, Self::StartsWith | Self::EndsWith)
261 }
262
263 #[must_use]
265 pub const fn supports_field_compare(self) -> bool {
266 self.is_equality_family() || self.is_ordering_family()
267 }
268
269 #[must_use]
272 pub const fn lower_bound_inclusive(self) -> Option<bool> {
273 match self {
274 Self::Gt => Some(false),
275 Self::Gte => Some(true),
276 Self::Eq
277 | Self::Ne
278 | Self::Lt
279 | Self::Lte
280 | Self::In
281 | Self::NotIn
282 | Self::Contains
283 | Self::StartsWith
284 | Self::EndsWith => None,
285 }
286 }
287
288 #[must_use]
291 pub const fn upper_bound_inclusive(self) -> Option<bool> {
292 match self {
293 Self::Lt => Some(false),
294 Self::Lte => Some(true),
295 Self::Eq
296 | Self::Ne
297 | Self::Gt
298 | Self::Gte
299 | Self::In
300 | Self::NotIn
301 | Self::Contains
302 | Self::StartsWith
303 | Self::EndsWith => None,
304 }
305 }
306
307 #[must_use]
309 pub const fn flipped(self) -> Self {
310 match self {
311 Self::Eq => Self::Eq,
312 Self::Ne => Self::Ne,
313 Self::Lt => Self::Gt,
314 Self::Lte => Self::Gte,
315 Self::Gt => Self::Lt,
316 Self::Gte => Self::Lte,
317 Self::In => Self::In,
318 Self::NotIn => Self::NotIn,
319 Self::Contains => Self::Contains,
320 Self::StartsWith => Self::StartsWith,
321 Self::EndsWith => Self::EndsWith,
322 }
323 }
324}
325
326#[cfg_attr(doc, doc = "ComparePredicate")]
327#[derive(Clone, Debug, Eq, PartialEq)]
328pub struct ComparePredicate {
329 pub(crate) field: String,
330 pub(crate) op: CompareOp,
331 pub(crate) value: Value,
332 pub(crate) coercion: CoercionSpec,
333}
334
335impl ComparePredicate {
336 fn new(field: String, op: CompareOp, value: Value) -> Self {
337 Self {
338 field,
339 op,
340 value,
341 coercion: CoercionSpec::default(),
342 }
343 }
344
345 #[must_use]
353 pub fn with_coercion(
354 field: impl Into<String>,
355 op: CompareOp,
356 value: Value,
357 coercion: CoercionId,
358 ) -> Self {
359 Self {
360 field: field.into(),
361 op,
362 value,
363 coercion: CoercionSpec::new(coercion),
364 }
365 }
366
367 #[must_use]
369 pub fn eq(field: String, value: Value) -> Self {
370 Self::new(field, CompareOp::Eq, value)
371 }
372
373 #[must_use]
375 pub fn ne(field: String, value: Value) -> Self {
376 Self::new(field, CompareOp::Ne, value)
377 }
378
379 #[must_use]
381 pub fn lt(field: String, value: Value) -> Self {
382 Self::new(field, CompareOp::Lt, value)
383 }
384
385 #[must_use]
387 pub fn lte(field: String, value: Value) -> Self {
388 Self::new(field, CompareOp::Lte, value)
389 }
390
391 #[must_use]
393 pub fn gt(field: String, value: Value) -> Self {
394 Self::new(field, CompareOp::Gt, value)
395 }
396
397 #[must_use]
399 pub fn gte(field: String, value: Value) -> Self {
400 Self::new(field, CompareOp::Gte, value)
401 }
402
403 #[must_use]
405 pub fn in_(field: String, values: Vec<Value>) -> Self {
406 Self::new(field, CompareOp::In, Value::List(values))
407 }
408
409 #[must_use]
411 pub fn not_in(field: String, values: Vec<Value>) -> Self {
412 Self::new(field, CompareOp::NotIn, Value::List(values))
413 }
414
415 #[must_use]
417 pub fn field(&self) -> &str {
418 &self.field
419 }
420
421 #[must_use]
423 pub const fn op(&self) -> CompareOp {
424 self.op
425 }
426
427 #[must_use]
429 pub const fn value(&self) -> &Value {
430 &self.value
431 }
432
433 #[must_use]
435 pub const fn coercion(&self) -> &CoercionSpec {
436 &self.coercion
437 }
438}
439
440#[derive(Clone, Debug, Eq, PartialEq)]
449pub struct CompareFieldsPredicate {
450 pub(crate) left_field: String,
451 pub(crate) op: CompareOp,
452 pub(crate) right_field: String,
453 pub(crate) coercion: CoercionSpec,
454}
455
456impl CompareFieldsPredicate {
457 fn canonicalize_symmetric_fields(
458 op: CompareOp,
459 left_field: String,
460 right_field: String,
461 ) -> (String, String) {
462 if op.is_equality_family() && left_field < right_field {
463 (right_field, left_field)
464 } else {
465 (left_field, right_field)
466 }
467 }
468
469 fn new(left_field: String, op: CompareOp, right_field: String) -> Self {
470 let (left_field, right_field) =
471 Self::canonicalize_symmetric_fields(op, left_field, right_field);
472
473 Self {
474 left_field,
475 op,
476 right_field,
477 coercion: CoercionSpec::default(),
478 }
479 }
480
481 #[must_use]
489 pub fn with_coercion(
490 left_field: impl Into<String>,
491 op: CompareOp,
492 right_field: impl Into<String>,
493 coercion: CoercionId,
494 ) -> Self {
495 let (left_field, right_field) =
496 Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
497
498 Self {
499 left_field,
500 op,
501 right_field,
502 coercion: CoercionSpec::new(coercion),
503 }
504 }
505
506 #[must_use]
508 pub fn eq(left_field: String, right_field: String) -> Self {
509 Self::new(left_field, CompareOp::Eq, right_field)
510 }
511
512 #[must_use]
514 pub fn ne(left_field: String, right_field: String) -> Self {
515 Self::new(left_field, CompareOp::Ne, right_field)
516 }
517
518 #[must_use]
520 pub fn lt(left_field: String, right_field: String) -> Self {
521 Self::new(left_field, CompareOp::Lt, right_field)
522 }
523
524 #[must_use]
526 pub fn lte(left_field: String, right_field: String) -> Self {
527 Self::new(left_field, CompareOp::Lte, right_field)
528 }
529
530 #[must_use]
532 pub fn gt(left_field: String, right_field: String) -> Self {
533 Self::new(left_field, CompareOp::Gt, right_field)
534 }
535
536 #[must_use]
538 pub fn gte(left_field: String, right_field: String) -> Self {
539 Self::new(left_field, CompareOp::Gte, right_field)
540 }
541
542 #[must_use]
544 pub fn left_field(&self) -> &str {
545 &self.left_field
546 }
547
548 #[must_use]
550 pub const fn op(&self) -> CompareOp {
551 self.op
552 }
553
554 #[must_use]
556 pub fn right_field(&self) -> &str {
557 &self.right_field
558 }
559
560 #[must_use]
562 pub const fn coercion(&self) -> &CoercionSpec {
563 &self.coercion
564 }
565}
566
567#[cfg_attr(
568 doc,
569 doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
570)]
571#[derive(Clone, Debug, Eq, PartialEq)]
572pub enum UnsupportedQueryFeature {
573 MapPredicate { field: String },
574}
575
576#[cfg(test)]
581mod tests {
582 use super::*;
583
584 #[test]
585 fn compare_predicate_builders_preserve_operator_shape() {
586 assert_eq!(
587 Predicate::gt("age".to_string(), Value::Nat64(7)),
588 Predicate::Compare(ComparePredicate::gt("age".to_string(), Value::Nat64(7))),
589 );
590 }
591}