1use 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
11pub 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 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 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 pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
59 self.order_by = order.into();
60 self
61 }
62
63 pub fn skip(mut self, n: u64) -> Self {
65 self.pagination = self.pagination.skip(n);
66 self
67 }
68
69 pub fn take(mut self, n: u64) -> Self {
71 self.pagination = self.pagination.take(n);
72 self
73 }
74
75 pub fn select(mut self, select: impl Into<Select>) -> Self {
77 self.select = select.into();
78 self
79 }
80
81 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 pub fn cursor(mut self, cursor: crate::pagination::Cursor) -> Self {
89 self.pagination = self.pagination.cursor(cursor);
90 self
91 }
92
93 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 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 sql.push_str(" FROM ");
110 sql.push_str(M::TABLE_NAME);
111
112 if !self.filter.is_none() {
114 sql.push_str(" WHERE ");
115 sql.push_str(&where_sql);
116 }
117
118 if !self.order_by.is_empty() {
120 sql.push_str(" ORDER BY ");
121 sql.push_str(&self.order_by.to_sql());
122 }
123
124 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 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 #[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 #[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 #[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 #[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 assert!(sql.contains("LIMIT 10"));
404 }
405
406 #[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 #[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 #[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 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 #[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()); }
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 #[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 #[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