Skip to main content

icydb_core/db/query/builder/
field.rs

1use crate::{
2    db::query::predicate::{
3        CoercionId, CompareOp, ComparePredicate, Predicate, UnsupportedQueryFeature,
4    },
5    traits::FieldValue,
6    value::Value,
7};
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, 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    // ------------------------------------------------------------------
34    // Comparison predicates
35    // ------------------------------------------------------------------
36
37    /// Strict equality comparison (no coercion).
38    #[must_use]
39    pub fn eq(self, value: impl FieldValue) -> Predicate {
40        Predicate::Compare(ComparePredicate::with_coercion(
41            self.0,
42            CompareOp::Eq,
43            value.to_value(),
44            CoercionId::Strict,
45        ))
46    }
47
48    /// Case-insensitive equality for text fields.
49    #[must_use]
50    pub fn text_eq_ci(self, value: impl FieldValue) -> Predicate {
51        Predicate::Compare(ComparePredicate::with_coercion(
52            self.0,
53            CompareOp::Eq,
54            value.to_value(),
55            CoercionId::TextCasefold,
56        ))
57    }
58
59    /// Strict inequality comparison.
60    #[must_use]
61    pub fn ne(self, value: impl FieldValue) -> Predicate {
62        Predicate::Compare(ComparePredicate::with_coercion(
63            self.0,
64            CompareOp::Ne,
65            value.to_value(),
66            CoercionId::Strict,
67        ))
68    }
69
70    /// Less-than comparison with numeric widening.
71    #[must_use]
72    pub fn lt(self, value: impl FieldValue) -> Predicate {
73        Predicate::Compare(ComparePredicate::with_coercion(
74            self.0,
75            CompareOp::Lt,
76            value.to_value(),
77            CoercionId::NumericWiden,
78        ))
79    }
80
81    /// Less-than-or-equal comparison with numeric widening.
82    #[must_use]
83    pub fn lte(self, value: impl FieldValue) -> Predicate {
84        Predicate::Compare(ComparePredicate::with_coercion(
85            self.0,
86            CompareOp::Lte,
87            value.to_value(),
88            CoercionId::NumericWiden,
89        ))
90    }
91
92    /// Greater-than comparison with numeric widening.
93    #[must_use]
94    pub fn gt(self, value: impl FieldValue) -> Predicate {
95        Predicate::Compare(ComparePredicate::with_coercion(
96            self.0,
97            CompareOp::Gt,
98            value.to_value(),
99            CoercionId::NumericWiden,
100        ))
101    }
102
103    /// Greater-than-or-equal comparison with numeric widening.
104    #[must_use]
105    pub fn gte(self, value: impl FieldValue) -> Predicate {
106        Predicate::Compare(ComparePredicate::with_coercion(
107            self.0,
108            CompareOp::Gte,
109            value.to_value(),
110            CoercionId::NumericWiden,
111        ))
112    }
113
114    /// Membership test against a fixed list (strict).
115    #[must_use]
116    pub fn in_list<I, V>(self, values: I) -> Predicate
117    where
118        I: IntoIterator<Item = V>,
119        V: FieldValue,
120    {
121        Predicate::Compare(ComparePredicate::with_coercion(
122            self.0,
123            CompareOp::In,
124            Value::List(values.into_iter().map(|v| v.to_value()).collect()),
125            CoercionId::Strict,
126        ))
127    }
128
129    // ------------------------------------------------------------------
130    // Structural predicates
131    // ------------------------------------------------------------------
132
133    /// Field is present and explicitly null.
134    #[must_use]
135    pub fn is_null(self) -> Predicate {
136        Predicate::IsNull {
137            field: self.0.to_string(),
138        }
139    }
140
141    /// Field is not present at all.
142    #[must_use]
143    pub fn is_missing(self) -> Predicate {
144        Predicate::IsMissing {
145            field: self.0.to_string(),
146        }
147    }
148
149    /// Field is present but empty (collection- or string-specific).
150    #[must_use]
151    pub fn is_empty(self) -> Predicate {
152        Predicate::IsEmpty {
153            field: self.0.to_string(),
154        }
155    }
156
157    /// Field is present and non-empty.
158    #[must_use]
159    pub fn is_not_empty(self) -> Predicate {
160        Predicate::IsNotEmpty {
161            field: self.0.to_string(),
162        }
163    }
164
165    // ------------------------------------------------------------------
166    // Map predicates
167    // ------------------------------------------------------------------
168
169    /// Map predicates are intentionally unsupported.
170    pub fn map_contains_key(
171        self,
172        _key: impl FieldValue,
173        _coercion: CoercionId,
174    ) -> Result<Predicate, UnsupportedQueryFeature> {
175        Err(UnsupportedQueryFeature::MapPredicate {
176            field: self.0.to_string(),
177        })
178    }
179
180    /// Map predicates are intentionally unsupported.
181    pub fn map_contains_value(
182        self,
183        _value: impl FieldValue,
184        _coercion: CoercionId,
185    ) -> Result<Predicate, UnsupportedQueryFeature> {
186        Err(UnsupportedQueryFeature::MapPredicate {
187            field: self.0.to_string(),
188        })
189    }
190
191    /// Map predicates are intentionally unsupported.
192    pub fn map_contains_entry(
193        self,
194        _key: impl FieldValue,
195        _value: impl FieldValue,
196        _coercion: CoercionId,
197    ) -> Result<Predicate, UnsupportedQueryFeature> {
198        Err(UnsupportedQueryFeature::MapPredicate {
199            field: self.0.to_string(),
200        })
201    }
202
203    /// Case-sensitive substring match for text fields.
204    #[must_use]
205    pub fn text_contains(self, value: impl FieldValue) -> Predicate {
206        Predicate::TextContains {
207            field: self.0.to_string(),
208            value: value.to_value(),
209        }
210    }
211
212    /// Case-insensitive substring match for text fields.
213    #[must_use]
214    pub fn text_contains_ci(self, value: impl FieldValue) -> Predicate {
215        Predicate::TextContainsCi {
216            field: self.0.to_string(),
217            value: value.to_value(),
218        }
219    }
220}
221
222// ----------------------------------------------------------------------
223// Boundary traits
224// ----------------------------------------------------------------------
225
226impl AsRef<str> for FieldRef {
227    fn as_ref(&self) -> &str {
228        self.0
229    }
230}
231
232impl std::ops::Deref for FieldRef {
233    type Target = str;
234
235    fn deref(&self) -> &Self::Target {
236        self.0
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use crate::db::query::predicate::UnsupportedQueryFeature;
243
244    use super::FieldRef;
245
246    #[test]
247    fn map_predicate_builder_fails_immediately() {
248        let err = FieldRef::new("attributes")
249            .map_contains_entry("k", 1u64, super::CoercionId::Strict)
250            .expect_err("map predicate builder must fail immediately");
251
252        assert_eq!(
253            err,
254            UnsupportedQueryFeature::MapPredicate {
255                field: "attributes".to_string(),
256            }
257        );
258    }
259}