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