sqlx_paginated/paginated_query_as/builders/
query_params_builder.rs

1use crate::paginated_query_as::internal::{
2    get_struct_field_names, QueryDateRangeParams, QueryPaginationParams, QuerySearchParams,
3    QuerySortParams, DEFAULT_DATE_RANGE_COLUMN_NAME, DEFAULT_MAX_PAGE_SIZE, DEFAULT_MIN_PAGE_SIZE,
4    DEFAULT_PAGE,
5};
6use crate::paginated_query_as::models::QuerySortDirection;
7use crate::paginated_query_as::models::{QueryFilterCondition, QueryFilterOperator};
8use crate::QueryParams;
9use chrono::{DateTime, Utc};
10use serde::Serialize;
11use std::collections::HashMap;
12
13pub struct QueryParamsBuilder<'q, T> {
14    query: QueryParams<'q, T>,
15}
16
17impl<T: Default + Serialize> Default for QueryParamsBuilder<'_, T> {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl<'q, T: Default + Serialize> QueryParamsBuilder<'q, T> {
24    /// Creates a new `QueryParamsBuilder` with default values.
25    ///
26    /// Default values include:
27    /// - Page: 1
28    /// - Page size: 10
29    /// - Sort column: "created_at"
30    /// - Sort direction: Descending
31    ///
32    /// # Examples
33    ///
34    /// ```rust
35    /// use serde::{Serialize};
36    /// use sqlx_paginated::{QueryParamsBuilder};
37    ///
38    /// #[derive(Serialize, Default)]
39    /// struct UserExample {
40    ///     name: String
41    /// }
42    /// let builder = QueryParamsBuilder::<UserExample>::new();
43    /// ```
44    pub fn new() -> Self {
45        Self {
46            query: QueryParams::default(),
47        }
48    }
49
50    /// Creates a new `QueryParamsBuilder` with default values.
51    ///
52    /// Default values include:
53    /// - Page: 1
54    /// - Page size: 10
55    /// - Sort column: "created_at"
56    /// - Sort direction: Descending
57    ///
58    /// # Examples
59    ///
60    /// ```rust
61    /// use serde::{Serialize};
62    /// use sqlx_paginated::{QueryParamsBuilder};
63    ///
64    /// #[derive(Serialize, Default)]
65    /// struct UserExample {
66    ///     name: String
67    /// }
68    /// let builder = QueryParamsBuilder::<UserExample>::new();
69    /// ```
70    pub fn with_pagination(mut self, page: i64, page_size: i64) -> Self {
71        self.query.pagination = QueryPaginationParams {
72            page: page.max(DEFAULT_PAGE),
73            page_size: page_size.clamp(DEFAULT_MIN_PAGE_SIZE, DEFAULT_MAX_PAGE_SIZE),
74        };
75        self
76    }
77
78    /// Sets sorting parameters.
79    ///
80    /// # Arguments
81    ///
82    /// * `sort_column` - Column name to sort by
83    /// * `sort_direction` - Direction of sort (Ascending or Descending)
84    ///
85    /// # Examples
86    ///
87    /// ```rust
88    /// use serde::{Serialize};
89    /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
90    ///
91    /// #[derive(Serialize, Default)]
92    /// struct UserExample {
93    ///     name: String
94    /// }
95    ///
96    /// let params = QueryParamsBuilder::<UserExample>::new()
97    ///     .with_sort("updated_at", QuerySortDirection::Ascending)
98    ///     .build();
99    /// ```
100    pub fn with_sort(
101        mut self,
102        sort_column: impl Into<String>,
103        sort_direction: QuerySortDirection,
104    ) -> Self {
105        self.query.sort = QuerySortParams {
106            sort_column: sort_column.into(),
107            sort_direction,
108        };
109        self
110    }
111
112    /// Sets search parameters with multiple columns support.
113    ///
114    /// # Arguments
115    ///
116    /// * `search` - Search term to look for
117    /// * `search_columns` - Vector of column names to search in
118    ///
119    /// # Examples
120    ///
121    /// ```rust
122    /// use serde::{Serialize};
123    /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
124    ///
125    /// #[derive(Serialize, Default)]
126    /// struct UserExample {
127    ///     name: String
128    /// }
129    ///
130    /// let params = QueryParamsBuilder::<UserExample>::new()
131    ///     .with_search("john", vec!["name", "email", "username"])
132    ///     .build();
133    /// ```
134    pub fn with_search(
135        mut self,
136        search: impl Into<String>,
137        search_columns: Vec<impl Into<String>>,
138    ) -> Self {
139        self.query.search = QuerySearchParams {
140            search: Some(search.into()),
141            search_columns: Some(search_columns.into_iter().map(Into::into).collect()),
142        };
143        self
144    }
145
146    /// Sets date range parameters for filtering by date.
147    ///
148    /// # Arguments
149    ///
150    /// * `date_after` - Optional start date (inclusive)
151    /// * `date_before` - Optional end date (inclusive)
152    /// * `column_name` - Optional column name to apply date range filter (defaults to created_at)
153    ///
154    /// # Examples
155    ///
156    /// ```rust
157    /// use chrono::{DateTime, Utc};
158    /// use serde::{Serialize};
159    /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
160    ///
161    /// #[derive(Serialize, Default)]
162    /// struct UserExample {
163    ///     name: String,
164    ///     updated_at: DateTime<Utc>
165    /// }
166    ///
167    /// let start = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z").unwrap().into();
168    /// let end = DateTime::parse_from_rfc3339("2024-12-31T23:59:59Z").unwrap().into();
169    ///
170    /// let params = QueryParamsBuilder::<UserExample>::new()
171    ///     .with_date_range(Some(start), Some(end), Some("updated_at"))
172    ///     .build();
173    /// ```
174    pub fn with_date_range(
175        mut self,
176        date_after: Option<DateTime<Utc>>,
177        date_before: Option<DateTime<Utc>>,
178        column_name: Option<impl Into<String>>,
179    ) -> Self {
180        self.query.date_range = QueryDateRangeParams {
181            date_after,
182            date_before,
183            date_column: column_name.map_or_else(
184                || Some(DEFAULT_DATE_RANGE_COLUMN_NAME.to_string()),
185                |column_name| Some(column_name.into()),
186            ),
187        };
188        self
189    }
190
191    /// Adds a single filter condition with an operator.
192    ///
193    /// # Arguments
194    ///
195    /// * `key` - Column name to filter on
196    /// * `operator` - The comparison operator to use
197    /// * `value` - Value to filter by (required for most operators except IS NULL/IS NOT NULL)
198    ///
199    /// # Details
200    ///
201    /// Only adds the filter if the column exists in the model struct.
202    /// Logs a warning if tracing is enabled and the column is invalid.
203    ///
204    /// # Examples
205    ///
206    /// ```rust
207    /// use serde::{Serialize};
208    /// use sqlx_paginated::{QueryParamsBuilder, QueryFilterOperator};
209    ///
210    /// #[derive(Serialize, Default)]
211    /// struct Product {
212    ///     name: String,
213    ///     price: f64,
214    ///     stock: i32,
215    ///     status: String,
216    /// }
217    ///
218    /// let params = QueryParamsBuilder::<Product>::new()
219    ///     .with_filter_operator("price", QueryFilterOperator::GreaterThan, "10.00")
220    ///     .with_filter_operator("stock", QueryFilterOperator::LessOrEqual, "100")
221    ///     .with_filter_operator("status", QueryFilterOperator::NotEqual, "deleted")
222    ///     .build();
223    /// ```
224    pub fn with_filter_operator(
225        mut self,
226        key: impl Into<String>,
227        operator: QueryFilterOperator,
228        value: impl Into<String>,
229    ) -> Self {
230        let key = key.into();
231        let valid_fields = get_struct_field_names::<T>();
232
233        if valid_fields.contains(&key) {
234            self.query
235                .filters
236                .insert(key, QueryFilterCondition::new(operator, Some(value)));
237        } else {
238            #[cfg(feature = "tracing")]
239            tracing::warn!(column = %key, "Skipping invalid filter column");
240        }
241        self
242    }
243
244    /// Adds a filter condition for IS NULL or IS NOT NULL checks.
245    ///
246    /// # Arguments
247    ///
248    /// * `key` - Column name to filter on
249    /// * `is_null` - If true, checks IS NULL; if false, checks IS NOT NULL
250    ///
251    /// # Examples
252    ///
253    /// ```rust
254    /// use serde::{Serialize};
255    /// use sqlx_paginated::{QueryParamsBuilder};
256    ///
257    /// #[derive(Serialize, Default)]
258    /// struct User {
259    ///     name: String,
260    ///     deleted_at: Option<String>,
261    /// }
262    ///
263    /// let params = QueryParamsBuilder::<User>::new()
264    ///     .with_filter_null("deleted_at", true)  // IS NULL
265    ///     .build();
266    /// ```
267    pub fn with_filter_null(mut self, key: impl Into<String>, is_null: bool) -> Self {
268        let key = key.into();
269        let valid_fields = get_struct_field_names::<T>();
270
271        if valid_fields.contains(&key) {
272            let condition = if is_null {
273                QueryFilterCondition::is_null()
274            } else {
275                QueryFilterCondition::is_not_null()
276            };
277            self.query.filters.insert(key, condition);
278        } else {
279            #[cfg(feature = "tracing")]
280            tracing::warn!(column = %key, "Skipping invalid filter column");
281        }
282        self
283    }
284
285    /// Adds an IN filter condition with multiple values.
286    ///
287    /// # Arguments
288    ///
289    /// * `key` - Column name to filter on
290    /// * `values` - Vector of values to check against
291    ///
292    /// # Examples
293    ///
294    /// ```rust
295    /// use serde::{Serialize};
296    /// use sqlx_paginated::{QueryParamsBuilder};
297    ///
298    /// #[derive(Serialize, Default)]
299    /// struct User {
300    ///     name: String,
301    ///     role: String,
302    /// }
303    ///
304    /// let params = QueryParamsBuilder::<User>::new()
305    ///     .with_filter_in("role", vec!["admin", "moderator", "user"])
306    ///     .build();
307    /// ```
308    pub fn with_filter_in(
309        mut self,
310        key: impl Into<String>,
311        values: Vec<impl Into<String>>,
312    ) -> Self {
313        let key = key.into();
314        let valid_fields = get_struct_field_names::<T>();
315
316        if valid_fields.contains(&key) {
317            self.query
318                .filters
319                .insert(key, QueryFilterCondition::in_list(values));
320        } else {
321            #[cfg(feature = "tracing")]
322            tracing::warn!(column = %key, "Skipping invalid filter column");
323        }
324        self
325    }
326
327    /// Adds a NOT IN filter condition with multiple values.
328    ///
329    /// # Arguments
330    ///
331    /// * `key` - Column name to filter on
332    /// * `values` - Vector of values to exclude
333    ///
334    /// # Examples
335    ///
336    /// ```rust
337    /// use serde::{Serialize};
338    /// use sqlx_paginated::{QueryParamsBuilder};
339    ///
340    /// #[derive(Serialize, Default)]
341    /// struct User {
342    ///     name: String,
343    ///     role: String,
344    /// }
345    ///
346    /// let params = QueryParamsBuilder::<User>::new()
347    ///     .with_filter_not_in("role", vec!["banned", "suspended"])
348    ///     .build();
349    /// ```
350    pub fn with_filter_not_in(
351        mut self,
352        key: impl Into<String>,
353        values: Vec<impl Into<String>>,
354    ) -> Self {
355        let key = key.into();
356        let valid_fields = get_struct_field_names::<T>();
357
358        if valid_fields.contains(&key) {
359            self.query
360                .filters
361                .insert(key, QueryFilterCondition::not_in_list(values));
362        } else {
363            #[cfg(feature = "tracing")]
364            tracing::warn!(column = %key, "Skipping invalid filter column");
365        }
366        self
367    }
368
369    /// Adds a simple equality filter condition (backward compatible).
370    ///
371    /// # Arguments
372    ///
373    /// * `key` - Column name to filter on
374    /// * `value` - Optional value to filter by
375    ///
376    /// # Details
377    ///
378    /// Only adds the filter if the column exists in the model struct.
379    /// Logs a warning if tracing is enabled and the column is invalid.
380    /// This method maintains backward compatibility with the original API.
381    ///
382    /// # Examples
383    ///
384    /// ```rust
385    /// use std::any::Any;
386    /// use serde::{Serialize};
387    /// use sqlx_paginated::{QueryParamsBuilder};
388    ///
389    /// #[derive(Serialize, Default)]
390    /// struct UserExample {
391    ///     name: String,
392    ///     status: String,
393    ///     role: String
394    /// }
395    ///
396    /// let params = QueryParamsBuilder::<UserExample>::new()
397    ///     .with_filter("status", Some("active"))
398    ///     .with_filter("role", Some("admin"))
399    ///     .build();
400    /// ```
401    pub fn with_filter(mut self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
402        let key = key.into();
403        let valid_fields = get_struct_field_names::<T>();
404
405        if valid_fields.contains(&key) {
406            if let Some(val) = value {
407                self.query
408                    .filters
409                    .insert(key, QueryFilterCondition::equal(val));
410            }
411        } else {
412            #[cfg(feature = "tracing")]
413            tracing::warn!(column = %key, "Skipping invalid filter column");
414        }
415        self
416    }
417
418    /// Adds multiple filter conditions from a HashMap (backward compatible).
419    ///
420    /// # Arguments
421    ///
422    /// * `filters` - HashMap of column names and their filter values (equality only)
423    ///
424    /// # Details
425    ///
426    /// Only adds filters for columns that exist in the model struct.
427    /// Logs a warning if tracing is enabled and a column is invalid.
428    ///
429    /// # Examples
430    ///
431    /// ```rust
432    /// use std::collections::HashMap;
433    /// use serde::{Serialize};
434    /// use sqlx_paginated::{QueryParamsBuilder};
435    ///
436    /// #[derive(Serialize, Default)]
437    /// struct UserExample {
438    ///     name: String,
439    ///     status: String,
440    ///     role: String
441    /// }
442    ///
443    /// let mut filters = HashMap::new();
444    /// filters.insert("status", Some("active"));
445    /// filters.insert("role", Some("admin"));
446    ///
447    /// let params = QueryParamsBuilder::<UserExample>::new()
448    ///     .with_filters(filters)
449    ///     .build();
450    /// ```
451    pub fn with_filters(
452        mut self,
453        filters: HashMap<impl Into<String>, Option<impl Into<String>>>,
454    ) -> Self {
455        let valid_fields = get_struct_field_names::<T>();
456
457        self.query
458            .filters
459            .extend(filters.into_iter().filter_map(|(key, value)| {
460                let key = key.into();
461                if valid_fields.contains(&key) {
462                    value.map(|v| (key, QueryFilterCondition::equal(v)))
463                } else {
464                    #[cfg(feature = "tracing")]
465                    tracing::warn!(column = %key, "Skipping invalid filter column");
466                    None
467                }
468            }));
469
470        self
471    }
472
473    /// Adds multiple filter conditions with operators from a HashMap.
474    ///
475    /// # Arguments
476    ///
477    /// * `filters` - HashMap of column names and their FilterConditions
478    ///
479    /// # Details
480    ///
481    /// Only adds filters for columns that exist in the model struct.
482    /// Logs a warning if tracing is enabled and a column is invalid.
483    ///
484    /// # Examples
485    ///
486    /// ```rust
487    /// use std::collections::HashMap;
488    /// use serde::{Serialize};
489    /// use sqlx_paginated::{QueryParamsBuilder, QueryFilterCondition, QueryFilterOperator};
490    ///
491    /// #[derive(Serialize, Default)]
492    /// struct Product {
493    ///     name: String,
494    ///     price: f64,
495    ///     stock: i32,
496    /// }
497    ///
498    /// let mut filters = HashMap::new();
499    /// filters.insert("price", QueryFilterCondition::greater_than("10.00"));
500    /// filters.insert("stock", QueryFilterCondition::less_or_equal("100"));
501    ///
502    /// let params = QueryParamsBuilder::<Product>::new()
503    ///     .with_filter_conditions(filters)
504    ///     .build();
505    /// ```
506    pub fn with_filter_conditions(
507        mut self,
508        filters: HashMap<impl Into<String>, QueryFilterCondition>,
509    ) -> Self {
510        let valid_fields = get_struct_field_names::<T>();
511
512        self.query
513            .filters
514            .extend(filters.into_iter().filter_map(|(key, condition)| {
515                let key = key.into();
516                if valid_fields.contains(&key) {
517                    Some((key, condition))
518                } else {
519                    #[cfg(feature = "tracing")]
520                    tracing::warn!(column = %key, "Skipping invalid filter column");
521                    None
522                }
523            }));
524
525        self
526    }
527
528    /// Builds and returns the final QueryParams.
529    ///
530    /// # Returns
531    ///
532    /// Returns the constructed `QueryParams<T>` with all the configured parameters.
533    ///
534    /// # Examples
535    ///
536    /// ```rust
537    /// use chrono::{DateTime, Utc};
538    /// use sqlx_paginated::{QueryParamsBuilder, QuerySortDirection};
539    /// use serde::{Serialize};
540    ///
541    /// #[derive(Serialize, Default)]
542    /// struct UserExample {
543    ///     name: String,
544    ///     status: String,
545    ///     email: String,
546    ///     created_at: DateTime<Utc>
547    /// }
548    ///
549    /// let params = QueryParamsBuilder::<UserExample>::new()
550    ///     .with_pagination(1, 20)
551    ///     .with_sort("created_at", QuerySortDirection::Descending)
552    ///     .with_search("john", vec!["name", "email"])
553    ///     .with_filter("status", Some("active"))
554    ///     .build();
555    /// ```
556    pub fn build(self) -> QueryParams<'q, T> {
557        self.query
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564    use crate::paginated_query_as::internal::{
565        DEFAULT_SEARCH_COLUMN_NAMES, DEFAULT_SORT_COLUMN_NAME,
566    };
567    use crate::paginated_query_as::models::QuerySortDirection;
568    use chrono::{DateTime, Utc};
569    use std::collections::HashMap;
570
571    #[derive(Debug, Default, Serialize)]
572    struct TestModel {
573        name: String,
574        title: String,
575        description: String,
576        status: String,
577        category: String,
578        updated_at: DateTime<Utc>,
579        created_at: DateTime<Utc>,
580    }
581
582    #[test]
583    fn test_pagination_defaults() {
584        let params = QueryParamsBuilder::<TestModel>::new().build();
585
586        assert_eq!(
587            params.pagination.page_size, DEFAULT_MIN_PAGE_SIZE,
588            "Default page size should be {}",
589            DEFAULT_MIN_PAGE_SIZE
590        );
591        assert_eq!(
592            params.pagination.page, DEFAULT_PAGE,
593            "Default page should be {}",
594            DEFAULT_PAGE
595        );
596
597        // Test page size clamping
598        let params = QueryParamsBuilder::<TestModel>::new()
599            .with_pagination(1, DEFAULT_MAX_PAGE_SIZE + 10)
600            .build();
601
602        assert_eq!(
603            params.pagination.page_size, DEFAULT_MAX_PAGE_SIZE,
604            "Page size should be clamped to maximum {}",
605            DEFAULT_MAX_PAGE_SIZE
606        );
607
608        let params = QueryParamsBuilder::<TestModel>::new()
609            .with_pagination(1, DEFAULT_MIN_PAGE_SIZE - 5)
610            .build();
611
612        assert_eq!(
613            params.pagination.page_size, DEFAULT_MIN_PAGE_SIZE,
614            "Page size should be clamped to minimum {}",
615            DEFAULT_MIN_PAGE_SIZE
616        );
617    }
618
619    #[test]
620    fn test_default_sort_column() {
621        let params = QueryParamsBuilder::<TestModel>::new().build();
622
623        assert_eq!(
624            params.sort.sort_column, DEFAULT_SORT_COLUMN_NAME,
625            "Default sort column should be '{}'",
626            DEFAULT_SORT_COLUMN_NAME
627        );
628    }
629
630    #[test]
631    fn test_date_range_defaults() {
632        let params = QueryParamsBuilder::<TestModel>::new().build();
633
634        assert_eq!(
635            params.date_range.date_column,
636            Some(DEFAULT_DATE_RANGE_COLUMN_NAME.to_string()),
637            "Default date range column should be '{}'",
638            DEFAULT_DATE_RANGE_COLUMN_NAME
639        );
640        assert!(
641            params.date_range.date_after.is_none(),
642            "Default date_after should be None"
643        );
644        assert!(
645            params.date_range.date_before.is_none(),
646            "Default date_before should be None"
647        );
648    }
649
650    #[test]
651    fn test_search_defaults() {
652        let params = QueryParamsBuilder::<TestModel>::new().build();
653
654        // Check if default search columns are set
655        assert_eq!(
656            params.search.search_columns,
657            Some(
658                DEFAULT_SEARCH_COLUMN_NAMES
659                    .iter()
660                    .map(|&s| s.to_string())
661                    .collect()
662            ),
663            "Default search columns should be {:?}",
664            DEFAULT_SEARCH_COLUMN_NAMES
665        );
666        assert!(
667            params.search.search.is_none(),
668            "Default search term should be None"
669        );
670    }
671
672    #[test]
673    fn test_combined_defaults() {
674        let params = QueryParamsBuilder::<TestModel>::new().build();
675
676        // Test all defaults together
677        assert_eq!(params.pagination.page, DEFAULT_PAGE);
678        assert_eq!(params.pagination.page_size, DEFAULT_MIN_PAGE_SIZE);
679        assert_eq!(params.sort.sort_column, DEFAULT_SORT_COLUMN_NAME);
680        assert_eq!(params.sort.sort_direction, QuerySortDirection::Descending);
681        assert_eq!(
682            params.date_range.date_column,
683            Some(DEFAULT_DATE_RANGE_COLUMN_NAME.to_string())
684        );
685        assert_eq!(
686            params.search.search_columns,
687            Some(
688                DEFAULT_SEARCH_COLUMN_NAMES
689                    .iter()
690                    .map(|&s| s.to_string())
691                    .collect()
692            )
693        );
694        assert!(params.search.search.is_none());
695        assert!(params.date_range.date_after.is_none());
696        assert!(params.date_range.date_before.is_none());
697    }
698
699    #[test]
700    fn test_empty_params() {
701        let params = QueryParamsBuilder::<TestModel>::new().build();
702
703        assert_eq!(params.pagination.page, 1);
704        assert_eq!(params.pagination.page_size, 10);
705        assert_eq!(params.sort.sort_column, "created_at");
706        assert!(matches!(
707            params.sort.sort_direction,
708            QuerySortDirection::Descending
709        ));
710    }
711
712    #[test]
713    fn test_partial_params() {
714        let params = QueryParamsBuilder::<TestModel>::new()
715            .with_pagination(2, 10)
716            .with_search("test".to_string(), vec!["name".to_string()])
717            .build();
718
719        assert_eq!(params.pagination.page, 2);
720        assert_eq!(params.search.search, Some("test".to_string()));
721        assert_eq!(params.pagination.page_size, 10);
722        assert_eq!(params.sort.sort_column, "created_at");
723        assert!(matches!(
724            params.sort.sort_direction,
725            QuerySortDirection::Descending
726        ));
727    }
728
729    #[test]
730    fn test_invalid_params() {
731        // For builder pattern, invalid params would be handled at compile time
732        // But we can test the defaults
733        let params = QueryParamsBuilder::<TestModel>::new()
734            .with_pagination(0, 0) // Should be clamped to minimum values
735            .build();
736
737        assert_eq!(params.pagination.page, 1);
738        assert_eq!(params.pagination.page_size, 10);
739    }
740
741    #[test]
742    fn test_filters() {
743        let mut filters = HashMap::new();
744        filters.insert("status".to_string(), Some("active".to_string()));
745        filters.insert("category".to_string(), Some("test".to_string()));
746
747        let params = QueryParamsBuilder::<TestModel>::new()
748            .with_filters(filters)
749            .build();
750
751        assert!(params.filters.contains_key("status"));
752        let status_filter = params.filters.get("status").unwrap();
753        assert_eq!(status_filter.operator, QueryFilterOperator::Equal);
754        assert_eq!(status_filter.value, Some("active".to_string()));
755
756        assert!(params.filters.contains_key("category"));
757        let category_filter = params.filters.get("category").unwrap();
758        assert_eq!(category_filter.operator, QueryFilterOperator::Equal);
759        assert_eq!(category_filter.value, Some("test".to_string()));
760    }
761
762    #[test]
763    fn test_search_with_columns() {
764        let params = QueryParamsBuilder::<TestModel>::new()
765            .with_search(
766                "test".to_string(),
767                vec!["title".to_string(), "description".to_string()],
768            )
769            .build();
770
771        assert_eq!(params.search.search, Some("test".to_string()));
772        assert_eq!(
773            params.search.search_columns,
774            Some(vec!["title".to_string(), "description".to_string()])
775        );
776    }
777
778    #[test]
779    fn test_full_params() {
780        let params = QueryParamsBuilder::<TestModel>::new()
781            .with_pagination(2, 20)
782            .with_sort("updated_at".to_string(), QuerySortDirection::Ascending)
783            .with_search(
784                "test".to_string(),
785                vec!["title".to_string(), "description".to_string()],
786            )
787            .with_date_range(Some(Utc::now()), None, None::<String>)
788            .build();
789
790        assert_eq!(params.pagination.page, 2);
791        assert_eq!(params.pagination.page_size, 20);
792        assert_eq!(params.sort.sort_column, "updated_at");
793        assert!(matches!(
794            params.sort.sort_direction,
795            QuerySortDirection::Ascending
796        ));
797        assert_eq!(params.search.search, Some("test".to_string()));
798        assert_eq!(
799            params.search.search_columns,
800            Some(vec!["title".to_string(), "description".to_string()])
801        );
802        assert!(params.date_range.date_after.is_some());
803        assert!(params.date_range.date_before.is_none());
804    }
805
806    #[test]
807    fn test_filter_chain() {
808        let params = QueryParamsBuilder::<TestModel>::new()
809            .with_filter("status", Some("active"))
810            .with_filter("category", Some("test"))
811            .build();
812
813        let status_filter = params.filters.get("status").unwrap();
814        assert_eq!(status_filter.operator, QueryFilterOperator::Equal);
815        assert_eq!(status_filter.value, Some("active".to_string()));
816
817        let category_filter = params.filters.get("category").unwrap();
818        assert_eq!(category_filter.operator, QueryFilterOperator::Equal);
819        assert_eq!(category_filter.value, Some("test".to_string()));
820    }
821
822    #[test]
823    fn test_mixed_pagination() {
824        let params = QueryParamsBuilder::<TestModel>::new()
825            .with_pagination(2, 10)
826            .with_search("test".to_string(), vec!["title".to_string()])
827            .with_filter("status", Some("active"))
828            .build();
829
830        assert_eq!(params.pagination.page, 2);
831        assert_eq!(params.pagination.page_size, 10);
832        assert_eq!(params.search.search, Some("test".to_string()));
833
834        let status_filter = params.filters.get("status").unwrap();
835        assert_eq!(status_filter.operator, QueryFilterOperator::Equal);
836        assert_eq!(status_filter.value, Some("active".to_string()));
837    }
838
839    #[test]
840    fn test_filter_operators() {
841        let params = QueryParamsBuilder::<TestModel>::new()
842            .with_filter_operator("title", QueryFilterOperator::Like, "%test%")
843            .with_filter_operator("status", QueryFilterOperator::NotEqual, "deleted")
844            .build();
845
846        let title_filter = params.filters.get("title").unwrap();
847        assert_eq!(title_filter.operator, QueryFilterOperator::Like);
848        assert_eq!(title_filter.value, Some("%test%".to_string()));
849
850        let status_filter = params.filters.get("status").unwrap();
851        assert_eq!(status_filter.operator, QueryFilterOperator::NotEqual);
852        assert_eq!(status_filter.value, Some("deleted".to_string()));
853    }
854
855    #[test]
856    fn test_filter_null() {
857        let params = QueryParamsBuilder::<TestModel>::new()
858            .with_filter_null("description", true)
859            .build();
860
861        let filter = params.filters.get("description").unwrap();
862        assert_eq!(filter.operator, QueryFilterOperator::IsNull);
863        assert_eq!(filter.value, None);
864    }
865
866    #[test]
867    fn test_filter_in() {
868        let params = QueryParamsBuilder::<TestModel>::new()
869            .with_filter_in("status", vec!["active", "pending", "approved"])
870            .build();
871
872        let filter = params.filters.get("status").unwrap();
873        assert_eq!(filter.operator, QueryFilterOperator::In);
874        assert_eq!(filter.value, Some("active,pending,approved".to_string()));
875
876        let values = filter.split_values();
877        assert_eq!(values, vec!["active", "pending", "approved"]);
878    }
879}