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
172impl BitAnd for Predicate {
173    type Output = Self;
174
175    fn bitand(self, rhs: Self) -> Self::Output {
176        Self::And(vec![self, rhs])
177    }
178}
179
180impl BitAnd for &Predicate {
181    type Output = Predicate;
182
183    fn bitand(self, rhs: Self) -> Self::Output {
184        Predicate::And(vec![self.clone(), rhs.clone()])
185    }
186}
187
188impl BitOr for Predicate {
189    type Output = Self;
190
191    fn bitor(self, rhs: Self) -> Self::Output {
192        Self::Or(vec![self, rhs])
193    }
194}
195
196impl BitOr for &Predicate {
197    type Output = Predicate;
198
199    fn bitor(self, rhs: Self) -> Self::Output {
200        Predicate::Or(vec![self.clone(), rhs.clone()])
201    }
202}
203
204#[cfg_attr(
205    doc,
206    doc = "Neutral predicate model consumed by executor/index layers."
207)]
208pub(crate) type PredicateExecutionModel = Predicate;
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
235#[cfg_attr(doc, doc = "ComparePredicate")]
236#[derive(Clone, Debug, Eq, PartialEq)]
237pub struct ComparePredicate {
238    pub(crate) field: String,
239    pub(crate) op: CompareOp,
240    pub(crate) value: Value,
241    pub(crate) coercion: CoercionSpec,
242}
243
244impl ComparePredicate {
245    fn new(field: String, op: CompareOp, value: Value) -> Self {
246        Self {
247            field,
248            op,
249            value,
250            coercion: CoercionSpec::default(),
251        }
252    }
253
254    /// Construct a comparison predicate with an explicit coercion policy.
255    #[must_use]
256    pub fn with_coercion(
257        field: impl Into<String>,
258        op: CompareOp,
259        value: Value,
260        coercion: CoercionId,
261    ) -> Self {
262        Self {
263            field: field.into(),
264            op,
265            value,
266            coercion: CoercionSpec::new(coercion),
267        }
268    }
269
270    /// Build `Eq` comparison.
271    #[must_use]
272    pub fn eq(field: String, value: Value) -> Self {
273        Self::new(field, CompareOp::Eq, value)
274    }
275
276    /// Build `Ne` comparison.
277    #[must_use]
278    pub fn ne(field: String, value: Value) -> Self {
279        Self::new(field, CompareOp::Ne, value)
280    }
281
282    /// Build `Lt` comparison.
283    #[must_use]
284    pub fn lt(field: String, value: Value) -> Self {
285        Self::new(field, CompareOp::Lt, value)
286    }
287
288    /// Build `Lte` comparison.
289    #[must_use]
290    pub fn lte(field: String, value: Value) -> Self {
291        Self::new(field, CompareOp::Lte, value)
292    }
293
294    /// Build `Gt` comparison.
295    #[must_use]
296    pub fn gt(field: String, value: Value) -> Self {
297        Self::new(field, CompareOp::Gt, value)
298    }
299
300    /// Build `Gte` comparison.
301    #[must_use]
302    pub fn gte(field: String, value: Value) -> Self {
303        Self::new(field, CompareOp::Gte, value)
304    }
305
306    /// Build `In` comparison.
307    #[must_use]
308    pub fn in_(field: String, values: Vec<Value>) -> Self {
309        Self::new(field, CompareOp::In, Value::List(values))
310    }
311
312    /// Build `NotIn` comparison.
313    #[must_use]
314    pub fn not_in(field: String, values: Vec<Value>) -> Self {
315        Self::new(field, CompareOp::NotIn, Value::List(values))
316    }
317
318    /// Borrow the compared field name.
319    #[must_use]
320    pub fn field(&self) -> &str {
321        &self.field
322    }
323
324    /// Return the compare operator.
325    #[must_use]
326    pub const fn op(&self) -> CompareOp {
327        self.op
328    }
329
330    /// Borrow the compared literal value.
331    #[must_use]
332    pub const fn value(&self) -> &Value {
333        &self.value
334    }
335
336    /// Borrow the comparison coercion policy.
337    #[must_use]
338    pub const fn coercion(&self) -> &CoercionSpec {
339        &self.coercion
340    }
341}
342
343///
344/// CompareFieldsPredicate
345///
346/// Canonical predicate-owned field-to-field comparison leaf.
347/// This keeps bounded compare expressions on the predicate authority seam
348/// instead of routing them through projection-expression ownership.
349///
350
351#[derive(Clone, Debug, Eq, PartialEq)]
352pub struct CompareFieldsPredicate {
353    pub(crate) left_field: String,
354    pub(crate) op: CompareOp,
355    pub(crate) right_field: String,
356    pub(crate) coercion: CoercionSpec,
357}
358
359impl CompareFieldsPredicate {
360    fn new(left_field: String, op: CompareOp, right_field: String) -> Self {
361        Self {
362            left_field,
363            op,
364            right_field,
365            coercion: CoercionSpec::default(),
366        }
367    }
368
369    /// Construct a field-to-field comparison predicate with an explicit
370    /// coercion policy.
371    #[must_use]
372    pub fn with_coercion(
373        left_field: impl Into<String>,
374        op: CompareOp,
375        right_field: impl Into<String>,
376        coercion: CoercionId,
377    ) -> Self {
378        Self {
379            left_field: left_field.into(),
380            op,
381            right_field: right_field.into(),
382            coercion: CoercionSpec::new(coercion),
383        }
384    }
385
386    /// Build `Eq` field-to-field comparison.
387    #[must_use]
388    pub fn eq(left_field: String, right_field: String) -> Self {
389        Self::new(left_field, CompareOp::Eq, right_field)
390    }
391
392    /// Build `Ne` field-to-field comparison.
393    #[must_use]
394    pub fn ne(left_field: String, right_field: String) -> Self {
395        Self::new(left_field, CompareOp::Ne, right_field)
396    }
397
398    /// Build `Lt` field-to-field comparison.
399    #[must_use]
400    pub fn lt(left_field: String, right_field: String) -> Self {
401        Self::new(left_field, CompareOp::Lt, right_field)
402    }
403
404    /// Build `Lte` field-to-field comparison.
405    #[must_use]
406    pub fn lte(left_field: String, right_field: String) -> Self {
407        Self::new(left_field, CompareOp::Lte, right_field)
408    }
409
410    /// Build `Gt` field-to-field comparison.
411    #[must_use]
412    pub fn gt(left_field: String, right_field: String) -> Self {
413        Self::new(left_field, CompareOp::Gt, right_field)
414    }
415
416    /// Build `Gte` field-to-field comparison.
417    #[must_use]
418    pub fn gte(left_field: String, right_field: String) -> Self {
419        Self::new(left_field, CompareOp::Gte, right_field)
420    }
421
422    /// Borrow the left compared field name.
423    #[must_use]
424    pub fn left_field(&self) -> &str {
425        &self.left_field
426    }
427
428    /// Return the compare operator.
429    #[must_use]
430    pub const fn op(&self) -> CompareOp {
431        self.op
432    }
433
434    /// Borrow the right compared field name.
435    #[must_use]
436    pub fn right_field(&self) -> &str {
437        &self.right_field
438    }
439
440    /// Borrow the comparison coercion policy.
441    #[must_use]
442    pub const fn coercion(&self) -> &CoercionSpec {
443        &self.coercion
444    }
445}
446
447#[cfg_attr(
448    doc,
449    doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
450)]
451#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
452pub enum UnsupportedQueryFeature {
453    #[error("map field '{field}' is not queryable; use scalar/indexed fields or list entries")]
454    MapPredicate { field: String },
455}