Skip to main content

icydb_core/db/predicate/
model.rs

1//! Module: predicate::model
2//! Responsibility: public predicate AST and construction helpers.
3//! Does not own: schema validation or runtime slot resolution.
4//! Boundary: user/query-facing predicate model.
5
6use 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    /// Build an `And` predicate from child predicates.
34    #[must_use]
35    pub const fn and(preds: Vec<Self>) -> Self {
36        Self::And(preds)
37    }
38
39    /// Build an `Or` predicate from child predicates.
40    #[must_use]
41    pub const fn or(preds: Vec<Self>) -> Self {
42        Self::Or(preds)
43    }
44
45    /// Negate one predicate.
46    #[must_use]
47    #[expect(clippy::should_implement_trait)]
48    pub fn not(pred: Self) -> Self {
49        Self::Not(Box::new(pred))
50    }
51
52    /// Compare `field == value`.
53    #[must_use]
54    pub fn eq(field: String, value: Value) -> Self {
55        Self::Compare(ComparePredicate::eq(field, value))
56    }
57
58    /// Compare `field != value`.
59    #[must_use]
60    pub fn ne(field: String, value: Value) -> Self {
61        Self::Compare(ComparePredicate::ne(field, value))
62    }
63
64    /// Compare `field < value`.
65    #[must_use]
66    pub fn lt(field: String, value: Value) -> Self {
67        Self::Compare(ComparePredicate::lt(field, value))
68    }
69
70    /// Compare `field <= value`.
71    #[must_use]
72    pub fn lte(field: String, value: Value) -> Self {
73        Self::Compare(ComparePredicate::lte(field, value))
74    }
75
76    /// Compare `field > value`.
77    #[must_use]
78    pub fn gt(field: String, value: Value) -> Self {
79        Self::Compare(ComparePredicate::gt(field, value))
80    }
81
82    /// Compare `field >= value`.
83    #[must_use]
84    pub fn gte(field: String, value: Value) -> Self {
85        Self::Compare(ComparePredicate::gte(field, value))
86    }
87
88    /// Compare `left_field == right_field`.
89    #[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    /// Compare `left_field != right_field`.
95    #[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    /// Compare `left_field < right_field`.
101    #[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    /// Compare `left_field <= right_field`.
112    #[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    /// Compare `left_field > right_field`.
123    #[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    /// Compare `left_field >= right_field`.
134    #[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    /// Compare `field IN values`.
145    #[must_use]
146    pub fn in_(field: String, values: Vec<Value>) -> Self {
147        Self::Compare(ComparePredicate::in_(field, values))
148    }
149
150    /// Compare `field NOT IN values`.
151    #[must_use]
152    pub fn not_in(field: String, values: Vec<Value>) -> Self {
153        Self::Compare(ComparePredicate::not_in(field, values))
154    }
155
156    /// Compare `field IS NOT NULL`.
157    #[must_use]
158    pub const fn is_not_null(field: String) -> Self {
159        Self::IsNotNull { field }
160    }
161
162    /// Compare `field BETWEEN lower AND upper`.
163    #[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    /// Compare `field NOT BETWEEN lower AND upper`.
172    #[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    /// Return the stable wire tag for this compare operator.
235    #[must_use]
236    pub const fn tag(self) -> u8 {
237        self as u8
238    }
239
240    /// Return the operator that preserves semantics when the two operands are swapped.
241    #[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    /// Construct a comparison predicate with an explicit coercion policy.
279    #[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    /// Build `Eq` comparison.
295    #[must_use]
296    pub fn eq(field: String, value: Value) -> Self {
297        Self::new(field, CompareOp::Eq, value)
298    }
299
300    /// Build `Ne` comparison.
301    #[must_use]
302    pub fn ne(field: String, value: Value) -> Self {
303        Self::new(field, CompareOp::Ne, value)
304    }
305
306    /// Build `Lt` comparison.
307    #[must_use]
308    pub fn lt(field: String, value: Value) -> Self {
309        Self::new(field, CompareOp::Lt, value)
310    }
311
312    /// Build `Lte` comparison.
313    #[must_use]
314    pub fn lte(field: String, value: Value) -> Self {
315        Self::new(field, CompareOp::Lte, value)
316    }
317
318    /// Build `Gt` comparison.
319    #[must_use]
320    pub fn gt(field: String, value: Value) -> Self {
321        Self::new(field, CompareOp::Gt, value)
322    }
323
324    /// Build `Gte` comparison.
325    #[must_use]
326    pub fn gte(field: String, value: Value) -> Self {
327        Self::new(field, CompareOp::Gte, value)
328    }
329
330    /// Build `In` comparison.
331    #[must_use]
332    pub fn in_(field: String, values: Vec<Value>) -> Self {
333        Self::new(field, CompareOp::In, Value::List(values))
334    }
335
336    /// Build `NotIn` comparison.
337    #[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    /// Borrow the compared field name.
343    #[must_use]
344    pub fn field(&self) -> &str {
345        &self.field
346    }
347
348    /// Return the compare operator.
349    #[must_use]
350    pub const fn op(&self) -> CompareOp {
351        self.op
352    }
353
354    /// Borrow the compared literal value.
355    #[must_use]
356    pub const fn value(&self) -> &Value {
357        &self.value
358    }
359
360    /// Borrow the comparison coercion policy.
361    #[must_use]
362    pub const fn coercion(&self) -> &CoercionSpec {
363        &self.coercion
364    }
365}
366
367///
368/// CompareFieldsPredicate
369///
370/// Canonical predicate-owned field-to-field comparison leaf.
371/// This keeps bounded compare expressions on the predicate authority seam
372/// instead of routing them through projection-expression ownership.
373///
374
375#[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    /// Construct a field-to-field comparison predicate with an explicit
409    /// coercion policy.
410    #[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    /// Build `Eq` field-to-field comparison.
429    #[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    /// Build `Ne` field-to-field comparison.
435    #[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    /// Build `Lt` field-to-field comparison.
441    #[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    /// Build `Lte` field-to-field comparison.
447    #[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    /// Build `Gt` field-to-field comparison.
453    #[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    /// Build `Gte` field-to-field comparison.
459    #[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    /// Borrow the left compared field name.
465    #[must_use]
466    pub fn left_field(&self) -> &str {
467        &self.left_field
468    }
469
470    /// Return the compare operator.
471    #[must_use]
472    pub const fn op(&self) -> CompareOp {
473        self.op
474    }
475
476    /// Borrow the right compared field name.
477    #[must_use]
478    pub fn right_field(&self) -> &str {
479        &self.right_field
480    }
481
482    /// Borrow the comparison coercion policy.
483    #[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}