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}