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};
11
12#[cfg_attr(doc, doc = "Predicate")]
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum Predicate {
15    True,
16    False,
17    And(Vec<Self>),
18    Or(Vec<Self>),
19    Not(Box<Self>),
20    Compare(ComparePredicate),
21    CompareFields(CompareFieldsPredicate),
22    IsNull { field: String },
23    IsNotNull { field: String },
24    IsMissing { field: String },
25    IsEmpty { field: String },
26    IsNotEmpty { field: String },
27    TextContains { field: String, value: Value },
28    TextContainsCi { field: String, value: Value },
29}
30
31impl Predicate {
32    /// Build an `And` predicate from child predicates.
33    #[must_use]
34    pub const fn and(preds: Vec<Self>) -> Self {
35        Self::And(preds)
36    }
37
38    /// Build an `Or` predicate from child predicates.
39    #[must_use]
40    pub const fn or(preds: Vec<Self>) -> Self {
41        Self::Or(preds)
42    }
43
44    /// Negate one predicate.
45    #[must_use]
46    #[expect(clippy::should_implement_trait)]
47    pub fn not(pred: Self) -> Self {
48        Self::Not(Box::new(pred))
49    }
50
51    /// Compare `field == value`.
52    #[must_use]
53    pub fn eq(field: String, value: Value) -> Self {
54        Self::Compare(ComparePredicate::eq(field, value))
55    }
56
57    /// Compare `field != value`.
58    #[must_use]
59    pub fn ne(field: String, value: Value) -> Self {
60        Self::Compare(ComparePredicate::ne(field, value))
61    }
62
63    /// Compare `field < value`.
64    #[must_use]
65    pub fn lt(field: String, value: Value) -> Self {
66        Self::Compare(ComparePredicate::lt(field, value))
67    }
68
69    /// Compare `field <= value`.
70    #[must_use]
71    pub fn lte(field: String, value: Value) -> Self {
72        Self::Compare(ComparePredicate::lte(field, value))
73    }
74
75    /// Compare `field > value`.
76    #[must_use]
77    pub fn gt(field: String, value: Value) -> Self {
78        Self::Compare(ComparePredicate::gt(field, value))
79    }
80
81    /// Compare `field >= value`.
82    #[must_use]
83    pub fn gte(field: String, value: Value) -> Self {
84        Self::Compare(ComparePredicate::gte(field, value))
85    }
86
87    /// Compare `left_field == right_field`.
88    #[must_use]
89    pub fn eq_fields(left_field: String, right_field: String) -> Self {
90        Self::CompareFields(CompareFieldsPredicate::eq(left_field, right_field))
91    }
92
93    /// Compare `left_field != right_field`.
94    #[must_use]
95    pub fn ne_fields(left_field: String, right_field: String) -> Self {
96        Self::CompareFields(CompareFieldsPredicate::ne(left_field, right_field))
97    }
98
99    /// Compare `left_field < right_field`.
100    #[must_use]
101    pub fn lt_fields(left_field: String, right_field: String) -> Self {
102        Self::CompareFields(CompareFieldsPredicate::with_coercion(
103            left_field,
104            CompareOp::Lt,
105            right_field,
106            CoercionId::NumericWiden,
107        ))
108    }
109
110    /// Compare `left_field <= right_field`.
111    #[must_use]
112    pub fn lte_fields(left_field: String, right_field: String) -> Self {
113        Self::CompareFields(CompareFieldsPredicate::with_coercion(
114            left_field,
115            CompareOp::Lte,
116            right_field,
117            CoercionId::NumericWiden,
118        ))
119    }
120
121    /// Compare `left_field > right_field`.
122    #[must_use]
123    pub fn gt_fields(left_field: String, right_field: String) -> Self {
124        Self::CompareFields(CompareFieldsPredicate::with_coercion(
125            left_field,
126            CompareOp::Gt,
127            right_field,
128            CoercionId::NumericWiden,
129        ))
130    }
131
132    /// Compare `left_field >= right_field`.
133    #[must_use]
134    pub fn gte_fields(left_field: String, right_field: String) -> Self {
135        Self::CompareFields(CompareFieldsPredicate::with_coercion(
136            left_field,
137            CompareOp::Gte,
138            right_field,
139            CoercionId::NumericWiden,
140        ))
141    }
142
143    /// Compare `field IN values`.
144    #[must_use]
145    pub fn in_(field: String, values: Vec<Value>) -> Self {
146        Self::Compare(ComparePredicate::in_(field, values))
147    }
148
149    /// Compare `field NOT IN values`.
150    #[must_use]
151    pub fn not_in(field: String, values: Vec<Value>) -> Self {
152        Self::Compare(ComparePredicate::not_in(field, values))
153    }
154
155    /// Compare `field IS NOT NULL`.
156    #[must_use]
157    pub const fn is_not_null(field: String) -> Self {
158        Self::IsNotNull { field }
159    }
160
161    /// Compare `field BETWEEN lower AND upper`.
162    #[must_use]
163    pub fn between(field: String, lower: Value, upper: Value) -> Self {
164        Self::And(vec![
165            Self::gte(field.clone(), lower),
166            Self::lte(field, upper),
167        ])
168    }
169
170    /// Compare `field NOT BETWEEN lower AND upper`.
171    #[must_use]
172    pub fn not_between(field: String, lower: Value, upper: Value) -> Self {
173        Self::Or(vec![Self::lt(field.clone(), lower), Self::gt(field, upper)])
174    }
175}
176
177impl BitAnd for Predicate {
178    type Output = Self;
179
180    fn bitand(self, rhs: Self) -> Self::Output {
181        Self::And(vec![self, rhs])
182    }
183}
184
185impl BitAnd for &Predicate {
186    type Output = Predicate;
187
188    fn bitand(self, rhs: Self) -> Self::Output {
189        Predicate::And(vec![self.clone(), rhs.clone()])
190    }
191}
192
193impl BitOr for Predicate {
194    type Output = Self;
195
196    fn bitor(self, rhs: Self) -> Self::Output {
197        Self::Or(vec![self, rhs])
198    }
199}
200
201impl BitOr for &Predicate {
202    type Output = Predicate;
203
204    fn bitor(self, rhs: Self) -> Self::Output {
205        Predicate::Or(vec![self.clone(), rhs.clone()])
206    }
207}
208
209#[cfg_attr(doc, doc = "CompareOp")]
210#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211#[repr(u8)]
212pub enum CompareOp {
213    Eq = 0x01,
214    Ne = 0x02,
215    Lt = 0x03,
216    Lte = 0x04,
217    Gt = 0x05,
218    Gte = 0x06,
219    In = 0x07,
220    NotIn = 0x08,
221    Contains = 0x09,
222    StartsWith = 0x0a,
223    EndsWith = 0x0b,
224}
225
226impl CompareOp {
227    /// Return the stable wire tag for this compare operator.
228    #[must_use]
229    pub const fn tag(self) -> u8 {
230        self as u8
231    }
232
233    /// Return whether this operator is one symmetric equality-style compare.
234    #[must_use]
235    pub const fn is_equality_family(self) -> bool {
236        matches!(self, Self::Eq | Self::Ne)
237    }
238
239    /// Return whether this operator is one ordered range-bound compare.
240    #[must_use]
241    pub const fn is_ordering_family(self) -> bool {
242        matches!(self, Self::Lt | Self::Lte | Self::Gt | Self::Gte)
243    }
244
245    /// Return whether this operator is one list-membership compare.
246    #[must_use]
247    pub const fn is_membership_family(self) -> bool {
248        matches!(self, Self::In | Self::NotIn)
249    }
250
251    /// Return whether this operator is one containment compare.
252    #[must_use]
253    pub const fn is_contains_family(self) -> bool {
254        matches!(self, Self::Contains)
255    }
256
257    /// Return whether this operator is one text-pattern compare.
258    #[must_use]
259    pub const fn is_text_pattern_family(self) -> bool {
260        matches!(self, Self::StartsWith | Self::EndsWith)
261    }
262
263    /// Return whether this operator supports direct field-to-field comparison.
264    #[must_use]
265    pub const fn supports_field_compare(self) -> bool {
266        self.is_equality_family() || self.is_ordering_family()
267    }
268
269    /// Return whether this operator contributes one lower bound and whether it
270    /// is inclusive when present.
271    #[must_use]
272    pub const fn lower_bound_inclusive(self) -> Option<bool> {
273        match self {
274            Self::Gt => Some(false),
275            Self::Gte => Some(true),
276            Self::Eq
277            | Self::Ne
278            | Self::Lt
279            | Self::Lte
280            | Self::In
281            | Self::NotIn
282            | Self::Contains
283            | Self::StartsWith
284            | Self::EndsWith => None,
285        }
286    }
287
288    /// Return whether this operator contributes one upper bound and whether it
289    /// is inclusive when present.
290    #[must_use]
291    pub const fn upper_bound_inclusive(self) -> Option<bool> {
292        match self {
293            Self::Lt => Some(false),
294            Self::Lte => Some(true),
295            Self::Eq
296            | Self::Ne
297            | Self::Gt
298            | Self::Gte
299            | Self::In
300            | Self::NotIn
301            | Self::Contains
302            | Self::StartsWith
303            | Self::EndsWith => None,
304        }
305    }
306
307    /// Return the operator that preserves semantics when the two operands are swapped.
308    #[must_use]
309    pub const fn flipped(self) -> Self {
310        match self {
311            Self::Eq => Self::Eq,
312            Self::Ne => Self::Ne,
313            Self::Lt => Self::Gt,
314            Self::Lte => Self::Gte,
315            Self::Gt => Self::Lt,
316            Self::Gte => Self::Lte,
317            Self::In => Self::In,
318            Self::NotIn => Self::NotIn,
319            Self::Contains => Self::Contains,
320            Self::StartsWith => Self::StartsWith,
321            Self::EndsWith => Self::EndsWith,
322        }
323    }
324}
325
326#[cfg_attr(doc, doc = "ComparePredicate")]
327#[derive(Clone, Debug, Eq, PartialEq)]
328pub struct ComparePredicate {
329    pub(crate) field: String,
330    pub(crate) op: CompareOp,
331    pub(crate) value: Value,
332    pub(crate) coercion: CoercionSpec,
333}
334
335impl ComparePredicate {
336    fn new(field: String, op: CompareOp, value: Value) -> Self {
337        Self {
338            field,
339            op,
340            value,
341            coercion: CoercionSpec::default(),
342        }
343    }
344
345    /// Construct a comparison predicate with an explicit coercion policy.
346    ///
347    /// This is the low-level predicate AST constructor used by SQL lowering,
348    /// generated index predicates, and tests that need a precise coercion
349    /// contract. It does not validate field existence, operator/literal
350    /// compatibility, or schema admissibility; those checks belong to predicate
351    /// validation and query planning.
352    #[must_use]
353    pub fn with_coercion(
354        field: impl Into<String>,
355        op: CompareOp,
356        value: Value,
357        coercion: CoercionId,
358    ) -> Self {
359        Self {
360            field: field.into(),
361            op,
362            value,
363            coercion: CoercionSpec::new(coercion),
364        }
365    }
366
367    /// Build `Eq` comparison.
368    #[must_use]
369    pub fn eq(field: String, value: Value) -> Self {
370        Self::new(field, CompareOp::Eq, value)
371    }
372
373    /// Build `Ne` comparison.
374    #[must_use]
375    pub fn ne(field: String, value: Value) -> Self {
376        Self::new(field, CompareOp::Ne, value)
377    }
378
379    /// Build `Lt` comparison.
380    #[must_use]
381    pub fn lt(field: String, value: Value) -> Self {
382        Self::new(field, CompareOp::Lt, value)
383    }
384
385    /// Build `Lte` comparison.
386    #[must_use]
387    pub fn lte(field: String, value: Value) -> Self {
388        Self::new(field, CompareOp::Lte, value)
389    }
390
391    /// Build `Gt` comparison.
392    #[must_use]
393    pub fn gt(field: String, value: Value) -> Self {
394        Self::new(field, CompareOp::Gt, value)
395    }
396
397    /// Build `Gte` comparison.
398    #[must_use]
399    pub fn gte(field: String, value: Value) -> Self {
400        Self::new(field, CompareOp::Gte, value)
401    }
402
403    /// Build `In` comparison.
404    #[must_use]
405    pub fn in_(field: String, values: Vec<Value>) -> Self {
406        Self::new(field, CompareOp::In, Value::List(values))
407    }
408
409    /// Build `NotIn` comparison.
410    #[must_use]
411    pub fn not_in(field: String, values: Vec<Value>) -> Self {
412        Self::new(field, CompareOp::NotIn, Value::List(values))
413    }
414
415    /// Borrow the compared field name.
416    #[must_use]
417    pub fn field(&self) -> &str {
418        &self.field
419    }
420
421    /// Return the compare operator.
422    #[must_use]
423    pub const fn op(&self) -> CompareOp {
424        self.op
425    }
426
427    /// Borrow the compared literal value.
428    #[must_use]
429    pub const fn value(&self) -> &Value {
430        &self.value
431    }
432
433    /// Borrow the comparison coercion policy.
434    #[must_use]
435    pub const fn coercion(&self) -> &CoercionSpec {
436        &self.coercion
437    }
438}
439
440///
441/// CompareFieldsPredicate
442///
443/// Canonical predicate-owned field-to-field comparison leaf.
444/// This keeps bounded compare expressions on the predicate authority seam
445/// instead of routing them through projection-expression ownership.
446///
447
448#[derive(Clone, Debug, Eq, PartialEq)]
449pub struct CompareFieldsPredicate {
450    pub(crate) left_field: String,
451    pub(crate) op: CompareOp,
452    pub(crate) right_field: String,
453    pub(crate) coercion: CoercionSpec,
454}
455
456impl CompareFieldsPredicate {
457    fn canonicalize_symmetric_fields(
458        op: CompareOp,
459        left_field: String,
460        right_field: String,
461    ) -> (String, String) {
462        if op.is_equality_family() && left_field < right_field {
463            (right_field, left_field)
464        } else {
465            (left_field, right_field)
466        }
467    }
468
469    fn new(left_field: String, op: CompareOp, right_field: String) -> Self {
470        let (left_field, right_field) =
471            Self::canonicalize_symmetric_fields(op, left_field, right_field);
472
473        Self {
474            left_field,
475            op,
476            right_field,
477            coercion: CoercionSpec::default(),
478        }
479    }
480
481    /// Construct a field-to-field comparison predicate with an explicit
482    /// coercion policy.
483    ///
484    /// This low-level constructor preserves the provided comparison contract
485    /// and only canonicalizes symmetric equality-family field order. It does
486    /// not validate that the operator is field-comparison-admissible for a
487    /// schema; that remains a validation/planning responsibility.
488    #[must_use]
489    pub fn with_coercion(
490        left_field: impl Into<String>,
491        op: CompareOp,
492        right_field: impl Into<String>,
493        coercion: CoercionId,
494    ) -> Self {
495        let (left_field, right_field) =
496            Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
497
498        Self {
499            left_field,
500            op,
501            right_field,
502            coercion: CoercionSpec::new(coercion),
503        }
504    }
505
506    /// Build `Eq` field-to-field comparison.
507    #[must_use]
508    pub fn eq(left_field: String, right_field: String) -> Self {
509        Self::new(left_field, CompareOp::Eq, right_field)
510    }
511
512    /// Build `Ne` field-to-field comparison.
513    #[must_use]
514    pub fn ne(left_field: String, right_field: String) -> Self {
515        Self::new(left_field, CompareOp::Ne, right_field)
516    }
517
518    /// Build `Lt` field-to-field comparison.
519    #[must_use]
520    pub fn lt(left_field: String, right_field: String) -> Self {
521        Self::new(left_field, CompareOp::Lt, right_field)
522    }
523
524    /// Build `Lte` field-to-field comparison.
525    #[must_use]
526    pub fn lte(left_field: String, right_field: String) -> Self {
527        Self::new(left_field, CompareOp::Lte, right_field)
528    }
529
530    /// Build `Gt` field-to-field comparison.
531    #[must_use]
532    pub fn gt(left_field: String, right_field: String) -> Self {
533        Self::new(left_field, CompareOp::Gt, right_field)
534    }
535
536    /// Build `Gte` field-to-field comparison.
537    #[must_use]
538    pub fn gte(left_field: String, right_field: String) -> Self {
539        Self::new(left_field, CompareOp::Gte, right_field)
540    }
541
542    /// Borrow the left compared field name.
543    #[must_use]
544    pub fn left_field(&self) -> &str {
545        &self.left_field
546    }
547
548    /// Return the compare operator.
549    #[must_use]
550    pub const fn op(&self) -> CompareOp {
551        self.op
552    }
553
554    /// Borrow the right compared field name.
555    #[must_use]
556    pub fn right_field(&self) -> &str {
557        &self.right_field
558    }
559
560    /// Borrow the comparison coercion policy.
561    #[must_use]
562    pub const fn coercion(&self) -> &CoercionSpec {
563        &self.coercion
564    }
565}
566
567#[cfg_attr(
568    doc,
569    doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
570)]
571#[derive(Clone, Debug, Eq, PartialEq)]
572pub enum UnsupportedQueryFeature {
573    MapPredicate { field: String },
574}
575
576///
577/// TESTS
578///
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    #[test]
585    fn compare_predicate_builders_preserve_operator_shape() {
586        assert_eq!(
587            Predicate::gt("age".to_string(), Value::Nat64(7)),
588            Predicate::Compare(ComparePredicate::gt("age".to_string(), Value::Nat64(7))),
589        );
590    }
591}