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 {}