Skip to main content

ferro_rs/database/
query_builder.rs

1//! Fluent query builder for Eloquent-like API
2//!
3//! Provides a chainable query interface that uses the global DB connection.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use ferro_rs::models::todos::{Todo, Column};
9//!
10//! // Simple query
11//! let todos = Todo::query().all().await?;
12//!
13//! // With filters
14//! let todo = Todo::query()
15//!     .filter(Column::Title.eq("test"))
16//!     .filter(Column::Id.gt(5))
17//!     .first()
18//!     .await?;
19//!
20//! // With ordering and pagination
21//! let todos = Todo::query()
22//!     .order_by_desc(Column::CreatedAt)
23//!     .limit(10)
24//!     .offset(20)
25//!     .all()
26//!     .await?;
27//!
28//! // With eager loading (avoids N+1)
29//! let (animals, shelters) = Animal::query()
30//!     .all_with(|animals| async {
31//!         Shelter::batch_load(animals.iter().map(|a| a.shelter_id)).await
32//!     })
33//!     .await?;
34//! ```
35
36use sea_orm::{
37    ColumnTrait, EntityTrait, Order, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Select,
38};
39use std::future::Future;
40
41use crate::database::DB;
42use crate::error::FrameworkError;
43
44/// Fluent query builder wrapper
45///
46/// Wraps SeaORM's `Select` with methods that use the global DB connection.
47/// This provides an Eloquent-like query API.
48///
49/// # Example
50///
51/// ```rust,ignore
52/// let todos = Todo::query()
53///     .filter(Column::Active.eq(true))
54///     .order_by_asc(Column::Title)
55///     .all()
56///     .await?;
57/// ```
58pub struct QueryBuilder<E>
59where
60    E: EntityTrait,
61{
62    select: Select<E>,
63}
64
65impl<E> QueryBuilder<E>
66where
67    E: EntityTrait,
68    E::Model: Send + Sync,
69{
70    /// Create a new query builder for the entity
71    pub fn new() -> Self {
72        Self { select: E::find() }
73    }
74
75    /// Add a filter condition
76    ///
77    /// # Example
78    ///
79    /// ```rust,ignore
80    /// let todos = Todo::query()
81    ///     .filter(Column::Title.eq("test"))
82    ///     .filter(Column::Active.eq(true))
83    ///     .all()
84    ///     .await?;
85    /// ```
86    pub fn filter<F>(mut self, filter: F) -> Self
87    where
88        F: sea_orm::sea_query::IntoCondition,
89    {
90        self.select = self.select.filter(filter);
91        self
92    }
93
94    /// Add an order by clause (ascending)
95    ///
96    /// # Example
97    ///
98    /// ```rust,ignore
99    /// let todos = Todo::query()
100    ///     .order_by_asc(Column::Title)
101    ///     .all()
102    ///     .await?;
103    /// ```
104    pub fn order_by_asc<C>(mut self, col: C) -> Self
105    where
106        C: ColumnTrait,
107    {
108        self.select = self.select.order_by(col, Order::Asc);
109        self
110    }
111
112    /// Add an order by clause (descending)
113    ///
114    /// # Example
115    ///
116    /// ```rust,ignore
117    /// let todos = Todo::query()
118    ///     .order_by_desc(Column::CreatedAt)
119    ///     .all()
120    ///     .await?;
121    /// ```
122    pub fn order_by_desc<C>(mut self, col: C) -> Self
123    where
124        C: ColumnTrait,
125    {
126        self.select = self.select.order_by(col, Order::Desc);
127        self
128    }
129
130    /// Add an order by clause with custom order
131    ///
132    /// # Example
133    ///
134    /// ```rust,ignore
135    /// use sea_orm::Order;
136    /// let todos = Todo::query()
137    ///     .order_by(Column::Title, Order::Asc)
138    ///     .all()
139    ///     .await?;
140    /// ```
141    pub fn order_by<C>(mut self, col: C, order: Order) -> Self
142    where
143        C: ColumnTrait,
144    {
145        self.select = self.select.order_by(col, order);
146        self
147    }
148
149    /// Limit the number of results
150    ///
151    /// # Example
152    ///
153    /// ```rust,ignore
154    /// let todos = Todo::query().limit(10).all().await?;
155    /// ```
156    pub fn limit(mut self, limit: u64) -> Self {
157        self.select = self.select.limit(limit);
158        self
159    }
160
161    /// Skip a number of results (offset)
162    ///
163    /// # Example
164    ///
165    /// ```rust,ignore
166    /// // Skip first 10, get next 10
167    /// let todos = Todo::query().offset(10).limit(10).all().await?;
168    /// ```
169    pub fn offset(mut self, offset: u64) -> Self {
170        self.select = self.select.offset(offset);
171        self
172    }
173
174    /// Execute query and return all results
175    ///
176    /// # Example
177    ///
178    /// ```rust,ignore
179    /// let todos = Todo::query().all().await?;
180    /// ```
181    pub async fn all(self) -> Result<Vec<E::Model>, FrameworkError> {
182        let db = DB::connection()?;
183        self.select
184            .all(db.inner())
185            .await
186            .map_err(|e| FrameworkError::database(e.to_string()))
187    }
188
189    /// Execute query and return first result
190    ///
191    /// Returns `None` if no record matches.
192    ///
193    /// # Example
194    ///
195    /// ```rust,ignore
196    /// let todo = Todo::query()
197    ///     .filter(Column::Id.eq(1))
198    ///     .first()
199    ///     .await?;
200    /// ```
201    pub async fn first(self) -> Result<Option<E::Model>, FrameworkError> {
202        let db = DB::connection()?;
203        self.select
204            .one(db.inner())
205            .await
206            .map_err(|e| FrameworkError::database(e.to_string()))
207    }
208
209    /// Execute query and return first result or error
210    ///
211    /// Returns an error if no record matches.
212    ///
213    /// # Example
214    ///
215    /// ```rust,ignore
216    /// let todo = Todo::query()
217    ///     .filter(Column::Id.eq(1))
218    ///     .first_or_fail()
219    ///     .await?;
220    /// ```
221    pub async fn first_or_fail(self) -> Result<E::Model, FrameworkError> {
222        self.first().await?.ok_or_else(|| {
223            FrameworkError::database(format!("{} not found", std::any::type_name::<E::Model>()))
224        })
225    }
226
227    /// Count matching records
228    ///
229    /// # Example
230    ///
231    /// ```rust,ignore
232    /// let count = Todo::query()
233    ///     .filter(Column::Active.eq(true))
234    ///     .count()
235    ///     .await?;
236    /// ```
237    pub async fn count(self) -> Result<u64, FrameworkError> {
238        let db = DB::connection()?;
239        self.select
240            .count(db.inner())
241            .await
242            .map_err(|e| FrameworkError::database(e.to_string()))
243    }
244
245    /// Check if any records exist matching the query
246    ///
247    /// # Example
248    ///
249    /// ```rust,ignore
250    /// let has_active = Todo::query()
251    ///     .filter(Column::Active.eq(true))
252    ///     .exists()
253    ///     .await?;
254    /// ```
255    pub async fn exists(self) -> Result<bool, FrameworkError> {
256        Ok(self.count().await? > 0)
257    }
258
259    /// Get access to the underlying SeaORM Select for advanced queries
260    ///
261    /// Use this when you need SeaORM features not exposed by QueryBuilder.
262    ///
263    /// # Example
264    ///
265    /// ```rust,ignore
266    /// let select = Todo::query()
267    ///     .filter(Column::Active.eq(true))
268    ///     .into_select();
269    ///
270    /// // Use with SeaORM directly
271    /// let todos = select.all(db.inner()).await?;
272    /// ```
273    pub fn into_select(self) -> Select<E> {
274        self.select
275    }
276
277    /// Execute query and load related entities in a single operation
278    ///
279    /// This method helps avoid N+1 queries by allowing you to batch load
280    /// related entities after fetching the main results.
281    ///
282    /// # Example
283    ///
284    /// ```rust,ignore
285    /// // Load animals with their shelters (2 queries instead of N+1)
286    /// let (animals, shelters) = Animal::query()
287    ///     .filter(Column::Status.eq("available"))
288    ///     .all_with(|animals| async {
289    ///         let ids: Vec<_> = animals.iter().map(|a| a.shelter_id).collect();
290    ///         Shelter::batch_load(ids).await
291    ///     })
292    ///     .await?;
293    ///
294    /// // Access related data
295    /// for animal in &animals {
296    ///     if let Some(shelter) = shelters.get(&animal.shelter_id) {
297    ///         println!("{} is at {}", animal.name, shelter.name);
298    ///     }
299    /// }
300    /// ```
301    pub async fn all_with<R, F, Fut>(self, loader: F) -> Result<(Vec<E::Model>, R), FrameworkError>
302    where
303        F: FnOnce(&[E::Model]) -> Fut,
304        Fut: Future<Output = Result<R, FrameworkError>>,
305    {
306        let models = self.all().await?;
307        let related = loader(&models).await?;
308        Ok((models, related))
309    }
310
311    /// Execute query and load multiple related entity types
312    ///
313    /// # Example
314    ///
315    /// ```rust,ignore
316    /// // Load animals with shelters and photos
317    /// let (animals, (shelters, photos)) = Animal::query()
318    ///     .all_with2(
319    ///         |animals| Shelter::batch_load(animals.iter().map(|a| a.shelter_id)),
320    ///         |animals| AnimalPhoto::load_for_animals(animals),
321    ///     )
322    ///     .await?;
323    /// ```
324    pub async fn all_with2<R1, R2, F1, F2, Fut1, Fut2>(
325        self,
326        loader1: F1,
327        loader2: F2,
328    ) -> Result<(Vec<E::Model>, (R1, R2)), FrameworkError>
329    where
330        F1: FnOnce(&[E::Model]) -> Fut1,
331        F2: FnOnce(&[E::Model]) -> Fut2,
332        Fut1: Future<Output = Result<R1, FrameworkError>>,
333        Fut2: Future<Output = Result<R2, FrameworkError>>,
334    {
335        let models = self.all().await?;
336        let (r1, r2) = tokio::try_join!(loader1(&models), loader2(&models))?;
337        Ok((models, (r1, r2)))
338    }
339
340    /// Execute query and load three related entity types
341    ///
342    /// # Example
343    ///
344    /// ```rust,ignore
345    /// let (animals, (shelters, photos, favorites)) = Animal::query()
346    ///     .all_with3(
347    ///         |a| Shelter::batch_load(a.iter().map(|x| x.shelter_id)),
348    ///         |a| AnimalPhoto::load_for_animals(a),
349    ///         |a| Favorite::load_for_animals(a),
350    ///     )
351    ///     .await?;
352    /// ```
353    pub async fn all_with3<R1, R2, R3, F1, F2, F3, Fut1, Fut2, Fut3>(
354        self,
355        loader1: F1,
356        loader2: F2,
357        loader3: F3,
358    ) -> Result<(Vec<E::Model>, (R1, R2, R3)), FrameworkError>
359    where
360        F1: FnOnce(&[E::Model]) -> Fut1,
361        F2: FnOnce(&[E::Model]) -> Fut2,
362        F3: FnOnce(&[E::Model]) -> Fut3,
363        Fut1: Future<Output = Result<R1, FrameworkError>>,
364        Fut2: Future<Output = Result<R2, FrameworkError>>,
365        Fut3: Future<Output = Result<R3, FrameworkError>>,
366    {
367        let models = self.all().await?;
368        let (r1, r2, r3) = tokio::try_join!(loader1(&models), loader2(&models), loader3(&models))?;
369        Ok((models, (r1, r2, r3)))
370    }
371}
372
373impl<E> Default for QueryBuilder<E>
374where
375    E: EntityTrait,
376    E::Model: Send + Sync,
377{
378    fn default() -> Self {
379        Self::new()
380    }
381}