1use 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
11pub struct FindFirstOperation<E: QueryEngine, M: Model> {
27 engine: E,
28 filter: Filter,
29 order_by: OrderBy,
30 select: Select,
31 includes: Vec<IncludeSpec>,
35 _model: PhantomData<M>,
36}
37
38impl<E: QueryEngine, M: Model + crate::row::FromRow> FindFirstOperation<E, M> {
39 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 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 pub fn order_by(mut self, order: impl Into<OrderBy>) -> Self {
60 self.order_by = order.into();
61 self
62 }
63
64 pub fn select(mut self, select: impl Into<Select>) -> Self {
66 self.select = select.into();
67 self
68 }
69
70 pub fn include(mut self, spec: IncludeSpec) -> Self {
72 self.includes.push(spec);
73 self
74 }
75
76 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 sql.push_str("SELECT ");
87 sql.push_str(&self.select.to_sql());
88
89 sql.push_str(" FROM ");
91 sql.push_str(M::TABLE_NAME);
92
93 if !self.filter.is_none() {
95 sql.push_str(" WHERE ");
96 sql.push_str(&where_sql);
97 }
98
99 if !self.order_by.is_empty() {
101 sql.push_str(" ORDER BY ");
102 sql.push_str(&self.order_by.to_sql());
103 }
104
105 sql.push_str(" LIMIT 1");
107
108 (sql, params)
109 }
110
111 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 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 #[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 #[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 #[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 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 #[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 #[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 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 #[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 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 assert!(result.is_err());
449 assert!(result.unwrap_err().is_not_found());
450 }
451
452 #[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 #[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}