Skip to main content

rainier_orm/
query.rs

1//! A fluent query builder over an [`Entity`] — `where` / `where_not` / `like` /
2//! `in` / null checks, ordering, paging, single-backend joins, and the
3//! terminals `all` / `first` / `count` / `delete` / `first_or_create`.
4//!
5//! It accumulates conditions and renders through the same dialect machinery as
6//! [`crate::repo`], so it runs on every backend and decodes straight into `E`.
7//! For anything past its surface, drop to the re-exported [`sea_query`] — the
8//! builder holds a [`sea_query::Cond`] you can extend via [`Query::filter`].
9//!
10//! ```ignore
11//! use rainier_orm::repo;
12//!
13//! // active authors named like "al%", newest first, first page
14//! let authors: Vec<Author> = repo::query::<Author>()
15//!     .where_eq("active", true)
16//!     .where_like("name", "al%")
17//!     .order_by_desc("created_at")
18//!     .limit(20)
19//!     .all(&db)
20//!     .await?;
21//!
22//! // posts whose author is active (join filters; we still select/decode posts)
23//! let posts: Vec<Post> = repo::query::<Post>()
24//!     .join("authors", "author_id", "id")
25//!     .where_eq("authors.active", true)
26//!     .all(&db)
27//!     .await?;
28//!
29//! // get-or-insert
30//! let tag = repo::query::<Tag>()
31//!     .where_eq("slug", "rust")
32//!     .first_or_create(&db, Tag { id: 0, slug: "rust".into() })
33//!     .await?;
34//! ```
35//!
36//! **Joins are single-backend.** A join targets another table *in the same
37//! executor*, for filtering or selecting the root entity — it never spans
38//! backends (that's impossible) and it does not decode the joined table. To
39//! assemble data across entities (or across backends), query each and compose
40//! in your code; see [`crate::repo::find_by`].
41
42use crate::route::route_for;
43use crate::{repo, Entity, Executor, Result, ShardRoute};
44use core::future::Future;
45use core::marker::PhantomData;
46use sea_query::{
47    Alias, Asterisk, ColumnRef, Cond, Expr, Func, IntoColumnRef, JoinType, Order, Query as SqQuery,
48    SimpleExpr, Value,
49};
50
51/// A builder for reads (and `WHERE`-scoped deletes) over `E`. Conditions are
52/// AND-combined; use [`filter`](Self::filter) to add an OR group.
53pub struct Query<E> {
54    cond: Cond,
55    joins: Vec<(JoinType, String, SimpleExpr)>,
56    orders: Vec<(ColumnRef, Order)>,
57    limit: Option<u64>,
58    offset: Option<u64>,
59    route: ShardRoute,
60    _entity: PhantomData<E>,
61}
62
63impl<E: Entity> Default for Query<E> {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl<E: Entity> Query<E> {
70    /// An unfiltered query over `E`'s table. Prefer [`repo::query`] for brevity.
71    pub fn new() -> Self {
72        Self {
73            cond: Cond::all(),
74            joins: Vec::new(),
75            orders: Vec::new(),
76            limit: None,
77            offset: None,
78            route: ShardRoute::Global,
79            _entity: PhantomData,
80        }
81    }
82
83    fn add(mut self, expr: SimpleExpr) -> Self {
84        let cond = core::mem::replace(&mut self.cond, Cond::all());
85        self.cond = cond.add(expr);
86        self
87    }
88
89    // --- predicates -------------------------------------------------------
90    // A column is `"name"` (qualified to `E`'s table) or `"table.name"` (for a
91    // joined table).
92
93    /// `column = value`. An equality on a shard-encoded column also fixes the
94    /// query's shard route (transparently).
95    pub fn where_eq(mut self, column: &str, value: impl Into<Value>) -> Self {
96        let val: Value = value.into();
97        if matches!(self.route, ShardRoute::Global) {
98            self.route = route_for::<E>(column, &val);
99        }
100        self.add(Expr::col(col_ref::<E>(column)).eq(val))
101    }
102
103    /// `column <> value`.
104    pub fn where_ne(self, column: &str, value: impl Into<Value>) -> Self {
105        self.add(Expr::col(col_ref::<E>(column)).ne(value.into()))
106    }
107
108    /// `column > value`.
109    pub fn where_gt(self, column: &str, value: impl Into<Value>) -> Self {
110        self.add(Expr::col(col_ref::<E>(column)).gt(value.into()))
111    }
112
113    /// `column >= value`.
114    pub fn where_gte(self, column: &str, value: impl Into<Value>) -> Self {
115        self.add(Expr::col(col_ref::<E>(column)).gte(value.into()))
116    }
117
118    /// `column < value`.
119    pub fn where_lt(self, column: &str, value: impl Into<Value>) -> Self {
120        self.add(Expr::col(col_ref::<E>(column)).lt(value.into()))
121    }
122
123    /// `column <= value`.
124    pub fn where_lte(self, column: &str, value: impl Into<Value>) -> Self {
125        self.add(Expr::col(col_ref::<E>(column)).lte(value.into()))
126    }
127
128    /// `column LIKE pattern` (use `%`/`_` wildcards).
129    pub fn where_like(self, column: &str, pattern: &str) -> Self {
130        self.add(Expr::col(col_ref::<E>(column)).like(pattern))
131    }
132
133    /// `column NOT LIKE pattern`.
134    pub fn where_not_like(self, column: &str, pattern: &str) -> Self {
135        self.add(Expr::col(col_ref::<E>(column)).not_like(pattern))
136    }
137
138    /// `column IN (values)`. An empty set matches nothing.
139    pub fn where_in<V: Into<Value>>(
140        self,
141        column: &str,
142        values: impl IntoIterator<Item = V>,
143    ) -> Self {
144        let vals: Vec<Value> = values.into_iter().map(Into::into).collect();
145        self.add(Expr::col(col_ref::<E>(column)).is_in(vals))
146    }
147
148    /// `column NOT IN (values)`.
149    pub fn where_not_in<V: Into<Value>>(
150        self,
151        column: &str,
152        values: impl IntoIterator<Item = V>,
153    ) -> Self {
154        let vals: Vec<Value> = values.into_iter().map(Into::into).collect();
155        self.add(Expr::col(col_ref::<E>(column)).is_not_in(vals))
156    }
157
158    /// `column IS NULL`.
159    pub fn where_null(self, column: &str) -> Self {
160        self.add(Expr::col(col_ref::<E>(column)).is_null())
161    }
162
163    /// `column IS NOT NULL`.
164    pub fn where_not_null(self, column: &str) -> Self {
165        self.add(Expr::col(col_ref::<E>(column)).is_not_null())
166    }
167
168    /// Add a raw [`sea_query::Cond`] (AND-combined with the rest). The escape
169    /// hatch for OR groups and any predicate the helpers don't cover:
170    /// `.filter(Cond::any().add(a).add(b))`.
171    pub fn filter(mut self, cond: Cond) -> Self {
172        let existing = core::mem::replace(&mut self.cond, Cond::all());
173        self.cond = existing.add(cond);
174        self
175    }
176
177    // --- joins (single backend, root-entity selection only) ---------------
178
179    /// `INNER JOIN foreign_table ON E.local_col = foreign_table.foreign_col`.
180    /// For filtering/selecting `E` only — the joined table is not decoded.
181    pub fn join(self, foreign_table: &str, local_col: &str, foreign_col: &str) -> Self {
182        self.join_with(JoinType::InnerJoin, foreign_table, local_col, foreign_col)
183    }
184
185    /// As [`join`](Self::join) but a `LEFT JOIN`.
186    pub fn left_join(self, foreign_table: &str, local_col: &str, foreign_col: &str) -> Self {
187        self.join_with(JoinType::LeftJoin, foreign_table, local_col, foreign_col)
188    }
189
190    fn join_with(
191        mut self,
192        kind: JoinType,
193        foreign_table: &str,
194        local_col: &str,
195        foreign_col: &str,
196    ) -> Self {
197        let on = Expr::col((Alias::new(E::table()), Alias::new(local_col)))
198            .equals((Alias::new(foreign_table.to_owned()), Alias::new(foreign_col.to_owned())));
199        self.joins.push((kind, foreign_table.to_owned(), on));
200        self
201    }
202
203    // --- ordering / paging ------------------------------------------------
204
205    /// `ORDER BY column ASC`.
206    pub fn order_by_asc(mut self, column: &str) -> Self {
207        self.orders.push((col_ref::<E>(column), Order::Asc));
208        self
209    }
210
211    /// `ORDER BY column DESC`.
212    pub fn order_by_desc(mut self, column: &str) -> Self {
213        self.orders.push((col_ref::<E>(column), Order::Desc));
214        self
215    }
216
217    /// `LIMIT n`.
218    pub fn limit(mut self, n: u64) -> Self {
219        self.limit = Some(n);
220        self
221    }
222
223    /// `OFFSET n`.
224    pub fn offset(mut self, n: u64) -> Self {
225        self.offset = Some(n);
226        self
227    }
228
229    // --- rendering --------------------------------------------------------
230    //
231    // Each terminal renders through one of these, which **consume** the
232    // builder. That is what keeps the terminals' futures `Send`: a `Query<E>`
233    // holds `Cond` and `ColumnRef`, both of which contain `Rc`, so a builder
234    // still alive across the executor's `.await` would be captured by the
235    // generated future and make it `!Send` — even though nothing reads it
236    // again. Consuming `self` here drops all of it before the await.
237    //
238    // See `repo`'s module docs for the full argument, and
239    // `tests/send_futures.rs` for the compile-time guard.
240
241    /// Render the `SELECT` this query describes.
242    fn render_select(self, dialect: crate::Dialect) -> (ShardRoute, String, Vec<Value>) {
243        let mut stmt = SqQuery::select();
244        stmt.from(Alias::new(E::table()));
245        for c in E::columns() {
246            stmt.column((Alias::new(E::table()), Alias::new(c.name)));
247        }
248        for (kind, table, on) in &self.joins {
249            stmt.join(*kind, Alias::new(table.clone()), on.clone());
250        }
251        stmt.cond_where(self.cond.clone());
252        for (col, ord) in &self.orders {
253            stmt.order_by(col.clone(), ord.clone());
254        }
255        if let Some(l) = self.limit {
256            stmt.limit(l);
257        }
258        if let Some(o) = self.offset {
259            stmt.offset(o);
260        }
261
262        let (sql, params) = dialect.build_query(&stmt);
263        (self.route, sql, params.0)
264    }
265
266    /// Render the `SELECT COUNT(*)` this query describes.
267    fn render_count(self, dialect: crate::Dialect) -> (ShardRoute, String, Vec<Value>) {
268        let mut stmt = SqQuery::select();
269        stmt.from(Alias::new(E::table()));
270        for (kind, table, on) in &self.joins {
271            stmt.join(*kind, Alias::new(table.clone()), on.clone());
272        }
273        stmt.cond_where(self.cond.clone());
274        stmt.expr_as(Func::count(Expr::col(Asterisk)), Alias::new("cnt"));
275
276        let (sql, params) = dialect.build_query(&stmt);
277        (self.route, sql, params.0)
278    }
279
280    /// Render an `UPDATE` setting `set` under this query's filters.
281    fn render_update(
282        self,
283        dialect: crate::Dialect,
284        set: Vec<(&str, Value)>,
285    ) -> (ShardRoute, String, Vec<Value>) {
286        let mut stmt = SqQuery::update();
287        stmt.table(Alias::new(E::table()));
288        for (c, v) in set {
289            stmt.value(Alias::new(c.to_owned()), v);
290        }
291        stmt.cond_where(self.cond.clone());
292
293        let (sql, params) = dialect.build_query(&stmt);
294        (self.route, sql, params.0)
295    }
296
297    /// Render an atomic `SET column = column + by` under this query's filters.
298    fn render_increment(
299        self,
300        dialect: crate::Dialect,
301        column: &str,
302        by: i64,
303    ) -> (ShardRoute, String, Vec<Value>) {
304        let mut stmt = SqQuery::update();
305        stmt.table(Alias::new(E::table()));
306        stmt.value(Alias::new(column.to_owned()), Expr::col(Alias::new(column.to_owned())).add(by));
307        stmt.cond_where(self.cond.clone());
308
309        let (sql, params) = dialect.build_query(&stmt);
310        (self.route, sql, params.0)
311    }
312
313    /// Render a `DELETE` under this query's filters.
314    fn render_delete(self, dialect: crate::Dialect) -> (ShardRoute, String, Vec<Value>) {
315        let mut stmt = SqQuery::delete();
316        stmt.from_table(Alias::new(E::table()));
317        stmt.cond_where(self.cond.clone());
318
319        let (sql, params) = dialect.build_query(&stmt);
320        (self.route, sql, params.0)
321    }
322
323    // --- terminals --------------------------------------------------------
324    //
325    // These are `fn … -> impl Future`, not `async fn`, and that is load-bearing
326    // rather than stylistic.
327    //
328    // The future returned by an `async fn` captures **every argument**, whether
329    // or not the body moves it out before the first `.await`. `self` is a
330    // `Query<E>`, which holds `Cond` and `ColumnRef` — both containing `Rc` —
331    // so an `async fn` terminal is `!Send` no matter how carefully its body is
332    // written, and could not be awaited in a handler a multi-threaded server
333    // will spawn.
334    //
335    // Consuming `self` *outside* the `async move` block leaves it out of the
336    // future entirely, so only the rendered `String` and `Vec<Value>` are
337    // captured. `tests/send_futures.rs` guards this at compile time.
338
339    /// Run the query and decode every matching row into `E`.
340    pub fn all<'a, X: Executor>(self, exec: &'a X) -> impl Future<Output = Result<Vec<E>>> + 'a
341    where
342        E: 'a,
343    {
344        let (route, sql, params) = self.render_select(exec.dialect());
345        async move {
346            let rows = exec.fetch_all_routed(route, &sql, params).await?;
347            rows.iter().map(|r| E::from_row(r.as_ref())).collect()
348        }
349    }
350
351    /// Run with `LIMIT 1` and decode the first match, if any.
352    pub fn first<'a, X: Executor>(self, exec: &'a X) -> impl Future<Output = Result<Option<E>>> + 'a
353    where
354        E: 'a,
355    {
356        let rows = self.limit(1).all(exec);
357        async move { Ok(rows.await?.into_iter().next()) }
358    }
359
360    /// `SELECT COUNT(*)` under the same filters and joins.
361    pub fn count<'a, X: Executor>(self, exec: &'a X) -> impl Future<Output = Result<u64>> + 'a
362    where
363        E: 'a,
364    {
365        let (route, sql, params) = self.render_count(exec.dialect());
366        async move {
367            let rows = exec.fetch_all_routed(route, &sql, params).await?;
368            let n = match rows.first() {
369                Some(r) => r.get_i64("cnt")?.unwrap_or(0),
370                None => 0,
371            };
372            Ok(n.max(0) as u64)
373        }
374    }
375
376    /// `UPDATE` the given `column = value` pairs on every row matching the
377    /// filters — a *partial* update (only the listed columns), unlike
378    /// [`repo::update`](crate::repo::update()) which writes the whole row.
379    /// Joins are ignored. Returns rows affected.
380    ///
381    /// ```ignore
382    /// repo::query::<Token>()
383    ///     .where_eq("id", id)
384    ///     .update(&db, vec![("last_used_at", now.into())]).await?;
385    /// ```
386    pub fn update<'a, X: Executor>(
387        self,
388        exec: &'a X,
389        set: Vec<(&str, Value)>,
390    ) -> impl Future<Output = Result<u64>> + 'a {
391        let (route, sql, params) = self.render_update(exec.dialect(), set);
392        async move { Ok(exec.execute_routed(route, &sql, params).await?.rows_affected) }
393    }
394
395    /// Atomically add `by` to `column` (`SET column = column + by`) on every
396    /// matching row — a column-relative update no value-binding can express
397    /// (e.g. `attempts = attempts + 1`). Returns rows affected.
398    pub fn increment<'a, X: Executor>(
399        self,
400        exec: &'a X,
401        column: &str,
402        by: i64,
403    ) -> impl Future<Output = Result<u64>> + 'a {
404        let (route, sql, params) = self.render_increment(exec.dialect(), column, by);
405        async move { Ok(exec.execute_routed(route, &sql, params).await?.rows_affected) }
406    }
407
408    /// `DELETE` every row matching the filters. Joins are ignored (portable
409    /// `DELETE` takes no joins); express the condition with `WHERE`/subqueries
410    /// instead. Returns rows affected.
411    pub fn delete<'a, X: Executor>(self, exec: &'a X) -> impl Future<Output = Result<u64>> + 'a {
412        let (route, sql, params) = self.render_delete(exec.dialect());
413        async move { Ok(exec.execute_routed(route, &sql, params).await?.rows_affected) }
414    }
415
416    /// Return the first match, or insert `default` and return it. Not atomic —
417    /// two racing callers can both insert; add a `UNIQUE` constraint on the
418    /// lookup column(s) if you need the database to arbitrate.
419    pub fn first_or_create<'a, X: Executor>(
420        self,
421        exec: &'a X,
422        default: E,
423    ) -> impl Future<Output = Result<E>> + 'a
424    where
425        E: 'a,
426    {
427        // `self` is consumed here, outside the block, so the builder never
428        // enters the future. See the note above the terminals.
429        let existing = self.first(exec);
430
431        async move {
432            if let Some(found) = existing.await? {
433                return Ok(found);
434            }
435            let id = repo::insert(exec, &default).await?;
436            if id > 0 {
437                if let Some(created) = repo::find_by_pk(exec, id).await? {
438                    return Ok(created);
439                }
440            }
441            Ok(default)
442        }
443    }
444}
445
446/// `"name"` → `E.table.name`; `"table.name"` → `table.name`.
447fn col_ref<E: Entity>(spec: &str) -> ColumnRef {
448    if let Some((t, c)) = spec.split_once('.') {
449        (Alias::new(t.to_owned()), Alias::new(c.to_owned())).into_column_ref()
450    } else {
451        (Alias::new(E::table()), Alias::new(spec.to_owned())).into_column_ref()
452    }
453}