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(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    /// Return the stable wire tag for this compare operator.
229    #[must_use]
230    pub const fn tag(self) -> u8 {
231        self as u8
232    }
233
234    /// Return whether this operator is one symmetric equality-style compare.
235    #[must_use]
236    pub const fn is_equality_family(self) -> bool {
237        matches!(self, Self::Eq | Self::Ne)
238    }
239
240    /// Return whether this operator is one ordered range-bound compare.
241    #[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    /// Return whether this operator is one list-membership compare.
247    #[must_use]
248    pub const fn is_membership_family(self) -> bool {
249        matches!(self, Self::In | Self::NotIn)
250    }
251
252    /// Return whether this operator is one containment compare.
253    #[must_use]
254    pub const fn is_contains_family(self) -> bool {
255        matches!(self, Self::Contains)
256    }
257
258    /// Return whether this operator is one text-pattern compare.
259    #[must_use]
260    pub const fn is_text_pattern_family(self) -> bool {
261        matches!(self, Self::StartsWith | Self::EndsWith)
262    }
263
264    /// Return whether this operator supports direct field-to-field comparison.
265    #[must_use]
266    pub const fn supports_field_compare(self) -> bool {
267        self.is_equality_family() || self.is_ordering_family()
268    }
269
270    /// Return whether this operator contributes one lower bound and whether it
271    /// is inclusive when present.
272    #[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    /// Return whether this operator contributes one upper bound and whether it
290    /// is inclusive when present.
291    #[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    /// Return the operator that preserves semantics when the two operands are swapped.
309    #[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    /// Construct a comparison predicate with an explicit coercion policy.
347    ///
348    /// This is the low-level predicate AST constructor used by SQL lowering,
349    /// generated index predicates, and tests that need a precise coercion
350    /// contract. It does not validate field existence, operator/literal
351    /// compatibility, or schema admissibility; those checks belong to predicate
352    /// validation and query planning.
353    #[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    /// Build `Eq` comparison.
369    #[must_use]
370    pub fn eq(field: String, value: Value) -> Self {
371        Self::new(field, CompareOp::Eq, value)
372    }
373
374    /// Build `Ne` comparison.
375    #[must_use]
376    pub fn ne(field: String, value: Value) -> Self {
377        Self::new(field, CompareOp::Ne, value)
378    }
379
380    /// Build `Lt` comparison.
381    #[must_use]
382    pub fn lt(field: String, value: Value) -> Self {
383        Self::new(field, CompareOp::Lt, value)
384    }
385
386    /// Build `Lte` comparison.
387    #[must_use]
388    pub fn lte(field: String, value: Value) -> Self {
389        Self::new(field, CompareOp::Lte, value)
390    }
391
392    /// Build `Gt` comparison.
393    #[must_use]
394    pub fn gt(field: String, value: Value) -> Self {
395        Self::new(field, CompareOp::Gt, value)
396    }
397
398    /// Build `Gte` comparison.
399    #[must_use]
400    pub fn gte(field: String, value: Value) -> Self {
401        Self::new(field, CompareOp::Gte, value)
402    }
403
404    /// Build `In` comparison.
405    #[must_use]
406    pub fn in_(field: String, values: Vec<Value>) -> Self {
407        Self::new(field, CompareOp::In, Value::List(values))
408    }
409
410    /// Build `NotIn` comparison.
411    #[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    /// Borrow the compared field name.
417    #[must_use]
418    pub fn field(&self) -> &str {
419        &self.field
420    }
421
422    /// Return the compare operator.
423    #[must_use]
424    pub const fn op(&self) -> CompareOp {
425        self.op
426    }
427
428    /// Borrow the compared literal value.
429    #[must_use]
430    pub const fn value(&self) -> &Value {
431        &self.value
432    }
433
434    /// Borrow the comparison coercion policy.
435    #[must_use]
436    pub const fn coercion(&self) -> &CoercionSpec {
437        &self.coercion
438    }
439}
440
441///
442/// CompareFieldsPredicate
443///
444/// Canonical predicate-owned field-to-field comparison leaf.
445/// This keeps bounded compare expressions on the predicate authority seam
446/// instead of routing them through projection-expression ownership.
447///
448
449#[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    /// Construct a field-to-field comparison predicate with an explicit
483    /// coercion policy.
484    ///
485    /// This low-level constructor preserves the provided comparison contract
486    /// and only canonicalizes symmetric equality-family field order. It does
487    /// not validate that the operator is field-comparison-admissible for a
488    /// schema; that remains a validation/planning responsibility.
489    #[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    /// Build `Eq` field-to-field comparison.
508    #[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    /// Build `Ne` field-to-field comparison.
514    #[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    /// Build `Lt` field-to-field comparison.
520    #[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    /// Build `Lte` field-to-field comparison.
526    #[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    /// Build `Gt` field-to-field comparison.
532    #[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    /// Build `Gte` field-to-field comparison.
538    #[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    /// Borrow the left compared field name.
544    #[must_use]
545    pub fn left_field(&self) -> &str {
546        &self.left_field
547    }
548
549    /// Return the compare operator.
550    #[must_use]
551    pub const fn op(&self) -> CompareOp {
552        self.op
553    }
554
555    /// Borrow the right compared field name.
556    #[must_use]
557    pub fn right_field(&self) -> &str {
558        &self.right_field
559    }
560
561    /// Borrow the comparison coercion policy.
562    #[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///
579/// TESTS
580///
581
582#[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}