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::db::query::expr::{FilterExpr, FilterValue};
7
8///
9/// FieldRef
10///
11/// Zero-cost wrapper around a static field name used in predicates.
12/// Enables method-based predicate builders without allocating.
13/// Carries only a `&'static str`; use `as_str` or `AsRef<str>` to borrow it.
14///
15
16#[derive(Clone, Copy, Eq, Hash, PartialEq)]
17pub struct FieldRef(&'static str);
18
19impl FieldRef {
20    /// Create a new field reference.
21    #[must_use]
22    pub const fn new(name: &'static str) -> Self {
23        Self(name)
24    }
25
26    /// Return the underlying field name.
27    #[must_use]
28    pub const fn as_str(self) -> &'static str {
29        self.0
30    }
31
32    /// Internal field-to-field comparison expression builder.
33    fn cmp_field(
34        self,
35        other: impl AsRef<str>,
36        build: impl FnOnce(String, String) -> FilterExpr,
37    ) -> FilterExpr {
38        build(self.0.to_string(), other.as_ref().to_string())
39    }
40
41    // ------------------------------------------------------------------
42    // Comparison predicates
43    // ------------------------------------------------------------------
44
45    /// Strict equality comparison (no coercion).
46    #[must_use]
47    pub fn eq(self, value: impl Into<FilterValue>) -> FilterExpr {
48        FilterExpr::eq(self.0, value)
49    }
50
51    /// Case-insensitive equality for text fields.
52    #[must_use]
53    pub fn text_eq_ci(self, value: impl Into<FilterValue>) -> FilterExpr {
54        FilterExpr::eq_ci(self.0, value)
55    }
56
57    /// Strict inequality comparison.
58    #[must_use]
59    pub fn ne(self, value: impl Into<FilterValue>) -> FilterExpr {
60        FilterExpr::ne(self.0, value)
61    }
62
63    /// Less-than comparison with numeric widening.
64    #[must_use]
65    pub fn lt(self, value: impl Into<FilterValue>) -> FilterExpr {
66        FilterExpr::lt(self.0, value)
67    }
68
69    /// Less-than-or-equal comparison with numeric widening.
70    #[must_use]
71    pub fn lte(self, value: impl Into<FilterValue>) -> FilterExpr {
72        FilterExpr::lte(self.0, value)
73    }
74
75    /// Greater-than comparison with numeric widening.
76    #[must_use]
77    pub fn gt(self, value: impl Into<FilterValue>) -> FilterExpr {
78        FilterExpr::gt(self.0, value)
79    }
80
81    /// Greater-than-or-equal comparison with numeric widening.
82    #[must_use]
83    pub fn gte(self, value: impl Into<FilterValue>) -> FilterExpr {
84        FilterExpr::gte(self.0, value)
85    }
86
87    /// Strict equality comparison against another field.
88    #[must_use]
89    pub fn eq_field(self, other: impl AsRef<str>) -> FilterExpr {
90        self.cmp_field(other, FilterExpr::eq_field)
91    }
92
93    /// Strict inequality comparison against another field.
94    #[must_use]
95    pub fn ne_field(self, other: impl AsRef<str>) -> FilterExpr {
96        self.cmp_field(other, FilterExpr::ne_field)
97    }
98
99    /// Less-than comparison against another numeric or text field.
100    #[must_use]
101    pub fn lt_field(self, other: impl AsRef<str>) -> FilterExpr {
102        self.cmp_field(other, FilterExpr::lt_field)
103    }
104
105    /// Less-than-or-equal comparison against another numeric or text field.
106    #[must_use]
107    pub fn lte_field(self, other: impl AsRef<str>) -> FilterExpr {
108        self.cmp_field(other, FilterExpr::lte_field)
109    }
110
111    /// Greater-than comparison against another numeric or text field.
112    #[must_use]
113    pub fn gt_field(self, other: impl AsRef<str>) -> FilterExpr {
114        self.cmp_field(other, FilterExpr::gt_field)
115    }
116
117    /// Greater-than-or-equal comparison against another numeric or text field.
118    #[must_use]
119    pub fn gte_field(self, other: impl AsRef<str>) -> FilterExpr {
120        self.cmp_field(other, FilterExpr::gte_field)
121    }
122
123    /// Membership test against a fixed list (strict).
124    #[must_use]
125    pub fn in_list<I, V>(self, values: I) -> FilterExpr
126    where
127        I: IntoIterator<Item = V>,
128        V: Into<FilterValue>,
129    {
130        FilterExpr::in_list(self.0, values)
131    }
132
133    // ------------------------------------------------------------------
134    // Structural predicates
135    // ------------------------------------------------------------------
136
137    /// Field is present and explicitly null.
138    #[must_use]
139    pub fn is_null(self) -> FilterExpr {
140        FilterExpr::is_null(self.0)
141    }
142
143    /// Field is present and not null.
144    #[must_use]
145    pub fn is_not_null(self) -> FilterExpr {
146        FilterExpr::is_not_null(self.0)
147    }
148
149    /// Field is not present at all.
150    #[must_use]
151    pub fn is_missing(self) -> FilterExpr {
152        FilterExpr::is_missing(self.0)
153    }
154
155    /// Field is present but empty (collection- or string-specific).
156    #[must_use]
157    pub fn is_empty(self) -> FilterExpr {
158        FilterExpr::is_empty(self.0)
159    }
160
161    /// Field is present and non-empty.
162    #[must_use]
163    pub fn is_not_empty(self) -> FilterExpr {
164        FilterExpr::is_not_empty(self.0)
165    }
166
167    /// Case-sensitive substring match for text fields.
168    #[must_use]
169    pub fn text_contains(self, value: impl Into<FilterValue>) -> FilterExpr {
170        FilterExpr::text_contains(self.0, value)
171    }
172
173    /// Case-insensitive substring match for text fields.
174    #[must_use]
175    pub fn text_contains_ci(self, value: impl Into<FilterValue>) -> FilterExpr {
176        FilterExpr::text_contains_ci(self.0, value)
177    }
178
179    /// Case-sensitive prefix match for text fields.
180    #[must_use]
181    pub fn text_starts_with(self, value: impl Into<FilterValue>) -> FilterExpr {
182        FilterExpr::starts_with(self.0, value)
183    }
184
185    /// Case-insensitive prefix match for text fields.
186    #[must_use]
187    pub fn text_starts_with_ci(self, value: impl Into<FilterValue>) -> FilterExpr {
188        FilterExpr::starts_with_ci(self.0, value)
189    }
190
191    /// Inclusive range predicate lowered as `field >= lower AND field <= upper`.
192    #[must_use]
193    pub fn between(
194        self,
195        lower: impl Into<FilterValue>,
196        upper: impl Into<FilterValue>,
197    ) -> FilterExpr {
198        FilterExpr::and(vec![self.gte(lower), self.lte(upper)])
199    }
200
201    /// Inclusive range predicate against two other fields.
202    #[must_use]
203    pub fn between_fields(self, lower: impl AsRef<str>, upper: impl AsRef<str>) -> FilterExpr {
204        FilterExpr::and(vec![self.gte_field(lower), self.lte_field(upper)])
205    }
206
207    /// Exclusive-outside range predicate lowered as `field < lower OR field > upper`.
208    #[must_use]
209    pub fn not_between(
210        self,
211        lower: impl Into<FilterValue>,
212        upper: impl Into<FilterValue>,
213    ) -> FilterExpr {
214        FilterExpr::or(vec![self.lt(lower), self.gt(upper)])
215    }
216
217    /// Exclusive-outside range predicate against two other fields.
218    #[must_use]
219    pub fn not_between_fields(self, lower: impl AsRef<str>, upper: impl AsRef<str>) -> FilterExpr {
220        FilterExpr::or(vec![self.lt_field(lower), self.gt_field(upper)])
221    }
222}
223
224// ----------------------------------------------------------------------
225// Boundary traits
226// ----------------------------------------------------------------------
227
228impl AsRef<str> for FieldRef {
229    fn as_ref(&self) -> &str {
230        self.0
231    }
232}
233
234///
235/// TESTS
236///
237
238#[cfg(test)]
239mod tests {
240    use super::{FieldRef, FilterExpr};
241
242    #[test]
243    fn field_ref_text_starts_with_uses_strict_prefix_compare() {
244        assert_eq!(
245            FieldRef::new("name").text_starts_with("Al"),
246            FilterExpr::starts_with("name", "Al"),
247        );
248    }
249
250    #[test]
251    fn field_ref_text_starts_with_ci_uses_casefold_prefix_compare() {
252        assert_eq!(
253            FieldRef::new("name").text_starts_with_ci("AL"),
254            FilterExpr::starts_with_ci("name", "AL"),
255        );
256    }
257
258    #[test]
259    fn field_ref_gt_field_builds_field_compare_filter_expr() {
260        assert_eq!(
261            FieldRef::new("age").gt_field("rank"),
262            FilterExpr::gt_field("age", "rank"),
263        );
264    }
265
266    #[test]
267    fn field_ref_not_between_builds_outside_range_filter_expr() {
268        assert_eq!(
269            FieldRef::new("age").not_between(10_u64, 20_u64),
270            FilterExpr::or(vec![
271                FilterExpr::lt("age", 10_u64),
272                FilterExpr::gt("age", 20_u64),
273            ])
274        );
275    }
276
277    #[test]
278    fn field_ref_between_fields_builds_field_bound_range_filter_expr() {
279        assert_eq!(
280            FieldRef::new("age").between_fields("min_age", "max_age"),
281            FilterExpr::and(vec![
282                FilterExpr::gte_field("age", "min_age"),
283                FilterExpr::lte_field("age", "max_age"),
284            ])
285        );
286    }
287}