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 the operator that preserves semantics when the two operands are swapped.
235    #[must_use]
236    pub const fn flipped(self) -> Self {
237        match self {
238            Self::Eq => Self::Eq,
239            Self::Ne => Self::Ne,
240            Self::Lt => Self::Gt,
241            Self::Lte => Self::Gte,
242            Self::Gt => Self::Lt,
243            Self::Gte => Self::Lte,
244            Self::In => Self::In,
245            Self::NotIn => Self::NotIn,
246            Self::Contains => Self::Contains,
247            Self::StartsWith => Self::StartsWith,
248            Self::EndsWith => Self::EndsWith,
249        }
250    }
251}
252
253#[cfg_attr(doc, doc = "ComparePredicate")]
254#[derive(Clone, Debug, Eq, PartialEq)]
255pub struct ComparePredicate {
256    pub(crate) field: String,
257    pub(crate) op: CompareOp,
258    pub(crate) value: Value,
259    pub(crate) coercion: CoercionSpec,
260}
261
262impl ComparePredicate {
263    fn new(field: String, op: CompareOp, value: Value) -> Self {
264        Self {
265            field,
266            op,
267            value,
268            coercion: CoercionSpec::default(),
269        }
270    }
271
272    /// Construct a comparison predicate with an explicit coercion policy.
273    #[must_use]
274    pub fn with_coercion(
275        field: impl Into<String>,
276        op: CompareOp,
277        value: Value,
278        coercion: CoercionId,
279    ) -> Self {
280        Self {
281            field: field.into(),
282            op,
283            value,
284            coercion: CoercionSpec::new(coercion),
285        }
286    }
287
288    /// Build `Eq` comparison.
289    #[must_use]
290    pub fn eq(field: String, value: Value) -> Self {
291        Self::new(field, CompareOp::Eq, value)
292    }
293
294    /// Build `Ne` comparison.
295    #[must_use]
296    pub fn ne(field: String, value: Value) -> Self {
297        Self::new(field, CompareOp::Ne, value)
298    }
299
300    /// Build `Lt` comparison.
301    #[must_use]
302    pub fn lt(field: String, value: Value) -> Self {
303        Self::new(field, CompareOp::Lt, value)
304    }
305
306    /// Build `Lte` comparison.
307    #[must_use]
308    pub fn lte(field: String, value: Value) -> Self {
309        Self::new(field, CompareOp::Lte, value)
310    }
311
312    /// Build `Gt` comparison.
313    #[must_use]
314    pub fn gt(field: String, value: Value) -> Self {
315        Self::new(field, CompareOp::Gt, value)
316    }
317
318    /// Build `Gte` comparison.
319    #[must_use]
320    pub fn gte(field: String, value: Value) -> Self {
321        Self::new(field, CompareOp::Gte, value)
322    }
323
324    /// Build `In` comparison.
325    #[must_use]
326    pub fn in_(field: String, values: Vec<Value>) -> Self {
327        Self::new(field, CompareOp::In, Value::List(values))
328    }
329
330    /// Build `NotIn` comparison.
331    #[must_use]
332    pub fn not_in(field: String, values: Vec<Value>) -> Self {
333        Self::new(field, CompareOp::NotIn, Value::List(values))
334    }
335
336    /// Borrow the compared field name.
337    #[must_use]
338    pub fn field(&self) -> &str {
339        &self.field
340    }
341
342    /// Return the compare operator.
343    #[must_use]
344    pub const fn op(&self) -> CompareOp {
345        self.op
346    }
347
348    /// Borrow the compared literal value.
349    #[must_use]
350    pub const fn value(&self) -> &Value {
351        &self.value
352    }
353
354    /// Borrow the comparison coercion policy.
355    #[must_use]
356    pub const fn coercion(&self) -> &CoercionSpec {
357        &self.coercion
358    }
359}
360
361///
362/// CompareFieldsPredicate
363///
364/// Canonical predicate-owned field-to-field comparison leaf.
365/// This keeps bounded compare expressions on the predicate authority seam
366/// instead of routing them through projection-expression ownership.
367///
368
369#[derive(Clone, Debug, Eq, PartialEq)]
370pub struct CompareFieldsPredicate {
371    pub(crate) left_field: String,
372    pub(crate) op: CompareOp,
373    pub(crate) right_field: String,
374    pub(crate) coercion: CoercionSpec,
375}
376
377impl CompareFieldsPredicate {
378    fn canonicalize_symmetric_fields(
379        op: CompareOp,
380        left_field: String,
381        right_field: String,
382    ) -> (String, String) {
383        if matches!(op, CompareOp::Eq | CompareOp::Ne) && left_field < right_field {
384            (right_field, left_field)
385        } else {
386            (left_field, right_field)
387        }
388    }
389
390    fn new(left_field: String, op: CompareOp, right_field: String) -> Self {
391        let (left_field, right_field) =
392            Self::canonicalize_symmetric_fields(op, left_field, right_field);
393
394        Self {
395            left_field,
396            op,
397            right_field,
398            coercion: CoercionSpec::default(),
399        }
400    }
401
402    /// Construct a field-to-field comparison predicate with an explicit
403    /// coercion policy.
404    #[must_use]
405    pub fn with_coercion(
406        left_field: impl Into<String>,
407        op: CompareOp,
408        right_field: impl Into<String>,
409        coercion: CoercionId,
410    ) -> Self {
411        let (left_field, right_field) =
412            Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
413
414        Self {
415            left_field,
416            op,
417            right_field,
418            coercion: CoercionSpec::new(coercion),
419        }
420    }
421
422    /// Build `Eq` field-to-field comparison.
423    #[must_use]
424    pub fn eq(left_field: String, right_field: String) -> Self {
425        Self::new(left_field, CompareOp::Eq, right_field)
426    }
427
428    /// Build `Ne` field-to-field comparison.
429    #[must_use]
430    pub fn ne(left_field: String, right_field: String) -> Self {
431        Self::new(left_field, CompareOp::Ne, right_field)
432    }
433
434    /// Build `Lt` field-to-field comparison.
435    #[must_use]
436    pub fn lt(left_field: String, right_field: String) -> Self {
437        Self::new(left_field, CompareOp::Lt, right_field)
438    }
439
440    /// Build `Lte` field-to-field comparison.
441    #[must_use]
442    pub fn lte(left_field: String, right_field: String) -> Self {
443        Self::new(left_field, CompareOp::Lte, right_field)
444    }
445
446    /// Build `Gt` field-to-field comparison.
447    #[must_use]
448    pub fn gt(left_field: String, right_field: String) -> Self {
449        Self::new(left_field, CompareOp::Gt, right_field)
450    }
451
452    /// Build `Gte` field-to-field comparison.
453    #[must_use]
454    pub fn gte(left_field: String, right_field: String) -> Self {
455        Self::new(left_field, CompareOp::Gte, right_field)
456    }
457
458    /// Borrow the left compared field name.
459    #[must_use]
460    pub fn left_field(&self) -> &str {
461        &self.left_field
462    }
463
464    /// Return the compare operator.
465    #[must_use]
466    pub const fn op(&self) -> CompareOp {
467        self.op
468    }
469
470    /// Borrow the right compared field name.
471    #[must_use]
472    pub fn right_field(&self) -> &str {
473        &self.right_field
474    }
475
476    /// Borrow the comparison coercion policy.
477    #[must_use]
478    pub const fn coercion(&self) -> &CoercionSpec {
479        &self.coercion
480    }
481}
482
483#[cfg_attr(
484    doc,
485    doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
486)]
487#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
488pub enum UnsupportedQueryFeature {
489    #[error("map field '{field}' is not queryable; use scalar/indexed fields or list entries")]
490    MapPredicate { field: String },
491}