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 between_fields(self, lower: impl AsRef<str>, upper: impl AsRef<str>) -> Predicate {
241 Predicate::and(vec![self.gte_field(lower), self.lte_field(upper)])
242 }
243
244 #[must_use]
246 pub fn not_between(self, lower: impl FieldValue, upper: impl FieldValue) -> Predicate {
247 Predicate::or(vec![self.lt(lower), self.gt(upper)])
248 }
249
250 #[must_use]
252 pub fn not_between_fields(self, lower: impl AsRef<str>, upper: impl AsRef<str>) -> Predicate {
253 Predicate::or(vec![self.lt_field(lower), self.gt_field(upper)])
254 }
255}
256
257impl AsRef<str> for FieldRef {
262 fn as_ref(&self) -> &str {
263 self.0
264 }
265}
266
267#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn field_ref_text_starts_with_uses_strict_prefix_compare() {
277 let predicate = FieldRef::new("name").text_starts_with("Al");
278 let Predicate::Compare(compare) = predicate else {
279 panic!("expected compare predicate");
280 };
281
282 assert_eq!(compare.field, "name");
283 assert_eq!(compare.op, CompareOp::StartsWith);
284 assert_eq!(compare.coercion.id, CoercionId::Strict);
285 assert_eq!(compare.value, Value::Text("Al".to_string()));
286 }
287
288 #[test]
289 fn field_ref_text_starts_with_ci_uses_casefold_prefix_compare() {
290 let predicate = FieldRef::new("name").text_starts_with_ci("AL");
291 let Predicate::Compare(compare) = predicate else {
292 panic!("expected compare predicate");
293 };
294
295 assert_eq!(compare.field, "name");
296 assert_eq!(compare.op, CompareOp::StartsWith);
297 assert_eq!(compare.coercion.id, CoercionId::TextCasefold);
298 assert_eq!(compare.value, Value::Text("AL".to_string()));
299 }
300
301 #[test]
302 fn field_ref_gt_field_builds_compare_fields_predicate() {
303 let predicate = FieldRef::new("age").gt_field("rank");
304 let Predicate::CompareFields(compare) = predicate else {
305 panic!("expected field-to-field compare predicate");
306 };
307
308 assert_eq!(compare.left_field(), "age");
309 assert_eq!(compare.op(), CompareOp::Gt);
310 assert_eq!(compare.right_field(), "rank");
311 assert_eq!(compare.coercion().id, CoercionId::NumericWiden);
312 }
313
314 #[test]
315 fn field_ref_not_between_builds_outside_range_predicate() {
316 let predicate = FieldRef::new("age").not_between(10_u64, 20_u64);
317
318 assert_eq!(
319 predicate,
320 Predicate::or(vec![
321 Predicate::Compare(ComparePredicate::with_coercion(
322 "age",
323 CompareOp::Lt,
324 Value::Uint(10),
325 CoercionId::NumericWiden,
326 )),
327 Predicate::Compare(ComparePredicate::with_coercion(
328 "age",
329 CompareOp::Gt,
330 Value::Uint(20),
331 CoercionId::NumericWiden,
332 )),
333 ])
334 );
335 }
336
337 #[test]
338 fn field_ref_between_fields_builds_field_bound_range_predicate() {
339 let predicate = FieldRef::new("age").between_fields("min_age", "max_age");
340
341 assert_eq!(
342 predicate,
343 Predicate::and(vec![
344 Predicate::CompareFields(CompareFieldsPredicate::with_coercion(
345 "age",
346 CompareOp::Gte,
347 "min_age",
348 CoercionId::NumericWiden,
349 )),
350 Predicate::CompareFields(CompareFieldsPredicate::with_coercion(
351 "age",
352 CompareOp::Lte,
353 "max_age",
354 CoercionId::NumericWiden,
355 )),
356 ])
357 );
358 }
359}