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