Skip to main content

modo_db/
query.rs

1use std::marker::PhantomData;
2
3use sea_orm::sea_query::IntoCondition;
4use sea_orm::sea_query::IntoValueTuple;
5use sea_orm::{
6    ColumnTrait, ConnectionTrait, EntityTrait, FromQueryResult, IntoIdentity, PaginatorTrait,
7    QueryFilter, QueryOrder, QuerySelect, Select,
8};
9
10use crate::error::db_err_to_error;
11use crate::pagination::{
12    CursorParams, CursorResult, PageParams, PageResult, paginate, paginate_cursor,
13};
14
15// ── EntityQuery ──────────────────────────────────────────────────────────────
16
17/// A chainable query builder that wraps SeaORM's `Select<E>` and
18/// auto-converts results to the domain type `T` via `From<E::Model>`.
19///
20/// The primary entry point is the `Record::query()` method generated on every
21/// entity struct. `EntityQuery::new` is available for advanced cases where you
22/// need to wrap a raw `Select<E>`.
23///
24/// # Example
25///
26/// ```rust,ignore
27/// let todos: Vec<Todo> = Todo::query()
28///     .filter(todo::Column::Completed.eq(false))
29///     .order_by_asc(todo::Column::CreatedAt)
30///     .limit(10)
31///     .all(&*db)
32///     .await?;
33/// ```
34pub struct EntityQuery<T, E: EntityTrait> {
35    select: Select<E>,
36    _phantom: PhantomData<T>,
37}
38
39impl<T, E> EntityQuery<T, E>
40where
41    E: EntityTrait,
42    T: From<E::Model> + Send + Sync,
43    E::Model: FromQueryResult + Send + Sync,
44{
45    /// Wrap an existing `Select<E>`.
46    pub fn new(select: Select<E>) -> Self {
47        Self {
48            select,
49            _phantom: PhantomData,
50        }
51    }
52
53    // ── Chainable methods ────────────────────────────────────────────────────
54
55    /// Apply a WHERE condition.
56    pub fn filter(self, f: impl IntoCondition) -> Self {
57        Self {
58            select: QueryFilter::filter(self.select, f),
59            _phantom: PhantomData,
60        }
61    }
62
63    /// ORDER BY `col` ASC.
64    pub fn order_by_asc<C: ColumnTrait>(self, col: C) -> Self {
65        Self {
66            select: QueryOrder::order_by_asc(self.select, col),
67            _phantom: PhantomData,
68        }
69    }
70
71    /// ORDER BY `col` DESC.
72    pub fn order_by_desc<C: ColumnTrait>(self, col: C) -> Self {
73        Self {
74            select: QueryOrder::order_by_desc(self.select, col),
75            _phantom: PhantomData,
76        }
77    }
78
79    /// LIMIT `n` rows.
80    pub fn limit(self, n: u64) -> Self {
81        Self {
82            select: QuerySelect::limit(self.select, Some(n)),
83            _phantom: PhantomData,
84        }
85    }
86
87    /// OFFSET `n` rows.
88    pub fn offset(self, n: u64) -> Self {
89        Self {
90            select: QuerySelect::offset(self.select, Some(n)),
91            _phantom: PhantomData,
92        }
93    }
94
95    // ── Terminal methods ─────────────────────────────────────────────────────
96
97    /// Fetch all matching rows and convert each model to `T`.
98    pub async fn all(self, db: &impl ConnectionTrait) -> Result<Vec<T>, modo::Error> {
99        let rows = self.select.all(db).await.map_err(db_err_to_error)?;
100        Ok(rows.into_iter().map(T::from).collect())
101    }
102
103    /// Fetch at most one row and convert to `T` if present.
104    pub async fn one(self, db: &impl ConnectionTrait) -> Result<Option<T>, modo::Error> {
105        let row = self.select.one(db).await.map_err(db_err_to_error)?;
106        Ok(row.map(T::from))
107    }
108
109    /// Return the number of rows that match the current query.
110    pub async fn count(self, db: &impl ConnectionTrait) -> Result<u64, modo::Error> {
111        self.select.count(db).await.map_err(db_err_to_error)
112    }
113
114    /// Offset-based pagination. Results are auto-converted to `T`.
115    pub async fn paginate(
116        self,
117        db: &impl ConnectionTrait,
118        params: &PageParams,
119    ) -> Result<PageResult<T>, modo::Error> {
120        paginate(self.select, db, params)
121            .await
122            .map_err(db_err_to_error)
123            .map(|r| r.map(T::from))
124    }
125
126    /// Cursor-based pagination. Results are auto-converted to `T`.
127    ///
128    /// - `col` — the column to paginate on (e.g. `Column::Id`).
129    /// - `cursor_fn` — extracts the cursor string from a model instance.
130    pub async fn paginate_cursor<C, V, F>(
131        self,
132        col: C,
133        cursor_fn: F,
134        db: &impl ConnectionTrait,
135        params: &CursorParams<V>,
136    ) -> Result<CursorResult<T>, modo::Error>
137    where
138        C: IntoIdentity,
139        V: IntoValueTuple + Clone,
140        F: Fn(&E::Model) -> String,
141    {
142        paginate_cursor(self.select, col, cursor_fn, db, params)
143            .await
144            .map_err(db_err_to_error)
145            .map(|r| r.map(T::from))
146    }
147
148    // ── Join methods ──────────────────────────────────────────────────────────
149
150    /// Perform a 1:1 join (LEFT JOIN) with a related entity.
151    ///
152    /// Requires that `E` implements `Related<F>`.
153    pub fn find_also_related<U, F>(self) -> JoinedQuery<T, U, E, F>
154    where
155        F: EntityTrait,
156        U: From<F::Model> + Send + Sync,
157        F::Model: FromQueryResult + Send + Sync,
158        E: sea_orm::Related<F>,
159    {
160        JoinedQuery {
161            select: self.select.find_also_related(F::default()),
162            _phantom: PhantomData,
163        }
164    }
165
166    /// Perform a 1:N join with a related entity.
167    ///
168    /// Requires that `E` implements `Related<F>`.
169    pub fn find_with_related<U, F>(self) -> JoinedManyQuery<T, U, E, F>
170    where
171        F: EntityTrait,
172        U: From<F::Model> + Send + Sync,
173        F::Model: FromQueryResult + Send + Sync,
174        E: sea_orm::Related<F>,
175    {
176        JoinedManyQuery {
177            select: self.select.find_with_related(F::default()),
178            _phantom: PhantomData,
179        }
180    }
181
182    // ── Escape hatch ─────────────────────────────────────────────────────────
183
184    /// Unwrap the inner `Select<E>` for advanced SeaORM usage.
185    pub fn into_select(self) -> Select<E> {
186        self.select
187    }
188}
189
190// ── JoinedQuery (1:1 / find_also_related) ──────────────────────────────────
191
192/// A query builder wrapping SeaORM's `SelectTwo<E, F>` for 1:1 joins.
193///
194/// Results are auto-converted to `(T, Option<U>)` tuples via `From<Model>`.
195pub struct JoinedQuery<T, U, E: EntityTrait, F: EntityTrait> {
196    select: sea_orm::SelectTwo<E, F>,
197    _phantom: PhantomData<(T, U)>,
198}
199
200impl<T, U, E, F> JoinedQuery<T, U, E, F>
201where
202    E: EntityTrait,
203    F: EntityTrait,
204    T: From<E::Model> + Send + Sync,
205    U: From<F::Model> + Send + Sync,
206    E::Model: FromQueryResult + Send + Sync,
207    F::Model: FromQueryResult + Send + Sync,
208{
209    /// Apply a WHERE condition.
210    pub fn filter(self, f: impl IntoCondition) -> Self {
211        Self {
212            select: QueryFilter::filter(self.select, f),
213            _phantom: PhantomData,
214        }
215    }
216
217    /// ORDER BY `col` ASC.
218    pub fn order_by_asc<C: ColumnTrait>(self, col: C) -> Self {
219        Self {
220            select: QueryOrder::order_by_asc(self.select, col),
221            _phantom: PhantomData,
222        }
223    }
224
225    /// ORDER BY `col` DESC.
226    pub fn order_by_desc<C: ColumnTrait>(self, col: C) -> Self {
227        Self {
228            select: QueryOrder::order_by_desc(self.select, col),
229            _phantom: PhantomData,
230        }
231    }
232
233    /// LIMIT `n` rows.
234    pub fn limit(self, n: u64) -> Self {
235        Self {
236            select: QuerySelect::limit(self.select, Some(n)),
237            _phantom: PhantomData,
238        }
239    }
240
241    /// OFFSET `n` rows.
242    pub fn offset(self, n: u64) -> Self {
243        Self {
244            select: QuerySelect::offset(self.select, Some(n)),
245            _phantom: PhantomData,
246        }
247    }
248
249    /// Fetch all matching rows, converting both models to domain types.
250    pub async fn all(self, db: &impl ConnectionTrait) -> Result<Vec<(T, Option<U>)>, modo::Error> {
251        let rows = self.select.all(db).await.map_err(db_err_to_error)?;
252        Ok(rows
253            .into_iter()
254            .map(|(a, b)| (T::from(a), b.map(U::from)))
255            .collect())
256    }
257
258    /// Fetch at most one row, converting both models to domain types.
259    pub async fn one(
260        self,
261        db: &impl ConnectionTrait,
262    ) -> Result<Option<(T, Option<U>)>, modo::Error> {
263        let row = self.select.one(db).await.map_err(db_err_to_error)?;
264        Ok(row.map(|(a, b)| (T::from(a), b.map(U::from))))
265    }
266
267    /// Unwrap the inner `SelectTwo<E, F>` for advanced SeaORM usage.
268    pub fn into_select(self) -> sea_orm::SelectTwo<E, F> {
269        self.select
270    }
271}
272
273// ── JoinedManyQuery (1:N / find_with_related) ──────────────────────────────
274
275/// A query builder wrapping SeaORM's `SelectTwoMany<E, F>` for 1:N joins.
276///
277/// Results are auto-converted to `(T, Vec<U>)` tuples via `From<Model>`.
278pub struct JoinedManyQuery<T, U, E: EntityTrait, F: EntityTrait> {
279    select: sea_orm::SelectTwoMany<E, F>,
280    _phantom: PhantomData<(T, U)>,
281}
282
283impl<T, U, E, F> JoinedManyQuery<T, U, E, F>
284where
285    E: EntityTrait,
286    F: EntityTrait,
287    T: From<E::Model> + Send + Sync,
288    U: From<F::Model> + Send + Sync,
289    E::Model: FromQueryResult + Send + Sync,
290    F::Model: FromQueryResult + Send + Sync,
291{
292    /// Apply a WHERE condition.
293    pub fn filter(self, f: impl IntoCondition) -> Self {
294        Self {
295            select: QueryFilter::filter(self.select, f),
296            _phantom: PhantomData,
297        }
298    }
299
300    /// ORDER BY `col` ASC.
301    pub fn order_by_asc<C: ColumnTrait>(self, col: C) -> Self {
302        Self {
303            select: QueryOrder::order_by_asc(self.select, col),
304            _phantom: PhantomData,
305        }
306    }
307
308    /// ORDER BY `col` DESC.
309    pub fn order_by_desc<C: ColumnTrait>(self, col: C) -> Self {
310        Self {
311            select: QueryOrder::order_by_desc(self.select, col),
312            _phantom: PhantomData,
313        }
314    }
315
316    /// LIMIT `n` rows.
317    pub fn limit(self, n: u64) -> Self {
318        Self {
319            select: QuerySelect::limit(self.select, Some(n)),
320            _phantom: PhantomData,
321        }
322    }
323
324    /// OFFSET `n` rows.
325    pub fn offset(self, n: u64) -> Self {
326        Self {
327            select: QuerySelect::offset(self.select, Some(n)),
328            _phantom: PhantomData,
329        }
330    }
331
332    /// Fetch all matching rows, converting both models to domain types.
333    pub async fn all(self, db: &impl ConnectionTrait) -> Result<Vec<(T, Vec<U>)>, modo::Error> {
334        let rows = self.select.all(db).await.map_err(db_err_to_error)?;
335        Ok(rows
336            .into_iter()
337            .map(|(a, bs)| (T::from(a), bs.into_iter().map(U::from).collect()))
338            .collect())
339    }
340
341    /// Unwrap the inner `SelectTwoMany<E, F>` for advanced SeaORM usage.
342    pub fn into_select(self) -> sea_orm::SelectTwoMany<E, F> {
343        self.select
344    }
345}
346
347// ── EntityUpdateMany ─────────────────────────────────────────────────────────
348
349/// A chainable wrapper around SeaORM's `UpdateMany<E>` that returns
350/// `rows_affected` on execution.
351///
352/// Typically obtained via `Record::update_many()` on an entity struct.
353///
354/// # Example
355///
356/// ```rust,ignore
357/// use sea_orm::sea_query::Expr;
358///
359/// let affected = Todo::update_many()
360///     .filter(todo::Column::Completed.eq(false))
361///     .col_expr(todo::Column::Completed, Expr::value(true))
362///     .exec(&*db)
363///     .await?;
364/// ```
365pub struct EntityUpdateMany<E: EntityTrait> {
366    update: sea_orm::UpdateMany<E>,
367}
368
369impl<E: EntityTrait> EntityUpdateMany<E> {
370    /// Wrap an existing `UpdateMany<E>`.
371    pub fn new(update: sea_orm::UpdateMany<E>) -> Self {
372        Self { update }
373    }
374
375    /// Apply a WHERE condition.
376    pub fn filter(self, f: impl IntoCondition) -> Self {
377        Self {
378            update: QueryFilter::filter(self.update, f),
379        }
380    }
381
382    /// Set a column to a `SimpleExpr` value.
383    ///
384    /// Use `sea_orm::sea_query::Expr::value` for simple literals.
385    pub fn col_expr<C: sea_orm::sea_query::IntoIden>(
386        self,
387        col: C,
388        expr: sea_orm::sea_query::SimpleExpr,
389    ) -> Self {
390        Self {
391            update: self.update.col_expr(col, expr),
392        }
393    }
394
395    /// Execute the update and return the number of rows affected.
396    pub async fn exec(self, db: &impl ConnectionTrait) -> Result<u64, modo::Error> {
397        self.update
398            .exec(db)
399            .await
400            .map(|r| r.rows_affected)
401            .map_err(db_err_to_error)
402    }
403}
404
405// ── EntityDeleteMany ─────────────────────────────────────────────────────────
406
407/// A chainable wrapper around SeaORM's `DeleteMany<E>` that returns
408/// `rows_affected` on execution.
409///
410/// Typically obtained via `Record::delete_many()` on an entity struct.
411/// For soft-delete entities, `delete_many()` performs a soft delete (UPDATE SET
412/// `deleted_at`). Use `force_delete_many()` to permanently remove rows.
413///
414/// # Example
415///
416/// ```rust,ignore
417/// let deleted = Todo::delete_many()
418///     .filter(todo::Column::Completed.eq(true))
419///     .exec(&*db)
420///     .await?;
421/// ```
422pub struct EntityDeleteMany<E: EntityTrait> {
423    delete: sea_orm::DeleteMany<E>,
424}
425
426impl<E: EntityTrait> EntityDeleteMany<E> {
427    /// Wrap an existing `DeleteMany<E>`.
428    pub fn new(delete: sea_orm::DeleteMany<E>) -> Self {
429        Self { delete }
430    }
431
432    /// Apply a WHERE condition.
433    pub fn filter(self, f: impl IntoCondition) -> Self {
434        Self {
435            delete: QueryFilter::filter(self.delete, f),
436        }
437    }
438
439    /// Execute the delete and return the number of rows affected.
440    pub async fn exec(self, db: &impl ConnectionTrait) -> Result<u64, modo::Error> {
441        self.delete
442            .exec(db)
443            .await
444            .map(|r| r.rows_affected)
445            .map_err(db_err_to_error)
446    }
447}