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]
348 pub fn with_coercion(
349 field: impl Into<String>,
350 op: CompareOp,
351 value: Value,
352 coercion: CoercionId,
353 ) -> Self {
354 Self {
355 field: field.into(),
356 op,
357 value,
358 coercion: CoercionSpec::new(coercion),
359 }
360 }
361
362 #[must_use]
364 pub fn eq(field: String, value: Value) -> Self {
365 Self::new(field, CompareOp::Eq, value)
366 }
367
368 #[must_use]
370 pub fn ne(field: String, value: Value) -> Self {
371 Self::new(field, CompareOp::Ne, value)
372 }
373
374 #[must_use]
376 pub fn lt(field: String, value: Value) -> Self {
377 Self::new(field, CompareOp::Lt, value)
378 }
379
380 #[must_use]
382 pub fn lte(field: String, value: Value) -> Self {
383 Self::new(field, CompareOp::Lte, value)
384 }
385
386 #[must_use]
388 pub fn gt(field: String, value: Value) -> Self {
389 Self::new(field, CompareOp::Gt, value)
390 }
391
392 #[must_use]
394 pub fn gte(field: String, value: Value) -> Self {
395 Self::new(field, CompareOp::Gte, value)
396 }
397
398 #[must_use]
400 pub fn in_(field: String, values: Vec<Value>) -> Self {
401 Self::new(field, CompareOp::In, Value::List(values))
402 }
403
404 #[must_use]
406 pub fn not_in(field: String, values: Vec<Value>) -> Self {
407 Self::new(field, CompareOp::NotIn, Value::List(values))
408 }
409
410 #[must_use]
412 pub fn field(&self) -> &str {
413 &self.field
414 }
415
416 #[must_use]
418 pub const fn op(&self) -> CompareOp {
419 self.op
420 }
421
422 #[must_use]
424 pub const fn value(&self) -> &Value {
425 &self.value
426 }
427
428 #[must_use]
430 pub const fn coercion(&self) -> &CoercionSpec {
431 &self.coercion
432 }
433}
434
435#[derive(Clone, Debug, Eq, PartialEq)]
444pub struct CompareFieldsPredicate {
445 pub(crate) left_field: String,
446 pub(crate) op: CompareOp,
447 pub(crate) right_field: String,
448 pub(crate) coercion: CoercionSpec,
449}
450
451impl CompareFieldsPredicate {
452 fn canonicalize_symmetric_fields(
453 op: CompareOp,
454 left_field: String,
455 right_field: String,
456 ) -> (String, String) {
457 if op.is_equality_family() && left_field < right_field {
458 (right_field, left_field)
459 } else {
460 (left_field, right_field)
461 }
462 }
463
464 fn new(left_field: String, op: CompareOp, right_field: String) -> Self {
465 let (left_field, right_field) =
466 Self::canonicalize_symmetric_fields(op, left_field, right_field);
467
468 Self {
469 left_field,
470 op,
471 right_field,
472 coercion: CoercionSpec::default(),
473 }
474 }
475
476 #[must_use]
479 pub fn with_coercion(
480 left_field: impl Into<String>,
481 op: CompareOp,
482 right_field: impl Into<String>,
483 coercion: CoercionId,
484 ) -> Self {
485 let (left_field, right_field) =
486 Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
487
488 Self {
489 left_field,
490 op,
491 right_field,
492 coercion: CoercionSpec::new(coercion),
493 }
494 }
495
496 #[must_use]
498 pub fn eq(left_field: String, right_field: String) -> Self {
499 Self::new(left_field, CompareOp::Eq, right_field)
500 }
501
502 #[must_use]
504 pub fn ne(left_field: String, right_field: String) -> Self {
505 Self::new(left_field, CompareOp::Ne, right_field)
506 }
507
508 #[must_use]
510 pub fn lt(left_field: String, right_field: String) -> Self {
511 Self::new(left_field, CompareOp::Lt, right_field)
512 }
513
514 #[must_use]
516 pub fn lte(left_field: String, right_field: String) -> Self {
517 Self::new(left_field, CompareOp::Lte, right_field)
518 }
519
520 #[must_use]
522 pub fn gt(left_field: String, right_field: String) -> Self {
523 Self::new(left_field, CompareOp::Gt, right_field)
524 }
525
526 #[must_use]
528 pub fn gte(left_field: String, right_field: String) -> Self {
529 Self::new(left_field, CompareOp::Gte, right_field)
530 }
531
532 #[must_use]
534 pub fn left_field(&self) -> &str {
535 &self.left_field
536 }
537
538 #[must_use]
540 pub const fn op(&self) -> CompareOp {
541 self.op
542 }
543
544 #[must_use]
546 pub fn right_field(&self) -> &str {
547 &self.right_field
548 }
549
550 #[must_use]
552 pub const fn coercion(&self) -> &CoercionSpec {
553 &self.coercion
554 }
555}
556
557#[cfg_attr(
558 doc,
559 doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
560)]
561#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
562pub enum UnsupportedQueryFeature {
563 #[error("map field '{field}' is not queryable; use scalar/indexed fields or list entries")]
564 MapPredicate { field: String },
565}
566
567#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn compare_predicate_builders_preserve_operator_shape() {
577 assert_eq!(
578 Predicate::gt("age".to_string(), Value::Uint(7)),
579 Predicate::Compare(ComparePredicate::gt("age".to_string(), Value::Uint(7))),
580 );
581 }
582}