Skip to main content

prax_query/operations/
find_first.rs

1//! FindFirst operation for querying the first matching record.
2
3use std::marker::PhantomData;
4
5use crate::capabilities::SupportsScalarSubqueryInSelect;
6use crate::error::QueryResult;
7use crate::filter::Filter;
8use crate::projection::ScalarProjection;
9use crate::relations::IncludeSpec;
10use crate::traits::{Model, ModelRelationLoader, QueryEngine};
11use crate::types::{OrderBy, Select};
12
13/// A query operation that finds the first record matching the filter.
14///
15/// Unlike `FindUnique`, this doesn't require a unique constraint.
16///
17/// # Example
18///
19/// ```rust,ignore
20/// let user = client
21///     .user()
22///     .find_first()
23///     .r#where(user::email::contains("@example.com"))
24///     .order_by(user::created_at::desc())
25///     .exec()
26///     .await?;
27/// ```
28pub struct FindFirstOperation<E: QueryEngine, M: Model> {
29    engine: E,
30    filter: Filter,
31    order_by: OrderBy,
32    select: Select,
33    /// Relations to eager-load alongside the first-match lookup. Same
34    /// loader dispatch path as `find_many`/`find_unique` — the
35    /// executor sees a 1-element slice when a row is found.
36    includes: Vec<IncludeSpec>,
37    /// Extra scalar-subquery columns appended to the SELECT clause.
38    /// Used by relation-aggregate virtual fields (`@count`, `@sum`, …).
39    pub extra_projections: Vec<ScalarProjection>,
40    _model: PhantomData<M>,
41}
42
43impl<E: QueryEngine, M: Model + crate::row::FromRow> FindFirstOperation<E, M> {
44    /// Create a new FindFirst operation.
45    pub fn new(engine: E) -> Self {
46        Self {
47            engine,
48            filter: Filter::None,
49            order_by: OrderBy::none(),
50            select: Select::All,
51            includes: Vec::new(),
52            extra_projections: Vec::new(),
53            _model: PhantomData,
54        }
55    }
56
57    /// Add a filter condition.
58    pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
59        let new_filter = filter.into();
60        self.filter = self.filter.and_then(new_filter);
61        self
62    }
63
64    /// Set the order by clause.
65    pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
66        self.order_by = order.into();
67        self
68    }
69
70    /// Select specific fields.
71    pub fn select(mut self, select: impl Into<Select>) -> Self {
72        self.select = select.into();
73        self
74    }
75
76    /// Eager-load a relation alongside the first-match lookup.
77    pub fn include(mut self, spec: IncludeSpec) -> Self {
78        self.includes.push(spec);
79        self
80    }
81
82    /// Apply a typed `WhereInput`. AND-composes with any previously set
83    /// filter — same semantics as calling `.r#where(...)` again.
84    pub fn with_where_input<W: crate::inputs::WhereInput<Model = M>>(mut self, w: W) -> Self {
85        let f = w.into_ir();
86        self.filter = self.filter.and_then(f);
87        self
88    }
89
90    /// Apply a typed `IncludeInput`. Merges into any previously set
91    /// includes (later wins on conflicting relation names).
92    pub fn with_include_input<I: crate::inputs::IncludeInput<Model = M>>(mut self, i: I) -> Self {
93        let inc = i.into_ir();
94        for spec in inc.specs() {
95            self.includes.push(spec.clone());
96        }
97        self
98    }
99
100    /// Apply a typed `SelectInput`.
101    pub fn with_select_input<S: crate::inputs::SelectInput<Model = M>>(mut self, s: S) -> Self {
102        self.select = s.into_ir();
103        self
104    }
105
106    /// Apply a typed `OrderByInput` (replaces current).
107    pub fn with_order_by_input<O: crate::inputs::OrderByInput<Model = M>>(mut self, o: O) -> Self {
108        self.order_by = o.into_ir();
109        self
110    }
111
112    /// Doc-hidden accessor for the current filter — needed for unit
113    /// tests that don't have a running engine to issue queries against.
114    #[doc(hidden)]
115    pub fn filter_for_test(&self) -> &Filter {
116        &self.filter
117    }
118}
119
120impl<E, M> FindFirstOperation<E, M>
121where
122    E: QueryEngine + SupportsScalarSubqueryInSelect,
123    M: Model + crate::row::FromRow,
124{
125    /// Append a scalar-subquery projection to the SELECT list.
126    ///
127    /// Available only on engines that implement
128    /// [`SupportsScalarSubqueryInSelect`]. SQL backends (Postgres, MySQL,
129    /// SQLite, MSSQL, DuckDB, SQLx) all satisfy this bound; MongoDB,
130    /// ScyllaDB, and Cassandra do not — calling this method on those
131    /// engines is a **compile-time error**.
132    pub fn with_scalar_projection(mut self, proj: ScalarProjection) -> Self {
133        self.extra_projections.push(proj);
134        self
135    }
136}
137
138impl<E: QueryEngine, M: Model + crate::row::FromRow> FindFirstOperation<E, M> {
139    /// Build the SQL query.
140    pub fn build_sql(
141        &self,
142        dialect: &dyn crate::dialect::SqlDialect,
143    ) -> (String, Vec<crate::filter::FilterValue>) {
144        // Projection params come first; WHERE params are offset so all
145        // dialect placeholders form a single contiguous sequence.
146        let proj_param_count: usize = self.extra_projections.iter().map(|p| p.params.len()).sum();
147        let (where_sql, where_params) = self.filter.to_sql(proj_param_count, dialect);
148
149        let mut params: Vec<crate::filter::FilterValue> =
150            Vec::with_capacity(proj_param_count + where_params.len());
151
152        let mut sql = String::new();
153
154        // SELECT clause
155        sql.push_str("SELECT ");
156        sql.push_str(&self.select.to_sql());
157
158        // Extra scalar-subquery projections
159        let mut proj_offset = 0usize;
160        for proj in &self.extra_projections {
161            sql.push_str(", ");
162            let frag = proj.to_sql(proj_offset, dialect, &mut params);
163            sql.push('(');
164            sql.push_str(&frag);
165            sql.push_str(") AS \"");
166            sql.push_str(proj.alias);
167            sql.push('"');
168            proj_offset += proj.params.len();
169        }
170
171        // FROM clause
172        sql.push_str(" FROM ");
173        sql.push_str(M::TABLE_NAME);
174
175        // WHERE clause
176        if !self.filter.is_none() {
177            sql.push_str(" WHERE ");
178            sql.push_str(&where_sql);
179        }
180        params.extend(where_params);
181
182        // ORDER BY clause
183        if !self.order_by.is_empty() {
184            sql.push_str(" ORDER BY ");
185            sql.push_str(&self.order_by.to_sql());
186        }
187
188        // LIMIT 1
189        sql.push_str(" LIMIT 1");
190
191        (sql, params)
192    }
193
194    /// Execute the query and return an optional result.
195    pub async fn exec(self) -> QueryResult<Option<M>>
196    where
197        M: Send + 'static + ModelRelationLoader<E>,
198    {
199        let dialect = self.engine.dialect();
200        let (sql, params) = self.build_sql(dialect);
201        match self.engine.query_optional::<M>(&sql, params).await? {
202            None => Ok(None),
203            Some(row) => {
204                let mut parents = vec![row];
205                for spec in &self.includes {
206                    <M as ModelRelationLoader<E>>::load_relation(&self.engine, &mut parents, spec)
207                        .await?;
208                }
209                Ok(parents.into_iter().next())
210            }
211        }
212    }
213
214    /// Execute the query and error if not found.
215    pub async fn exec_required(self) -> QueryResult<M>
216    where
217        M: Send + 'static + ModelRelationLoader<E>,
218    {
219        let dialect = self.engine.dialect();
220        let (sql, params) = self.build_sql(dialect);
221        let row = self.engine.query_one::<M>(&sql, params).await?;
222        let mut parents = vec![row];
223        for spec in &self.includes {
224            <M as ModelRelationLoader<E>>::load_relation(&self.engine, &mut parents, spec).await?;
225        }
226        Ok(parents.into_iter().next().expect("1-element vec"))
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::error::QueryError;
234    use crate::filter::FilterValue;
235    use crate::types::OrderByField;
236
237    #[derive(Debug)]
238    struct TestModel;
239
240    impl Model for TestModel {
241        const MODEL_NAME: &'static str = "TestModel";
242        const TABLE_NAME: &'static str = "test_models";
243        const PRIMARY_KEY: &'static [&'static str] = &["id"];
244        const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
245    }
246
247    impl crate::row::FromRow for TestModel {
248        fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
249            Ok(TestModel)
250        }
251    }
252
253    impl crate::traits::ModelRelationLoader<MockEngine> for TestModel {
254        fn load_relation<'a>(
255            _engine: &'a MockEngine,
256            _parents: &'a mut [Self],
257            spec: &'a crate::relations::IncludeSpec,
258        ) -> crate::traits::BoxFuture<'a, QueryResult<()>> {
259            let name = spec.relation_name.clone();
260            Box::pin(async move {
261                Err(QueryError::internal(format!(
262                    "unknown relation '{name}' on TestModel (mock)",
263                )))
264            })
265        }
266    }
267
268    #[derive(Clone)]
269    struct MockEngine;
270
271    impl QueryEngine for MockEngine {
272        fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
273            &crate::dialect::Postgres
274        }
275
276        fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
277            &self,
278            _sql: &str,
279            _params: Vec<FilterValue>,
280        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
281            Box::pin(async { Ok(Vec::new()) })
282        }
283
284        fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
285            &self,
286            _sql: &str,
287            _params: Vec<FilterValue>,
288        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
289            Box::pin(async { Err(QueryError::not_found("test")) })
290        }
291
292        fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
293            &self,
294            _sql: &str,
295            _params: Vec<FilterValue>,
296        ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
297            Box::pin(async { Ok(None) })
298        }
299
300        fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
301            &self,
302            _sql: &str,
303            _params: Vec<FilterValue>,
304        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
305            Box::pin(async { Err(QueryError::not_found("test")) })
306        }
307
308        fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
309            &self,
310            _sql: &str,
311            _params: Vec<FilterValue>,
312        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
313            Box::pin(async { Ok(Vec::new()) })
314        }
315
316        fn execute_delete(
317            &self,
318            _sql: &str,
319            _params: Vec<FilterValue>,
320        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
321            Box::pin(async { Ok(0) })
322        }
323
324        fn execute_raw(
325            &self,
326            _sql: &str,
327            _params: Vec<FilterValue>,
328        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
329            Box::pin(async { Ok(0) })
330        }
331
332        fn count(
333            &self,
334            _sql: &str,
335            _params: Vec<FilterValue>,
336        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
337            Box::pin(async { Ok(0) })
338        }
339    }
340
341    // ========== Construction Tests ==========
342
343    #[test]
344    fn test_find_first_new() {
345        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
346        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
347
348        assert!(sql.contains("SELECT * FROM test_models"));
349        assert!(sql.contains("LIMIT 1"));
350        assert!(params.is_empty());
351    }
352
353    // ========== Filter Tests ==========
354
355    #[test]
356    fn test_find_first_with_filter() {
357        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine).r#where(
358            Filter::Equals("status".into(), FilterValue::String("active".to_string())),
359        );
360
361        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
362
363        assert!(sql.contains("WHERE"));
364        assert!(sql.contains(r#""status" = $1"#));
365        assert_eq!(params.len(), 1);
366    }
367
368    #[test]
369    fn test_find_first_with_compound_filter() {
370        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
371            .r#where(Filter::Equals(
372                "department".into(),
373                FilterValue::String("engineering".to_string()),
374            ))
375            .r#where(Filter::Gt("salary".into(), FilterValue::Int(50000)));
376
377        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
378
379        assert!(sql.contains("WHERE"));
380        assert!(sql.contains("AND"));
381        assert_eq!(params.len(), 2);
382    }
383
384    #[test]
385    fn test_find_first_with_or_filter() {
386        let op =
387            FindFirstOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::or([
388                Filter::Equals("role".into(), FilterValue::String("admin".to_string())),
389                Filter::Equals("role".into(), FilterValue::String("superadmin".to_string())),
390            ]));
391
392        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
393
394        assert!(sql.contains("OR"));
395        assert_eq!(params.len(), 2);
396    }
397
398    #[test]
399    fn test_find_first_without_filter() {
400        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
401        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
402
403        assert!(!sql.contains("WHERE"));
404        assert!(params.is_empty());
405    }
406
407    // ========== Order By Tests ==========
408
409    #[test]
410    fn test_find_first_with_order() {
411        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
412            .r#where(Filter::Gt("age".into(), FilterValue::Int(18)))
413            .order_by(OrderByField::desc("created_at"));
414
415        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
416
417        assert!(sql.contains("WHERE"));
418        assert!(sql.contains("ORDER BY created_at DESC"));
419        assert!(sql.contains("LIMIT 1"));
420        assert_eq!(params.len(), 1);
421    }
422
423    #[test]
424    fn test_find_first_with_asc_order() {
425        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
426            .order_by(OrderByField::asc("name"));
427
428        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
429
430        assert!(sql.contains("ORDER BY name ASC"));
431    }
432
433    #[test]
434    fn test_find_first_without_order() {
435        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
436        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
437
438        assert!(!sql.contains("ORDER BY"));
439    }
440
441    #[test]
442    fn test_find_first_order_replaces() {
443        // Later order_by should replace the previous one
444        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
445            .order_by(OrderByField::asc("name"))
446            .order_by(OrderByField::desc("created_at"));
447
448        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
449
450        assert!(sql.contains("ORDER BY created_at DESC"));
451        assert!(!sql.contains("ORDER BY name"));
452    }
453
454    // ========== Select Tests ==========
455
456    #[test]
457    fn test_find_first_with_select() {
458        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
459            .select(Select::fields(["id", "email"]));
460
461        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
462
463        assert!(sql.contains("SELECT id, email FROM"));
464        assert!(!sql.contains("SELECT *"));
465    }
466
467    #[test]
468    fn test_find_first_select_single_field() {
469        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
470            .select(Select::fields(["count"]));
471
472        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
473
474        assert!(sql.contains("SELECT count FROM"));
475    }
476
477    // ========== SQL Structure Tests ==========
478
479    #[test]
480    fn test_find_first_sql_structure() {
481        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
482            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
483            .order_by(OrderByField::desc("created_at"))
484            .select(Select::fields(["id", "name"]));
485
486        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
487
488        // Check correct SQL clause ordering
489        let select_pos = sql.find("SELECT").unwrap();
490        let from_pos = sql.find("FROM").unwrap();
491        let where_pos = sql.find("WHERE").unwrap();
492        let order_pos = sql.find("ORDER BY").unwrap();
493        let limit_pos = sql.find("LIMIT 1").unwrap();
494
495        assert!(select_pos < from_pos);
496        assert!(from_pos < where_pos);
497        assert!(where_pos < order_pos);
498        assert!(order_pos < limit_pos);
499    }
500
501    #[test]
502    fn test_find_first_table_name() {
503        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
504        let (sql, _) = op.build_sql(&crate::dialect::Postgres);
505
506        assert!(sql.contains("test_models"));
507    }
508
509    // ========== Async Execution Tests ==========
510
511    #[tokio::test]
512    async fn test_find_first_exec() {
513        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
514            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
515
516        let result = op.exec().await;
517
518        // MockEngine returns Ok(None)
519        assert!(result.is_ok());
520        assert!(result.unwrap().is_none());
521    }
522
523    #[tokio::test]
524    async fn test_find_first_exec_required() {
525        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
526            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
527
528        let result = op.exec_required().await;
529
530        // MockEngine returns not_found error
531        assert!(result.is_err());
532        assert!(result.unwrap_err().is_not_found());
533    }
534
535    // ========== Method Chaining Tests ==========
536
537    #[test]
538    fn test_find_first_full_chain() {
539        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
540            .r#where(Filter::Equals(
541                "status".into(),
542                FilterValue::String("active".to_string()),
543            ))
544            .order_by(OrderByField::desc("created_at"))
545            .select(Select::fields(["id", "name", "email"]));
546
547        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
548
549        assert!(sql.contains("SELECT id, name, email FROM"));
550        assert!(sql.contains("WHERE"));
551        assert!(sql.contains("ORDER BY created_at DESC"));
552        assert!(sql.contains("LIMIT 1"));
553        assert_eq!(params.len(), 1);
554    }
555
556    // ========== Edge Cases ==========
557
558    #[test]
559    fn test_find_first_with_like_filter() {
560        let op =
561            FindFirstOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Contains(
562                "email".into(),
563                FilterValue::String("@example.com".to_string()),
564            ));
565
566        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
567
568        assert!(sql.contains("LIKE"));
569        assert_eq!(params.len(), 1);
570    }
571
572    #[test]
573    fn test_find_first_with_null_filter() {
574        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
575            .r#where(Filter::IsNull("deleted_at".into()));
576
577        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
578
579        assert!(sql.contains("IS NULL"));
580        assert!(params.is_empty());
581    }
582
583    #[test]
584    fn test_find_first_with_not_filter() {
585        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Not(
586            Box::new(Filter::Equals(
587                "status".into(),
588                FilterValue::String("deleted".to_string()),
589            )),
590        ));
591
592        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
593
594        assert!(sql.contains("NOT"));
595        assert_eq!(params.len(), 1);
596    }
597
598    #[test]
599    fn test_find_first_with_in_filter() {
600        let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::In(
601            "status".into(),
602            vec![
603                FilterValue::String("pending".to_string()),
604                FilterValue::String("processing".to_string()),
605            ],
606        ));
607
608        let (sql, params) = op.build_sql(&crate::dialect::Postgres);
609
610        assert!(sql.contains("IN"));
611        assert_eq!(params.len(), 2);
612    }
613}