Skip to main content

prax_query/operations/
find_many.rs

1//! FindMany operation for querying multiple records.
2
3use 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
16/// A query operation that finds multiple records.
17///
18/// # Example
19///
20/// ```rust,ignore
21/// let users = client
22///     .user()
23///     .find_many()
24///     .r#where(user::email::contains("@example.com"))
25///     .order_by(user::created_at::desc())
26///     .skip(0)
27///     .take(10)
28///     .exec()
29///     .await?;
30/// ```
31pub 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    /// Relations to eager-load after the main query returns. Each
39    /// spec drives one follow-up SELECT via the model's
40    /// [`ModelRelationLoader`] impl. Inlined for up to two specs
41    /// (the typical 0-2 case) to avoid a heap allocation on the hot
42    /// builder path.
43    includes: SmallVec<[IncludeSpec; 2]>,
44    /// Extra scalar-subquery columns appended to the SELECT clause.
45    /// Used by relation-aggregate virtual fields (`@count`, `@sum`, …).
46    pub extra_projections: Vec<ScalarProjection>,
47    _model: PhantomData<M>,
48}
49
50impl<E: QueryEngine, M: Model + crate::row::FromRow> FindManyOperation<E, M> {
51    /// Create a new FindMany operation.
52    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    /// Eager-load a relation alongside the main query.
67    ///
68    /// Each `.include()` call appends one follow-up SELECT that
69    /// fetches the target rows for every parent returned by this
70    /// find. Children get stitched onto the parent slice by the
71    /// [`ModelRelationLoader`] impl emitted by `#[derive(Model)]`.
72    pub fn include(mut self, spec: IncludeSpec) -> Self {
73        self.includes.push(spec);
74        self
75    }
76
77    /// Add a filter condition.
78    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    /// Set the order by clause.
85    pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
86        self.order_by = order.into();
87        self
88    }
89
90    /// Skip a number of records.
91    pub fn skip(mut self, n: u64) -> Self {
92        self.pagination = self.pagination.skip(n);
93        self
94    }
95
96    /// Take a limited number of records.
97    pub fn take(mut self, n: u64) -> Self {
98        self.pagination = self.pagination.take(n);
99        self
100    }
101
102    /// Select specific fields.
103    pub fn select(mut self, select: impl Into<Select>) -> Self {
104        self.select = select.into();
105        self
106    }
107
108    /// Make the query distinct.
109    pub fn distinct(mut self, columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
110        self.distinct = Some(columns.into_iter().map(Into::into).collect());
111        self
112    }
113
114    /// Set cursor for cursor-based pagination.
115    pub fn cursor(mut self, cursor: crate::pagination::Cursor) -> Self {
116        self.pagination = self.pagination.cursor(cursor);
117        self
118    }
119
120    /// Apply a typed `WhereInput`. AND-composes with any previously set
121    /// filter — same semantics as calling `.r#where(...)` again.
122    pub fn with_where_input<W: crate::inputs::WhereInput<Model = M>>(mut self, w: W) -> Self {
123        let f = w.into_ir();
124        self.filter = self.filter.and_then(f);
125        self
126    }
127
128    /// Apply a typed `IncludeInput`. Merges into any previously set
129    /// includes (later wins on conflicting relation names).
130    pub fn with_include_input<I: crate::inputs::IncludeInput<Model = M>>(mut self, i: I) -> Self {
131        let inc = i.into_ir();
132        for spec in inc.specs() {
133            self.includes.push(spec.clone());
134        }
135        self
136    }
137
138    /// Apply a typed `SelectInput`.
139    pub fn with_select_input<S: crate::inputs::SelectInput<Model = M>>(mut self, s: S) -> Self {
140        self.select = s.into_ir();
141        self
142    }
143
144    /// Apply a typed `OrderByInput` (replaces current).
145    pub fn with_order_by_input<O: crate::inputs::OrderByInput<Model = M>>(mut self, o: O) -> Self {
146        self.order_by = o.into_ir();
147        self
148    }
149
150    /// Doc-hidden accessor for the current filter — needed for unit
151    /// tests that don't have a running engine to issue queries against.
152    #[doc(hidden)]
153    pub fn filter_for_test(&self) -> &Filter {
154        &self.filter
155    }
156}
157
158impl<E, M> FindManyOperation<E, M>
159where
160    E: QueryEngine + SupportsScalarSubqueryInSelect,
161    M: Model + crate::row::FromRow,
162{
163    /// Append a scalar-subquery projection to the SELECT list.
164    ///
165    /// Available only on engines that implement
166    /// [`SupportsScalarSubqueryInSelect`]. SQL backends (Postgres, MySQL,
167    /// SQLite, MSSQL, DuckDB, SQLx) all satisfy this bound; MongoDB,
168    /// ScyllaDB, and Cassandra do not — calling this method on those
169    /// engines is a **compile-time error**.
170    pub fn with_scalar_projection(mut self, proj: ScalarProjection) -> Self {
171        self.extra_projections.push(proj);
172        self
173    }
174}
175
176impl<E: QueryEngine, M: Model + crate::row::FromRow> FindManyOperation<E, M> {
177    /// Build the SQL query.
178    pub fn build_sql(
179        &self,
180        dialect: &dyn crate::dialect::SqlDialect,
181    ) -> (String, Vec<crate::filter::FilterValue>) {
182        // Projection params come first; WHERE params are offset by the
183        // number of params already consumed by the extra projections so
184        // that all dialect placeholders form a single contiguous sequence.
185        let proj_param_count: usize = self.extra_projections.iter().map(|p| p.params.len()).sum();
186        let (where_sql, where_params) = self.filter.to_sql(proj_param_count, dialect);
187
188        let mut params: Vec<crate::filter::FilterValue> =
189            Vec::with_capacity(proj_param_count + where_params.len());
190
191        let mut sql = String::new();
192
193        // SELECT clause
194        sql.push_str("SELECT ");
195        if let Some(ref cols) = self.distinct {
196            sql.push_str("DISTINCT ON (");
197            sql.push_str(&cols.join(", "));
198            sql.push_str(") ");
199        }
200        sql.push_str(&self.select.to_sql());
201
202        // Extra scalar-subquery projections
203        let mut proj_offset = 0usize;
204        for proj in &self.extra_projections {
205            sql.push_str(", ");
206            let frag = proj.to_sql(proj_offset, dialect, &mut params);
207            sql.push('(');
208            sql.push_str(&frag);
209            sql.push_str(") AS \"");
210            sql.push_str(proj.alias);
211            sql.push('"');
212            proj_offset += proj.params.len();
213        }
214
215        // FROM clause
216        sql.push_str(" FROM ");
217        sql.push_str(M::TABLE_NAME);
218
219        // WHERE clause
220        if !self.filter.is_none() {
221            sql.push_str(" WHERE ");
222            sql.push_str(&where_sql);
223        }
224        params.extend(where_params);
225
226        // ORDER BY clause
227        if !self.order_by.is_empty() {
228            sql.push_str(" ORDER BY ");
229            sql.push_str(&self.order_by.to_sql());
230        }
231
232        // LIMIT/OFFSET clause
233        let pagination_sql = self.pagination.to_sql();
234        if !pagination_sql.is_empty() {
235            sql.push(' ');
236            sql.push_str(&pagination_sql);
237        }
238
239        (sql, params)
240    }
241
242    /// Execute the query.
243    ///
244    /// After the main SELECT hydrates the parent rows, any pending
245    /// `.include()` specs are dispatched through
246    /// [`ModelRelationLoader::load_relation`] which issues one
247    /// additional SELECT per relation and stitches the children onto
248    /// the parent slice.
249    pub async fn exec(self) -> QueryResult<Vec<M>>
250    where
251        M: Send + 'static + ModelRelationLoader<E>,
252    {
253        let dialect = self.engine.dialect();
254        let (sql, params) = self.build_sql(dialect);
255        let mut parents = self.engine.query_many::<M>(&sql, params).await?;
256        for spec in &self.includes {
257            <M as ModelRelationLoader<E>>::load_relation(&self.engine, &mut parents, spec).await?;
258        }
259        Ok(parents)
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::error::QueryError;
267    use crate::filter::FilterValue;
268    use crate::pagination::{Cursor, CursorDirection, CursorValue};
269    use crate::types::OrderByField;
270
271    struct TestModel;
272
273    impl Model for TestModel {
274        const MODEL_NAME: &'static str = "TestModel";
275        const TABLE_NAME: &'static str = "test_models";
276        const PRIMARY_KEY: &'static [&'static str] = &["id"];
277        const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
278    }
279
280    impl crate::row::FromRow for TestModel {
281        fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
282            Ok(TestModel)
283        }
284    }
285
286    // Minimal `ModelRelationLoader` impl for the mock — real models
287    // get one from codegen. Errors on any include name (the tests
288    // never register an include).
289    impl crate::traits::ModelRelationLoader<MockEngine> for TestModel {
290        fn load_relation<'a>(
291            _engine: &'a MockEngine,
292            _parents: &'a mut [Self],
293            spec: &'a crate::relations::IncludeSpec,
294        ) -> crate::traits::BoxFuture<'a, QueryResult<()>> {
295            let name = spec.relation_name.clone();
296            Box::pin(async move {
297                Err(QueryError::internal(format!(
298                    "unknown relation '{name}' on TestModel (mock)",
299                )))
300            })
301        }
302    }
303
304    #[derive(Clone)]
305    struct MockEngine;
306
307    impl QueryEngine for MockEngine {
308        fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
309            &crate::dialect::Postgres
310        }
311
312        fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
313            &self,
314            _sql: &str,
315            _params: Vec<FilterValue>,
316        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
317            Box::pin(async { Ok(Vec::new()) })
318        }
319
320        fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
321            &self,
322            _sql: &str,
323            _params: Vec<FilterValue>,
324        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
325            Box::pin(async { Err(QueryError::not_found("test")) })
326        }
327
328        fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
329            &self,
330            _sql: &str,
331            _params: Vec<FilterValue>,
332        ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
333            Box::pin(async { Ok(None) })
334        }
335
336        fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
337            &self,
338            _sql: &str,
339            _params: Vec<FilterValue>,
340        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
341            Box::pin(async { Err(QueryError::not_found("test")) })
342        }
343
344        fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
345            &self,
346            _sql: &str,
347            _params: Vec<FilterValue>,
348        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
349            Box::pin(async { Ok(Vec::new()) })
350        }
351
352        fn execute_delete(
353            &self,
354            _sql: &str,
355            _params: Vec<FilterValue>,
356        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
357            Box::pin(async { Ok(0) })
358        }
359
360        fn execute_raw(
361            &self,
362            _sql: &str,
363            _params: Vec<FilterValue>,
364        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
365            Box::pin(async { Ok(0) })
366        }
367
368        fn count(
369            &self,
370            _sql: &str,
371            _params: Vec<FilterValue>,
372        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
373            Box::pin(async { Ok(0) })
374        }
375    }
376
377    // ========== Construction Tests ==========
378
379    #[test]
380    fn test_find_many_new() {
381        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
382        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
383
384        assert!(sql.contains("SELECT * FROM test_models"));
385        assert!(params.is_empty());
386    }
387
388    #[test]
389    fn test_find_many_basic() {
390        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
391        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
392
393        assert_eq!(sql, "SELECT * FROM test_models");
394        assert!(params.is_empty());
395    }
396
397    // ========== Filter Tests ==========
398
399    #[test]
400    fn test_find_many_with_filter() {
401        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
402            .r#where(Filter::Equals("name".into(), "Alice".into()));
403
404        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
405
406        assert!(sql.contains("WHERE"));
407        assert!(sql.contains(r#""name" = $1"#));
408        assert_eq!(params.len(), 1);
409    }
410
411    #[test]
412    fn test_find_many_with_compound_filter() {
413        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
414            .r#where(Filter::Equals(
415                "status".into(),
416                FilterValue::String("active".to_string()),
417            ))
418            .r#where(Filter::Gte("age".into(), FilterValue::Int(18)));
419
420        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
421
422        assert!(sql.contains("WHERE"));
423        assert!(sql.contains("AND"));
424        assert_eq!(params.len(), 2);
425    }
426
427    #[test]
428    fn test_find_many_with_or_filter() {
429        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::or([
430            Filter::Equals("role".into(), FilterValue::String("admin".to_string())),
431            Filter::Equals("role".into(), FilterValue::String("moderator".to_string())),
432        ]));
433
434        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
435
436        assert!(sql.contains("OR"));
437        assert_eq!(params.len(), 2);
438    }
439
440    #[test]
441    fn test_find_many_with_in_filter() {
442        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::In(
443            "status".into(),
444            vec![
445                FilterValue::String("pending".to_string()),
446                FilterValue::String("processing".to_string()),
447            ],
448        ));
449
450        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
451
452        assert!(sql.contains("IN"));
453        assert_eq!(params.len(), 2);
454    }
455
456    #[test]
457    fn test_find_many_without_filter() {
458        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
459        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
460
461        assert!(!sql.contains("WHERE"));
462        assert!(params.is_empty());
463    }
464
465    // ========== Order By Tests ==========
466
467    #[test]
468    fn test_find_many_with_order() {
469        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
470            .order_by(OrderByField::desc("created_at"));
471
472        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
473
474        assert!(sql.contains("ORDER BY created_at DESC"));
475    }
476
477    #[test]
478    fn test_find_many_with_asc_order() {
479        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
480            .order_by(OrderByField::asc("name"));
481
482        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
483
484        assert!(sql.contains("ORDER BY name ASC"));
485    }
486
487    #[test]
488    fn test_find_many_without_order() {
489        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
490        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
491
492        assert!(!sql.contains("ORDER BY"));
493    }
494
495    #[test]
496    fn test_find_many_order_replaces() {
497        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
498            .order_by(OrderByField::asc("name"))
499            .order_by(OrderByField::desc("created_at"));
500
501        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
502
503        assert!(sql.contains("ORDER BY created_at DESC"));
504        assert!(!sql.contains("ORDER BY name"));
505    }
506
507    // ========== Pagination Tests ==========
508
509    #[test]
510    fn test_find_many_with_pagination() {
511        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
512            .skip(10)
513            .take(20);
514
515        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
516
517        assert!(sql.contains("LIMIT 20"));
518        assert!(sql.contains("OFFSET 10"));
519    }
520
521    #[test]
522    fn test_find_many_with_skip_only() {
523        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).skip(5);
524
525        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
526
527        assert!(sql.contains("OFFSET 5"));
528    }
529
530    #[test]
531    fn test_find_many_with_take_only() {
532        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).take(100);
533
534        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
535
536        assert!(sql.contains("LIMIT 100"));
537    }
538
539    #[test]
540    fn test_find_many_with_cursor() {
541        let cursor = Cursor::new("id", CursorValue::Int(100), CursorDirection::After);
542        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
543            .cursor(cursor)
544            .take(10);
545
546        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
547
548        // Cursor pagination should add some cursor-based filtering
549        assert!(sql.contains("LIMIT 10"));
550    }
551
552    // ========== Select Tests ==========
553
554    #[test]
555    fn test_find_many_with_select() {
556        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
557            .select(Select::fields(["id", "name"]));
558
559        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
560
561        assert!(sql.contains("SELECT id, name FROM"));
562        assert!(!sql.contains("SELECT *"));
563    }
564
565    #[test]
566    fn test_find_many_select_single_field() {
567        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
568            .select(Select::fields(["id"]));
569
570        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
571
572        assert!(sql.contains("SELECT id FROM"));
573    }
574
575    #[test]
576    fn test_find_many_select_all() {
577        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).select(Select::All);
578
579        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
580
581        assert!(sql.contains("SELECT * FROM"));
582    }
583
584    /// Task 28 regression test: a narrow `Select::fields` list must turn
585    /// the emitted `SELECT *` into an explicit column list so wide models
586    /// don't waste bandwidth. The projection still hydrates as the full
587    /// struct, so callers are responsible for covering every non-`Option`
588    /// field — see the CHANGELOG migration note.
589    #[test]
590    fn find_many_emits_explicit_column_list_when_select_narrows() {
591        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
592            .select(Select::fields(["id", "email"]));
593        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
594        assert!(
595            sql.contains("SELECT id, email FROM") && !sql.contains("SELECT *"),
596            "expected narrow select list, got: {sql}"
597        );
598    }
599
600    /// Counterpart to the narrowing test: with no `.select(...)` call,
601    /// the default `Select::All` must still emit `SELECT *`. Guards
602    /// against a regression where a future refactor of the default
603    /// value silently drops back to an empty column list.
604    #[test]
605    fn find_many_emits_star_when_no_select() {
606        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
607        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
608        assert!(sql.contains("SELECT *"), "expected SELECT *, got: {sql}");
609    }
610
611    // ========== Distinct Tests ==========
612
613    #[test]
614    fn test_find_many_with_distinct() {
615        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).distinct(["category"]);
616
617        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
618
619        assert!(sql.contains("DISTINCT ON (category)"));
620    }
621
622    #[test]
623    fn test_find_many_with_multiple_distinct() {
624        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
625            .distinct(["category", "status"]);
626
627        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
628
629        assert!(sql.contains("DISTINCT ON (category, status)"));
630    }
631
632    #[test]
633    fn test_find_many_without_distinct() {
634        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
635        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
636
637        assert!(!sql.contains("DISTINCT"));
638    }
639
640    // ========== SQL Structure Tests ==========
641
642    #[test]
643    fn test_find_many_sql_structure() {
644        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
645            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
646            .order_by(OrderByField::desc("created_at"))
647            .skip(10)
648            .take(20)
649            .select(Select::fields(["id", "name"]));
650
651        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
652
653        // Check correct SQL clause ordering
654        let select_pos = sql.find("SELECT").unwrap();
655        let from_pos = sql.find("FROM").unwrap();
656        let where_pos = sql.find("WHERE").unwrap();
657        let order_pos = sql.find("ORDER BY").unwrap();
658        let limit_pos = sql.find("LIMIT").unwrap();
659        let offset_pos = sql.find("OFFSET").unwrap();
660
661        assert!(select_pos < from_pos);
662        assert!(from_pos < where_pos);
663        assert!(where_pos < order_pos);
664        assert!(order_pos < limit_pos);
665        assert!(limit_pos < offset_pos);
666    }
667
668    #[test]
669    fn test_find_many_table_name() {
670        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
671        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
672
673        assert!(sql.contains("test_models"));
674    }
675
676    // ========== Async Execution Tests ==========
677
678    #[tokio::test]
679    async fn test_find_many_exec() {
680        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(
681            Filter::Equals("status".into(), FilterValue::String("active".to_string())),
682        );
683
684        let result = op.exec().await;
685
686        assert!(result.is_ok());
687        assert!(result.unwrap().is_empty()); // MockEngine returns empty vec
688    }
689
690    #[tokio::test]
691    async fn test_find_many_exec_no_filter() {
692        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine);
693
694        let result = op.exec().await;
695
696        assert!(result.is_ok());
697    }
698
699    // ========== Method Chaining Tests ==========
700
701    #[test]
702    fn test_find_many_full_chain() {
703        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
704            .r#where(Filter::Equals(
705                "status".into(),
706                FilterValue::String("active".to_string()),
707            ))
708            .order_by(OrderByField::desc("created_at"))
709            .skip(10)
710            .take(20)
711            .select(Select::fields(["id", "name", "email"]))
712            .distinct(["category"]);
713
714        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
715
716        assert!(sql.contains("DISTINCT ON (category)"));
717        assert!(sql.contains("SELECT"));
718        assert!(sql.contains("WHERE"));
719        assert!(sql.contains("ORDER BY created_at DESC"));
720        assert!(sql.contains("LIMIT 20"));
721        assert!(sql.contains("OFFSET 10"));
722        assert_eq!(params.len(), 1);
723    }
724
725    // ========== Edge Cases ==========
726
727    #[test]
728    fn test_find_many_with_like_filter() {
729        let op =
730            FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Contains(
731                "email".into(),
732                FilterValue::String("@example.com".to_string()),
733            ));
734
735        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
736
737        assert!(sql.contains("LIKE"));
738        assert_eq!(params.len(), 1);
739    }
740
741    #[test]
742    fn test_find_many_with_null_filter() {
743        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
744            .r#where(Filter::IsNull("deleted_at".into()));
745
746        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
747
748        assert!(sql.contains("IS NULL"));
749        assert!(params.is_empty());
750    }
751
752    #[test]
753    fn test_find_many_with_not_filter() {
754        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Not(
755            Box::new(Filter::Equals(
756                "status".into(),
757                FilterValue::String("deleted".to_string()),
758            )),
759        ));
760
761        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
762
763        assert!(sql.contains("NOT"));
764        assert_eq!(params.len(), 1);
765    }
766
767    #[test]
768    fn test_find_many_with_between_equivalent() {
769        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
770            .r#where(Filter::Gte("age".into(), FilterValue::Int(18)))
771            .r#where(Filter::Lte("age".into(), FilterValue::Int(65)));
772
773        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
774
775        assert!(sql.contains("AND"));
776        assert_eq!(params.len(), 2);
777    }
778
779    // ========== Cross-Dialect Tests ==========
780
781    #[test]
782    fn builds_mysql_placeholders() {
783        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
784            .r#where(Filter::Equals("name".into(), "a".into()));
785        let (sql, _) = op.build_sql(&crate::dialect::Mysql);
786        assert!(
787            sql.contains("?") && !sql.contains("$1"),
788            "expected ? placeholders, got: {sql}"
789        );
790    }
791
792    #[test]
793    fn builds_mssql_placeholders() {
794        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
795            .r#where(Filter::Equals("name".into(), "a".into()));
796        let (sql, _) = op.build_sql(&crate::dialect::Mssql);
797        assert!(sql.contains("@P1"), "expected @P1 placeholders, got: {sql}");
798    }
799
800    #[test]
801    fn builds_sqlite_placeholders() {
802        let op = FindManyOperation::<MockEngine, TestModel>::new(MockEngine)
803            .r#where(Filter::Equals("name".into(), "a".into()));
804        let (sql, _) = op.build_sql(&crate::dialect::Sqlite);
805        assert!(sql.contains("?1"), "expected ?1 placeholders, got: {sql}");
806    }
807}