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(
211 doc,
212 doc = "Neutral predicate model consumed by executor/index layers."
213)]
214pub(crate) type PredicateExecutionModel = Predicate;
215
216#[cfg_attr(doc, doc = "CompareOp")]
217#[derive(Clone, Copy, Debug, Eq, PartialEq)]
218#[repr(u8)]
219pub enum CompareOp {
220 Eq = 0x01,
221 Ne = 0x02,
222 Lt = 0x03,
223 Lte = 0x04,
224 Gt = 0x05,
225 Gte = 0x06,
226 In = 0x07,
227 NotIn = 0x08,
228 Contains = 0x09,
229 StartsWith = 0x0a,
230 EndsWith = 0x0b,
231}
232
233impl CompareOp {
234 #[must_use]
236 pub const fn tag(self) -> u8 {
237 self as u8
238 }
239
240 #[must_use]
242 pub const fn flipped(self) -> Self {
243 match self {
244 Self::Eq => Self::Eq,
245 Self::Ne => Self::Ne,
246 Self::Lt => Self::Gt,
247 Self::Lte => Self::Gte,
248 Self::Gt => Self::Lt,
249 Self::Gte => Self::Lte,
250 Self::In => Self::In,
251 Self::NotIn => Self::NotIn,
252 Self::Contains => Self::Contains,
253 Self::StartsWith => Self::StartsWith,
254 Self::EndsWith => Self::EndsWith,
255 }
256 }
257}
258
259#[cfg_attr(doc, doc = "ComparePredicate")]
260#[derive(Clone, Debug, Eq, PartialEq)]
261pub struct ComparePredicate {
262 pub(crate) field: String,
263 pub(crate) op: CompareOp,
264 pub(crate) value: Value,
265 pub(crate) coercion: CoercionSpec,
266}
267
268impl ComparePredicate {
269 fn new(field: String, op: CompareOp, value: Value) -> Self {
270 Self {
271 field,
272 op,
273 value,
274 coercion: CoercionSpec::default(),
275 }
276 }
277
278 #[must_use]
280 pub fn with_coercion(
281 field: impl Into<String>,
282 op: CompareOp,
283 value: Value,
284 coercion: CoercionId,
285 ) -> Self {
286 Self {
287 field: field.into(),
288 op,
289 value,
290 coercion: CoercionSpec::new(coercion),
291 }
292 }
293
294 #[must_use]
296 pub fn eq(field: String, value: Value) -> Self {
297 Self::new(field, CompareOp::Eq, value)
298 }
299
300 #[must_use]
302 pub fn ne(field: String, value: Value) -> Self {
303 Self::new(field, CompareOp::Ne, value)
304 }
305
306 #[must_use]
308 pub fn lt(field: String, value: Value) -> Self {
309 Self::new(field, CompareOp::Lt, value)
310 }
311
312 #[must_use]
314 pub fn lte(field: String, value: Value) -> Self {
315 Self::new(field, CompareOp::Lte, value)
316 }
317
318 #[must_use]
320 pub fn gt(field: String, value: Value) -> Self {
321 Self::new(field, CompareOp::Gt, value)
322 }
323
324 #[must_use]
326 pub fn gte(field: String, value: Value) -> Self {
327 Self::new(field, CompareOp::Gte, value)
328 }
329
330 #[must_use]
332 pub fn in_(field: String, values: Vec<Value>) -> Self {
333 Self::new(field, CompareOp::In, Value::List(values))
334 }
335
336 #[must_use]
338 pub fn not_in(field: String, values: Vec<Value>) -> Self {
339 Self::new(field, CompareOp::NotIn, Value::List(values))
340 }
341
342 #[must_use]
344 pub fn field(&self) -> &str {
345 &self.field
346 }
347
348 #[must_use]
350 pub const fn op(&self) -> CompareOp {
351 self.op
352 }
353
354 #[must_use]
356 pub const fn value(&self) -> &Value {
357 &self.value
358 }
359
360 #[must_use]
362 pub const fn coercion(&self) -> &CoercionSpec {
363 &self.coercion
364 }
365}
366
367#[derive(Clone, Debug, Eq, PartialEq)]
376pub struct CompareFieldsPredicate {
377 pub(crate) left_field: String,
378 pub(crate) op: CompareOp,
379 pub(crate) right_field: String,
380 pub(crate) coercion: CoercionSpec,
381}
382
383impl CompareFieldsPredicate {
384 fn canonicalize_symmetric_fields(
385 op: CompareOp,
386 left_field: String,
387 right_field: String,
388 ) -> (String, String) {
389 if matches!(op, CompareOp::Eq | CompareOp::Ne) && left_field < right_field {
390 (right_field, left_field)
391 } else {
392 (left_field, right_field)
393 }
394 }
395
396 fn new(left_field: String, op: CompareOp, right_field: String) -> Self {
397 let (left_field, right_field) =
398 Self::canonicalize_symmetric_fields(op, left_field, right_field);
399
400 Self {
401 left_field,
402 op,
403 right_field,
404 coercion: CoercionSpec::default(),
405 }
406 }
407
408 #[must_use]
411 pub fn with_coercion(
412 left_field: impl Into<String>,
413 op: CompareOp,
414 right_field: impl Into<String>,
415 coercion: CoercionId,
416 ) -> Self {
417 let (left_field, right_field) =
418 Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
419
420 Self {
421 left_field,
422 op,
423 right_field,
424 coercion: CoercionSpec::new(coercion),
425 }
426 }
427
428 #[must_use]
430 pub fn eq(left_field: String, right_field: String) -> Self {
431 Self::new(left_field, CompareOp::Eq, right_field)
432 }
433
434 #[must_use]
436 pub fn ne(left_field: String, right_field: String) -> Self {
437 Self::new(left_field, CompareOp::Ne, right_field)
438 }
439
440 #[must_use]
442 pub fn lt(left_field: String, right_field: String) -> Self {
443 Self::new(left_field, CompareOp::Lt, right_field)
444 }
445
446 #[must_use]
448 pub fn lte(left_field: String, right_field: String) -> Self {
449 Self::new(left_field, CompareOp::Lte, right_field)
450 }
451
452 #[must_use]
454 pub fn gt(left_field: String, right_field: String) -> Self {
455 Self::new(left_field, CompareOp::Gt, right_field)
456 }
457
458 #[must_use]
460 pub fn gte(left_field: String, right_field: String) -> Self {
461 Self::new(left_field, CompareOp::Gte, right_field)
462 }
463
464 #[must_use]
466 pub fn left_field(&self) -> &str {
467 &self.left_field
468 }
469
470 #[must_use]
472 pub const fn op(&self) -> CompareOp {
473 self.op
474 }
475
476 #[must_use]
478 pub fn right_field(&self) -> &str {
479 &self.right_field
480 }
481
482 #[must_use]
484 pub const fn coercion(&self) -> &CoercionSpec {
485 &self.coercion
486 }
487}
488
489#[cfg_attr(
490 doc,
491 doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
492)]
493#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
494pub enum UnsupportedQueryFeature {
495 #[error("map field '{field}' is not queryable; use scalar/indexed fields or list entries")]
496 MapPredicate { field: String },
497}