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(
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 #[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 #[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 assert!(sql.contains("LIMIT 10"));
403 }
404
405 #[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 #[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 #[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 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 #[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()); }
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 #[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 #[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}