1use crate::{
7 db::predicate::coercion::{CoercionId, CoercionSpec},
8 value::Value,
9};
10use std::ops::{BitAnd, BitOr};
11use thiserror::Error as ThisError;
12
13#[cfg_attr(doc, doc = "Predicate")]
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub enum Predicate {
16 True,
17 False,
18 And(Vec<Self>),
19 Or(Vec<Self>),
20 Not(Box<Self>),
21 Compare(ComparePredicate),
22 CompareFields(CompareFieldsPredicate),
23 IsNull { field: String },
24 IsNotNull { field: String },
25 IsMissing { field: String },
26 IsEmpty { field: String },
27 IsNotEmpty { field: String },
28 TextContains { field: String, value: Value },
29 TextContainsCi { field: String, value: Value },
30}
31
32impl Predicate {
33 #[must_use]
35 pub const fn and(preds: Vec<Self>) -> Self {
36 Self::And(preds)
37 }
38
39 #[must_use]
41 pub const fn or(preds: Vec<Self>) -> Self {
42 Self::Or(preds)
43 }
44
45 #[must_use]
47 #[expect(clippy::should_implement_trait)]
48 pub fn not(pred: Self) -> Self {
49 Self::Not(Box::new(pred))
50 }
51
52 #[must_use]
54 pub fn eq(field: String, value: Value) -> Self {
55 Self::Compare(ComparePredicate::eq(field, value))
56 }
57
58 #[must_use]
60 pub fn ne(field: String, value: Value) -> Self {
61 Self::Compare(ComparePredicate::ne(field, value))
62 }
63
64 #[must_use]
66 pub fn lt(field: String, value: Value) -> Self {
67 Self::Compare(ComparePredicate::lt(field, value))
68 }
69
70 #[must_use]
72 pub fn lte(field: String, value: Value) -> Self {
73 Self::Compare(ComparePredicate::lte(field, value))
74 }
75
76 #[must_use]
78 pub fn gt(field: String, value: Value) -> Self {
79 Self::Compare(ComparePredicate::gt(field, value))
80 }
81
82 #[must_use]
84 pub fn gte(field: String, value: Value) -> Self {
85 Self::Compare(ComparePredicate::gte(field, value))
86 }
87
88 #[must_use]
90 pub fn eq_fields(left_field: String, right_field: String) -> Self {
91 Self::CompareFields(CompareFieldsPredicate::eq(left_field, right_field))
92 }
93
94 #[must_use]
96 pub fn ne_fields(left_field: String, right_field: String) -> Self {
97 Self::CompareFields(CompareFieldsPredicate::ne(left_field, right_field))
98 }
99
100 #[must_use]
102 pub fn lt_fields(left_field: String, right_field: String) -> Self {
103 Self::CompareFields(CompareFieldsPredicate::with_coercion(
104 left_field,
105 CompareOp::Lt,
106 right_field,
107 CoercionId::NumericWiden,
108 ))
109 }
110
111 #[must_use]
113 pub fn lte_fields(left_field: String, right_field: String) -> Self {
114 Self::CompareFields(CompareFieldsPredicate::with_coercion(
115 left_field,
116 CompareOp::Lte,
117 right_field,
118 CoercionId::NumericWiden,
119 ))
120 }
121
122 #[must_use]
124 pub fn gt_fields(left_field: String, right_field: String) -> Self {
125 Self::CompareFields(CompareFieldsPredicate::with_coercion(
126 left_field,
127 CompareOp::Gt,
128 right_field,
129 CoercionId::NumericWiden,
130 ))
131 }
132
133 #[must_use]
135 pub fn gte_fields(left_field: String, right_field: String) -> Self {
136 Self::CompareFields(CompareFieldsPredicate::with_coercion(
137 left_field,
138 CompareOp::Gte,
139 right_field,
140 CoercionId::NumericWiden,
141 ))
142 }
143
144 #[must_use]
146 pub fn in_(field: String, values: Vec<Value>) -> Self {
147 Self::Compare(ComparePredicate::in_(field, values))
148 }
149
150 #[must_use]
152 pub fn not_in(field: String, values: Vec<Value>) -> Self {
153 Self::Compare(ComparePredicate::not_in(field, values))
154 }
155
156 #[must_use]
158 pub const fn is_not_null(field: String) -> Self {
159 Self::IsNotNull { field }
160 }
161
162 #[must_use]
164 pub fn between(field: String, lower: Value, upper: Value) -> Self {
165 Self::And(vec![
166 Self::gte(field.clone(), lower),
167 Self::lte(field, upper),
168 ])
169 }
170
171 #[must_use]
173 pub fn not_between(field: String, lower: Value, upper: Value) -> Self {
174 Self::Or(vec![Self::lt(field.clone(), lower), Self::gt(field, upper)])
175 }
176}
177
178impl BitAnd for Predicate {
179 type Output = Self;
180
181 fn bitand(self, rhs: Self) -> Self::Output {
182 Self::And(vec![self, rhs])
183 }
184}
185
186impl BitAnd for &Predicate {
187 type Output = Predicate;
188
189 fn bitand(self, rhs: Self) -> Self::Output {
190 Predicate::And(vec![self.clone(), rhs.clone()])
191 }
192}
193
194impl BitOr for Predicate {
195 type Output = Self;
196
197 fn bitor(self, rhs: Self) -> Self::Output {
198 Self::Or(vec![self, rhs])
199 }
200}
201
202impl BitOr for &Predicate {
203 type Output = Predicate;
204
205 fn bitor(self, rhs: Self) -> Self::Output {
206 Predicate::Or(vec![self.clone(), rhs.clone()])
207 }
208}
209
210#[cfg_attr(doc, doc = "CompareOp")]
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212#[repr(u8)]
213pub enum CompareOp {
214 Eq = 0x01,
215 Ne = 0x02,
216 Lt = 0x03,
217 Lte = 0x04,
218 Gt = 0x05,
219 Gte = 0x06,
220 In = 0x07,
221 NotIn = 0x08,
222 Contains = 0x09,
223 StartsWith = 0x0a,
224 EndsWith = 0x0b,
225}
226
227impl CompareOp {
228 #[must_use]
230 pub const fn tag(self) -> u8 {
231 self as u8
232 }
233
234 #[must_use]
236 pub const fn is_equality_family(self) -> bool {
237 matches!(self, Self::Eq | Self::Ne)
238 }
239
240 #[must_use]
242 pub const fn is_ordering_family(self) -> bool {
243 matches!(self, Self::Lt | Self::Lte | Self::Gt | Self::Gte)
244 }
245
246 #[must_use]
248 pub const fn is_membership_family(self) -> bool {
249 matches!(self, Self::In | Self::NotIn)
250 }
251
252 #[must_use]
254 pub const fn is_contains_family(self) -> bool {
255 matches!(self, Self::Contains)
256 }
257
258 #[must_use]
260 pub const fn is_text_pattern_family(self) -> bool {
261 matches!(self, Self::StartsWith | Self::EndsWith)
262 }
263
264 #[must_use]
266 pub const fn supports_field_compare(self) -> bool {
267 self.is_equality_family() || self.is_ordering_family()
268 }
269
270 #[must_use]
273 pub const fn lower_bound_inclusive(self) -> Option<bool> {
274 match self {
275 Self::Gt => Some(false),
276 Self::Gte => Some(true),
277 Self::Eq
278 | Self::Ne
279 | Self::Lt
280 | Self::Lte
281 | Self::In
282 | Self::NotIn
283 | Self::Contains
284 | Self::StartsWith
285 | Self::EndsWith => None,
286 }
287 }
288
289 #[must_use]
292 pub const fn upper_bound_inclusive(self) -> Option<bool> {
293 match self {
294 Self::Lt => Some(false),
295 Self::Lte => Some(true),
296 Self::Eq
297 | Self::Ne
298 | Self::Gt
299 | Self::Gte
300 | Self::In
301 | Self::NotIn
302 | Self::Contains
303 | Self::StartsWith
304 | Self::EndsWith => None,
305 }
306 }
307
308 #[must_use]
310 pub const fn flipped(self) -> Self {
311 match self {
312 Self::Eq => Self::Eq,
313 Self::Ne => Self::Ne,
314 Self::Lt => Self::Gt,
315 Self::Lte => Self::Gte,
316 Self::Gt => Self::Lt,
317 Self::Gte => Self::Lte,
318 Self::In => Self::In,
319 Self::NotIn => Self::NotIn,
320 Self::Contains => Self::Contains,
321 Self::StartsWith => Self::StartsWith,
322 Self::EndsWith => Self::EndsWith,
323 }
324 }
325}
326
327#[cfg_attr(doc, doc = "ComparePredicate")]
328#[derive(Clone, Debug, Eq, PartialEq)]
329pub struct ComparePredicate {
330 pub(crate) field: String,
331 pub(crate) op: CompareOp,
332 pub(crate) value: Value,
333 pub(crate) coercion: CoercionSpec,
334}
335
336impl ComparePredicate {
337 fn new(field: String, op: CompareOp, value: Value) -> Self {
338 Self {
339 field,
340 op,
341 value,
342 coercion: CoercionSpec::default(),
343 }
344 }
345
346 #[must_use]
354 pub fn with_coercion(
355 field: impl Into<String>,
356 op: CompareOp,
357 value: Value,
358 coercion: CoercionId,
359 ) -> Self {
360 Self {
361 field: field.into(),
362 op,
363 value,
364 coercion: CoercionSpec::new(coercion),
365 }
366 }
367
368 #[must_use]
370 pub fn eq(field: String, value: Value) -> Self {
371 Self::new(field, CompareOp::Eq, value)
372 }
373
374 #[must_use]
376 pub fn ne(field: String, value: Value) -> Self {
377 Self::new(field, CompareOp::Ne, value)
378 }
379
380 #[must_use]
382 pub fn lt(field: String, value: Value) -> Self {
383 Self::new(field, CompareOp::Lt, value)
384 }
385
386 #[must_use]
388 pub fn lte(field: String, value: Value) -> Self {
389 Self::new(field, CompareOp::Lte, value)
390 }
391
392 #[must_use]
394 pub fn gt(field: String, value: Value) -> Self {
395 Self::new(field, CompareOp::Gt, value)
396 }
397
398 #[must_use]
400 pub fn gte(field: String, value: Value) -> Self {
401 Self::new(field, CompareOp::Gte, value)
402 }
403
404 #[must_use]
406 pub fn in_(field: String, values: Vec<Value>) -> Self {
407 Self::new(field, CompareOp::In, Value::List(values))
408 }
409
410 #[must_use]
412 pub fn not_in(field: String, values: Vec<Value>) -> Self {
413 Self::new(field, CompareOp::NotIn, Value::List(values))
414 }
415
416 #[must_use]
418 pub fn field(&self) -> &str {
419 &self.field
420 }
421
422 #[must_use]
424 pub const fn op(&self) -> CompareOp {
425 self.op
426 }
427
428 #[must_use]
430 pub const fn value(&self) -> &Value {
431 &self.value
432 }
433
434 #[must_use]
436 pub const fn coercion(&self) -> &CoercionSpec {
437 &self.coercion
438 }
439}
440
441#[derive(Clone, Debug, Eq, PartialEq)]
450pub struct CompareFieldsPredicate {
451 pub(crate) left_field: String,
452 pub(crate) op: CompareOp,
453 pub(crate) right_field: String,
454 pub(crate) coercion: CoercionSpec,
455}
456
457impl CompareFieldsPredicate {
458 fn canonicalize_symmetric_fields(
459 op: CompareOp,
460 left_field: String,
461 right_field: String,
462 ) -> (String, String) {
463 if op.is_equality_family() && left_field < right_field {
464 (right_field, left_field)
465 } else {
466 (left_field, right_field)
467 }
468 }
469
470 fn new(left_field: String, op: CompareOp, right_field: String) -> Self {
471 let (left_field, right_field) =
472 Self::canonicalize_symmetric_fields(op, left_field, right_field);
473
474 Self {
475 left_field,
476 op,
477 right_field,
478 coercion: CoercionSpec::default(),
479 }
480 }
481
482 #[must_use]
490 pub fn with_coercion(
491 left_field: impl Into<String>,
492 op: CompareOp,
493 right_field: impl Into<String>,
494 coercion: CoercionId,
495 ) -> Self {
496 let (left_field, right_field) =
497 Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
498
499 Self {
500 left_field,
501 op,
502 right_field,
503 coercion: CoercionSpec::new(coercion),
504 }
505 }
506
507 #[must_use]
509 pub fn eq(left_field: String, right_field: String) -> Self {
510 Self::new(left_field, CompareOp::Eq, right_field)
511 }
512
513 #[must_use]
515 pub fn ne(left_field: String, right_field: String) -> Self {
516 Self::new(left_field, CompareOp::Ne, right_field)
517 }
518
519 #[must_use]
521 pub fn lt(left_field: String, right_field: String) -> Self {
522 Self::new(left_field, CompareOp::Lt, right_field)
523 }
524
525 #[must_use]
527 pub fn lte(left_field: String, right_field: String) -> Self {
528 Self::new(left_field, CompareOp::Lte, right_field)
529 }
530
531 #[must_use]
533 pub fn gt(left_field: String, right_field: String) -> Self {
534 Self::new(left_field, CompareOp::Gt, right_field)
535 }
536
537 #[must_use]
539 pub fn gte(left_field: String, right_field: String) -> Self {
540 Self::new(left_field, CompareOp::Gte, right_field)
541 }
542
543 #[must_use]
545 pub fn left_field(&self) -> &str {
546 &self.left_field
547 }
548
549 #[must_use]
551 pub const fn op(&self) -> CompareOp {
552 self.op
553 }
554
555 #[must_use]
557 pub fn right_field(&self) -> &str {
558 &self.right_field
559 }
560
561 #[must_use]
563 pub const fn coercion(&self) -> &CoercionSpec {
564 &self.coercion
565 }
566}
567
568#[cfg_attr(
569 doc,
570 doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
571)]
572#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
573pub enum UnsupportedQueryFeature {
574 #[error("map field '{field}' is not queryable; use scalar/indexed fields or list entries")]
575 MapPredicate { field: String },
576}
577
578#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn compare_predicate_builders_preserve_operator_shape() {
588 assert_eq!(
589 Predicate::gt("age".to_string(), Value::Uint(7)),
590 Predicate::Compare(ComparePredicate::gt("age".to_string(), Value::Uint(7))),
591 );
592 }
593}