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).r#where(
218 Filter::Equals("status".into(), FilterValue::String("active".to_string())),
219 );
220
221 let (sql, params) = op.build_sql();
222
223 assert!(sql.contains("WHERE"));
224 assert!(sql.contains("status = $1"));
225 assert_eq!(params.len(), 1);
226 }
227
228 #[test]
229 fn test_find_first_with_compound_filter() {
230 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
231 .r#where(Filter::Equals(
232 "department".into(),
233 FilterValue::String("engineering".to_string()),
234 ))
235 .r#where(Filter::Gt("salary".into(), FilterValue::Int(50000)));
236
237 let (sql, params) = op.build_sql();
238
239 assert!(sql.contains("WHERE"));
240 assert!(sql.contains("AND"));
241 assert_eq!(params.len(), 2);
242 }
243
244 #[test]
245 fn test_find_first_with_or_filter() {
246 let op =
247 FindFirstOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::or([
248 Filter::Equals("role".into(), FilterValue::String("admin".to_string())),
249 Filter::Equals("role".into(), FilterValue::String("superadmin".to_string())),
250 ]));
251
252 let (sql, params) = op.build_sql();
253
254 assert!(sql.contains("OR"));
255 assert_eq!(params.len(), 2);
256 }
257
258 #[test]
259 fn test_find_first_without_filter() {
260 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
261 let (sql, params) = op.build_sql();
262
263 assert!(!sql.contains("WHERE"));
264 assert!(params.is_empty());
265 }
266
267 #[test]
270 fn test_find_first_with_order() {
271 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
272 .r#where(Filter::Gt("age".into(), FilterValue::Int(18)))
273 .order_by(OrderByField::desc("created_at"));
274
275 let (sql, params) = op.build_sql();
276
277 assert!(sql.contains("WHERE"));
278 assert!(sql.contains("ORDER BY created_at DESC"));
279 assert!(sql.contains("LIMIT 1"));
280 assert_eq!(params.len(), 1);
281 }
282
283 #[test]
284 fn test_find_first_with_asc_order() {
285 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
286 .order_by(OrderByField::asc("name"));
287
288 let (sql, _) = op.build_sql();
289
290 assert!(sql.contains("ORDER BY name ASC"));
291 }
292
293 #[test]
294 fn test_find_first_without_order() {
295 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
296 let (sql, _) = op.build_sql();
297
298 assert!(!sql.contains("ORDER BY"));
299 }
300
301 #[test]
302 fn test_find_first_order_replaces() {
303 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
305 .order_by(OrderByField::asc("name"))
306 .order_by(OrderByField::desc("created_at"));
307
308 let (sql, _) = op.build_sql();
309
310 assert!(sql.contains("ORDER BY created_at DESC"));
311 assert!(!sql.contains("ORDER BY name"));
312 }
313
314 #[test]
317 fn test_find_first_with_select() {
318 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
319 .select(Select::fields(["id", "email"]));
320
321 let (sql, _) = op.build_sql();
322
323 assert!(sql.contains("SELECT id, email FROM"));
324 assert!(!sql.contains("SELECT *"));
325 }
326
327 #[test]
328 fn test_find_first_select_single_field() {
329 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
330 .select(Select::fields(["count"]));
331
332 let (sql, _) = op.build_sql();
333
334 assert!(sql.contains("SELECT count FROM"));
335 }
336
337 #[test]
340 fn test_find_first_sql_structure() {
341 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
342 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)))
343 .order_by(OrderByField::desc("created_at"))
344 .select(Select::fields(["id", "name"]));
345
346 let (sql, _) = op.build_sql();
347
348 let select_pos = sql.find("SELECT").unwrap();
350 let from_pos = sql.find("FROM").unwrap();
351 let where_pos = sql.find("WHERE").unwrap();
352 let order_pos = sql.find("ORDER BY").unwrap();
353 let limit_pos = sql.find("LIMIT 1").unwrap();
354
355 assert!(select_pos < from_pos);
356 assert!(from_pos < where_pos);
357 assert!(where_pos < order_pos);
358 assert!(order_pos < limit_pos);
359 }
360
361 #[test]
362 fn test_find_first_table_name() {
363 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine);
364 let (sql, _) = op.build_sql();
365
366 assert!(sql.contains("test_models"));
367 }
368
369 #[tokio::test]
372 async fn test_find_first_exec() {
373 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
374 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
375
376 let result = op.exec().await;
377
378 assert!(result.is_ok());
380 assert!(result.unwrap().is_none());
381 }
382
383 #[tokio::test]
384 async fn test_find_first_exec_required() {
385 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
386 .r#where(Filter::Equals("id".into(), FilterValue::Int(1)));
387
388 let result = op.exec_required().await;
389
390 assert!(result.is_err());
392 assert!(result.unwrap_err().is_not_found());
393 }
394
395 #[test]
398 fn test_find_first_full_chain() {
399 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
400 .r#where(Filter::Equals(
401 "status".into(),
402 FilterValue::String("active".to_string()),
403 ))
404 .order_by(OrderByField::desc("created_at"))
405 .select(Select::fields(["id", "name", "email"]));
406
407 let (sql, params) = op.build_sql();
408
409 assert!(sql.contains("SELECT id, name, email FROM"));
410 assert!(sql.contains("WHERE"));
411 assert!(sql.contains("ORDER BY created_at DESC"));
412 assert!(sql.contains("LIMIT 1"));
413 assert_eq!(params.len(), 1);
414 }
415
416 #[test]
419 fn test_find_first_with_like_filter() {
420 let op =
421 FindFirstOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Contains(
422 "email".into(),
423 FilterValue::String("@example.com".to_string()),
424 ));
425
426 let (sql, params) = op.build_sql();
427
428 assert!(sql.contains("LIKE"));
429 assert_eq!(params.len(), 1);
430 }
431
432 #[test]
433 fn test_find_first_with_null_filter() {
434 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine)
435 .r#where(Filter::IsNull("deleted_at".into()));
436
437 let (sql, params) = op.build_sql();
438
439 assert!(sql.contains("IS NULL"));
440 assert!(params.is_empty());
441 }
442
443 #[test]
444 fn test_find_first_with_not_filter() {
445 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::Not(
446 Box::new(Filter::Equals(
447 "status".into(),
448 FilterValue::String("deleted".to_string()),
449 )),
450 ));
451
452 let (sql, params) = op.build_sql();
453
454 assert!(sql.contains("NOT"));
455 assert_eq!(params.len(), 1);
456 }
457
458 #[test]
459 fn test_find_first_with_in_filter() {
460 let op = FindFirstOperation::<MockEngine, TestModel>::new(MockEngine).r#where(Filter::In(
461 "status".into(),
462 vec![
463 FilterValue::String("pending".to_string()),
464 FilterValue::String("processing".to_string()),
465 ],
466 ));
467
468 let (sql, params) = op.build_sql();
469
470 assert!(sql.contains("IN"));
471 assert_eq!(params.len(), 2);
472 }
473}