1use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::Filter;
7use crate::traits::{Model, QueryEngine};
8use crate::types::Select;
9
10pub 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 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 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
42 self.filter = filter.into();
43 self
44 }
45
46 pub fn select(mut self, select: impl Into<Select>) -> Self {
48 self.select = select.into();
49 self
50 }
51
52 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 sql.push_str("SELECT ");
60 sql.push_str(&self.select.to_sql());
61
62 sql.push_str(" FROM ");
64 sql.push_str(M::TABLE_NAME);
65
66 if !self.filter.is_none() {
68 sql.push_str(" WHERE ");
69 sql.push_str(&where_sql);
70 }
71
72 sql.push_str(" LIMIT 1");
74
75 (sql, params)
76 }
77
78 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 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 #[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 #[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 #[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 assert!(!sql.contains("WHERE"));
297 assert!(params.is_empty());
298 }
299
300 #[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 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 #[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 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 assert!(result.is_ok());
352 assert!(result.unwrap().is_none());
353 }
354
355 #[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 #[test]
394 fn test_find_unique_method_chaining() {
395 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 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 assert_eq!(params.len(), 1);
418 assert_eq!(params[0], FilterValue::Int(2));
419 }
420}