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
239// ----------------------------------------------------------------------
240// Boundary traits
241// ----------------------------------------------------------------------
242
243impl AsRef<str> for FieldRef {
244    fn as_ref(&self) -> &str {
245        self.0
246    }
247}
248
249///
250/// TESTS
251///
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn field_ref_text_starts_with_uses_strict_prefix_compare() {
259        let predicate = FieldRef::new("name").text_starts_with("Al");
260        let Predicate::Compare(compare) = predicate else {
261            panic!("expected compare predicate");
262        };
263
264        assert_eq!(compare.field, "name");
265        assert_eq!(compare.op, CompareOp::StartsWith);
266        assert_eq!(compare.coercion.id, CoercionId::Strict);
267        assert_eq!(compare.value, Value::Text("Al".to_string()));
268    }
269
270    #[test]
271    fn field_ref_text_starts_with_ci_uses_casefold_prefix_compare() {
272        let predicate = FieldRef::new("name").text_starts_with_ci("AL");
273        let Predicate::Compare(compare) = predicate else {
274            panic!("expected compare predicate");
275        };
276
277        assert_eq!(compare.field, "name");
278        assert_eq!(compare.op, CompareOp::StartsWith);
279        assert_eq!(compare.coercion.id, CoercionId::TextCasefold);
280        assert_eq!(compare.value, Value::Text("AL".to_string()));
281    }
282
283    #[test]
284    fn field_ref_gt_field_builds_compare_fields_predicate() {
285        let predicate = FieldRef::new("age").gt_field("rank");
286        let Predicate::CompareFields(compare) = predicate else {
287            panic!("expected field-to-field compare predicate");
288        };
289
290        assert_eq!(compare.left_field(), "age");
291        assert_eq!(compare.op(), CompareOp::Gt);
292        assert_eq!(compare.right_field(), "rank");
293        assert_eq!(compare.coercion().id, CoercionId::NumericWiden);
294    }
295}