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    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
115impl BitAnd for Predicate {
116    type Output = Self;
117
118    fn bitand(self, rhs: Self) -> Self::Output {
119        Self::And(vec![self, rhs])
120    }
121}
122
123impl BitAnd for &Predicate {
124    type Output = Predicate;
125
126    fn bitand(self, rhs: Self) -> Self::Output {
127        Predicate::And(vec![self.clone(), rhs.clone()])
128    }
129}
130
131impl BitOr for Predicate {
132    type Output = Self;
133
134    fn bitor(self, rhs: Self) -> Self::Output {
135        Self::Or(vec![self, rhs])
136    }
137}
138
139impl BitOr for &Predicate {
140    type Output = Predicate;
141
142    fn bitor(self, rhs: Self) -> Self::Output {
143        Predicate::Or(vec![self.clone(), rhs.clone()])
144    }
145}
146
147#[cfg_attr(
148    doc,
149    doc = "Neutral predicate model consumed by executor/index layers."
150)]
151pub(crate) type PredicateExecutionModel = Predicate;
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
178#[cfg_attr(doc, doc = "ComparePredicate")]
179#[derive(Clone, Debug, Eq, PartialEq)]
180pub struct ComparePredicate {
181    pub(crate) field: String,
182    pub(crate) op: CompareOp,
183    pub(crate) value: Value,
184    pub(crate) coercion: CoercionSpec,
185}
186
187impl ComparePredicate {
188    fn new(field: String, op: CompareOp, value: Value) -> Self {
189        Self {
190            field,
191            op,
192            value,
193            coercion: CoercionSpec::default(),
194        }
195    }
196
197    /// Construct a comparison predicate with an explicit coercion policy.
198    #[must_use]
199    pub fn with_coercion(
200        field: impl Into<String>,
201        op: CompareOp,
202        value: Value,
203        coercion: CoercionId,
204    ) -> Self {
205        Self {
206            field: field.into(),
207            op,
208            value,
209            coercion: CoercionSpec::new(coercion),
210        }
211    }
212
213    /// Build `Eq` comparison.
214    #[must_use]
215    pub fn eq(field: String, value: Value) -> Self {
216        Self::new(field, CompareOp::Eq, value)
217    }
218
219    /// Build `Ne` comparison.
220    #[must_use]
221    pub fn ne(field: String, value: Value) -> Self {
222        Self::new(field, CompareOp::Ne, value)
223    }
224
225    /// Build `Lt` comparison.
226    #[must_use]
227    pub fn lt(field: String, value: Value) -> Self {
228        Self::new(field, CompareOp::Lt, value)
229    }
230
231    /// Build `Lte` comparison.
232    #[must_use]
233    pub fn lte(field: String, value: Value) -> Self {
234        Self::new(field, CompareOp::Lte, value)
235    }
236
237    /// Build `Gt` comparison.
238    #[must_use]
239    pub fn gt(field: String, value: Value) -> Self {
240        Self::new(field, CompareOp::Gt, value)
241    }
242
243    /// Build `Gte` comparison.
244    #[must_use]
245    pub fn gte(field: String, value: Value) -> Self {
246        Self::new(field, CompareOp::Gte, value)
247    }
248
249    /// Build `In` comparison.
250    #[must_use]
251    pub fn in_(field: String, values: Vec<Value>) -> Self {
252        Self::new(field, CompareOp::In, Value::List(values))
253    }
254
255    /// Build `NotIn` comparison.
256    #[must_use]
257    pub fn not_in(field: String, values: Vec<Value>) -> Self {
258        Self::new(field, CompareOp::NotIn, Value::List(values))
259    }
260
261    /// Borrow the compared field name.
262    #[must_use]
263    pub fn field(&self) -> &str {
264        &self.field
265    }
266
267    /// Return the compare operator.
268    #[must_use]
269    pub const fn op(&self) -> CompareOp {
270        self.op
271    }
272
273    /// Borrow the compared literal value.
274    #[must_use]
275    pub const fn value(&self) -> &Value {
276        &self.value
277    }
278
279    /// Borrow the comparison coercion policy.
280    #[must_use]
281    pub const fn coercion(&self) -> &CoercionSpec {
282        &self.coercion
283    }
284}
285
286#[cfg_attr(
287    doc,
288    doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
289)]
290#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
291pub enum UnsupportedQueryFeature {
292    #[error("map field '{field}' is not queryable; use scalar/indexed fields or list entries")]
293    MapPredicate { field: String },
294}