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 `field IN values`.
88    #[must_use]
89    pub fn in_(field: String, values: Vec<Value>) -> Self {
90        Self::Compare(ComparePredicate::in_(field, values))
91    }
92
93    /// Compare `field NOT IN values`.
94    #[must_use]
95    pub fn not_in(field: String, values: Vec<Value>) -> Self {
96        Self::Compare(ComparePredicate::not_in(field, values))
97    }
98
99    /// Compare `field IS NOT NULL`.
100    #[must_use]
101    pub const fn is_not_null(field: String) -> Self {
102        Self::IsNotNull { field }
103    }
104
105    /// Compare `field BETWEEN lower AND upper`.
106    #[must_use]
107    pub fn between(field: String, lower: Value, upper: Value) -> Self {
108        Self::And(vec![
109            Self::gte(field.clone(), lower),
110            Self::lte(field, upper),
111        ])
112    }
113
114    /// Compare `field NOT BETWEEN lower AND upper`.
115    #[must_use]
116    pub fn not_between(field: String, lower: Value, upper: Value) -> Self {
117        Self::Or(vec![Self::lt(field.clone(), lower), Self::gt(field, upper)])
118    }
119}
120
121impl BitAnd for Predicate {
122    type Output = Self;
123
124    fn bitand(self, rhs: Self) -> Self::Output {
125        Self::And(vec![self, rhs])
126    }
127}
128
129impl BitAnd for &Predicate {
130    type Output = Predicate;
131
132    fn bitand(self, rhs: Self) -> Self::Output {
133        Predicate::And(vec![self.clone(), rhs.clone()])
134    }
135}
136
137impl BitOr for Predicate {
138    type Output = Self;
139
140    fn bitor(self, rhs: Self) -> Self::Output {
141        Self::Or(vec![self, rhs])
142    }
143}
144
145impl BitOr for &Predicate {
146    type Output = Predicate;
147
148    fn bitor(self, rhs: Self) -> Self::Output {
149        Predicate::Or(vec![self.clone(), rhs.clone()])
150    }
151}
152
153#[cfg_attr(doc, doc = "CompareOp")]
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155#[repr(u8)]
156pub enum CompareOp {
157    Eq = 0x01,
158    Ne = 0x02,
159    Lt = 0x03,
160    Lte = 0x04,
161    Gt = 0x05,
162    Gte = 0x06,
163    In = 0x07,
164    NotIn = 0x08,
165    Contains = 0x09,
166    StartsWith = 0x0a,
167    EndsWith = 0x0b,
168}
169
170impl CompareOp {
171    /// Return the stable wire tag for this compare operator.
172    #[must_use]
173    pub const fn tag(self) -> u8 {
174        self as u8
175    }
176
177    /// Return whether this operator is one symmetric equality-style compare.
178    #[must_use]
179    pub const fn is_equality_family(self) -> bool {
180        matches!(self, Self::Eq | Self::Ne)
181    }
182
183    /// Return whether this operator is one ordered range-bound compare.
184    #[must_use]
185    pub const fn is_ordering_family(self) -> bool {
186        matches!(self, Self::Lt | Self::Lte | Self::Gt | Self::Gte)
187    }
188
189    /// Return whether this operator is one list-membership compare.
190    #[must_use]
191    pub const fn is_membership_family(self) -> bool {
192        matches!(self, Self::In | Self::NotIn)
193    }
194
195    /// Return whether this operator is one containment compare.
196    #[must_use]
197    pub const fn is_contains_family(self) -> bool {
198        matches!(self, Self::Contains)
199    }
200
201    /// Return whether this operator is one text-pattern compare.
202    #[must_use]
203    pub const fn is_text_pattern_family(self) -> bool {
204        matches!(self, Self::StartsWith | Self::EndsWith)
205    }
206
207    /// Return whether this operator supports direct field-to-field comparison.
208    #[must_use]
209    pub const fn supports_field_compare(self) -> bool {
210        self.is_equality_family() || self.is_ordering_family()
211    }
212
213    /// Return whether this operator contributes one lower bound and whether it
214    /// is inclusive when present.
215    #[must_use]
216    pub const fn lower_bound_inclusive(self) -> Option<bool> {
217        match self {
218            Self::Gt => Some(false),
219            Self::Gte => Some(true),
220            Self::Eq
221            | Self::Ne
222            | Self::Lt
223            | Self::Lte
224            | Self::In
225            | Self::NotIn
226            | Self::Contains
227            | Self::StartsWith
228            | Self::EndsWith => None,
229        }
230    }
231
232    /// Return whether this operator contributes one upper bound and whether it
233    /// is inclusive when present.
234    #[must_use]
235    pub const fn upper_bound_inclusive(self) -> Option<bool> {
236        match self {
237            Self::Lt => Some(false),
238            Self::Lte => Some(true),
239            Self::Eq
240            | Self::Ne
241            | Self::Gt
242            | Self::Gte
243            | Self::In
244            | Self::NotIn
245            | Self::Contains
246            | Self::StartsWith
247            | Self::EndsWith => None,
248        }
249    }
250
251    /// Return the operator that preserves semantics when the two operands are swapped.
252    #[must_use]
253    pub const fn flipped(self) -> Self {
254        match self {
255            Self::Eq => Self::Eq,
256            Self::Ne => Self::Ne,
257            Self::Lt => Self::Gt,
258            Self::Lte => Self::Gte,
259            Self::Gt => Self::Lt,
260            Self::Gte => Self::Lte,
261            Self::In => Self::In,
262            Self::NotIn => Self::NotIn,
263            Self::Contains => Self::Contains,
264            Self::StartsWith => Self::StartsWith,
265            Self::EndsWith => Self::EndsWith,
266        }
267    }
268}
269
270#[cfg_attr(doc, doc = "ComparePredicate")]
271#[derive(Clone, Debug, Eq, PartialEq)]
272pub struct ComparePredicate {
273    pub(crate) field: String,
274    pub(crate) op: CompareOp,
275    pub(crate) value: Value,
276    pub(crate) coercion: CoercionSpec,
277}
278
279impl ComparePredicate {
280    fn new(field: String, op: CompareOp, value: Value) -> Self {
281        Self {
282            field,
283            op,
284            value,
285            coercion: CoercionSpec::default(),
286        }
287    }
288
289    /// Construct a comparison predicate with an explicit coercion policy.
290    ///
291    /// This is the low-level predicate AST constructor used by SQL lowering,
292    /// generated index predicates, and tests that need a precise coercion
293    /// contract. It does not validate field existence, operator/literal
294    /// compatibility, or schema admissibility; those checks belong to predicate
295    /// validation and query planning.
296    #[must_use]
297    pub fn with_coercion(
298        field: impl Into<String>,
299        op: CompareOp,
300        value: Value,
301        coercion: CoercionId,
302    ) -> Self {
303        Self {
304            field: field.into(),
305            op,
306            value,
307            coercion: CoercionSpec::new(coercion),
308        }
309    }
310
311    /// Build `Eq` comparison.
312    #[must_use]
313    pub fn eq(field: String, value: Value) -> Self {
314        Self::new(field, CompareOp::Eq, value)
315    }
316
317    /// Build `Ne` comparison.
318    #[must_use]
319    pub fn ne(field: String, value: Value) -> Self {
320        Self::new(field, CompareOp::Ne, value)
321    }
322
323    /// Build `Lt` comparison.
324    #[must_use]
325    pub fn lt(field: String, value: Value) -> Self {
326        Self::new(field, CompareOp::Lt, value)
327    }
328
329    /// Build `Lte` comparison.
330    #[must_use]
331    pub fn lte(field: String, value: Value) -> Self {
332        Self::new(field, CompareOp::Lte, value)
333    }
334
335    /// Build `Gt` comparison.
336    #[must_use]
337    pub fn gt(field: String, value: Value) -> Self {
338        Self::new(field, CompareOp::Gt, value)
339    }
340
341    /// Build `Gte` comparison.
342    #[must_use]
343    pub fn gte(field: String, value: Value) -> Self {
344        Self::new(field, CompareOp::Gte, value)
345    }
346
347    /// Build `In` comparison.
348    #[must_use]
349    pub fn in_(field: String, values: Vec<Value>) -> Self {
350        Self::new(field, CompareOp::In, Value::List(values))
351    }
352
353    /// Build `NotIn` comparison.
354    #[must_use]
355    pub fn not_in(field: String, values: Vec<Value>) -> Self {
356        Self::new(field, CompareOp::NotIn, Value::List(values))
357    }
358
359    /// Borrow the compared field name.
360    #[must_use]
361    pub fn field(&self) -> &str {
362        &self.field
363    }
364
365    /// Return the compare operator.
366    #[must_use]
367    pub const fn op(&self) -> CompareOp {
368        self.op
369    }
370
371    /// Borrow the compared literal value.
372    #[must_use]
373    pub const fn value(&self) -> &Value {
374        &self.value
375    }
376
377    /// Borrow the comparison coercion policy.
378    #[must_use]
379    pub const fn coercion(&self) -> &CoercionSpec {
380        &self.coercion
381    }
382}
383
384///
385/// CompareFieldsPredicate
386///
387/// Canonical predicate-owned field-to-field comparison leaf.
388/// This keeps bounded compare expressions on the predicate authority seam
389/// instead of routing them through projection-expression ownership.
390///
391
392#[derive(Clone, Debug, Eq, PartialEq)]
393pub struct CompareFieldsPredicate {
394    pub(crate) left_field: String,
395    pub(crate) op: CompareOp,
396    pub(crate) right_field: String,
397    pub(crate) coercion: CoercionSpec,
398}
399
400impl CompareFieldsPredicate {
401    fn canonicalize_symmetric_fields(
402        op: CompareOp,
403        left_field: String,
404        right_field: String,
405    ) -> (String, String) {
406        if op.is_equality_family() && left_field < right_field {
407            (right_field, left_field)
408        } else {
409            (left_field, right_field)
410        }
411    }
412
413    /// Construct a field-to-field comparison predicate with an explicit
414    /// coercion policy.
415    ///
416    /// This low-level constructor preserves the provided comparison contract
417    /// and only canonicalizes symmetric equality-family field order. It does
418    /// not validate that the operator is field-comparison-admissible for a
419    /// schema; that remains a validation/planning responsibility.
420    #[must_use]
421    pub fn with_coercion(
422        left_field: impl Into<String>,
423        op: CompareOp,
424        right_field: impl Into<String>,
425        coercion: CoercionId,
426    ) -> Self {
427        let (left_field, right_field) =
428            Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
429
430        Self {
431            left_field,
432            op,
433            right_field,
434            coercion: CoercionSpec::new(coercion),
435        }
436    }
437
438    /// Borrow the left compared field name.
439    #[must_use]
440    pub fn left_field(&self) -> &str {
441        &self.left_field
442    }
443
444    /// Return the compare operator.
445    #[must_use]
446    pub const fn op(&self) -> CompareOp {
447        self.op
448    }
449
450    /// Borrow the right compared field name.
451    #[must_use]
452    pub fn right_field(&self) -> &str {
453        &self.right_field
454    }
455
456    /// Borrow the comparison coercion policy.
457    #[must_use]
458    pub const fn coercion(&self) -> &CoercionSpec {
459        &self.coercion
460    }
461}
462
463///
464/// TESTS
465///
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn compare_predicate_builders_preserve_operator_shape() {
473        assert_eq!(
474            Predicate::gt("age".to_string(), Value::Nat64(7)),
475            Predicate::Compare(ComparePredicate::gt("age".to_string(), Value::Nat64(7))),
476        );
477    }
478}