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