prax_query/operations/
find_unique.rs

1//! FindUnique operation for querying a single record by unique constraint.
2
3use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::Filter;
7use crate::traits::{Model, QueryEngine};
8use crate::types::Select;
9
10/// A query operation that finds a single record by unique constraint.
11///
12/// # Example
13///
14/// ```rust,ignore
15/// let user = client
16///     .user()
17///     .find_unique()
18///     .r#where(user::id::equals(1))
19///     .exec()
20///     .await?;
21/// ```
22pub struct FindUniqueOperation<E: QueryEngine, M: Model> {
23    engine: E,
24    filter: Filter,
25    select: Select,
26    _model: PhantomData<M>,
27}
28
29impl<E: QueryEngine, M: Model> FindUniqueOperation<E, M> {
30    /// Create a new FindUnique operation.
31    pub fn new(engine: E) -> Self {
32        Self {
33            engine,
34            filter: Filter::None,
35            select: Select::All,
36            _model: PhantomData,
37        }
38    }
39
40    /// Add a filter condition (should be on unique fields).
41    pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
42        self.filter = filter.into();
43        self
44    }
45
46    /// Select specific fields.
47    pub fn select(mut self, select: impl Into<Select>) -> Self {
48        self.select = select.into();
49        self
50    }
51
52    /// Build the SQL query.
53    pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
54        let (where_sql, params) = self.filter.to_sql(0);
55
56        let mut sql = String::new();
57
58        // SELECT clause
59        sql.push_str("SELECT ");
60        sql.push_str(&self.select.to_sql());
61
62        // FROM clause
63        sql.push_str(" FROM ");
64        sql.push_str(M::TABLE_NAME);
65
66        // WHERE clause
67        if !self.filter.is_none() {
68            sql.push_str(" WHERE ");
69            sql.push_str(&where_sql);
70        }
71
72        // LIMIT 1 for unique query
73        sql.push_str(" LIMIT 1");
74
75        (sql, params)
76    }
77
78    /// Execute the query and return the result (errors if not found).
79    pub async fn exec(self) -> QueryResult<M>
80    where
81        M: Send + 'static,
82    {
83        let (sql, params) = self.build_sql();
84        self.engine.query_one::<M>(&sql, params).await
85    }
86
87    /// Execute the query and return an optional result.
88    pub async fn exec_optional(self) -> QueryResult<Option<M>>
89    where
90        M: Send + 'static,
91    {
92        let (sql, params) = self.build_sql();
93        self.engine.query_optional::<M>(&sql, params).await
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::error::QueryError;
101    use crate::filter::FilterValue;
102
103    #[derive(Debug)]
104    struct TestModel;
105
106    impl Model for TestModel {
107        const MODEL_NAME: &'static str = "TestModel";
108        const TABLE_NAME: &'static str = "test_models";
109        const PRIMARY_KEY: &'static [&'static str] = &["id"];
110        const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
111    }
112
113    #[derive(Clone)]
114    struct MockEngine;
115
116    impl QueryEngine for MockEngine {
117        fn query_many<T: Model + Send + 'static>(
118            &self,
119            _sql: &str,
120            _params: Vec<FilterValue>,
121        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
122            Box::pin(async { Ok(Vec::new()) })
123        }
124
125        fn query_one<T: Model + Send + 'static>(
126            &self,
127            _sql: &str,
128            _params: Vec<FilterValue>,
129        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
130            Box::pin(async { Err(QueryError::not_found("test")) })
131        }
132
133        fn query_optional<T: Model + Send + 'static>(
134            &self,
135            _sql: &str,
136            _params: Vec<FilterValue>,
137        ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
138            Box::pin(async { Ok(None) })
139        }
140
141        fn execute_insert<T: Model + Send + 'static>(
142            &self,
143            _sql: &str,
144            _params: Vec<FilterValue>,
145        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
146            Box::pin(async { Err(QueryError::not_found("test")) })
147        }
148
149        fn execute_update<T: Model + Send + 'static>(
150            &self,
151            _sql: &str,
152            _params: Vec<FilterValue>,
153        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
154            Box::pin(async { Ok(Vec::new()) })
155        }
156
157        fn execute_delete(
158            &self,
159            _sql: &str,
160            _params: Vec<FilterValue>,
161        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
162            Box::pin(async { Ok(0) })
163        }
164
165        fn execute_raw(
166            &self,
167            _sql: &str,
168            _params: Vec<FilterValue>,
169        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
170            Box::pin(async { Ok(0) })
171        }
172
173        fn count(
174            &self,
175            _sql: &str,
176            _params: Vec<FilterValue>,
177        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
178            Box::pin(async { Ok(0) })
179        }
180    }
181
182    // ========== Construction and Basic Tests ==========
183
184    #[test]
185    fn test_find_unique_new() {
186        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine);
187        let (sql, params) = op.build_sql();
188
189        assert!(sql.contains("SELECT * FROM test_models"));
190        assert!(sql.contains("LIMIT 1"));
191        assert!(params.is_empty());
192    }
193
194    #[test]
195    fn test_find_unique_basic() {
196        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
197            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
198
199        let (sql, params) = op.build_sql();
200
201        assert!(sql.contains("SELECT * FROM test_models"));
202        assert!(sql.contains("WHERE"));
203        assert!(sql.contains("id = $1"));
204        assert!(sql.contains("LIMIT 1"));
205        assert_eq!(params.len(), 1);
206    }
207
208    #[test]
209    fn test_find_unique_by_email() {
210        let op =
211            FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Equals(
212                "email".into(),
213                FilterValue::String("test@example.com".to_string()),
214            ));
215
216        let (sql, params) = op.build_sql();
217
218        assert!(sql.contains("WHERE"));
219        assert!(sql.contains("email = $1"));
220        assert_eq!(params.len(), 1);
221    }
222
223    // ========== Select Tests ==========
224
225    #[test]
226    fn test_find_unique_with_select() {
227        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
228            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
229            .select(Select::fields(["id", "name"]));
230
231        let (sql, _) = op.build_sql();
232
233        assert!(sql.contains("SELECT id, name FROM"));
234        assert!(!sql.contains("SELECT *"));
235    }
236
237    #[test]
238    fn test_find_unique_select_single_field() {
239        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
240            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
241            .select(Select::fields(["id"]));
242
243        let (sql, _) = op.build_sql();
244
245        assert!(sql.contains("SELECT id FROM"));
246    }
247
248    #[test]
249    fn test_find_unique_select_all_explicitly() {
250        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
251            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
252            .select(Select::All);
253
254        let (sql, _) = op.build_sql();
255
256        assert!(sql.contains("SELECT * FROM"));
257    }
258
259    // ========== Filter Tests ==========
260
261    #[test]
262    fn test_find_unique_with_compound_filter() {
263        let op =
264            FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::and([
265                Filter::Equals(
266                    "email".into(),
267                    FilterValue::String("test@example.com".to_string()),
268                ),
269                Filter::Equals("tenant_id".into(), FilterValue::Int(1)),
270            ]));
271
272        let (sql, params) = op.build_sql();
273
274        assert!(sql.contains("WHERE"));
275        assert!(sql.contains("AND"));
276        assert_eq!(params.len(), 2);
277    }
278
279    #[test]
280    fn test_find_unique_without_filter() {
281        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine);
282        let (sql, params) = op.build_sql();
283
284        assert!(!sql.contains("WHERE"));
285        assert!(params.is_empty());
286    }
287
288    #[test]
289    fn test_find_unique_with_none_filter() {
290        let op =
291            FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::None);
292
293        let (sql, params) = op.build_sql();
294
295        // Filter::None should not produce WHERE clause
296        assert!(!sql.contains("WHERE"));
297        assert!(params.is_empty());
298    }
299
300    // ========== SQL Structure Tests ==========
301
302    #[test]
303    fn test_find_unique_sql_order() {
304        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
305            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
306            .select(Select::fields(["id", "name"]));
307
308        let (sql, _) = op.build_sql();
309
310        // Check SQL structure order
311        let select_pos = sql.find("SELECT").unwrap();
312        let from_pos = sql.find("FROM").unwrap();
313        let where_pos = sql.find("WHERE").unwrap();
314        let limit_pos = sql.find("LIMIT 1").unwrap();
315
316        assert!(select_pos < from_pos);
317        assert!(from_pos < where_pos);
318        assert!(where_pos < limit_pos);
319    }
320
321    #[test]
322    fn test_find_unique_table_name() {
323        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine);
324        let (sql, _) = op.build_sql();
325
326        assert!(sql.contains("test_models"));
327    }
328
329    // ========== Async Execution Tests ==========
330
331    #[tokio::test]
332    async fn test_find_unique_exec() {
333        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
334            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
335
336        let result = op.exec().await;
337
338        // MockEngine returns not_found error
339        assert!(result.is_err());
340        assert!(result.unwrap_err().is_not_found());
341    }
342
343    #[tokio::test]
344    async fn test_find_unique_exec_optional() {
345        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
346            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
347
348        let result = op.exec_optional().await;
349
350        // MockEngine returns Ok(None)
351        assert!(result.is_ok());
352        assert!(result.unwrap().is_none());
353    }
354
355    // ========== Parameter Tests ==========
356
357    #[test]
358    fn test_find_unique_with_string_param() {
359        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine).r#where(
360            Filter::Equals("name".into(), FilterValue::String("Alice".to_string())),
361        );
362
363        let (_, params) = op.build_sql();
364
365        assert_eq!(params.len(), 1);
366        assert_eq!(params[0], FilterValue::String("Alice".to_string()));
367    }
368
369    #[test]
370    fn test_find_unique_with_int_param() {
371        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
372            .r#where(Filter::Equals("id".into(), FilterValue::Int(42)));
373
374        let (_, params) = op.build_sql();
375
376        assert_eq!(params.len(), 1);
377        assert_eq!(params[0], FilterValue::Int(42));
378    }
379
380    #[test]
381    fn test_find_unique_with_boolean_param() {
382        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
383            .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)));
384
385        let (_, params) = op.build_sql();
386
387        assert_eq!(params.len(), 1);
388        assert_eq!(params[0], FilterValue::Bool(true));
389    }
390
391    // ========== Method Chaining Tests ==========
392
393    #[test]
394    fn test_find_unique_method_chaining() {
395        // Test that methods return Self and can be chained
396        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
397            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
398            .select(Select::fields(["id", "name"]));
399
400        let (sql, params) = op.build_sql();
401
402        assert!(sql.contains("SELECT id, name"));
403        assert!(sql.contains("WHERE"));
404        assert_eq!(params.len(), 1);
405    }
406
407    #[test]
408    fn test_find_unique_replace_filter() {
409        // Later where_ calls should replace the filter
410        let op = FindUniqueOperation::<MockEngine, TestModel>::new(MockEngine)
411            .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
412            .r#where(Filter::Equals("id".into(), FilterValue::Int(2)));
413
414        let (_, params) = op.build_sql();
415
416        // Should only have the second filter's param
417        assert_eq!(params.len(), 1);
418        assert_eq!(params[0], FilterValue::Int(2));
419    }
420}