1use crate::schema::{AsFieldKey, AsFieldName, FieldRef, Schema};
53use crate::{
54 ODataOrderBy, ODataQuery, OrderKey, SortDir, ast::Expr, pagination::short_filter_hash,
55};
56use std::marker::PhantomData;
57
58pub struct QueryBuilder<S: Schema> {
74 filter: Option<Expr>,
75 order: Vec<OrderKey>,
76 select: Option<Vec<S::Field>>,
77 limit: Option<u64>,
78 _phantom: PhantomData<S>,
79}
80
81impl<S: Schema> QueryBuilder<S> {
82 #[must_use]
84 pub fn new() -> Self {
85 Self {
86 filter: None,
87 order: Vec::new(),
88 select: None,
89 limit: None,
90 _phantom: PhantomData,
91 }
92 }
93
94 #[must_use]
102 pub fn filter(mut self, expr: Expr) -> Self {
103 self.filter = Some(expr);
104 self
105 }
106
107 #[must_use]
119 pub fn order_by<F>(mut self, field: F, dir: SortDir) -> Self
120 where
121 F: AsFieldName,
122 {
123 self.order.push(OrderKey {
124 field: field.as_field_name().to_owned(),
125 dir,
126 });
127 self
128 }
129
130 #[must_use]
142 pub fn select<I>(mut self, fields: I) -> Self
143 where
144 I: IntoIterator,
145 I::Item: AsFieldKey<S>,
146 {
147 let iter = fields.into_iter();
148 let (lower, _) = iter.size_hint();
149 let mut out = Vec::with_capacity(lower);
150 for f in iter {
151 out.push(f.as_field_key());
152 }
153 self.select = Some(out);
154 self
155 }
156
157 #[must_use]
165 pub fn page_size(mut self, limit: u64) -> Self {
166 self.limit = Some(limit);
167 self
168 }
169
170 pub fn build(self) -> ODataQuery {
175 let filter_hash = short_filter_hash(self.filter.as_ref());
176
177 let mut query = ODataQuery::new();
178
179 if let Some(expr) = self.filter {
180 query = query.with_filter(expr);
181 }
182
183 if !self.order.is_empty() {
184 query = query.with_order(ODataOrderBy(self.order));
185 }
186
187 if let Some(limit) = self.limit {
188 query = query.with_limit(limit);
189 }
190
191 if let Some(hash) = filter_hash {
192 query = query.with_filter_hash(hash);
193 }
194
195 if let Some(fields) = self.select {
196 let names: Vec<String> = fields
197 .into_iter()
198 .map(|k| FieldRef::<S, ()>::new(k).name().to_owned())
199 .collect();
200 query = query.with_select(names);
201 }
202
203 query
204 }
205}
206
207impl<S: Schema> Default for QueryBuilder<S> {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[cfg(test)]
214#[cfg_attr(coverage_nightly, coverage(off))]
215mod tests {
216 use super::*;
217 use crate::ast::{CompareOperator, Value};
218 use crate::schema::FieldRef;
219
220 #[derive(Copy, Clone, Eq, PartialEq, Debug)]
221 enum UserField {
222 Id,
223 Name,
224 Email,
225 Age,
226 }
227
228 struct UserSchema;
229
230 impl Schema for UserSchema {
231 type Field = UserField;
232
233 fn field_name(field: Self::Field) -> &'static str {
234 match field {
235 UserField::Id => "id",
236 UserField::Name => "name",
237 UserField::Email => "email",
238 UserField::Age => "age",
239 }
240 }
241 }
242
243 const NAME: FieldRef<UserSchema, String> = FieldRef::new(UserField::Name);
244 const EMAIL: FieldRef<UserSchema, String> = FieldRef::new(UserField::Email);
245 const AGE: FieldRef<UserSchema, i32> = FieldRef::new(UserField::Age);
246 const ID: FieldRef<UserSchema, uuid::Uuid> = FieldRef::new(UserField::Id);
247
248 #[test]
249 fn test_field_name_mapping() {
250 assert_eq!(NAME.name(), "name");
251 assert_eq!(EMAIL.name(), "email");
252 assert_eq!(AGE.name(), "age");
253 }
254
255 #[test]
256 fn test_simple_eq_filter() {
257 let user_id = uuid::Uuid::new_v4();
258 let query = QueryBuilder::<UserSchema>::new()
259 .filter(ID.eq(user_id))
260 .build();
261
262 assert!(query.has_filter());
263 assert!(query.filter_hash.is_some());
264 }
265
266 #[test]
267 fn test_string_contains() {
268 let query = QueryBuilder::<UserSchema>::new()
269 .filter(NAME.contains("john"))
270 .build();
271
272 assert!(query.has_filter());
273 if let Some(filter) = query.filter() {
274 if let Expr::Function(name, args) = filter {
275 assert_eq!(name, "contains");
276 assert_eq!(args.len(), 2);
277 } else {
278 panic!("Expected Function expression");
279 }
280 }
281 }
282
283 #[test]
284 fn test_string_startswith() {
285 let query = QueryBuilder::<UserSchema>::new()
286 .filter(NAME.startswith("jo"))
287 .build();
288
289 assert!(query.has_filter());
290 if let Some(filter) = query.filter() {
291 if let Expr::Function(name, _) = filter {
292 assert_eq!(name, "startswith");
293 } else {
294 panic!("Expected Function expression");
295 }
296 }
297 }
298
299 #[test]
300 fn test_string_endswith() {
301 let query = QueryBuilder::<UserSchema>::new()
302 .filter(EMAIL.endswith("@example.com"))
303 .build();
304
305 assert!(query.has_filter());
306 if let Some(filter) = query.filter() {
307 if let Expr::Function(name, _) = filter {
308 assert_eq!(name, "endswith");
309 } else {
310 panic!("Expected Function expression");
311 }
312 }
313 }
314
315 #[test]
316 fn test_comparison_operators() {
317 let query = QueryBuilder::<UserSchema>::new().filter(AGE.gt(18)).build();
318 assert!(query.has_filter());
319
320 let query = QueryBuilder::<UserSchema>::new().filter(AGE.ge(18)).build();
321 assert!(query.has_filter());
322
323 let query = QueryBuilder::<UserSchema>::new().filter(AGE.lt(65)).build();
324 assert!(query.has_filter());
325
326 let query = QueryBuilder::<UserSchema>::new().filter(AGE.le(65)).build();
327 assert!(query.has_filter());
328
329 let query = QueryBuilder::<UserSchema>::new().filter(AGE.ne(0)).build();
330 assert!(query.has_filter());
331 }
332
333 #[test]
334 fn test_and_combinator() {
335 let user_id = uuid::Uuid::new_v4();
336 let query = QueryBuilder::<UserSchema>::new()
337 .filter(ID.eq(user_id).and(AGE.gt(18)))
338 .build();
339
340 assert!(query.has_filter());
341 if let Some(filter) = query.filter() {
342 if let Expr::And(_, _) = filter {
343 } else {
344 panic!("Expected And expression");
345 }
346 }
347 }
348
349 #[test]
350 fn test_or_combinator() {
351 let query = QueryBuilder::<UserSchema>::new()
352 .filter(AGE.lt(18).or(AGE.gt(65)))
353 .build();
354
355 assert!(query.has_filter());
356 if let Some(filter) = query.filter() {
357 if let Expr::Or(_, _) = filter {
358 } else {
359 panic!("Expected Or expression");
360 }
361 }
362 }
363
364 #[test]
365 fn test_not_combinator() {
366 let query = QueryBuilder::<UserSchema>::new()
367 .filter(NAME.contains("test").not())
368 .build();
369
370 assert!(query.has_filter());
371 if let Some(filter) = query.filter() {
372 if let Expr::Not(_) = filter {
373 } else {
374 panic!("Expected Not expression");
375 }
376 }
377 }
378
379 #[test]
380 fn test_complex_filter() {
381 let user_id = uuid::Uuid::new_v4();
382 let query = QueryBuilder::<UserSchema>::new()
383 .filter(
384 ID.eq(user_id)
385 .and(NAME.contains("john"))
386 .and(AGE.ge(18).and(AGE.le(65))),
387 )
388 .build();
389
390 assert!(query.has_filter());
391 assert!(query.filter_hash.is_some());
392 }
393
394 #[test]
395 fn test_order_by_single() {
396 let query = QueryBuilder::<UserSchema>::new()
397 .order_by(NAME, SortDir::Asc)
398 .build();
399
400 assert_eq!(query.order.0.len(), 1);
401 assert_eq!(query.order.0[0].field, "name");
402 assert_eq!(query.order.0[0].dir, SortDir::Asc);
403 }
404
405 #[test]
406 fn test_order_by_multiple() {
407 let query = QueryBuilder::<UserSchema>::new()
408 .order_by(NAME, SortDir::Asc)
409 .order_by(AGE, SortDir::Desc)
410 .build();
411
412 assert_eq!(query.order.0.len(), 2);
413 assert_eq!(query.order.0[0].field, "name");
414 assert_eq!(query.order.0[0].dir, SortDir::Asc);
415 assert_eq!(query.order.0[1].field, "age");
416 assert_eq!(query.order.0[1].dir, SortDir::Desc);
417 }
418
419 #[test]
420 fn test_select_fields() {
421 let query = QueryBuilder::<UserSchema>::new()
422 .select([NAME, EMAIL])
423 .build();
424
425 assert!(query.has_select());
426 let fields = query.selected_fields().unwrap();
427 assert_eq!(fields.len(), 2);
428 assert_eq!(fields[0], "name");
429 assert_eq!(fields[1], "email");
430 }
431
432 #[test]
433 fn test_select_fields_vec() {
434 let query = QueryBuilder::<UserSchema>::new()
435 .select(vec![NAME, EMAIL])
436 .build();
437
438 assert!(query.has_select());
439 let fields = query.selected_fields().unwrap();
440 assert_eq!(fields, &["name", "email"]);
441 }
442
443 #[test]
444 fn test_select_fields_legacy_slice_syntax() {
445 let query = QueryBuilder::<UserSchema>::new()
446 .select(&[&NAME, &EMAIL])
447 .build();
448
449 assert!(query.has_select());
450 let fields = query.selected_fields().unwrap();
451 assert_eq!(fields, &["name", "email"]);
452 }
453
454 #[test]
455 fn test_page_size() {
456 let query = QueryBuilder::<UserSchema>::new().page_size(50).build();
457
458 assert_eq!(query.limit, Some(50));
459 }
460
461 #[test]
462 fn test_full_query_build() {
463 let user_id = uuid::Uuid::new_v4();
464 let query = QueryBuilder::<UserSchema>::new()
465 .filter(ID.eq(user_id).and(AGE.gt(18)))
466 .order_by(NAME, SortDir::Asc)
467 .select([NAME, EMAIL])
468 .page_size(25)
469 .build();
470
471 assert!(query.has_filter());
472 assert!(query.filter_hash.is_some());
473 assert_eq!(query.order.0.len(), 1);
474 assert!(query.has_select());
475 assert_eq!(query.limit, Some(25));
476 }
477
478 #[test]
479 fn test_filter_hash_stability() {
480 let user_id = uuid::Uuid::new_v4();
481
482 let query1 = QueryBuilder::<UserSchema>::new()
483 .filter(ID.eq(user_id))
484 .build();
485
486 let query2 = QueryBuilder::<UserSchema>::new()
487 .filter(ID.eq(user_id))
488 .build();
489
490 assert_eq!(query1.filter_hash, query2.filter_hash);
491 assert!(query1.filter_hash.is_some());
492 }
493
494 #[test]
495 fn test_filter_hash_different_for_different_filters() {
496 let query1 = QueryBuilder::<UserSchema>::new()
497 .filter(NAME.eq("alice"))
498 .build();
499
500 let query2 = QueryBuilder::<UserSchema>::new().filter(AGE.gt(18)).build();
501
502 assert_ne!(query1.filter_hash, query2.filter_hash);
503 }
504
505 #[test]
506 fn test_no_filter_no_hash() {
507 let query = QueryBuilder::<UserSchema>::new()
508 .order_by(NAME, SortDir::Asc)
509 .build();
510
511 assert!(!query.has_filter());
512 assert!(query.filter_hash.is_none());
513 }
514
515 #[test]
516 fn test_empty_query() {
517 let query = QueryBuilder::<UserSchema>::new().build();
518
519 assert!(!query.has_filter());
520 assert!(query.filter_hash.is_none());
521 assert!(query.order.is_empty());
522 assert!(!query.has_select());
523 assert_eq!(query.limit, None);
524 }
525
526 #[test]
527 fn test_normalized_filter_consistency() {
528 use crate::pagination::normalize_filter_for_hash;
529
530 let expr1 = NAME.eq("test");
531 let expr2 = NAME.eq("test");
532
533 let norm1 = normalize_filter_for_hash(&expr1);
534 let norm2 = normalize_filter_for_hash(&expr2);
535
536 assert_eq!(norm1, norm2);
537 }
538
539 #[test]
540 fn test_is_null() {
541 let query = QueryBuilder::<UserSchema>::new()
542 .filter(NAME.is_null())
543 .build();
544
545 assert!(query.has_filter());
546 if let Some(filter) = query.filter() {
547 if let Expr::Compare(_, op, value) = filter {
548 assert_eq!(*op, CompareOperator::Eq);
549 if let Expr::Value(Value::Null) = **value {
550 } else {
551 panic!("Expected Value::Null");
552 }
553 } else {
554 panic!("Expected Compare expression");
555 }
556 }
557 }
558
559 #[test]
560 fn test_is_not_null() {
561 let query = QueryBuilder::<UserSchema>::new()
562 .filter(EMAIL.is_not_null())
563 .build();
564
565 assert!(query.has_filter());
566 if let Some(filter) = query.filter() {
567 if let Expr::Compare(_, op, value) = filter {
568 assert_eq!(*op, CompareOperator::Ne);
569 if let Expr::Value(Value::Null) = **value {
570 } else {
571 panic!("Expected Value::Null");
572 }
573 } else {
574 panic!("Expected Compare expression");
575 }
576 }
577 }
578
579 #[cfg(feature = "chrono")]
580 #[test]
581 fn test_chrono_datetime_conversion() {
582 use chrono::Utc;
583
584 const CREATED_AT: FieldRef<UserSchema, chrono::DateTime<Utc>> =
585 FieldRef::new(UserField::Age);
586
587 let now = Utc::now();
588 let query = QueryBuilder::<UserSchema>::new()
589 .filter(CREATED_AT.eq(now))
590 .build();
591
592 assert!(query.has_filter());
593 }
594
595 #[cfg(feature = "chrono")]
596 #[test]
597 fn test_chrono_naive_date_conversion() {
598 use chrono::NaiveDate;
599
600 const DATE_FIELD: FieldRef<UserSchema, NaiveDate> = FieldRef::new(UserField::Age);
601
602 let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
603 let query = QueryBuilder::<UserSchema>::new()
604 .filter(DATE_FIELD.eq(date))
605 .build();
606
607 assert!(query.has_filter());
608 }
609
610 #[cfg(feature = "chrono")]
611 #[test]
612 fn test_chrono_naive_time_conversion() {
613 use chrono::NaiveTime;
614
615 const TIME_FIELD: FieldRef<UserSchema, NaiveTime> = FieldRef::new(UserField::Age);
616
617 let time = NaiveTime::from_hms_opt(12, 30, 0).unwrap();
618 let query = QueryBuilder::<UserSchema>::new()
619 .filter(TIME_FIELD.eq(time))
620 .build();
621
622 assert!(query.has_filter());
623 }
624}