prax_query/operations/
find_many.rs

1//! FindMany operation for querying multiple records.
2
3use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::Filter;
7use crate::pagination::Pagination;
8use crate::traits::{Model, QueryEngine};
9use crate::types::{OrderBy, Select};
10
11/// A query operation that finds multiple records.
12///
13/// # Example
14///
15/// ```rust,ignore
16/// let users = client
17///     .user()
18///     .find_many()
19///     .r#where(user::email::contains("@example.com"))
20///     .order_by(user::created_at::desc())
21///     .skip(0)
22///     .take(10)
23///     .exec()
24///     .await?;
25/// ```
26pub struct FindManyOperation<E: QueryEngine, M: Model> {
27    engine: E,
28    filter: Filter,
29    order_by: OrderBy,
30    pagination: Pagination,
31    select: Select,
32    distinct: Option<Vec<String>>,
33    _model: PhantomData<M>,
34}
35
36impl<E: QueryEngine, M: Model> FindManyOperation<E, M> {
37    /// Create a new FindMany operation.
38    pub fn new(engine: E) -> Self {
39        Self {
40            engine,
41            filter: Filter::None,
42            order_by: OrderBy::none(),
43            pagination: Pagination::new(),
44            select: Select::All,
45            distinct: None,
46            _model: PhantomData,
47        }
48    }
49
50    /// Add a filter condition.
51    pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
52        let new_filter = filter.into();
53        self.filter = self.filter.and_then(new_filter);
54        self
55    }
56
57    /// Set the order by clause.
58    pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
59        self.order_by = order.into();
60        self
61    }
62
63    /// Skip a number of records.
64    pub fn skip(mut self, n: u64) -> Self {
65        self.pagination = self.pagination.skip(n);
66        self
67    }
68
69    /// Take a limited number of records.
70    pub fn take(mut self, n: u64) -> Self {
71        self.pagination = self.pagination.take(n);
72        self
73    }
74
75    /// Select specific fields.
76    pub fn select(mut self, select: impl Into<Select>) -> Self {
77        self.select = select.into();
78        self
79    }
80
81    /// Make the query distinct.
82    pub fn distinct(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
83        self.distinct = Some(columns.into_iter().map(Into::into).collect());
84        self
85    }
86
87    /// Set cursor for cursor-based pagination.
88    pub fn cursor(mut self, cursor: crate::pagination::Cursor) -> Self {
89        self.pagination = self.pagination.cursor(cursor);
90        self
91    }
92
93    /// Build the SQL query.
94    pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
95        let (where_sql, params) = self.filter.to_sql(0);
96
97        let mut sql = String::new();
98
99        // SELECT clause
100        sql.push_str("SELECT ");
101        if let Some(ref cols) = self.distinct {
102            sql.push_str("DISTINCT ON (");
103            sql.push_str(&cols.join(", "));
104            sql.push_str(") ");
105        }
106        sql.push_str(&self.select.to_sql());
107
108        // FROM clause
109        sql.push_str(" FROM ");
110        sql.push_str(M::TABLE_NAME);
111
112        // WHERE clause
113        if !self.filter.is_none() {
114            sql.push_str(" WHERE ");
115            sql.push_str(&where_sql);
116        }
117
118        // ORDER BY clause
119        if !self.order_by.is_empty() {
120            sql.push_str(" ORDER BY ");
121            sql.push_str(&self.order_by.to_sql());
122        }
123
124        // LIMIT/OFFSET clause
125        let pagination_sql = self.pagination.to_sql();
126        if !pagination_sql.is_empty() {
127            sql.push(' ');
128            sql.push_str(&pagination_sql);
129        }
130
131        (sql, params)
132    }
133
134    /// Execute the query.
135    pub async fn exec(self) -> QueryResult<Vec<M>>
136    where
137        M: Send + 'static,
138    {
139        let (sql, params) = self.build_sql();
140        self.engine.query_many::<M>(&sql, params).await
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::error::QueryError;
148    use crate::filter::FilterValue;
149    use crate::pagination::{Cursor, CursorDirection, CursorValue};
150    use crate::types::OrderByField;
151
152    struct TestModel;
153
154    impl Model for TestModel {
155        const MODEL_NAME: &'static str = "TestModel";
156        const TABLE_NAME: &'static str = "test_models";
157        const PRIMARY_KEY: &'static [&'static str] = &["id"];
158        const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
159    }
160
161    #[derive(Clone)]
162    struct MockEngine;
163
164    impl QueryEngine for MockEngine {
165        fn query_many<T: Model + Send + 'static>(
166            &self,
167            _sql: &str,
168            _params: Vec<FilterValue>,
169        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
170            Box::pin(async { Ok(Vec::new()) })
171        }
172
173        fn query_one<T: Model + Send + 'static>(
174            &self,
175            _sql: &str,
176            _params: Vec<FilterValue>,
177        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
178            Box::pin(async { Err(QueryError::not_found("test")) })
179        }
180
181        fn query_optional<T: Model + Send + 'static>(
182            &self,
183            _sql: &str,
184            _params: Vec<FilterValue>,
185        ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
186            Box::pin(async { Ok(None) })
187        }
188
189        fn execute_insert<T: Model + Send + 'static>(
190            &self,
191            _sql: &str,
192            _params: Vec<FilterValue>,
193        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
194            Box::pin(async { Err(QueryError::not_found("test")) })
195        }
196
197        fn execute_update<T: Model + Send + 'static>(
198            &self,
199            _sql: &str,
200            _params: Vec<FilterValue>,
201        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
202            Box::pin(async { Ok(Vec::new()) })
203        }
204
205        fn execute_delete(
206            &self,
207            _sql: &str,
208            _params: Vec<FilterValue>,
209        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
210            Box::pin(async { Ok(0) })
211        }
212
213        fn execute_raw(
214            &self,
215            _sql: &str,
216            _params: Vec<FilterValue>,
217        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
218            Box::pin(async { Ok(0) })
219        }
220
221        fn count(
222            &self,
223            _sql: &str,
224            _params: Vec<FilterValue>,
225        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
226            Box::pin(async { Ok(0) })
227        }
228    }
229
230    // ========== Construction Tests ==========
231
232    #[test]
233    fn test_find_many_new() {
234        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
235        let (sql, params) = op.build_sql();
236
237        assert!(sql.contains("SELECT * FROM test_models"));
238        assert!(params.is_empty());
239    }
240
241    #[test]
242    fn test_find_many_basic() {
243        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
244        let (sql, params) = op.build_sql();
245
246        assert_eq!(sql, "SELECT * FROM test_models");
247        assert!(params.is_empty());
248    }
249
250    // ========== Filter Tests ==========
251
252    #[test]
253    fn test_find_many_with_filter() {
254        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
255            .r#where(Filter::Equals("name".into(), "Alice".into()));
256
257        let (sql, params) = op.build_sql();
258
259        assert!(sql.contains("WHERE"));
260        assert!(sql.contains("name = $1"));
261        assert_eq!(params.len(), 1);
262    }
263
264    #[test]
265    fn test_find_many_with_compound_filter() {
266        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
267            .r#where(Filter::Equals("status".into(), FilterValue::String("active".to_string())))
268            .r#where(Filter::Gte("age".into(), FilterValue::Int(18)));
269
270        let (sql, params) = op.build_sql();
271
272        assert!(sql.contains("WHERE"));
273        assert!(sql.contains("AND"));
274        assert_eq!(params.len(), 2);
275    }
276
277    #[test]
278    fn test_find_many_with_or_filter() {
279        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
280            .r#where(Filter::or([
281                Filter::Equals("role".into(), FilterValue::String("admin".to_string())),
282                Filter::Equals("role".into(), FilterValue::String("moderator".to_string())),
283            ]));
284
285        let (sql, params) = op.build_sql();
286
287        assert!(sql.contains("OR"));
288        assert_eq!(params.len(), 2);
289    }
290
291    #[test]
292    fn test_find_many_with_in_filter() {
293        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
294            .r#where(Filter::In(
295                "status".into(),
296                vec![
297                    FilterValue::String("pending".to_string()),
298                    FilterValue::String("processing".to_string()),
299                ],
300            ));
301
302        let (sql, params) = op.build_sql();
303
304        assert!(sql.contains("IN"));
305        assert_eq!(params.len(), 2);
306    }
307
308    #[test]
309    fn test_find_many_without_filter() {
310        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
311        let (sql, params) = op.build_sql();
312
313        assert!(!sql.contains("WHERE"));
314        assert!(params.is_empty());
315    }
316
317    // ========== Order By Tests ==========
318
319    #[test]
320    fn test_find_many_with_order() {
321        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
322            .order_by(OrderByField::desc("created_at"));
323
324        let (sql, _) = op.build_sql();
325
326        assert!(sql.contains("ORDER BY created_at DESC"));
327    }
328
329    #[test]
330    fn test_find_many_with_asc_order() {
331        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
332            .order_by(OrderByField::asc("name"));
333
334        let (sql, _) = op.build_sql();
335
336        assert!(sql.contains("ORDER BY name ASC"));
337    }
338
339    #[test]
340    fn test_find_many_without_order() {
341        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
342        let (sql, _) = op.build_sql();
343
344        assert!(!sql.contains("ORDER BY"));
345    }
346
347    #[test]
348    fn test_find_many_order_replaces() {
349        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
350            .order_by(OrderByField::asc("name"))
351            .order_by(OrderByField::desc("created_at"));
352
353        let (sql, _) = op.build_sql();
354
355        assert!(sql.contains("ORDER BY created_at DESC"));
356        assert!(!sql.contains("ORDER BY name"));
357    }
358
359    // ========== Pagination Tests ==========
360
361    #[test]
362    fn test_find_many_with_pagination() {
363        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
364            .skip(10)
365            .take(20);
366
367        let (sql, _) = op.build_sql();
368
369        assert!(sql.contains("LIMIT 20"));
370        assert!(sql.contains("OFFSET 10"));
371    }
372
373    #[test]
374    fn test_find_many_with_skip_only() {
375        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
376            .skip(5);
377
378        let (sql, _) = op.build_sql();
379
380        assert!(sql.contains("OFFSET 5"));
381    }
382
383    #[test]
384    fn test_find_many_with_take_only() {
385        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
386            .take(100);
387
388        let (sql, _) = op.build_sql();
389
390        assert!(sql.contains("LIMIT 100"));
391    }
392
393    #[test]
394    fn test_find_many_with_cursor() {
395        let cursor = Cursor::new("id", CursorValue::Int(100), CursorDirection::After);
396        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
397            .cursor(cursor)
398            .take(10);
399
400        let (sql, _) = op.build_sql();
401
402        // Cursor pagination should add some cursor-based filtering
403        assert!(sql.contains("LIMIT 10"));
404    }
405
406    // ========== Select Tests ==========
407
408    #[test]
409    fn test_find_many_with_select() {
410        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
411            .select(Select::fields(["id", "name"]));
412
413        let (sql, _) = op.build_sql();
414
415        assert!(sql.contains("SELECT id, name FROM"));
416        assert!(!sql.contains("SELECT *"));
417    }
418
419    #[test]
420    fn test_find_many_select_single_field() {
421        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
422            .select(Select::fields(["id"]));
423
424        let (sql, _) = op.build_sql();
425
426        assert!(sql.contains("SELECT id FROM"));
427    }
428
429    #[test]
430    fn test_find_many_select_all() {
431        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
432            .select(Select::All);
433
434        let (sql, _) = op.build_sql();
435
436        assert!(sql.contains("SELECT * FROM"));
437    }
438
439    // ========== Distinct Tests ==========
440
441    #[test]
442    fn test_find_many_with_distinct() {
443        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
444            .distinct(["category"]);
445
446        let (sql, _) = op.build_sql();
447
448        assert!(sql.contains("DISTINCT ON (category)"));
449    }
450
451    #[test]
452    fn test_find_many_with_multiple_distinct() {
453        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
454            .distinct(["category", "status"]);
455
456        let (sql, _) = op.build_sql();
457
458        assert!(sql.contains("DISTINCT ON (category, status)"));
459    }
460
461    #[test]
462    fn test_find_many_without_distinct() {
463        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
464        let (sql, _) = op.build_sql();
465
466        assert!(!sql.contains("DISTINCT"));
467    }
468
469    // ========== SQL Structure Tests ==========
470
471    #[test]
472    fn test_find_many_sql_structure() {
473        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
474            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
475            .order_by(OrderByField::desc("created_at"))
476            .skip(10)
477            .take(20)
478            .select(Select::fields(["id", "name"]));
479
480        let (sql, _) = op.build_sql();
481
482        // Check correct SQL clause ordering
483        let select_pos = sql.find("SELECT").unwrap();
484        let from_pos = sql.find("FROM").unwrap();
485        let where_pos = sql.find("WHERE").unwrap();
486        let order_pos = sql.find("ORDER BY").unwrap();
487        let limit_pos = sql.find("LIMIT").unwrap();
488        let offset_pos = sql.find("OFFSET").unwrap();
489
490        assert!(select_pos < from_pos);
491        assert!(from_pos < where_pos);
492        assert!(where_pos < order_pos);
493        assert!(order_pos < limit_pos);
494        assert!(limit_pos < offset_pos);
495    }
496
497    #[test]
498    fn test_find_many_table_name() {
499        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
500        let (sql, _) = op.build_sql();
501
502        assert!(sql.contains("test_models"));
503    }
504
505    // ========== Async Execution Tests ==========
506
507    #[tokio::test]
508    async fn test_find_many_exec() {
509        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
510            .r#where(Filter::Equals("status".into(), FilterValue::String("active".to_string())));
511
512        let result = op.exec().await;
513
514        assert!(result.is_ok());
515        assert!(result.unwrap().is_empty()); // MockEngine returns empty vec
516    }
517
518    #[tokio::test]
519    async fn test_find_many_exec_no_filter() {
520        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
521
522        let result = op.exec().await;
523
524        assert!(result.is_ok());
525    }
526
527    // ========== Method Chaining Tests ==========
528
529    #[test]
530    fn test_find_many_full_chain() {
531        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
532            .r#where(Filter::Equals("status".into(), FilterValue::String("active".to_string())))
533            .order_by(OrderByField::desc("created_at"))
534            .skip(10)
535            .take(20)
536            .select(Select::fields(["id", "name", "email"]))
537            .distinct(["category"]);
538
539        let (sql, params) = op.build_sql();
540
541        assert!(sql.contains("DISTINCT ON (category)"));
542        assert!(sql.contains("SELECT"));
543        assert!(sql.contains("WHERE"));
544        assert!(sql.contains("ORDER BY created_at DESC"));
545        assert!(sql.contains("LIMIT 20"));
546        assert!(sql.contains("OFFSET 10"));
547        assert_eq!(params.len(), 1);
548    }
549
550    // ========== Edge Cases ==========
551
552    #[test]
553    fn test_find_many_with_like_filter() {
554        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
555            .r#where(Filter::Contains("email".into(), FilterValue::String("@example.com".to_string())));
556
557        let (sql, params) = op.build_sql();
558
559        assert!(sql.contains("LIKE"));
560        assert_eq!(params.len(), 1);
561    }
562
563    #[test]
564    fn test_find_many_with_null_filter() {
565        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
566            .r#where(Filter::IsNull("deleted_at".into()));
567
568        let (sql, params) = op.build_sql();
569
570        assert!(sql.contains("IS NULL"));
571        assert!(params.is_empty());
572    }
573
574    #[test]
575    fn test_find_many_with_not_filter() {
576        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
577            .r#where(Filter::Not(Box::new(Filter::Equals(
578                "status".into(),
579                FilterValue::String("deleted".to_string()),
580            ))));
581
582        let (sql, params) = op.build_sql();
583
584        assert!(sql.contains("NOT"));
585        assert_eq!(params.len(), 1);
586    }
587
588    #[test]
589    fn test_find_many_with_between_equivalent() {
590        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
591            .r#where(Filter::Gte("age".into(), FilterValue::Int(18)))
592            .r#where(Filter::Lte("age".into(), FilterValue::Int(65)));
593
594        let (sql, params) = op.build_sql();
595
596        assert!(sql.contains("AND"));
597        assert_eq!(params.len(), 2);
598    }
599}
600