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(
268                "status".into(),
269                FilterValue::String("active".to_string()),
270            ))
271            .r#where(Filter::Gte("age".into(), FilterValue::Int(18)));
272
273        let (sql, params) = op.build_sql();
274
275        assert!(sql.contains("WHERE"));
276        assert!(sql.contains("AND"));
277        assert_eq!(params.len(), 2);
278    }
279
280    #[test]
281    fn test_find_many_with_or_filter() {
282        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::or([
283            Filter::Equals("role".into(), FilterValue::String("admin".to_string())),
284            Filter::Equals("role".into(), FilterValue::String("moderator".to_string())),
285        ]));
286
287        let (sql, params) = op.build_sql();
288
289        assert!(sql.contains("OR"));
290        assert_eq!(params.len(), 2);
291    }
292
293    #[test]
294    fn test_find_many_with_in_filter() {
295        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::In(
296            "status".into(),
297            vec![
298                FilterValue::String("pending".to_string()),
299                FilterValue::String("processing".to_string()),
300            ],
301        ));
302
303        let (sql, params) = op.build_sql();
304
305        assert!(sql.contains("IN"));
306        assert_eq!(params.len(), 2);
307    }
308
309    #[test]
310    fn test_find_many_without_filter() {
311        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
312        let (sql, params) = op.build_sql();
313
314        assert!(!sql.contains("WHERE"));
315        assert!(params.is_empty());
316    }
317
318    // ========== Order By Tests ==========
319
320    #[test]
321    fn test_find_many_with_order() {
322        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
323            .order_by(OrderByField::desc("created_at"));
324
325        let (sql, _) = op.build_sql();
326
327        assert!(sql.contains("ORDER BY created_at DESC"));
328    }
329
330    #[test]
331    fn test_find_many_with_asc_order() {
332        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
333            .order_by(OrderByField::asc("name"));
334
335        let (sql, _) = op.build_sql();
336
337        assert!(sql.contains("ORDER BY name ASC"));
338    }
339
340    #[test]
341    fn test_find_many_without_order() {
342        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
343        let (sql, _) = op.build_sql();
344
345        assert!(!sql.contains("ORDER BY"));
346    }
347
348    #[test]
349    fn test_find_many_order_replaces() {
350        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
351            .order_by(OrderByField::asc("name"))
352            .order_by(OrderByField::desc("created_at"));
353
354        let (sql, _) = op.build_sql();
355
356        assert!(sql.contains("ORDER BY created_at DESC"));
357        assert!(!sql.contains("ORDER BY name"));
358    }
359
360    // ========== Pagination Tests ==========
361
362    #[test]
363    fn test_find_many_with_pagination() {
364        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
365            .skip(10)
366            .take(20);
367
368        let (sql, _) = op.build_sql();
369
370        assert!(sql.contains("LIMIT 20"));
371        assert!(sql.contains("OFFSET 10"));
372    }
373
374    #[test]
375    fn test_find_many_with_skip_only() {
376        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).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).take(100);
386
387        let (sql, _) = op.build_sql();
388
389        assert!(sql.contains("LIMIT 100"));
390    }
391
392    #[test]
393    fn test_find_many_with_cursor() {
394        let cursor = Cursor::new("id", CursorValue::Int(100), CursorDirection::After);
395        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
396            .cursor(cursor)
397            .take(10);
398
399        let (sql, _) = op.build_sql();
400
401        // Cursor pagination should add some cursor-based filtering
402        assert!(sql.contains("LIMIT 10"));
403    }
404
405    // ========== Select Tests ==========
406
407    #[test]
408    fn test_find_many_with_select() {
409        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
410            .select(Select::fields(["id", "name"]));
411
412        let (sql, _) = op.build_sql();
413
414        assert!(sql.contains("SELECT id, name FROM"));
415        assert!(!sql.contains("SELECT *"));
416    }
417
418    #[test]
419    fn test_find_many_select_single_field() {
420        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
421            .select(Select::fields(["id"]));
422
423        let (sql, _) = op.build_sql();
424
425        assert!(sql.contains("SELECT id FROM"));
426    }
427
428    #[test]
429    fn test_find_many_select_all() {
430        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).select(Select::All);
431
432        let (sql, _) = op.build_sql();
433
434        assert!(sql.contains("SELECT * FROM"));
435    }
436
437    // ========== Distinct Tests ==========
438
439    #[test]
440    fn test_find_many_with_distinct() {
441        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).distinct(["category"]);
442
443        let (sql, _) = op.build_sql();
444
445        assert!(sql.contains("DISTINCT ON (category)"));
446    }
447
448    #[test]
449    fn test_find_many_with_multiple_distinct() {
450        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
451            .distinct(["category", "status"]);
452
453        let (sql, _) = op.build_sql();
454
455        assert!(sql.contains("DISTINCT ON (category, status)"));
456    }
457
458    #[test]
459    fn test_find_many_without_distinct() {
460        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
461        let (sql, _) = op.build_sql();
462
463        assert!(!sql.contains("DISTINCT"));
464    }
465
466    // ========== SQL Structure Tests ==========
467
468    #[test]
469    fn test_find_many_sql_structure() {
470        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
471            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
472            .order_by(OrderByField::desc("created_at"))
473            .skip(10)
474            .take(20)
475            .select(Select::fields(["id", "name"]));
476
477        let (sql, _) = op.build_sql();
478
479        // Check correct SQL clause ordering
480        let select_pos = sql.find("SELECT").unwrap();
481        let from_pos = sql.find("FROM").unwrap();
482        let where_pos = sql.find("WHERE").unwrap();
483        let order_pos = sql.find("ORDER BY").unwrap();
484        let limit_pos = sql.find("LIMIT").unwrap();
485        let offset_pos = sql.find("OFFSET").unwrap();
486
487        assert!(select_pos < from_pos);
488        assert!(from_pos < where_pos);
489        assert!(where_pos < order_pos);
490        assert!(order_pos < limit_pos);
491        assert!(limit_pos < offset_pos);
492    }
493
494    #[test]
495    fn test_find_many_table_name() {
496        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
497        let (sql, _) = op.build_sql();
498
499        assert!(sql.contains("test_models"));
500    }
501
502    // ========== Async Execution Tests ==========
503
504    #[tokio::test]
505    async fn test_find_many_exec() {
506        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(
507            Filter::Equals("status".into(), FilterValue::String("active".to_string())),
508        );
509
510        let result = op.exec().await;
511
512        assert!(result.is_ok());
513        assert!(result.unwrap().is_empty()); // MockEngine returns empty vec
514    }
515
516    #[tokio::test]
517    async fn test_find_many_exec_no_filter() {
518        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
519
520        let result = op.exec().await;
521
522        assert!(result.is_ok());
523    }
524
525    // ========== Method Chaining Tests ==========
526
527    #[test]
528    fn test_find_many_full_chain() {
529        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
530            .r#where(Filter::Equals(
531                "status".into(),
532                FilterValue::String("active".to_string()),
533            ))
534            .order_by(OrderByField::desc("created_at"))
535            .skip(10)
536            .take(20)
537            .select(Select::fields(["id", "name", "email"]))
538            .distinct(["category"]);
539
540        let (sql, params) = op.build_sql();
541
542        assert!(sql.contains("DISTINCT ON (category)"));
543        assert!(sql.contains("SELECT"));
544        assert!(sql.contains("WHERE"));
545        assert!(sql.contains("ORDER BY created_at DESC"));
546        assert!(sql.contains("LIMIT 20"));
547        assert!(sql.contains("OFFSET 10"));
548        assert_eq!(params.len(), 1);
549    }
550
551    // ========== Edge Cases ==========
552
553    #[test]
554    fn test_find_many_with_like_filter() {
555        let op =
556            FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Contains(
557                "email".into(),
558                FilterValue::String("@example.com".to_string()),
559            ));
560
561        let (sql, params) = op.build_sql();
562
563        assert!(sql.contains("LIKE"));
564        assert_eq!(params.len(), 1);
565    }
566
567    #[test]
568    fn test_find_many_with_null_filter() {
569        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
570            .r#where(Filter::IsNull("deleted_at".into()));
571
572        let (sql, params) = op.build_sql();
573
574        assert!(sql.contains("IS NULL"));
575        assert!(params.is_empty());
576    }
577
578    #[test]
579    fn test_find_many_with_not_filter() {
580        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Not(
581            Box::new(Filter::Equals(
582                "status".into(),
583                FilterValue::String("deleted".to_string()),
584            )),
585        ));
586
587        let (sql, params) = op.build_sql();
588
589        assert!(sql.contains("NOT"));
590        assert_eq!(params.len(), 1);
591    }
592
593    #[test]
594    fn test_find_many_with_between_equivalent() {
595        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
596            .r#where(Filter::Gte("age".into(), FilterValue::Int(18)))
597            .r#where(Filter::Lte("age".into(), FilterValue::Int(65)));
598
599        let (sql, params) = op.build_sql();
600
601        assert!(sql.contains("AND"));
602        assert_eq!(params.len(), 2);
603    }
604}