1use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::Filter;
7use crate::pagination::Pagination;
8use crate::relations::IncludeSpec;
9use crate::traits::{Model, ModelRelationLoader, QueryEngine};
10use crate::types::{OrderBy, Select};
11
12pub struct FindManyOperation<E: QueryEngine, M: Model> {
28 engine: E,
29 filter: Filter,
30 order_by: OrderBy,
31 pagination: Pagination,
32 select: Select,
33 distinct: Option<Vec<String>>,
34 includes: Vec<IncludeSpec>,
38 _model: PhantomData<M>,
39}
40
41impl<E: QueryEngine, M: Model + crate::row::FromRow> FindManyOperation<E, M> {
42 pub fn new(engine: E) -> Self {
44 Self {
45 engine,
46 filter: Filter::None,
47 order_by: OrderBy::none(),
48 pagination: Pagination::new(),
49 select: Select::All,
50 distinct: None,
51 includes: Vec::new(),
52 _model: PhantomData,
53 }
54 }
55
56 pub fn include(mut self, spec: IncludeSpec) -> Self {
63 self.includes.push(spec);
64 self
65 }
66
67 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
69 let new_filter = filter.into();
70 self.filter = self.filter.and_then(new_filter);
71 self
72 }
73
74 pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
76 self.order_by = order.into();
77 self
78 }
79
80 pub fn skip(mut self, n: u64) -> Self {
82 self.pagination = self.pagination.skip(n);
83 self
84 }
85
86 pub fn take(mut self, n: u64) -> Self {
88 self.pagination = self.pagination.take(n);
89 self
90 }
91
92 pub fn select(mut self, select: impl Into<Select>) -> Self {
94 self.select = select.into();
95 self
96 }
97
98 pub fn distinct(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
100 self.distinct = Some(columns.into_iter().map(Into::into).collect());
101 self
102 }
103
104 pub fn cursor(mut self, cursor: crate::pagination::Cursor) -> Self {
106 self.pagination = self.pagination.cursor(cursor);
107 self
108 }
109
110 pub fn build_sql(
112 &self,
113 dialect: &dyn crate::dialect::SqlDialect,
114 ) -> (String, Vec<crate::filter::FilterValue>) {
115 let (where_sql, params) = self.filter.to_sql(0, dialect);
116
117 let mut sql = String::new();
118
119 sql.push_str("SELECT ");
121 if let Some(ref cols) = self.distinct {
122 sql.push_str("DISTINCT ON (");
123 sql.push_str(&cols.join(", "));
124 sql.push_str(") ");
125 }
126 sql.push_str(&self.select.to_sql());
127
128 sql.push_str(" FROM ");
130 sql.push_str(M::TABLE_NAME);
131
132 if !self.filter.is_none() {
134 sql.push_str(" WHERE ");
135 sql.push_str(&where_sql);
136 }
137
138 if !self.order_by.is_empty() {
140 sql.push_str(" ORDER BY ");
141 sql.push_str(&self.order_by.to_sql());
142 }
143
144 let pagination_sql = self.pagination.to_sql();
146 if !pagination_sql.is_empty() {
147 sql.push(' ');
148 sql.push_str(&pagination_sql);
149 }
150
151 (sql, params)
152 }
153
154 pub async fn exec(self) -> QueryResult<Vec<M>>
162 where
163 M: Send + 'static + ModelRelationLoader<E>,
164 {
165 let dialect = self.engine.dialect();
166 let (sql, params) = self.build_sql(dialect);
167 let mut parents = self.engine.query_many::<M>(&sql, params).await?;
168 for spec in &self.includes {
169 <M as ModelRelationLoader<E>>::load_relation(&self.engine, &mut parents, spec).await?;
170 }
171 Ok(parents)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::error::QueryError;
179 use crate::filter::FilterValue;
180 use crate::pagination::{Cursor, CursorDirection, CursorValue};
181 use crate::types::OrderByField;
182
183 struct TestModel;
184
185 impl Model for TestModel {
186 const MODEL_NAME: &'static str = "TestModel";
187 const TABLE_NAME: &'static str = "test_models";
188 const PRIMARY_KEY: &'static [&'static str] = &["id"];
189 const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
190 }
191
192 impl crate::row::FromRow for TestModel {
193 fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
194 Ok(TestModel)
195 }
196 }
197
198 impl crate::traits::ModelRelationLoader<MockEngine> for TestModel {
202 fn load_relation<'a>(
203 _engine: &'a MockEngine,
204 _parents: &'a mut [Self],
205 spec: &'a crate::relations::IncludeSpec,
206 ) -> crate::traits::BoxFuture<'a, QueryResult<()>> {
207 let name = spec.relation_name.clone();
208 Box::pin(async move {
209 Err(QueryError::internal(format!(
210 "unknown relation '{name}' on TestModel (mock)",
211 )))
212 })
213 }
214 }
215
216 #[derive(Clone)]
217 struct MockEngine;
218
219 impl QueryEngine for MockEngine {
220 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
221 &crate::dialect::Postgres
222 }
223
224 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
225 &self,
226 _sql: &str,
227 _params: Vec<FilterValue>,
228 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
229 Box::pin(async { Ok(Vec::new()) })
230 }
231
232 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
233 &self,
234 _sql: &str,
235 _params: Vec<FilterValue>,
236 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
237 Box::pin(async { Err(QueryError::not_found("test")) })
238 }
239
240 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
241 &self,
242 _sql: &str,
243 _params: Vec<FilterValue>,
244 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
245 Box::pin(async { Ok(None) })
246 }
247
248 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
249 &self,
250 _sql: &str,
251 _params: Vec<FilterValue>,
252 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
253 Box::pin(async { Err(QueryError::not_found("test")) })
254 }
255
256 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
257 &self,
258 _sql: &str,
259 _params: Vec<FilterValue>,
260 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
261 Box::pin(async { Ok(Vec::new()) })
262 }
263
264 fn execute_delete(
265 &self,
266 _sql: &str,
267 _params: Vec<FilterValue>,
268 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
269 Box::pin(async { Ok(0) })
270 }
271
272 fn execute_raw(
273 &self,
274 _sql: &str,
275 _params: Vec<FilterValue>,
276 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
277 Box::pin(async { Ok(0) })
278 }
279
280 fn count(
281 &self,
282 _sql: &str,
283 _params: Vec<FilterValue>,
284 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
285 Box::pin(async { Ok(0) })
286 }
287 }
288
289 #[test]
292 fn test_find_many_new() {
293 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
294 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
295
296 assert!(sql.contains("SELECT * FROM test_models"));
297 assert!(params.is_empty());
298 }
299
300 #[test]
301 fn test_find_many_basic() {
302 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
303 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
304
305 assert_eq!(sql, "SELECT * FROM test_models");
306 assert!(params.is_empty());
307 }
308
309 #[test]
312 fn test_find_many_with_filter() {
313 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
314 .r#where(Filter::Equals("name".into(), "Alice".into()));
315
316 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
317
318 assert!(sql.contains("WHERE"));
319 assert!(sql.contains(r#""name" = $1"#));
320 assert_eq!(params.len(), 1);
321 }
322
323 #[test]
324 fn test_find_many_with_compound_filter() {
325 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
326 .r#where(Filter::Equals(
327 "status".into(),
328 FilterValue::String("active".to_string()),
329 ))
330 .r#where(Filter::Gte("age".into(), FilterValue::Int(18)));
331
332 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
333
334 assert!(sql.contains("WHERE"));
335 assert!(sql.contains("AND"));
336 assert_eq!(params.len(), 2);
337 }
338
339 #[test]
340 fn test_find_many_with_or_filter() {
341 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::or([
342 Filter::Equals("role".into(), FilterValue::String("admin".to_string())),
343 Filter::Equals("role".into(), FilterValue::String("moderator".to_string())),
344 ]));
345
346 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
347
348 assert!(sql.contains("OR"));
349 assert_eq!(params.len(), 2);
350 }
351
352 #[test]
353 fn test_find_many_with_in_filter() {
354 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::In(
355 "status".into(),
356 vec![
357 FilterValue::String("pending".to_string()),
358 FilterValue::String("processing".to_string()),
359 ],
360 ));
361
362 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
363
364 assert!(sql.contains("IN"));
365 assert_eq!(params.len(), 2);
366 }
367
368 #[test]
369 fn test_find_many_without_filter() {
370 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
371 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
372
373 assert!(!sql.contains("WHERE"));
374 assert!(params.is_empty());
375 }
376
377 #[test]
380 fn test_find_many_with_order() {
381 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
382 .order_by(OrderByField::desc("created_at"));
383
384 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
385
386 assert!(sql.contains("ORDER BY created_at DESC"));
387 }
388
389 #[test]
390 fn test_find_many_with_asc_order() {
391 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
392 .order_by(OrderByField::asc("name"));
393
394 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
395
396 assert!(sql.contains("ORDER BY name ASC"));
397 }
398
399 #[test]
400 fn test_find_many_without_order() {
401 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
402 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
403
404 assert!(!sql.contains("ORDER BY"));
405 }
406
407 #[test]
408 fn test_find_many_order_replaces() {
409 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
410 .order_by(OrderByField::asc("name"))
411 .order_by(OrderByField::desc("created_at"));
412
413 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
414
415 assert!(sql.contains("ORDER BY created_at DESC"));
416 assert!(!sql.contains("ORDER BY name"));
417 }
418
419 #[test]
422 fn test_find_many_with_pagination() {
423 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
424 .skip(10)
425 .take(20);
426
427 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
428
429 assert!(sql.contains("LIMIT 20"));
430 assert!(sql.contains("OFFSET 10"));
431 }
432
433 #[test]
434 fn test_find_many_with_skip_only() {
435 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).skip(5);
436
437 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
438
439 assert!(sql.contains("OFFSET 5"));
440 }
441
442 #[test]
443 fn test_find_many_with_take_only() {
444 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).take(100);
445
446 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
447
448 assert!(sql.contains("LIMIT 100"));
449 }
450
451 #[test]
452 fn test_find_many_with_cursor() {
453 let cursor = Cursor::new("id", CursorValue::Int(100), CursorDirection::After);
454 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
455 .cursor(cursor)
456 .take(10);
457
458 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
459
460 assert!(sql.contains("LIMIT 10"));
462 }
463
464 #[test]
467 fn test_find_many_with_select() {
468 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
469 .select(Select::fields(["id", "name"]));
470
471 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
472
473 assert!(sql.contains("SELECT id, name FROM"));
474 assert!(!sql.contains("SELECT *"));
475 }
476
477 #[test]
478 fn test_find_many_select_single_field() {
479 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
480 .select(Select::fields(["id"]));
481
482 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
483
484 assert!(sql.contains("SELECT id FROM"));
485 }
486
487 #[test]
488 fn test_find_many_select_all() {
489 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).select(Select::All);
490
491 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
492
493 assert!(sql.contains("SELECT * FROM"));
494 }
495
496 #[test]
502 fn find_many_emits_explicit_column_list_when_select_narrows() {
503 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
504 .select(Select::fields(["id", "email"]));
505 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
506 assert!(
507 sql.contains("SELECT id, email FROM") && !sql.contains("SELECT *"),
508 "expected narrow select list, got: {sql}"
509 );
510 }
511
512 #[test]
517 fn find_many_emits_star_when_no_select() {
518 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
519 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
520 assert!(sql.contains("SELECT *"), "expected SELECT *, got: {sql}");
521 }
522
523 #[test]
526 fn test_find_many_with_distinct() {
527 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).distinct(["category"]);
528
529 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
530
531 assert!(sql.contains("DISTINCT ON (category)"));
532 }
533
534 #[test]
535 fn test_find_many_with_multiple_distinct() {
536 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
537 .distinct(["category", "status"]);
538
539 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
540
541 assert!(sql.contains("DISTINCT ON (category, status)"));
542 }
543
544 #[test]
545 fn test_find_many_without_distinct() {
546 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
547 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
548
549 assert!(!sql.contains("DISTINCT"));
550 }
551
552 #[test]
555 fn test_find_many_sql_structure() {
556 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
557 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
558 .order_by(OrderByField::desc("created_at"))
559 .skip(10)
560 .take(20)
561 .select(Select::fields(["id", "name"]));
562
563 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
564
565 let select_pos = sql.find("SELECT").unwrap();
567 let from_pos = sql.find("FROM").unwrap();
568 let where_pos = sql.find("WHERE").unwrap();
569 let order_pos = sql.find("ORDER BY").unwrap();
570 let limit_pos = sql.find("LIMIT").unwrap();
571 let offset_pos = sql.find("OFFSET").unwrap();
572
573 assert!(select_pos < from_pos);
574 assert!(from_pos < where_pos);
575 assert!(where_pos < order_pos);
576 assert!(order_pos < limit_pos);
577 assert!(limit_pos < offset_pos);
578 }
579
580 #[test]
581 fn test_find_many_table_name() {
582 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
583 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
584
585 assert!(sql.contains("test_models"));
586 }
587
588 #[tokio::test]
591 async fn test_find_many_exec() {
592 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(
593 Filter::Equals("status".into(), FilterValue::String("active".to_string())),
594 );
595
596 let result = op.exec().await;
597
598 assert!(result.is_ok());
599 assert!(result.unwrap().is_empty()); }
601
602 #[tokio::test]
603 async fn test_find_many_exec_no_filter() {
604 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
605
606 let result = op.exec().await;
607
608 assert!(result.is_ok());
609 }
610
611 #[test]
614 fn test_find_many_full_chain() {
615 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
616 .r#where(Filter::Equals(
617 "status".into(),
618 FilterValue::String("active".to_string()),
619 ))
620 .order_by(OrderByField::desc("created_at"))
621 .skip(10)
622 .take(20)
623 .select(Select::fields(["id", "name", "email"]))
624 .distinct(["category"]);
625
626 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
627
628 assert!(sql.contains("DISTINCT ON (category)"));
629 assert!(sql.contains("SELECT"));
630 assert!(sql.contains("WHERE"));
631 assert!(sql.contains("ORDER BY created_at DESC"));
632 assert!(sql.contains("LIMIT 20"));
633 assert!(sql.contains("OFFSET 10"));
634 assert_eq!(params.len(), 1);
635 }
636
637 #[test]
640 fn test_find_many_with_like_filter() {
641 let op =
642 FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Contains(
643 "email".into(),
644 FilterValue::String("@example.com".to_string()),
645 ));
646
647 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
648
649 assert!(sql.contains("LIKE"));
650 assert_eq!(params.len(), 1);
651 }
652
653 #[test]
654 fn test_find_many_with_null_filter() {
655 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
656 .r#where(Filter::IsNull("deleted_at".into()));
657
658 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
659
660 assert!(sql.contains("IS NULL"));
661 assert!(params.is_empty());
662 }
663
664 #[test]
665 fn test_find_many_with_not_filter() {
666 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Not(
667 Box::new(Filter::Equals(
668 "status".into(),
669 FilterValue::String("deleted".to_string()),
670 )),
671 ));
672
673 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
674
675 assert!(sql.contains("NOT"));
676 assert_eq!(params.len(), 1);
677 }
678
679 #[test]
680 fn test_find_many_with_between_equivalent() {
681 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
682 .r#where(Filter::Gte("age".into(), FilterValue::Int(18)))
683 .r#where(Filter::Lte("age".into(), FilterValue::Int(65)));
684
685 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
686
687 assert!(sql.contains("AND"));
688 assert_eq!(params.len(), 2);
689 }
690
691 #[test]
694 fn builds_mysql_placeholders() {
695 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
696 .r#where(Filter::Equals("name".into(), "a".into()));
697 let (sql, _) = op.build_sql(&crate::dialect::Mysql);
698 assert!(
699 sql.contains("?") && !sql.contains("$1"),
700 "expected ? placeholders, got: {sql}"
701 );
702 }
703
704 #[test]
705 fn builds_mssql_placeholders() {
706 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
707 .r#where(Filter::Equals("name".into(), "a".into()));
708 let (sql, _) = op.build_sql(&crate::dialect::Mssql);
709 assert!(sql.contains("@P1"), "expected @P1 placeholders, got: {sql}");
710 }
711
712 #[test]
713 fn builds_sqlite_placeholders() {
714 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
715 .r#where(Filter::Equals("name".into(), "a".into()));
716 let (sql, _) = op.build_sql(&crate::dialect::Sqlite);
717 assert!(sql.contains("?1"), "expected ?1 placeholders, got: {sql}");
718 }
719}