1use std::marker::PhantomData;
4
5use smallvec::SmallVec;
6
7use crate::capabilities::SupportsScalarSubqueryInSelect;
8use crate::error::QueryResult;
9use crate::filter::Filter;
10use crate::pagination::Pagination;
11use crate::projection::ScalarProjection;
12use crate::relations::IncludeSpec;
13use crate::traits::{Model, ModelRelationLoader, QueryEngine};
14use crate::types::{OrderBy, Select};
15
16pub struct FindManyOperation<E: QueryEngine, M: Model> {
32 engine: E,
33 filter: Filter,
34 order_by: OrderBy,
35 pagination: Pagination,
36 select: Select,
37 distinct: Option<Vec<String>>,
38 includes: SmallVec<[IncludeSpec; 2]>,
44 pub extra_projections: Vec<ScalarProjection>,
47 _model: PhantomData<M>,
48}
49
50impl<E: QueryEngine, M: Model + crate::row::FromRow> FindManyOperation<E, M> {
51 pub fn new(engine: E) -> Self {
53 Self {
54 engine,
55 filter: Filter::None,
56 order_by: OrderBy::none(),
57 pagination: Pagination::new(),
58 select: Select::All,
59 distinct: None,
60 includes: SmallVec::new(),
61 extra_projections: Vec::new(),
62 _model: PhantomData,
63 }
64 }
65
66 pub fn include(mut self, spec: IncludeSpec) -> Self {
73 self.includes.push(spec);
74 self
75 }
76
77 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
79 let new_filter = filter.into();
80 self.filter = self.filter.and_then(new_filter);
81 self
82 }
83
84 pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
86 self.order_by = order.into();
87 self
88 }
89
90 pub fn skip(mut self, n: u64) -> Self {
92 self.pagination = self.pagination.skip(n);
93 self
94 }
95
96 pub fn take(mut self, n: u64) -> Self {
98 self.pagination = self.pagination.take(n);
99 self
100 }
101
102 pub fn select(mut self, select: impl Into<Select>) -> Self {
104 self.select = select.into();
105 self
106 }
107
108 pub fn distinct(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
116 self.distinct = Some(columns.into_iter().map(Into::into).collect());
117 self
118 }
119
120 pub fn cursor(mut self, cursor: crate::pagination::Cursor) -> Self {
128 self.pagination = self.pagination.cursor(cursor);
129 self
130 }
131
132 pub fn with_where_input<W: crate::inputs::WhereInput<Model = M>>(mut self, w: W) -> Self {
135 let f = w.into_ir();
136 self.filter = self.filter.and_then(f);
137 self
138 }
139
140 pub fn with_include_input<I: crate::inputs::IncludeInput<Model = M>>(mut self, i: I) -> Self {
143 let inc = i.into_ir();
144 for spec in inc.specs() {
145 self.includes.push(spec.clone());
146 }
147 self
148 }
149
150 pub fn with_select_input<S: crate::inputs::SelectInput<Model = M>>(mut self, s: S) -> Self {
152 self.select = s.into_ir();
153 self
154 }
155
156 pub fn with_order_by_input<O: crate::inputs::OrderByInput<Model = M>>(mut self, o: O) -> Self {
158 self.order_by = o.into_ir();
159 self
160 }
161
162 #[doc(hidden)]
165 pub fn filter_for_test(&self) -> &Filter {
166 &self.filter
167 }
168}
169
170impl<E, M> FindManyOperation<E, M>
171where
172 E: QueryEngine + SupportsScalarSubqueryInSelect,
173 M: Model + crate::row::FromRow,
174{
175 pub fn with_scalar_projection(mut self, proj: ScalarProjection) -> Self {
183 self.extra_projections.push(proj);
184 self
185 }
186}
187
188impl<E: QueryEngine, M: Model + crate::row::FromRow> FindManyOperation<E, M> {
189 pub fn build_sql(
191 &self,
192 dialect: &dyn crate::dialect::SqlDialect,
193 ) -> (String, Vec<crate::filter::FilterValue>) {
194 let proj_param_count: usize = self.extra_projections.iter().map(|p| p.params.len()).sum();
198 let (where_sql, where_params) = self.filter.to_sql(proj_param_count, dialect);
199
200 let cursor = self.pagination.cursor.as_ref();
201 let mut params: Vec<crate::filter::FilterValue> = Vec::with_capacity(
202 proj_param_count + where_params.len() + usize::from(cursor.is_some()),
203 );
204
205 let mut sql = String::with_capacity(64 + M::TABLE_NAME.len() + where_sql.len());
208
209 sql.push_str("SELECT ");
211 if let Some(ref cols) = self.distinct {
212 if dialect.supports_distinct_on() {
213 sql.push_str("DISTINCT ON (");
214 sql.push_str(&cols.join(", "));
215 sql.push_str(") ");
216 } else {
217 sql.push_str("DISTINCT ");
220 }
221 }
222 self.select.write_sql(&mut sql);
223
224 let mut proj_offset = 0usize;
226 for proj in &self.extra_projections {
227 sql.push_str(", ");
228 let frag = proj.to_sql(proj_offset, dialect, &mut params);
229 sql.push('(');
230 sql.push_str(&frag);
231 sql.push_str(") AS \"");
232 sql.push_str(proj.alias);
233 sql.push('"');
234 proj_offset += proj.params.len();
235 }
236
237 sql.push_str(" FROM ");
239 sql.push_str(M::TABLE_NAME);
240
241 if !self.filter.is_none() || cursor.is_some() {
245 sql.push_str(" WHERE ");
246 let mut conjunct = false;
247 if !self.filter.is_none() {
248 sql.push_str(&where_sql);
249 conjunct = true;
250 }
251 if let Some(cursor) = cursor {
252 if conjunct {
253 sql.push_str(" AND ");
254 }
255 sql.push_str(&dialect.quote_ident(&cursor.column));
256 sql.push(' ');
257 sql.push_str(cursor.operator());
258 sql.push(' ');
259 sql.push_str(&dialect.placeholder(proj_param_count + where_params.len() + 1));
260 }
261 }
262 params.extend(where_params);
263 if let Some(cursor) = cursor {
264 params.push(match &cursor.value {
265 crate::pagination::CursorValue::Int(v) => crate::filter::FilterValue::Int(*v),
266 crate::pagination::CursorValue::String(s) => {
267 crate::filter::FilterValue::String(s.clone())
268 }
269 });
270 }
271
272 if !self.order_by.is_empty() {
277 sql.push_str(" ORDER BY ");
278 self.order_by.write_sql(&mut sql);
279 } else if let Some(cursor) = cursor {
280 sql.push_str(" ORDER BY ");
281 sql.push_str(&dialect.quote_ident(&cursor.column));
282 sql.push_str(match cursor.direction {
283 crate::pagination::CursorDirection::After => " ASC",
284 crate::pagination::CursorDirection::Before => " DESC",
285 });
286 }
287
288 if self.pagination.take.is_some() || self.pagination.skip.is_some() {
290 sql.push(' ');
291 self.pagination.write_sql(&mut sql);
292 }
293
294 (sql, params)
295 }
296
297 pub async fn exec(self) -> QueryResult<Vec<M>>
305 where
306 M: Send + 'static + ModelRelationLoader<E>,
307 {
308 let dialect = self.engine.dialect();
309 let (sql, params) = self.build_sql(dialect);
310 let mut parents = self.engine.query_many::<M>(&sql, params).await?;
311 for spec in &self.includes {
312 <M as ModelRelationLoader<E>>::load_relation(&self.engine, &mut parents, spec).await?;
313 }
314 Ok(parents)
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use crate::error::QueryError;
322 use crate::filter::FilterValue;
323 use crate::pagination::{Cursor, CursorDirection, CursorValue};
324 use crate::types::OrderByField;
325
326 struct TestModel;
327
328 impl Model for TestModel {
329 const MODEL_NAME: &'static str = "TestModel";
330 const TABLE_NAME: &'static str = "test_models";
331 const PRIMARY_KEY: &'static [&'static str] = &["id"];
332 const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
333 }
334
335 impl crate::row::FromRow for TestModel {
336 fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
337 Ok(TestModel)
338 }
339 }
340
341 impl crate::traits::ModelRelationLoader<MockEngine> for TestModel {
345 fn load_relation<'a>(
346 _engine: &'a MockEngine,
347 _parents: &'a mut [Self],
348 spec: &'a crate::relations::IncludeSpec,
349 ) -> crate::traits::BoxFuture<'a, QueryResult<()>> {
350 let name = spec.relation_name.clone();
351 Box::pin(async move {
352 Err(QueryError::internal(format!(
353 "unknown relation '{name}' on TestModel (mock)",
354 )))
355 })
356 }
357 }
358
359 #[derive(Clone)]
360 struct MockEngine;
361
362 impl QueryEngine for MockEngine {
363 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
364 &crate::dialect::Postgres
365 }
366
367 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
368 &self,
369 _sql: &str,
370 _params: Vec<FilterValue>,
371 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
372 Box::pin(async { Ok(Vec::new()) })
373 }
374
375 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
376 &self,
377 _sql: &str,
378 _params: Vec<FilterValue>,
379 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
380 Box::pin(async { Err(QueryError::not_found("test")) })
381 }
382
383 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
384 &self,
385 _sql: &str,
386 _params: Vec<FilterValue>,
387 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
388 Box::pin(async { Ok(None) })
389 }
390
391 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
392 &self,
393 _sql: &str,
394 _params: Vec<FilterValue>,
395 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
396 Box::pin(async { Err(QueryError::not_found("test")) })
397 }
398
399 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
400 &self,
401 _sql: &str,
402 _params: Vec<FilterValue>,
403 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
404 Box::pin(async { Ok(Vec::new()) })
405 }
406
407 fn execute_delete(
408 &self,
409 _sql: &str,
410 _params: Vec<FilterValue>,
411 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
412 Box::pin(async { Ok(0) })
413 }
414
415 fn execute_raw(
416 &self,
417 _sql: &str,
418 _params: Vec<FilterValue>,
419 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
420 Box::pin(async { Ok(0) })
421 }
422
423 fn count(
424 &self,
425 _sql: &str,
426 _params: Vec<FilterValue>,
427 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
428 Box::pin(async { Ok(0) })
429 }
430 }
431
432 #[test]
435 fn test_find_many_new() {
436 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
437 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
438
439 assert!(sql.contains("SELECT * FROM test_models"));
440 assert!(params.is_empty());
441 }
442
443 #[test]
444 fn test_find_many_basic() {
445 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
446 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
447
448 assert_eq!(sql, "SELECT * FROM test_models");
449 assert!(params.is_empty());
450 }
451
452 #[test]
455 fn test_find_many_with_filter() {
456 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
457 .r#where(Filter::Equals("name".into(), "Alice".into()));
458
459 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
460
461 assert!(sql.contains("WHERE"));
462 assert!(sql.contains(r#""name" = $1"#));
463 assert_eq!(params.len(), 1);
464 }
465
466 #[test]
467 fn test_find_many_with_compound_filter() {
468 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
469 .r#where(Filter::Equals(
470 "status".into(),
471 FilterValue::String("active".to_string()),
472 ))
473 .r#where(Filter::Gte("age".into(), FilterValue::Int(18)));
474
475 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
476
477 assert!(sql.contains("WHERE"));
478 assert!(sql.contains("AND"));
479 assert_eq!(params.len(), 2);
480 }
481
482 #[test]
483 fn test_find_many_with_or_filter() {
484 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::or([
485 Filter::Equals("role".into(), FilterValue::String("admin".to_string())),
486 Filter::Equals("role".into(), FilterValue::String("moderator".to_string())),
487 ]));
488
489 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
490
491 assert!(sql.contains("OR"));
492 assert_eq!(params.len(), 2);
493 }
494
495 #[test]
496 fn test_find_many_with_in_filter() {
497 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::In(
498 "status".into(),
499 vec![
500 FilterValue::String("pending".to_string()),
501 FilterValue::String("processing".to_string()),
502 ],
503 ));
504
505 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
506
507 assert!(sql.contains("IN"));
508 assert_eq!(params.len(), 2);
509 }
510
511 #[test]
512 fn test_find_many_without_filter() {
513 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
514 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
515
516 assert!(!sql.contains("WHERE"));
517 assert!(params.is_empty());
518 }
519
520 #[test]
523 fn test_find_many_with_order() {
524 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
525 .order_by(OrderByField::desc("created_at"));
526
527 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
528
529 assert!(sql.contains("ORDER BY created_at DESC"));
530 }
531
532 #[test]
533 fn test_find_many_with_asc_order() {
534 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
535 .order_by(OrderByField::asc("name"));
536
537 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
538
539 assert!(sql.contains("ORDER BY name ASC"));
540 }
541
542 #[test]
543 fn test_find_many_without_order() {
544 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
545 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
546
547 assert!(!sql.contains("ORDER BY"));
548 }
549
550 #[test]
551 fn test_find_many_order_replaces() {
552 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
553 .order_by(OrderByField::asc("name"))
554 .order_by(OrderByField::desc("created_at"));
555
556 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
557
558 assert!(sql.contains("ORDER BY created_at DESC"));
559 assert!(!sql.contains("ORDER BY name"));
560 }
561
562 #[test]
565 fn test_find_many_with_pagination() {
566 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
567 .skip(10)
568 .take(20);
569
570 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
571
572 assert!(sql.contains("LIMIT 20"));
573 assert!(sql.contains("OFFSET 10"));
574 }
575
576 #[test]
577 fn test_find_many_with_skip_only() {
578 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).skip(5);
579
580 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
581
582 assert!(sql.contains("OFFSET 5"));
583 }
584
585 #[test]
586 fn test_find_many_with_take_only() {
587 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).take(100);
588
589 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
590
591 assert!(sql.contains("LIMIT 100"));
592 }
593
594 #[test]
595 fn test_find_many_with_cursor() {
596 let cursor = Cursor::new("id", CursorValue::Int(100), CursorDirection::After);
597 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
598 .cursor(cursor)
599 .take(10);
600
601 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
602
603 assert!(sql.contains("LIMIT 10"));
605 assert!(sql.contains(r#"WHERE "id" > $1"#), "got: {sql}");
606 assert_eq!(params, vec![FilterValue::Int(100)]);
607 }
608
609 #[test]
610 fn test_find_many_cursor_exact_sql() {
611 let cursor = Cursor::new("id", CursorValue::Int(100), CursorDirection::After);
612 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
613 .r#where(Filter::Equals("name".into(), "a".into()))
614 .cursor(cursor)
615 .take(10);
616
617 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
618
619 assert_eq!(
620 sql,
621 r#"SELECT * FROM test_models WHERE "name" = $1 AND "id" > $2 ORDER BY "id" ASC LIMIT 10"#
622 );
623 assert_eq!(
624 params,
625 vec![FilterValue::String("a".to_string()), FilterValue::Int(100)]
626 );
627 }
628
629 #[test]
633 fn test_find_many_cursor_orders_by_cursor_column() {
634 let after = Cursor::new("id", CursorValue::Int(100), CursorDirection::After);
635 let (sql, _) = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
636 .cursor(after)
637 .build_sql(&crate::dialect::Postgres);
638 assert!(
639 sql.contains(r#"ORDER BY "id" ASC"#),
640 "After cursor must order ASC, got: {sql}"
641 );
642
643 let before = Cursor::new("id", CursorValue::Int(100), CursorDirection::Before);
644 let (sql, _) = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
645 .cursor(before)
646 .build_sql(&crate::dialect::Postgres);
647 assert!(
648 sql.contains(r#"WHERE "id" < $1"#) && sql.contains(r#"ORDER BY "id" DESC"#),
649 "Before cursor must order DESC, got: {sql}"
650 );
651 }
652
653 #[test]
655 fn test_find_many_cursor_explicit_order_by_wins() {
656 let cursor = Cursor::new("id", CursorValue::Int(100), CursorDirection::After);
657 let (sql, _) = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
658 .cursor(cursor)
659 .order_by(OrderByField::desc("created_at"))
660 .build_sql(&crate::dialect::Postgres);
661
662 assert!(sql.contains("ORDER BY created_at DESC"), "got: {sql}");
663 assert!(!sql.contains(r#"ORDER BY "id""#), "got: {sql}");
664 }
665
666 #[test]
669 fn test_find_many_with_select() {
670 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
671 .select(Select::fields(["id", "name"]));
672
673 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
674
675 assert!(sql.contains("SELECT id, name FROM"));
676 assert!(!sql.contains("SELECT *"));
677 }
678
679 #[test]
680 fn test_find_many_select_single_field() {
681 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
682 .select(Select::fields(["id"]));
683
684 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
685
686 assert!(sql.contains("SELECT id FROM"));
687 }
688
689 #[test]
690 fn test_find_many_select_all() {
691 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).select(Select::All);
692
693 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
694
695 assert!(sql.contains("SELECT * FROM"));
696 }
697
698 #[test]
704 fn find_many_emits_explicit_column_list_when_select_narrows() {
705 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
706 .select(Select::fields(["id", "email"]));
707 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
708 assert!(
709 sql.contains("SELECT id, email FROM") && !sql.contains("SELECT *"),
710 "expected narrow select list, got: {sql}"
711 );
712 }
713
714 #[test]
719 fn find_many_emits_star_when_no_select() {
720 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
721 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
722 assert!(sql.contains("SELECT *"), "expected SELECT *, got: {sql}");
723 }
724
725 #[test]
728 fn test_find_many_with_distinct() {
729 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).distinct(["category"]);
730
731 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
732
733 assert!(sql.contains("DISTINCT ON (category)"));
734 }
735
736 #[test]
737 fn test_find_many_with_multiple_distinct() {
738 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
739 .distinct(["category", "status"]);
740
741 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
742
743 assert!(sql.contains("DISTINCT ON (category, status)"));
744 }
745 #[test]
746 fn test_find_many_without_distinct() {
747 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
748
749 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
750
751 assert!(!sql.contains("DISTINCT"));
752 }
753
754 #[test]
755 fn test_find_many_distinct_falls_back_without_distinct_on_support() {
756 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).distinct(["category"]);
757
758 let (sql, _) = op.build_sql(&crate::dialect::Mysql);
761
762 assert!(sql.contains("SELECT DISTINCT "), "got: {sql}");
763 assert!(!sql.contains("DISTINCT ON"), "got: {sql}");
764 }
765
766 #[test]
769 fn test_find_many_sql_structure() {
770 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
771 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
772 .order_by(OrderByField::desc("created_at"))
773 .skip(10)
774 .take(20)
775 .select(Select::fields(["id", "name"]));
776
777 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
778
779 let select_pos = sql.find("SELECT").unwrap();
781 let from_pos = sql.find("FROM").unwrap();
782 let where_pos = sql.find("WHERE").unwrap();
783 let order_pos = sql.find("ORDER BY").unwrap();
784 let limit_pos = sql.find("LIMIT").unwrap();
785 let offset_pos = sql.find("OFFSET").unwrap();
786
787 assert!(select_pos < from_pos);
788 assert!(from_pos < where_pos);
789 assert!(where_pos < order_pos);
790 assert!(order_pos < limit_pos);
791 assert!(limit_pos < offset_pos);
792 }
793
794 #[test]
795 fn test_find_many_table_name() {
796 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
797 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
798
799 assert!(sql.contains("test_models"));
800 }
801
802 #[tokio::test]
805 async fn test_find_many_exec() {
806 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(
807 Filter::Equals("status".into(), FilterValue::String("active".to_string())),
808 );
809
810 let result = op.exec().await;
811
812 assert!(result.is_ok());
813 assert!(result.unwrap().is_empty()); }
815
816 #[tokio::test]
817 async fn test_find_many_exec_no_filter() {
818 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
819
820 let result = op.exec().await;
821
822 assert!(result.is_ok());
823 }
824
825 #[test]
828 fn test_find_many_full_chain() {
829 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
830 .r#where(Filter::Equals(
831 "status".into(),
832 FilterValue::String("active".to_string()),
833 ))
834 .order_by(OrderByField::desc("created_at"))
835 .skip(10)
836 .take(20)
837 .select(Select::fields(["id", "name", "email"]))
838 .distinct(["category"]);
839
840 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
841
842 assert!(sql.contains("DISTINCT ON (category)"));
843 assert!(sql.contains("SELECT"));
844 assert!(sql.contains("WHERE"));
845 assert!(sql.contains("ORDER BY created_at DESC"));
846 assert!(sql.contains("LIMIT 20"));
847 assert!(sql.contains("OFFSET 10"));
848 assert_eq!(params.len(), 1);
849 }
850
851 #[test]
854 fn test_find_many_with_like_filter() {
855 let op =
856 FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Contains(
857 "email".into(),
858 FilterValue::String("@example.com".to_string()),
859 ));
860
861 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
862
863 assert!(sql.contains("LIKE"));
864 assert_eq!(params.len(), 1);
865 }
866
867 #[test]
868 fn test_find_many_with_null_filter() {
869 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
870 .r#where(Filter::IsNull("deleted_at".into()));
871
872 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
873
874 assert!(sql.contains("IS NULL"));
875 assert!(params.is_empty());
876 }
877
878 #[test]
879 fn test_find_many_with_not_filter() {
880 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Not(
881 Box::new(Filter::Equals(
882 "status".into(),
883 FilterValue::String("deleted".to_string()),
884 )),
885 ));
886
887 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
888
889 assert!(sql.contains("NOT"));
890 assert_eq!(params.len(), 1);
891 }
892
893 #[test]
894 fn test_find_many_with_between_equivalent() {
895 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
896 .r#where(Filter::Gte("age".into(), FilterValue::Int(18)))
897 .r#where(Filter::Lte("age".into(), FilterValue::Int(65)));
898
899 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
900
901 assert!(sql.contains("AND"));
902 assert_eq!(params.len(), 2);
903 }
904
905 #[test]
908 fn builds_mysql_placeholders() {
909 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
910 .r#where(Filter::Equals("name".into(), "a".into()));
911 let (sql, _) = op.build_sql(&crate::dialect::Mysql);
912 assert!(
913 sql.contains("?") && !sql.contains("$1"),
914 "expected ? placeholders, got: {sql}"
915 );
916 }
917
918 #[test]
919 fn builds_mssql_placeholders() {
920 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
921 .r#where(Filter::Equals("name".into(), "a".into()));
922 let (sql, _) = op.build_sql(&crate::dialect::Mssql);
923 assert!(sql.contains("@P1"), "expected @P1 placeholders, got: {sql}");
924 }
925
926 #[test]
927 fn builds_sqlite_placeholders() {
928 let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
929 .r#where(Filter::Equals("name".into(), "a".into()));
930 let (sql, _) = op.build_sql(&crate::dialect::Sqlite);
931 assert!(sql.contains("?1"), "expected ?1 placeholders, got: {sql}");
932 }
933}