Skip to main content

rainier_orm/
repo.rs

1//! Generic CRUD over any [`Entity`], plus a keyset [`Cursor`] for walking
2//! large tables without holding them in memory.
3//!
4//! Each function builds a `sea_query` statement from the entity's metadata,
5//! renders it for the executor's dialect, and runs it. No per-entity SQL is
6//! written anywhere — this module *is* the repository for every derived
7//! struct.
8//!
9//! # Every future here must be `Send`
10//!
11//! `sea_query`'s statement builders hold `Rc<dyn Iden>`, which is `!Send`. A
12//! value that is merely **in scope** across an `.await` is captured by the
13//! generated future — even if it is never read again — so leaving a statement
14//! alive across the executor call makes the whole future `!Send`.
15//!
16//! That is not a cosmetic problem: a `!Send` future cannot be `tokio::spawn`ed,
17//! so a multi-threaded web server could not call any of these from a request
18//! handler.
19//!
20//! So every function here follows the same shape — build and render the
21//! statement inside a block that **ends before the await**, and carry only the
22//! rendered `String` and `Vec<Value>` across it:
23//!
24//! ```ignore
25//! let (route, sql, params) = {
26//!     let mut stmt = Query::select();
27//!     // …
28//!     let (sql, params) = exec.dialect().build_query(&stmt);
29//!     (route, sql, into_vec(params))
30//! };                                    // <- stmt dropped here
31//! let rows = exec.fetch_all_routed(route, &sql, params).await?;
32//! ```
33//!
34//! `tests/send_futures.rs` asserts this at compile time for every public async
35//! API, so a regression fails the build rather than surfacing as a confusing
36//! error in a downstream crate.
37
38use crate::route::route_for;
39use crate::{Entity, Executor, Result, ShardRoute};
40use sea_query::{Alias, Expr, OnConflict, Order, Query, SimpleExpr, Value};
41
42fn col(name: &str) -> Alias {
43    Alias::new(name)
44}
45
46/// The shard route for an entity instance: the first of its `(column, value)`
47/// pairs that names a shard-encoded column. `Global` if none (a global entity,
48/// or a single-database backend ignores it anyway).
49fn route_from_pairs<E: Entity>(pairs: &[(&'static str, Value)]) -> ShardRoute {
50    if E::shard_columns().is_empty() {
51        return ShardRoute::Global;
52    }
53    for (c, v) in pairs {
54        let r = route_for::<E>(c, v);
55        if !matches!(r, ShardRoute::Global) {
56            return r;
57        }
58    }
59    ShardRoute::Global
60}
61
62/// If `E`'s primary key is itself a shard key, is not auto-increment, and is
63/// unset (0), mint a shard-encoded id for it from another shard-key column's
64/// value (the owning `user_id`) via the executor — transparent id assignment
65/// for the sharded data tier. Returns the new id (caller writes it into the pk
66/// pair). `None` for global entities, auto-increment keys, an already-set key,
67/// or an executor that doesn't allocate (single-DB).
68fn assign_shard_pk<E: Entity, X: Executor>(exec: &X, pairs: &[(&str, Value)]) -> Option<u64> {
69    let pk = E::primary_key();
70    if !E::shard_columns().contains(&pk) {
71        return None;
72    }
73    let pk_spec = E::columns().iter().find(|c| c.name == pk)?;
74    if pk_spec.auto_increment {
75        return None;
76    }
77    // pk currently unset?
78    let pk_val = pairs.iter().find(|(c, _)| *c == pk).map(|(_, v)| v)?;
79    if crate::route::routing_key(pk_val) != Some(0) {
80        return None;
81    }
82    // derive the shard from another shard-key column (the user id)
83    let (_, sk_val) = pairs.iter().find(|(c, v)| {
84        *c != pk && E::shard_columns().contains(c) && crate::route::routing_key(v).is_some()
85    })?;
86    let shard_key = crate::route::routing_key(sk_val)?;
87    exec.allocate_id(shard_key)
88}
89
90/// Start a fluent [`Query`](crate::Query) over `E` — `where_eq` / `where_like`
91/// / `where_in` / joins / `first_or_create` / etc. Sugar for
92/// `Query::<E>::new()`.
93pub fn query<E: Entity>() -> crate::Query<E> {
94    crate::Query::new()
95}
96
97/// Insert `entity`. Returns the autogenerated key
98/// ([`last_insert_id`](crate::ExecOutcome::last_insert_id)) when the table has an auto-increment
99/// primary key, else `0`.
100pub async fn insert<E, X>(exec: &X, entity: &E) -> Result<i64>
101where
102    E: Entity,
103    X: Executor,
104{
105    // Everything `sea_query` is built *and dropped* inside this block, so only
106    // the rendered `String` and `Vec<Value>` survive to the await. See the
107    // module docs on why that matters.
108    let (route, sql, params, assigned) = {
109        let mut pairs = entity.insert_values();
110        // Transparent shard-encoded pk: the connector mints it (see `allocate_id`).
111        let assigned = assign_shard_pk::<E, X>(exec, &pairs);
112        if let Some(id) = assigned {
113            let pk = E::primary_key();
114            for (c, v) in pairs.iter_mut() {
115                if *c == pk {
116                    *v = id.into();
117                }
118            }
119        }
120        let route = route_from_pairs::<E>(&pairs);
121        let mut stmt = Query::insert();
122        stmt.into_table(col(E::table()));
123        stmt.columns(pairs.iter().map(|(c, _)| col(c)));
124        stmt.values_panic(pairs.into_iter().map(|(_, v)| SimpleExpr::from(Expr::val(v))));
125
126        let (sql, params) = exec.dialect().build_query(&stmt);
127        (route, sql, into_vec(params), assigned)
128    };
129
130    let outcome = exec.execute_routed(route, &sql, params).await?;
131    // The assigned id is the truth when we minted one (the DB has no
132    // auto-increment to report for an app-assigned key).
133    Ok(assigned.map(|i| i as i64).unwrap_or(outcome.last_insert_id))
134}
135
136/// Insert `entity`, or on a conflict with `conflict_columns` update
137/// `update_columns` to the inserted values (`INSERT … ON CONFLICT … DO UPDATE`
138/// / MySQL `ON DUPLICATE KEY UPDATE`, rendered per dialect by sea-query). An
139/// empty `update_columns` makes it insert-or-ignore (a no-op self-update,
140/// portable across dialects). Returns rows affected.
141pub async fn upsert<E, X>(
142    exec: &X,
143    entity: &E,
144    conflict_columns: &[&str],
145    update_columns: &[&str],
146) -> Result<u64>
147where
148    E: Entity,
149    X: Executor,
150{
151    let (route, sql, params) = {
152        let pairs = entity.insert_values();
153        let route = route_from_pairs::<E>(&pairs);
154        let mut stmt = Query::insert();
155        stmt.into_table(col(E::table()));
156        stmt.columns(pairs.iter().map(|(c, _)| col(c)));
157        stmt.values_panic(pairs.into_iter().map(|(_, v)| SimpleExpr::from(Expr::val(v))));
158
159        let mut on_conflict = OnConflict::columns(conflict_columns.iter().map(|c| col(c)));
160        if !update_columns.is_empty() {
161            on_conflict.update_columns(update_columns.iter().map(|c| col(c)));
162        } else if let Some(c) = conflict_columns.first() {
163            // Insert-or-ignore. MySQL has no portable `DO NOTHING` — sea-query
164            // renders `do_nothing()` as invalid `IGNORE` syntax there — so emit a
165            // no-op self-update on a conflict column instead. `col = col` is valid on
166            // every dialect and leaves the existing row untouched.
167            on_conflict.value(col(c), Expr::col(col(c)));
168        } else {
169            on_conflict.do_nothing();
170        }
171        stmt.on_conflict(on_conflict);
172
173        let (sql, params) = exec.dialect().build_query(&stmt);
174        (route, sql, into_vec(params))
175    };
176
177    Ok(exec.execute_routed(route, &sql, params).await?.rows_affected)
178}
179
180/// Find the row whose primary key equals `pk`, decoded into `E`. `None` if no
181/// row matches.
182pub async fn find_by_pk<E, X, V>(exec: &X, pk: V) -> Result<Option<E>>
183where
184    E: Entity,
185    X: Executor,
186    V: Into<Value>,
187{
188    let (route, sql, params) = {
189        let pk_val: Value = pk.into();
190        let route = route_for::<E>(E::primary_key(), &pk_val);
191        let mut stmt = Query::select();
192        stmt.from(col(E::table()))
193            .columns(E::columns().iter().map(|c| col(c.name)))
194            .and_where(Expr::col(col(E::primary_key())).eq(pk_val))
195            .limit(1);
196
197        let (sql, params) = exec.dialect().build_query(&stmt);
198        (route, sql, into_vec(params))
199    };
200
201    let rows = exec.fetch_all_routed(route, &sql, params).await?;
202    match rows.first() {
203        Some(row) => Ok(Some(E::from_row(row.as_ref())?)),
204        None => Ok(None),
205    }
206}
207
208/// Fetch every row of `E` where `column` equals `value`. This is how you
209/// traverse a relationship — e.g. a user's posts is
210/// `find_by::<Post, _, _>(db, "author_id", user_id)`. There is no generated
211/// navigation method; traversal is an explicit query against the flat FK
212/// column, because the related rows may live in a different backend entirely
213/// (a cross-backend JOIN is impossible) and even within one database this
214/// crate doesn't replace the query planner.
215pub async fn find_by<E, X, V>(exec: &X, column: &str, value: V) -> Result<Vec<E>>
216where
217    E: Entity,
218    X: Executor,
219    V: Into<Value>,
220{
221    let (route, sql, params) = {
222        let val: Value = value.into();
223        let route = route_for::<E>(column, &val);
224        let mut stmt = Query::select();
225        stmt.from(col(E::table()))
226            .columns(E::columns().iter().map(|c| col(c.name)))
227            .and_where(Expr::col(col(column)).eq(val));
228
229        let (sql, params) = exec.dialect().build_query(&stmt);
230        (route, sql, into_vec(params))
231    };
232
233    let rows = exec.fetch_all_routed(route, &sql, params).await?;
234    rows.iter().map(|r| E::from_row(r.as_ref())).collect()
235}
236
237/// Like [`find_by`] but for a column you expect to be unique — returns the
238/// first match, or `None`.
239pub async fn find_one_by<E, X, V>(exec: &X, column: &str, value: V) -> Result<Option<E>>
240where
241    E: Entity,
242    X: Executor,
243    V: Into<Value>,
244{
245    let (route, sql, params) = {
246        let val: Value = value.into();
247        let route = route_for::<E>(column, &val);
248        let mut stmt = Query::select();
249        stmt.from(col(E::table()))
250            .columns(E::columns().iter().map(|c| col(c.name)))
251            .and_where(Expr::col(col(column)).eq(val))
252            .limit(1);
253
254        let (sql, params) = exec.dialect().build_query(&stmt);
255        (route, sql, into_vec(params))
256    };
257
258    let rows = exec.fetch_all_routed(route, &sql, params).await?;
259    match rows.first() {
260        Some(row) => Ok(Some(E::from_row(row.as_ref())?)),
261        None => Ok(None),
262    }
263}
264
265/// Fetch every row of `E`'s table. Convenient for small tables; prefer
266/// [`cursor`] for large ones. Routes `Global`: on a sharded entity this hits
267/// only the global/directory database, so use it for global tables — a
268/// cross-shard scan is an explicit fan-out, not a single `all`.
269pub async fn all<E, X>(exec: &X) -> Result<Vec<E>>
270where
271    E: Entity,
272    X: Executor,
273{
274    let (sql, params) = {
275        let mut stmt = Query::select();
276        stmt.from(col(E::table())).columns(E::columns().iter().map(|c| col(c.name)));
277
278        let (sql, params) = exec.dialect().build_query(&stmt);
279        (sql, into_vec(params))
280    };
281
282    let rows = exec.fetch_all_routed(ShardRoute::Global, &sql, params).await?;
283    rows.iter().map(|r| E::from_row(r.as_ref())).collect()
284}
285
286/// Update every non-primary-key column of `entity`, matched by its primary
287/// key. Returns the number of rows affected (0 or 1 for a unique key).
288pub async fn update<E, X>(exec: &X, entity: &E) -> Result<u64>
289where
290    E: Entity,
291    X: Executor,
292{
293    let (route, sql, params) = {
294        let pk_val = entity.pk_value();
295        let route = route_for::<E>(E::primary_key(), &pk_val);
296        let mut stmt = Query::update();
297        stmt.table(col(E::table()));
298        for (c, v) in entity.update_values() {
299            stmt.value(col(c), v);
300        }
301        stmt.and_where(Expr::col(col(E::primary_key())).eq(pk_val));
302
303        let (sql, params) = exec.dialect().build_query(&stmt);
304        (route, sql, into_vec(params))
305    };
306
307    let outcome = exec.execute_routed(route, &sql, params).await?;
308    Ok(outcome.rows_affected)
309}
310
311/// Delete the row whose primary key equals `pk`. Returns rows affected.
312pub async fn delete_by_pk<E, X, V>(exec: &X, pk: V) -> Result<u64>
313where
314    E: Entity,
315    X: Executor,
316    V: Into<Value>,
317{
318    let (route, sql, params) = {
319        let pk_val: Value = pk.into();
320        let route = route_for::<E>(E::primary_key(), &pk_val);
321        let mut stmt = Query::delete();
322        stmt.from_table(col(E::table())).and_where(Expr::col(col(E::primary_key())).eq(pk_val));
323
324        let (sql, params) = exec.dialect().build_query(&stmt);
325        (route, sql, into_vec(params))
326    };
327
328    let outcome = exec.execute_routed(route, &sql, params).await?;
329    Ok(outcome.rows_affected)
330}
331
332/// Open a [`Cursor`] over `E`'s whole table, walking it in primary-key order
333/// in pages of `page_size`.
334pub fn cursor<E, X>(exec: &X, page_size: u64) -> Cursor<'_, E, X>
335where
336    E: Entity,
337    X: Executor,
338{
339    Cursor::new(exec, page_size)
340}
341
342/// A forward-only keyset cursor for streaming large tables.
343///
344/// Rather than `OFFSET` (which the database must scan past, getting slower
345/// deeper in) or loading the whole table, the cursor remembers the last
346/// primary key it saw and asks for `WHERE pk > ? ORDER BY pk LIMIT page_size`.
347/// Every page is an index seek, so cost stays flat however deep you walk —
348/// and only one page is in memory at a time. This is the only iteration
349/// strategy that is also correct on D1-over-HTTP, which cannot hold a
350/// streaming server-side cursor.
351///
352/// ```ignore
353/// let mut cur = repo::cursor::<User, _>(&exec, 500);
354/// while let Some(page) = cur.next_page().await? {
355///     for user in page { /* ... */ }
356/// }
357/// ```
358pub struct Cursor<'a, E, X> {
359    exec: &'a X,
360    page_size: u64,
361    after: Option<Value>,
362    done: bool,
363    _entity: core::marker::PhantomData<E>,
364}
365
366impl<'a, E, X> Cursor<'a, E, X>
367where
368    E: Entity,
369    X: Executor,
370{
371    fn new(exec: &'a X, page_size: u64) -> Self {
372        Self {
373            exec,
374            page_size: page_size.max(1),
375            after: None,
376            done: false,
377            _entity: core::marker::PhantomData,
378        }
379    }
380
381    /// Resume from a known primary key — the next page starts strictly after
382    /// `pk`. Lets a walk be checkpointed and continued across requests (e.g.
383    /// one D1 HTTP call per invocation).
384    pub fn after(mut self, pk: impl Into<Value>) -> Self {
385        self.after = Some(pk.into());
386        self
387    }
388
389    /// Fetch the next page. Returns `None` once the table is exhausted. A
390    /// final short page (fewer than `page_size` rows) marks the end, so the
391    /// following call returns `None` without a wasted round-trip.
392    pub async fn next_page(&mut self) -> Result<Option<Vec<E>>> {
393        if self.done {
394            return Ok(None);
395        }
396
397        let pk = E::primary_key();
398        let (route, sql, params) = {
399            let mut stmt = Query::select();
400            stmt.from(col(E::table()))
401                .columns(E::columns().iter().map(|c| col(c.name)))
402                .order_by(col(pk), Order::Asc)
403                .limit(self.page_size);
404            if let Some(after) = &self.after {
405                stmt.and_where(Expr::col(col(pk)).gt(after.clone()));
406            }
407
408            // Route by the keyset position when it's a shard-encoded pk (walks one
409            // shard); otherwise `Global` (a single/global table).
410            let route = match &self.after {
411                Some(after) => route_for::<E>(pk, after),
412                None => ShardRoute::Global,
413            };
414            let (sql, params) = self.exec.dialect().build_query(&stmt);
415            (route, sql, into_vec(params))
416        };
417
418        let rows = self.exec.fetch_all_routed(route, &sql, params).await?;
419        if rows.is_empty() {
420            self.done = true;
421            return Ok(None);
422        }
423
424        let page: Vec<E> = rows.iter().map(|r| E::from_row(r.as_ref())).collect::<Result<_>>()?;
425
426        // Advance the keyset to the last row's primary key for the next page.
427        if let Some(last) = rows.last() {
428            self.after = Some(pk_from_row::<E>(last.as_ref())?);
429        }
430        if (rows.len() as u64) < self.page_size {
431            self.done = true;
432        }
433        Ok(Some(page))
434    }
435
436    /// The primary key the next page will start strictly after, if a page has
437    /// been read. Persist this to resume the walk later via [`after`].
438    ///
439    /// [`after`]: Cursor::after
440    pub fn checkpoint(&self) -> Option<&Value> {
441        self.after.as_ref()
442    }
443}
444
445/// Read just the primary-key column out of a row, by its declared
446/// [`ColumnType`](crate::ColumnType), to advance the keyset.
447fn pk_from_row<E: Entity>(row: &dyn crate::Row) -> Result<Value> {
448    use crate::ColumnType;
449    let pk = E::primary_key();
450    let spec = E::columns()
451        .iter()
452        .find(|c| c.name == pk)
453        .ok_or_else(|| crate::Error::msg("primary key column not in metadata"))?;
454    let null = || crate::Error::msg(format!("primary key `{pk}` was NULL"));
455    Ok(match spec.ty {
456        ColumnType::Bool => row.get_bool(pk)?.ok_or_else(null)?.into(),
457        ColumnType::Int => row.get_i32(pk)?.ok_or_else(null)?.into(),
458        ColumnType::BigInt => row.get_i64(pk)?.ok_or_else(null)?.into(),
459        ColumnType::Uint => row.get_u32(pk)?.ok_or_else(null)?.into(),
460        ColumnType::BigUint => row.get_u64(pk)?.ok_or_else(null)?.into(),
461        ColumnType::Double => row.get_f64(pk)?.ok_or_else(null)?.into(),
462        ColumnType::Text => row.get_string(pk)?.ok_or_else(null)?.into(),
463        ColumnType::Binary => row.get_bytes(pk)?.ok_or_else(null)?.into(),
464        ColumnType::Timestamp => row.get_datetime(pk)?.ok_or_else(null)?.into(),
465        ColumnType::Date => row.get_naive_date(pk)?.ok_or_else(null)?.into(),
466    })
467}
468
469/// `sea_query::Values` (newtype over `Vec<Value>`) → the `Vec<Value>` the
470/// executor binds.
471fn into_vec(values: sea_query::Values) -> Vec<Value> {
472    values.0
473}