1use std::marker::PhantomData;
4
5use crate::error::QueryResult;
6use crate::filter::Filter;
7use crate::traits::{Model, QueryEngine};
8use crate::types::{OrderBy, Select};
9
10pub struct FindFirstOperation<E: QueryEngine, M: Model> {
26 engine: E,
27 filter: Filter,
28 order_by: OrderBy,
29 select: Select,
30 _model: PhantomData<M>,
31}
32
33impl<E: QueryEngine, M: Model> FindFirstOperation<E, M> {
34 pub fn new(engine: E) -> Self {
36 Self {
37 engine,
38 filter: Filter::None,
39 order_by: OrderBy::none(),
40 select: Select::All,
41 _model: PhantomData,
42 }
43 }
44
45 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
47 let new_filter = filter.into();
48 self.filter = self.filter.and_then(new_filter);
49 self
50 }
51
52 pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
54 self.order_by = order.into();
55 self
56 }
57
58 pub fn select(mut self, select: impl Into<Select>) -> Self {
60 self.select = select.into();
61 self
62 }
63
64 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
66 let (where_sql, params) = self.filter.to_sql(0);
67
68 let mut sql = String::new();
69
70 sql.push_str("SELECT ");
72 sql.push_str(&self.select.to_sql());
73
74 sql.push_str(" FROM ");
76 sql.push_str(M::TABLE_NAME);
77
78 if !self.filter.is_none() {
80 sql.push_str(" WHERE ");
81 sql.push_str(&where_sql);
82 }
83
84 if !self.order_by.is_empty() {
86 sql.push_str(" ORDER BY ");
87 sql.push_str(&self.order_by.to_sql());
88 }
89
90 sql.push_str(" LIMIT 1");
92
93 (sql, params)
94 }
95
96 pub async fn exec(self) -> QueryResult<Option<M>>
98 where
99 M: Send + 'static,
100 {
101 let (sql, params) = self.build_sql();
102 self.engine.query_optional::<M>(&sql, params).await
103 }
104
105 pub async fn exec_required(self) -> QueryResult<M>
107 where
108 M: Send + 'static,
109 {
110 let (sql, params) = self.build_sql();
111 self.engine.query_one::<M>(&sql, params).await
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::error::QueryError;
119 use crate::filter::FilterValue;
120 use crate::types::OrderByField;
121
122 #[derive(Debug)]
123 struct TestModel;
124
125 impl Model for TestModel {
126 const MODEL_NAME: &'static str = "TestModel";
127 const TABLE_NAME: &'static str = "test_models";
128 const PRIMARY_KEY: &'static [&'static str] = &["id"];
129 const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
130 }
131
132 #[derive(Clone)]
133 struct MockEngine;
134
135 impl QueryEngine for MockEngine {
136 fn query_many<T: Model + Send + 'static>(
137 &self,
138 _sql: &str,
139 _params: Vec<FilterValue>,
140 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
141 Box::pin(async { Ok(Vec::new()) })
142 }
143
144 fn query_one<T: Model + Send + 'static>(
145 &self,
146 _sql: &str,
147 _params: Vec<FilterValue>,
148 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
149 Box::pin(async { Err(QueryError::not_found("test")) })
150 }
151
152 fn query_optional<T: Model + Send + 'static>(
153 &self,
154 _sql: &str,
155 _params: Vec<FilterValue>,
156 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
157 Box::pin(async { Ok(None) })
158 }
159
160 fn execute_insert<T: Model + Send + 'static>(
161 &self,
162 _sql: &str,
163 _params: Vec<FilterValue>,
164 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
165 Box::pin(async { Err(QueryError::not_found("test")) })
166 }
167
168 fn execute_update<T: Model + Send + 'static>(
169 &self,
170 _sql: &str,
171 _params: Vec<FilterValue>,
172 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
173 Box::pin(async { Ok(Vec::new()) })
174 }
175
176 fn execute_delete(
177 &self,
178 _sql: &str,
179 _params: Vec<FilterValue>,
180 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
181 Box::pin(async { Ok(0) })
182 }
183
184 fn execute_raw(
185 &self,
186 _sql: &str,
187 _params: Vec<FilterValue>,
188 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
189 Box::pin(async { Ok(0) })
190 }
191
192 fn count(
193 &self,
194 _sql: &str,
195 _params: Vec<FilterValue>,
196 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
197 Box::pin(async { Ok(0) })
198 }
199 }
200
201 #[test]
204 fn test_find_first_new() {
205 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
206 let (sql, params) = op.build_sql();
207
208 assert!(sql.contains("SELECT * FROM test_models"));
209 assert!(sql.contains("LIMIT 1"));
210 assert!(params.is_empty());
211 }
212
213 #[test]
216 fn test_find_first_with_filter() {
217 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
218 .r#where(Filter::Equals("status".into(), FilterValue::String("active".to_string())));
219
220 let (sql, params) = op.build_sql();
221
222 assert!(sql.contains("WHERE"));
223 assert!(sql.contains("status = $1"));
224 assert_eq!(params.len(), 1);
225 }
226
227 #[test]
228 fn test_find_first_with_compound_filter() {
229 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
230 .r#where(Filter::Equals("department".into(), FilterValue::String("engineering".to_string())))
231 .r#where(Filter::Gt("salary".into(), FilterValue::Int(50000)));
232
233 let (sql, params) = op.build_sql();
234
235 assert!(sql.contains("WHERE"));
236 assert!(sql.contains("AND"));
237 assert_eq!(params.len(), 2);
238 }
239
240 #[test]
241 fn test_find_first_with_or_filter() {
242 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
243 .r#where(Filter::or([
244 Filter::Equals("role".into(), FilterValue::String("admin".to_string())),
245 Filter::Equals("role".into(), FilterValue::String("superadmin".to_string())),
246 ]));
247
248 let (sql, params) = op.build_sql();
249
250 assert!(sql.contains("OR"));
251 assert_eq!(params.len(), 2);
252 }
253
254 #[test]
255 fn test_find_first_without_filter() {
256 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
257 let (sql, params) = op.build_sql();
258
259 assert!(!sql.contains("WHERE"));
260 assert!(params.is_empty());
261 }
262
263 #[test]
266 fn test_find_first_with_order() {
267 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
268 .r#where(Filter::Gt("age".into(), FilterValue::Int(18)))
269 .order_by(OrderByField::desc("created_at"));
270
271 let (sql, params) = op.build_sql();
272
273 assert!(sql.contains("WHERE"));
274 assert!(sql.contains("ORDER BY created_at DESC"));
275 assert!(sql.contains("LIMIT 1"));
276 assert_eq!(params.len(), 1);
277 }
278
279 #[test]
280 fn test_find_first_with_asc_order() {
281 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
282 .order_by(OrderByField::asc("name"));
283
284 let (sql, _) = op.build_sql();
285
286 assert!(sql.contains("ORDER BY name ASC"));
287 }
288
289 #[test]
290 fn test_find_first_without_order() {
291 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
292 let (sql, _) = op.build_sql();
293
294 assert!(!sql.contains("ORDER BY"));
295 }
296
297 #[test]
298 fn test_find_first_order_replaces() {
299 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
301 .order_by(OrderByField::asc("name"))
302 .order_by(OrderByField::desc("created_at"));
303
304 let (sql, _) = op.build_sql();
305
306 assert!(sql.contains("ORDER BY created_at DESC"));
307 assert!(!sql.contains("ORDER BY name"));
308 }
309
310 #[test]
313 fn test_find_first_with_select() {
314 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
315 .select(Select::fields(["id", "email"]));
316
317 let (sql, _) = op.build_sql();
318
319 assert!(sql.contains("SELECT id, email FROM"));
320 assert!(!sql.contains("SELECT *"));
321 }
322
323 #[test]
324 fn test_find_first_select_single_field() {
325 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
326 .select(Select::fields(["count"]));
327
328 let (sql, _) = op.build_sql();
329
330 assert!(sql.contains("SELECT count FROM"));
331 }
332
333 #[test]
336 fn test_find_first_sql_structure() {
337 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
338 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
339 .order_by(OrderByField::desc("created_at"))
340 .select(Select::fields(["id", "name"]));
341
342 let (sql, _) = op.build_sql();
343
344 let select_pos = sql.find("SELECT").unwrap();
346 let from_pos = sql.find("FROM").unwrap();
347 let where_pos = sql.find("WHERE").unwrap();
348 let order_pos = sql.find("ORDER BY").unwrap();
349 let limit_pos = sql.find("LIMIT 1").unwrap();
350
351 assert!(select_pos < from_pos);
352 assert!(from_pos < where_pos);
353 assert!(where_pos < order_pos);
354 assert!(order_pos < limit_pos);
355 }
356
357 #[test]
358 fn test_find_first_table_name() {
359 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
360 let (sql, _) = op.build_sql();
361
362 assert!(sql.contains("test_models"));
363 }
364
365 #[tokio::test]
368 async fn test_find_first_exec() {
369 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
370 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
371
372 let result = op.exec().await;
373
374 assert!(result.is_ok());
376 assert!(result.unwrap().is_none());
377 }
378
379 #[tokio::test]
380 async fn test_find_first_exec_required() {
381 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
382 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
383
384 let result = op.exec_required().await;
385
386 assert!(result.is_err());
388 assert!(result.unwrap_err().is_not_found());
389 }
390
391 #[test]
394 fn test_find_first_full_chain() {
395 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
396 .r#where(Filter::Equals("status".into(), FilterValue::String("active".to_string())))
397 .order_by(OrderByField::desc("created_at"))
398 .select(Select::fields(["id", "name", "email"]));
399
400 let (sql, params) = op.build_sql();
401
402 assert!(sql.contains("SELECT id, name, email FROM"));
403 assert!(sql.contains("WHERE"));
404 assert!(sql.contains("ORDER BY created_at DESC"));
405 assert!(sql.contains("LIMIT 1"));
406 assert_eq!(params.len(), 1);
407 }
408
409 #[test]
412 fn test_find_first_with_like_filter() {
413 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
414 .r#where(Filter::Contains("email".into(), FilterValue::String("@example.com".to_string())));
415
416 let (sql, params) = op.build_sql();
417
418 assert!(sql.contains("LIKE"));
419 assert_eq!(params.len(), 1);
420 }
421
422 #[test]
423 fn test_find_first_with_null_filter() {
424 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
425 .r#where(Filter::IsNull("deleted_at".into()));
426
427 let (sql, params) = op.build_sql();
428
429 assert!(sql.contains("IS NULL"));
430 assert!(params.is_empty());
431 }
432
433 #[test]
434 fn test_find_first_with_not_filter() {
435 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
436 .r#where(Filter::Not(Box::new(Filter::Equals(
437 "status".into(),
438 FilterValue::String("deleted".to_string()),
439 ))));
440
441 let (sql, params) = op.build_sql();
442
443 assert!(sql.contains("NOT"));
444 assert_eq!(params.len(), 1);
445 }
446
447 #[test]
448 fn test_find_first_with_in_filter() {
449 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
450 .r#where(Filter::In(
451 "status".into(),
452 vec![
453 FilterValue::String("pending".to_string()),
454 FilterValue::String("processing".to_string()),
455 ],
456 ));
457
458 let (sql, params) = op.build_sql();
459
460 assert!(sql.contains("IN"));
461 assert_eq!(params.len(), 2);
462 }
463}
464