Skip to main content

icydb_core/db/query/builder/
field.rs

1//! Module: query::builder::field
2//! Responsibility: zero-allocation field references and field-scoped predicate builders.
3//! Does not own: predicate validation or runtime execution.
4//! Boundary: ergonomic query-builder surface for field expressions.
5
6use crate::{
7    db::predicate::{CoercionId, CompareFieldsPredicate, CompareOp, ComparePredicate, Predicate},
8    traits::FieldValue,
9    value::Value,
10};
11use derive_more::Deref;
12
13///
14/// FieldRef
15///
16/// Zero-cost wrapper around a static field name used in predicates.
17/// Enables method-based predicate builders without allocating.
18/// Carries only a `&'static str` and derefs to `str`.
19///
20
21#[derive(Clone, Copy, Deref, Eq, Hash, PartialEq)]
22pub struct FieldRef(&'static str);
23
24impl FieldRef {
25    /// Create a new field reference.
26    #[must_use]
27    pub const fn new(name: &'static str) -> Self {
28        Self(name)
29    }
30
31    /// Return the underlying field name.
32    #[must_use]
33    pub const fn as_str(self) -> &'static str {
34        self.0
35    }
36
37    // ------------------------------------------------------------------
38    // Internal helpers
39    // ------------------------------------------------------------------
40
41    /// Internal comparison predicate builder.
42    fn cmp(self, op: CompareOp, value: impl FieldValue, coercion: CoercionId) -> Predicate {
43        Predicate::Compare(ComparePredicate::with_coercion(
44            self.0,
45            op,
46            value.to_value(),
47            coercion,
48        ))
49    }
50
51    /// Internal field-to-field comparison predicate builder.
52    fn cmp_field(self, op: CompareOp, other: impl AsRef<str>, coercion: CoercionId) -> Predicate {
53        Predicate::CompareFields(CompareFieldsPredicate::with_coercion(
54            self.0,
55            op,
56            other.as_ref(),
57            coercion,
58        ))
59    }
60
61    // ------------------------------------------------------------------
62    // Comparison predicates
63    // ------------------------------------------------------------------
64
65    /// Strict equality comparison (no coercion).
66    #[must_use]
67    pub fn eq(self, value: impl FieldValue) -> Predicate {
68        self.cmp(CompareOp::Eq, value, CoercionId::Strict)
69    }
70
71    /// Case-insensitive equality for text fields.
72    #[must_use]
73    pub fn text_eq_ci(self, value: impl FieldValue) -> Predicate {
74        self.cmp(CompareOp::Eq, value, CoercionId::TextCasefold)
75    }
76
77    /// Strict inequality comparison.
78    #[must_use]
79    pub fn ne(self, value: impl FieldValue) -> Predicate {
80        self.cmp(CompareOp::Ne, value, CoercionId::Strict)
81    }
82
83    /// Less-than comparison with numeric widening.
84    #[must_use]
85    pub fn lt(self, value: impl FieldValue) -> Predicate {
86        self.cmp(CompareOp::Lt, value, CoercionId::NumericWiden)
87    }
88
89    /// Less-than-or-equal comparison with numeric widening.
90    #[must_use]
91    pub fn lte(self, value: impl FieldValue) -> Predicate {
92        self.cmp(CompareOp::Lte, value, CoercionId::NumericWiden)
93    }
94
95    /// Greater-than comparison with numeric widening.
96    #[must_use]
97    pub fn gt(self, value: impl FieldValue) -> Predicate {
98        self.cmp(CompareOp::Gt, value, CoercionId::NumericWiden)
99    }
100
101    /// Greater-than-or-equal comparison with numeric widening.
102    #[must_use]
103    pub fn gte(self, value: impl FieldValue) -> Predicate {
104        self.cmp(CompareOp::Gte, value, CoercionId::NumericWiden)
105    }
106
107    /// Strict equality comparison against another field.
108    #[must_use]
109    pub fn eq_field(self, other: impl AsRef<str>) -> Predicate {
110        self.cmp_field(CompareOp::Eq, other, CoercionId::Strict)
111    }
112
113    /// Strict inequality comparison against another field.
114    #[must_use]
115    pub fn ne_field(self, other: impl AsRef<str>) -> Predicate {
116        self.cmp_field(CompareOp::Ne, other, CoercionId::Strict)
117    }
118
119    /// Less-than comparison against another numeric or text field.
120    #[must_use]
121    pub fn lt_field(self, other: impl AsRef<str>) -> Predicate {
122        self.cmp_field(CompareOp::Lt, other, CoercionId::NumericWiden)
123    }
124
125    /// Less-than-or-equal comparison against another numeric or text field.
126    #[must_use]
127    pub fn lte_field(self, other: impl AsRef<str>) -> Predicate {
128        self.cmp_field(CompareOp::Lte, other, CoercionId::NumericWiden)
129    }
130
131    /// Greater-than comparison against another numeric or text field.
132    #[must_use]
133    pub fn gt_field(self, other: impl AsRef<str>) -> Predicate {
134        self.cmp_field(CompareOp::Gt, other, CoercionId::NumericWiden)
135    }
136
137    /// Greater-than-or-equal comparison against another numeric or text field.
138    #[must_use]
139    pub fn gte_field(self, other: impl AsRef<str>) -> Predicate {
140        self.cmp_field(CompareOp::Gte, other, CoercionId::NumericWiden)
141    }
142
143    /// Membership test against a fixed list (strict).
144    #[must_use]
145    pub fn in_list<I, V>(self, values: I) -> Predicate
146    where
147        I: IntoIterator<Item = V>,
148        V: FieldValue,
149    {
150        Predicate::Compare(ComparePredicate::with_coercion(
151            self.0,
152            CompareOp::In,
153            Value::List(values.into_iter().map(|v| v.to_value()).collect()),
154            CoercionId::Strict,
155        ))
156    }
157
158    // ------------------------------------------------------------------
159    // Structural predicates
160    // ------------------------------------------------------------------
161
162    /// Field is present and explicitly null.
163    #[must_use]
164    pub fn is_null(self) -> Predicate {
165        Predicate::IsNull {
166            field: self.0.to_string(),
167        }
168    }
169
170    /// Field is present and not null.
171    #[must_use]
172    pub fn is_not_null(self) -> Predicate {
173        Predicate::IsNotNull {
174            field: self.0.to_string(),
175        }
176    }
177
178    /// Field is not present at all.
179    #[must_use]
180    pub fn is_missing(self) -> Predicate {
181        Predicate::IsMissing {
182            field: self.0.to_string(),
183        }
184    }
185
186    /// Field is present but empty (collection- or string-specific).
187    #[must_use]
188    pub fn is_empty(self) -> Predicate {
189        Predicate::IsEmpty {
190            field: self.0.to_string(),
191        }
192    }
193
194    /// Field is present and non-empty.
195    #[must_use]
196    pub fn is_not_empty(self) -> Predicate {
197        Predicate::IsNotEmpty {
198            field: self.0.to_string(),
199        }
200    }
201
202    /// Case-sensitive substring match for text fields.
203    #[must_use]
204    pub fn text_contains(self, value: impl FieldValue) -> Predicate {
205        Predicate::TextContains {
206            field: self.0.to_string(),
207            value: value.to_value(),
208        }
209    }
210
211    /// Case-insensitive substring match for text fields.
212    #[must_use]
213    pub fn text_contains_ci(self, value: impl FieldValue) -> Predicate {
214        Predicate::TextContainsCi {
215            field: self.0.to_string(),
216            value: value.to_value(),
217        }
218    }
219
220    /// Case-sensitive prefix match for text fields.
221    #[must_use]
222    pub fn text_starts_with(self, value: impl FieldValue) -> Predicate {
223        self.cmp(CompareOp::StartsWith, value, CoercionId::Strict)
224    }
225
226    /// Case-insensitive prefix match for text fields.
227    #[must_use]
228    pub fn text_starts_with_ci(self, value: impl FieldValue) -> Predicate {
229        self.cmp(CompareOp::StartsWith, value, CoercionId::TextCasefold)
230    }
231
232    /// Inclusive range predicate lowered as `field >= lower AND field <= upper`.
233    #[must_use]
234    pub fn between(self, lower: impl FieldValue, upper: impl FieldValue) -> Predicate {
235        Predicate::and(vec![self.gte(lower), self.lte(upper)])
236    }
237
238    /// Inclusive range predicate against two other fields.
239    #[must_use]
240    pub fn between_fields(self, lower: impl AsRef<str>, upper: impl AsRef<str>) -> Predicate {
241        Predicate::and(vec![self.gte_field(lower), self.lte_field(upper)])
242    }
243
244    /// Exclusive-outside range predicate lowered as `field < lower OR field > upper`.
245    #[must_use]
246    pub fn not_between(self, lower: impl FieldValue, upper: impl FieldValue) -> Predicate {
247        Predicate::or(vec![self.lt(lower), self.gt(upper)])
248    }
249
250    /// Exclusive-outside range predicate against two other fields.
251    #[must_use]
252    pub fn not_between_fields(self, lower: impl AsRef<str>, upper: impl AsRef<str>) -> Predicate {
253        Predicate::or(vec![self.lt_field(lower), self.gt_field(upper)])
254    }
255}
256
257// ----------------------------------------------------------------------
258// Boundary traits
259// ----------------------------------------------------------------------
260
261impl AsRef<str> for FieldRef {
262    fn as_ref(&self) -> &str {
263        self.0
264    }
265}
266
267///
268/// TESTS
269///
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn field_ref_text_starts_with_uses_strict_prefix_compare() {
277        let predicate = FieldRef::new("name").text_starts_with("Al");
278        let Predicate::Compare(compare) = predicate else {
279            panic!("expected compare predicate");
280        };
281
282        assert_eq!(compare.field, "name");
283        assert_eq!(compare.op, CompareOp::StartsWith);
284        assert_eq!(compare.coercion.id, CoercionId::Strict);
285        assert_eq!(compare.value, Value::Text("Al".to_string()));
286    }
287
288    #[test]
289    fn field_ref_text_starts_with_ci_uses_casefold_prefix_compare() {
290        let predicate = FieldRef::new("name").text_starts_with_ci("AL");
291        let Predicate::Compare(compare) = predicate else {
292            panic!("expected compare predicate");
293        };
294
295        assert_eq!(compare.field, "name");
296        assert_eq!(compare.op, CompareOp::StartsWith);
297        assert_eq!(compare.coercion.id, CoercionId::TextCasefold);
298        assert_eq!(compare.value, Value::Text("AL".to_string()));
299    }
300
301    #[test]
302    fn field_ref_gt_field_builds_compare_fields_predicate() {
303        let predicate = FieldRef::new("age").gt_field("rank");
304        let Predicate::CompareFields(compare) = predicate else {
305            panic!("expected field-to-field compare predicate");
306        };
307
308        assert_eq!(compare.left_field(), "age");
309        assert_eq!(compare.op(), CompareOp::Gt);
310        assert_eq!(compare.right_field(), "rank");
311        assert_eq!(compare.coercion().id, CoercionId::NumericWiden);
312    }
313
314    #[test]
315    fn field_ref_not_between_builds_outside_range_predicate() {
316        let predicate = FieldRef::new("age").not_between(10_u64, 20_u64);
317
318        assert_eq!(
319            predicate,
320            Predicate::or(vec![
321                Predicate::Compare(ComparePredicate::with_coercion(
322                    "age",
323                    CompareOp::Lt,
324                    Value::Uint(10),
325                    CoercionId::NumericWiden,
326                )),
327                Predicate::Compare(ComparePredicate::with_coercion(
328                    "age",
329                    CompareOp::Gt,
330                    Value::Uint(20),
331                    CoercionId::NumericWiden,
332                )),
333            ])
334        );
335    }
336
337    #[test]
338    fn field_ref_between_fields_builds_field_bound_range_predicate() {
339        let predicate = FieldRef::new("age").between_fields("min_age", "max_age");
340
341        assert_eq!(
342            predicate,
343            Predicate::and(vec![
344                Predicate::CompareFields(CompareFieldsPredicate::with_coercion(
345                    "age",
346                    CompareOp::Gte,
347                    "min_age",
348                    CoercionId::NumericWiden,
349                )),
350                Predicate::CompareFields(CompareFieldsPredicate::with_coercion(
351                    "age",
352                    CompareOp::Lte,
353                    "max_age",
354                    CoercionId::NumericWiden,
355                )),
356            ])
357        );
358    }
359}