icydb_core/db/query/builder/
field.rs1use crate::{
7 db::predicate::{CoercionId, CompareFieldsPredicate, CompareOp, ComparePredicate, Predicate},
8 traits::FieldValue,
9 value::Value,
10};
11use derive_more::Deref;
12
13#[derive(Clone, Copy, Deref, Eq, Hash, PartialEq)]
22pub struct FieldRef(&'static str);
23
24impl FieldRef {
25 #[must_use]
27 pub const fn new(name: &'static str) -> Self {
28 Self(name)
29 }
30
31 #[must_use]
33 pub const fn as_str(self) -> &'static str {
34 self.0
35 }
36
37 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 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 #[must_use]
67 pub fn eq(self, value: impl FieldValue) -> Predicate {
68 self.cmp(CompareOp::Eq, value, CoercionId::Strict)
69 }
70
71 #[must_use]
73 pub fn text_eq_ci(self, value: impl FieldValue) -> Predicate {
74 self.cmp(CompareOp::Eq, value, CoercionId::TextCasefold)
75 }
76
77 #[must_use]
79 pub fn ne(self, value: impl FieldValue) -> Predicate {
80 self.cmp(CompareOp::Ne, value, CoercionId::Strict)
81 }
82
83 #[must_use]
85 pub fn lt(self, value: impl FieldValue) -> Predicate {
86 self.cmp(CompareOp::Lt, value, CoercionId::NumericWiden)
87 }
88
89 #[must_use]
91 pub fn lte(self, value: impl FieldValue) -> Predicate {
92 self.cmp(CompareOp::Lte, value, CoercionId::NumericWiden)
93 }
94
95 #[must_use]
97 pub fn gt(self, value: impl FieldValue) -> Predicate {
98 self.cmp(CompareOp::Gt, value, CoercionId::NumericWiden)
99 }
100
101 #[must_use]
103 pub fn gte(self, value: impl FieldValue) -> Predicate {
104 self.cmp(CompareOp::Gte, value, CoercionId::NumericWiden)
105 }
106
107 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
164 pub fn is_null(self) -> Predicate {
165 Predicate::IsNull {
166 field: self.0.to_string(),
167 }
168 }
169
170 #[must_use]
172 pub fn is_not_null(self) -> Predicate {
173 Predicate::IsNotNull {
174 field: self.0.to_string(),
175 }
176 }
177
178 #[must_use]
180 pub fn is_missing(self) -> Predicate {
181 Predicate::IsMissing {
182 field: self.0.to_string(),
183 }
184 }
185
186 #[must_use]
188 pub fn is_empty(self) -> Predicate {
189 Predicate::IsEmpty {
190 field: self.0.to_string(),
191 }
192 }
193
194 #[must_use]
196 pub fn is_not_empty(self) -> Predicate {
197 Predicate::IsNotEmpty {
198 field: self.0.to_string(),
199 }
200 }
201
202 #[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 #[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 #[must_use]
222 pub fn text_starts_with(self, value: impl FieldValue) -> Predicate {
223 self.cmp(CompareOp::StartsWith, value, CoercionId::Strict)
224 }
225
226 #[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 #[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 #[must_use]
240 pub fn not_between(self, lower: impl FieldValue, upper: impl FieldValue) -> Predicate {
241 Predicate::or(vec![self.lt(lower), self.gt(upper)])
242 }
243}
244
245impl AsRef<str> for FieldRef {
250 fn as_ref(&self) -> &str {
251 self.0
252 }
253}
254
255#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn field_ref_text_starts_with_uses_strict_prefix_compare() {
265 let predicate = FieldRef::new("name").text_starts_with("Al");
266 let Predicate::Compare(compare) = predicate else {
267 panic!("expected compare predicate");
268 };
269
270 assert_eq!(compare.field, "name");
271 assert_eq!(compare.op, CompareOp::StartsWith);
272 assert_eq!(compare.coercion.id, CoercionId::Strict);
273 assert_eq!(compare.value, Value::Text("Al".to_string()));
274 }
275
276 #[test]
277 fn field_ref_text_starts_with_ci_uses_casefold_prefix_compare() {
278 let predicate = FieldRef::new("name").text_starts_with_ci("AL");
279 let Predicate::Compare(compare) = predicate else {
280 panic!("expected compare predicate");
281 };
282
283 assert_eq!(compare.field, "name");
284 assert_eq!(compare.op, CompareOp::StartsWith);
285 assert_eq!(compare.coercion.id, CoercionId::TextCasefold);
286 assert_eq!(compare.value, Value::Text("AL".to_string()));
287 }
288
289 #[test]
290 fn field_ref_gt_field_builds_compare_fields_predicate() {
291 let predicate = FieldRef::new("age").gt_field("rank");
292 let Predicate::CompareFields(compare) = predicate else {
293 panic!("expected field-to-field compare predicate");
294 };
295
296 assert_eq!(compare.left_field(), "age");
297 assert_eq!(compare.op(), CompareOp::Gt);
298 assert_eq!(compare.right_field(), "rank");
299 assert_eq!(compare.coercion().id, CoercionId::NumericWiden);
300 }
301
302 #[test]
303 fn field_ref_not_between_builds_outside_range_predicate() {
304 let predicate = FieldRef::new("age").not_between(10_u64, 20_u64);
305
306 assert_eq!(
307 predicate,
308 Predicate::or(vec![
309 Predicate::Compare(ComparePredicate::with_coercion(
310 "age",
311 CompareOp::Lt,
312 Value::Uint(10),
313 CoercionId::NumericWiden,
314 )),
315 Predicate::Compare(ComparePredicate::with_coercion(
316 "age",
317 CompareOp::Gt,
318 Value::Uint(20),
319 CoercionId::NumericWiden,
320 )),
321 ])
322 );
323 }
324}