Skip to main content

arcature_data/query/
builder.rs

1//! The explicit-ownership typed query builder over SeaORM entities.
2//!
3//! [`Query<E>`] borrows a [`Db`] and wraps a SeaORM `Select<E>`. The golden
4//! path reaches it via the blanket [`QueryModel`] trait: any SeaORM
5//! `Entity` gains `Entity::query(&db)` with **no application-side trait
6//! implementation** — the blanket `impl<E: EntityTrait> QueryModel for E`
7//! covers every entity (PROGRAM.md AP2.1-6: "choose by actual Rust ergonomics").
8//!
9//! # Explicit ownership — no hidden global
10//!
11//! Every terminal method (`.all()`, `.one()`) resolves the query against the
12//! `&Db` the builder already holds. The caller never re-passes `db.orm()` on
13//! each terminal call, and there is no thread-local, task-local, or
14//! request-global pool the framework resolves on the caller's behalf
15//! (AGENTS.md §20; PROGRAM.md AP2.1-6).
16//!
17//! # What this does not own
18//!
19//! `Query<E>` does not reimplement SeaORM's query builder, condition engine,
20//! or relation engine. It delegates to SeaORM's `Select<E>` for filter, order,
21//! limit, offset, and pagination, and to `Entity::find_by_id` /
22//! `find_also_related` for primary-key lookup and eager loading. Raw SeaORM
23//! remains a first-class escape hatch (`db.orm()`).
24
25use arcature_db::Db;
26use arcature_db::sea_orm;
27use arcature_db::sea_orm::sea_query::Order;
28use arcature_db::sea_orm::{QueryFilter, QueryOrder, QuerySelect};
29
30use crate::error::DataError;
31
32use super::paginate::Paginated;
33
34/// A typed query bound to an explicit `&Db`, over a SeaORM entity `E`.
35///
36/// Construct on the golden path with [`QueryModel::query`]:
37///
38/// ```ignore
39/// use arcature_data::QueryModel;
40/// # async fn run(db: &arcature_db::Db) -> Result<(), arcature_data::DataError> {
41/// let posts = post::Entity::query(db)
42///     .filter(post::Column::UserId.eq(7))
43///     .order_by_desc(post::Column::CreatedAt)
44///     .all()
45///     .await?;
46/// # Ok(())
47/// # }
48/// ```
49///
50/// Or construct directly with [`Query::new`]. The builder carries the `&Db`
51/// for the lifetime of the query, so terminal methods need no extra argument.
52pub struct Query<'db, E>
53where
54    E: sea_orm::EntityTrait,
55{
56    pub(crate) db: &'db Db,
57    pub(crate) select: sea_orm::Select<E>,
58}
59
60impl<'db, E> Query<'db, E>
61where
62    E: sea_orm::EntityTrait,
63{
64    /// Construct a query over all rows of `E`, bound to `db`.
65    #[must_use]
66    pub fn new(db: &'db Db) -> Self {
67        Self {
68            db,
69            select: E::find(),
70        }
71    }
72
73    /// Add a filter condition. Accepts anything SeaORM's `Select::filter`
74    /// accepts — typically `Column::X.eq(v)` or a built `Condition`.
75    ///
76    /// ```ignore
77    /// post::Entity::query(db).filter(post::Column::Active.eq(true))
78    /// ```
79    #[must_use]
80    pub fn filter<C>(mut self, condition: C) -> Self
81    where
82        C: sea_orm::sea_query::IntoCondition,
83    {
84        self.select = self.select.filter(condition);
85        self
86    }
87
88    /// Add an `ORDER BY` clause on a column with an explicit direction.
89    #[must_use]
90    pub fn order_by<C>(mut self, column: C, order: Order) -> Self
91    where
92        C: sea_orm::IntoSimpleExpr,
93    {
94        self.select = self.select.order_by(column, order);
95        self
96    }
97
98    /// Add an ascending `ORDER BY` clause on a column.
99    #[must_use]
100    pub fn order_by_asc<C>(mut self, column: C) -> Self
101    where
102        C: sea_orm::IntoSimpleExpr,
103    {
104        self.select = self.select.order_by_asc(column);
105        self
106    }
107
108    /// Add a descending `ORDER BY` clause on a column.
109    #[must_use]
110    pub fn order_by_desc<C>(mut self, column: C) -> Self
111    where
112        C: sea_orm::IntoSimpleExpr,
113    {
114        self.select = self.select.order_by_desc(column);
115        self
116    }
117
118    /// Limit the number of rows returned.
119    #[must_use]
120    pub fn limit(mut self, n: u64) -> Self {
121        self.select = self.select.limit(n);
122        self
123    }
124
125    /// Skip the first `n` rows.
126    #[must_use]
127    pub fn offset(mut self, n: u64) -> Self {
128        self.select = self.select.offset(n);
129        self
130    }
131
132    /// Begin pagination with the given per-page size (must be >= 1). Chain
133    /// `.page(n)` (1-based) and then `.fetch()` to run the count + page query.
134    #[must_use]
135    pub fn paginate(self, per_page: u64) -> Paginated<'db, E> {
136        Paginated::new(self.db, self.select, per_page)
137    }
138
139    /// Execute the query and return all matching rows.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`DataError::Database`] if the underlying SeaORM query fails.
144    pub async fn all(self) -> Result<Vec<E::Model>, DataError> {
145        self.select
146            .all(self.db.orm())
147            .await
148            .map_err(DataError::from)
149    }
150
151    /// Execute the query and return the first matching row, if any.
152    ///
153    /// # Errors
154    ///
155    /// Returns [`DataError::Database`] if the underlying SeaORM query fails.
156    pub async fn one(self) -> Result<Option<E::Model>, DataError> {
157        self.select
158            .one(self.db.orm())
159            .await
160            .map_err(DataError::from)
161    }
162}
163
164/// The golden-path entry point: every SeaORM `Entity` gains `Entity::query(&db)`.
165///
166/// This is a blanket implementation over [`sea_orm::EntityTrait`] — applications
167/// do **not** implement it by hand, and no `#[model]` macro is required
168/// (PROGRAM.md AP2.1-6; the macro is deferred to a later AP2.1 phase). The
169/// trait adds the single `query` associated function; all other ergonomics
170/// live on [`Query<E>`] and the free functions [`crate::find_by_pk`],
171/// [`crate::insert`], [`crate::update`], [`crate::delete`].
172pub trait QueryModel: sea_orm::EntityTrait {
173    /// Start a typed query over this entity, bound to the explicit `db`.
174    #[must_use]
175    fn query(db: &Db) -> Query<'_, Self> {
176        Query::new(db)
177    }
178}
179
180impl<E> QueryModel for E where E: sea_orm::EntityTrait {}