Skip to main content

renox_core/db/
relations.rs

1//! Relations as plain, typed calls: load the related rows of a whole page in
2//! one query instead of one per row (no N+1). See docs/relations.md.
3//!
4//! ```
5//! # use renox::prelude::*;
6//! use renox::db::relations::{Pivot, belongs_to, has_many};
7//!
8//! # #[derive(Model, serde::Serialize, Default, Clone)] struct Category { id: i64, name: String }
9//! # #[derive(Model, serde::Serialize, Default, Clone)] struct Product { id: i64, category_id: Option<i64>, name: String }
10//! # #[derive(Model, serde::Serialize, Default, Clone)] struct Review { id: i64, product_id: i64, stars: i64 }
11//! # #[derive(Model, serde::Serialize, Default, Clone)] struct Tag { id: i64, name: String }
12//! const PRODUCT_TAGS: Pivot = Pivot::new("product_tags", "product_id", "tag_id");
13//!
14//! # async fn demo(db: Db) -> Result {
15//! let products = Product::query().order_by("name").limit(20).get(&db).await?;
16//! let categories = belongs_to::<Category, _, _>(&db, &products, |p| p.category_id).await?;
17//! let reviews = has_many(&db, &products, Review::query().latest(), "product_id", |r| r.product_id).await?;
18//! let tags = PRODUCT_TAGS.load_for::<Tag, _>(&db, &products).await?;
19//!
20//! for product in &products {
21//!     let category = product.category_id.and_then(|id| categories.get(&id));
22//!     let reviews = reviews.get(&product.id).map_or(&[][..], Vec::as_slice);
23//!     let tags = tags.get(&product.id).map_or(&[][..], Vec::as_slice);
24//! #   let _ = (category, reviews, tags);
25//! }
26//! PRODUCT_TAGS.sync(&db, products[0].id, [1, 2]).await?; // the first product's tags are now 1 and 2
27//! # Ok(()) }
28//! ```
29
30use std::collections::HashMap;
31use std::marker::PhantomData;
32
33use super::{Db, Executor, Model, ModelKey, Query, ToDbValue, quote, sql};
34use crate::Result;
35
36/// A foreign key's value for a parent keyed by `K`: the key itself (`i64`,
37/// a `Ulid`…), or an `Option` of it for an optional relation.
38pub trait ForeignKey<K>: foreign_key::Sealed<K> {
39    /// The parent's key, or `None` when the relation is empty.
40    fn key(self) -> Option<K>;
41}
42
43mod foreign_key {
44    /// Only a key or an `Option` of one.
45    pub trait Sealed<K> {}
46    impl<K: super::ModelKey> Sealed<K> for K {}
47    impl<K: super::ModelKey> Sealed<K> for Option<K> {}
48}
49
50impl<K: ModelKey> ForeignKey<K> for K {
51    fn key(self) -> Option<K> {
52        Some(self)
53    }
54}
55
56impl<K: ModelKey> ForeignKey<K> for Option<K> {
57    fn key(self) -> Option<K> {
58        self
59    }
60}
61
62/// The parent of each child (`product.category_id` → `Category`), by id, in
63/// one query. Children without a parent, or whose parent is gone, have no
64/// entry.
65//
66// Not an `async fn`: the ids are read before the future is made, so the
67// future holds no closure or borrowed children. Holding a `Fn(&C)` across an
68// `.await` makes the handler's future fail axum's `Send` check (rustc issue
69// #100013), and so do the other loaders below.
70pub fn belongs_to<'a, P: Model, C, K: ForeignKey<P::Key>>(
71    db: &'a Db,
72    children: &[C],
73    foreign_key: impl Fn(&C) -> K,
74) -> impl Future<Output = Result<HashMap<P::Key, P>>> + Send + 'a {
75    let mut ids: Vec<P::Key> = children
76        .iter()
77        .filter_map(|c| foreign_key(c).key())
78        .collect();
79    ids.sort_unstable();
80    ids.dedup();
81    async move {
82        if ids.is_empty() {
83            return Ok(HashMap::new());
84        }
85        Ok(P::find_many(db, ids)
86            .await?
87            .into_iter()
88            .map(|parent| (parent.id(), parent))
89            .collect())
90    }
91}
92
93/// The children of each parent (`order` → its `OrderItem`s), grouped by the
94/// parent's id, in one query. `children` is the query to start from, for
95/// order and filters (`OrderItem::query()` for all); `column` is the
96/// children's foreign key column and `foreign_key` reads it.
97pub fn has_many<'a, C: Model, P: Model, K: ForeignKey<P::Key>>(
98    db: &'a Db,
99    parents: &[P],
100    children: Query<C>,
101    column: &str,
102    foreign_key: impl Fn(&C) -> K + Send + 'a,
103) -> impl Future<Output = Result<HashMap<P::Key, Vec<C>>>> + Send + 'a {
104    let ids: Vec<P::Key> = parents.iter().map(Model::id).collect();
105    let query = (!ids.is_empty()).then(|| children.where_in(column, ids));
106    async move {
107        let mut grouped: HashMap<P::Key, Vec<C>> = HashMap::new();
108        let Some(query) = query else {
109            return Ok(grouped);
110        };
111        let rows = query.get(db).await?;
112        for child in rows {
113            if let Some(parent) = foreign_key(&child).key() {
114                grouped.entry(parent).or_default().push(child);
115            }
116        }
117        Ok(grouped)
118    }
119}
120
121/// The children of each parent reached through a middle model (Laravel's
122/// `hasManyThrough`), in two queries: a country's posts through its users,
123/// a project's deployments through its environments. `through` is the
124/// middle query (`User::query()`) with its column pointing at the parent
125/// (`"country_id"`) read by `through_key`; `children`, `column` and
126/// `foreign_key` are the children's, pointing at the middle model.
127///
128/// ```
129/// # use renox::prelude::*;
130/// use renox::db::relations::has_many_through;
131/// # #[derive(Model, serde::Serialize, Default)] struct Country { id: i64, name: String }
132/// # #[derive(Model, serde::Serialize, Default)] struct Customer { id: i64, country_id: i64 }
133/// # #[derive(Model, serde::Serialize, Default)] struct Order { id: i64, customer_id: i64, total: i64 }
134/// # async fn demo(db: Db, countries: Vec<Country>) -> Result {
135/// let orders = has_many_through(
136///     &db,
137///     &countries,
138///     Customer::query(), "country_id", |c: &Customer| c.country_id,
139///     Order::query().latest(), "customer_id", |o: &Order| o.customer_id,
140/// )
141/// .await?;
142/// let first = orders.get(&countries[0].id).map_or(&[][..], Vec::as_slice);
143/// # let _ = first; Ok(()) }
144/// ```
145#[allow(clippy::too_many_arguments)]
146pub fn has_many_through<'a, C, T, P, KT, KC>(
147    db: &'a Db,
148    parents: &[P],
149    through: Query<T>,
150    through_column: &str,
151    through_key: impl Fn(&T) -> KT + Send + 'a,
152    children: Query<C>,
153    column: &str,
154    foreign_key: impl Fn(&C) -> KC + Send + 'a,
155) -> impl Future<Output = Result<HashMap<P::Key, Vec<C>>>> + Send + 'a
156where
157    C: Model,
158    T: Model,
159    P: Model,
160    KT: ForeignKey<P::Key>,
161    KC: ForeignKey<T::Key>,
162{
163    let ids: Vec<P::Key> = parents.iter().map(Model::id).collect();
164    let middle = (!ids.is_empty()).then(|| through.where_in(through_column, ids));
165    let column = column.to_owned();
166    async move {
167        let mut grouped: HashMap<P::Key, Vec<C>> = HashMap::new();
168        let Some(middle) = middle else {
169            return Ok(grouped);
170        };
171        // Each middle row's parent, by the middle row's key.
172        let parent_of: HashMap<T::Key, P::Key> = middle
173            .get(db)
174            .await?
175            .iter()
176            .filter_map(|row| Some((row.id(), through_key(row).key()?)))
177            .collect();
178        if parent_of.is_empty() {
179            return Ok(grouped);
180        }
181        let middle_ids: Vec<T::Key> = parent_of.keys().cloned().collect();
182        for child in children.where_in(&column, middle_ids).get(db).await? {
183            let parent = foreign_key(&child)
184                .key()
185                .and_then(|middle| parent_of.get(&middle).cloned());
186            if let Some(parent) = parent {
187                grouped.entry(parent).or_default().push(child);
188            }
189        }
190        Ok(grouped)
191    }
192}
193
194/// How many children each parent has (`withCount`), in one `GROUP BY`
195/// query; parents without children get 0. `children` sets the filters.
196///
197/// ```
198/// # use renox::prelude::*;
199/// # use renox::db::relations::{count_many, sum_many};
200/// # #[derive(Model, serde::Serialize, Default)] struct Post { id: i64, title: String }
201/// # #[derive(Model, serde::Serialize, Default)] struct Comment { id: i64, post_id: i64, approved: bool }
202/// # #[derive(Model, serde::Serialize, Default)] struct Order { id: i64, user_id: i64, total: i64 }
203/// # async fn demo(db: Db, posts: Vec<Post>, users: Vec<User>) -> Result {
204/// let comments = count_many(&db, &posts, Comment::where_eq("approved", true), "post_id").await?;
205/// let spent = sum_many::<i64, _, _>(&db, &users, Order::query(), "user_id", "total").await?;
206/// let n = comments[&posts[0].id]; // 0 when a post has none
207/// # let _ = (n, spent); Ok(()) }
208/// ```
209pub fn count_many<'a, C: Model, P: Model>(
210    db: &'a Db,
211    parents: &[P],
212    children: Query<C>,
213    column: &str,
214) -> impl Future<Output = Result<HashMap<P::Key, i64>>> + Send + 'a {
215    grouped(db, parents, children, column, "COUNT(*)".to_owned())
216}
217
218/// The sum of `sum_column` over each parent's children (`withSum`), 0 for
219/// parents without children; `T` is `i64` or `f64`.
220pub fn sum_many<'a, T: super::Number + Default + Send + 'a, C: Model, P: Model>(
221    db: &'a Db,
222    parents: &[P],
223    children: Query<C>,
224    column: &str,
225    sum_column: &str,
226) -> impl Future<Output = Result<HashMap<P::Key, T>>> + Send + 'a {
227    let expression = if C::COLUMNS.contains(&sum_column) {
228        format!(
229            "CAST(COALESCE(SUM({}), 0) AS {})",
230            quote(sum_column),
231            T::SQL_TYPE
232        )
233    } else {
234        // Let the query report the unknown column.
235        format!("SUM({})", quote(sum_column))
236    };
237    let checked = children.check_column(sum_column);
238    grouped(db, parents, checked, column, expression)
239}
240
241fn grouped<'a, T: crate::db::FromDb + Default + Send + 'a, C: Model, P: Model>(
242    db: &'a Db,
243    parents: &[P],
244    children: Query<C>,
245    column: &str,
246    expression: String,
247) -> impl Future<Output = Result<HashMap<P::Key, T>>> + Send + 'a {
248    let ids: Vec<P::Key> = parents.iter().map(Model::id).collect();
249    let column = column.to_owned();
250    async move {
251        let mut totals: HashMap<P::Key, T> =
252            ids.iter().map(|id| (id.clone(), T::default())).collect();
253        if ids.is_empty() {
254            return Ok(totals);
255        }
256        let rows: Vec<(P::Key, T)> = children
257            .where_in(&column, ids)
258            .group_by(&column)
259            .select_as(db, &format!("{}, {expression}", quote(&column)))
260            .await?;
261        totals.extend(rows);
262        Ok(totals)
263    }
264}
265
266/// A many-to-many relation through a pivot table with two id columns, e.g.
267/// `Pivot::new("product_tags", "product_id", "tag_id")`. Give the table a
268/// unique index on both columns. The table may have more columns (a role, a
269/// quantity): set them with [`Pivot::attach_with`] and
270/// [`Pivot::update_pivot`], read them with [`Pivot::load_with_pivot`].
271///
272/// `L` and `R` are the key types of the two sides, `i64` unless given:
273/// `const TAGS: Pivot<Ulid, i64> = Pivot::new("invoice_tags", "invoice_id", "tag_id");`
274pub struct Pivot<L = i64, R = i64> {
275    table: &'static str,
276    left: &'static str,
277    right: &'static str,
278    timestamps: bool,
279    keys: PhantomData<fn() -> (L, R)>,
280}
281
282impl<L, R> Clone for Pivot<L, R> {
283    fn clone(&self) -> Self {
284        *self
285    }
286}
287
288impl<L, R> Copy for Pivot<L, R> {}
289
290impl<L, R> std::fmt::Debug for Pivot<L, R> {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        f.debug_struct("Pivot")
293            .field("table", &self.table)
294            .field("left", &self.left)
295            .field("right", &self.right)
296            .field("timestamps", &self.timestamps)
297            .finish()
298    }
299}
300
301/// Extra pivot columns and their values, e.g. `&[("role", &"admin")]`.
302pub type PivotData<'a> = &'a [(&'a str, &'a (dyn super::ToDbValue + Sync))];
303
304impl<L, R> Pivot<L, R> {
305    /// A pivot `table` joining the `left` key column to the `right` key column.
306    pub const fn new(table: &'static str, left: &'static str, right: &'static str) -> Self {
307        Self {
308            table,
309            left,
310            right,
311            timestamps: false,
312            keys: PhantomData,
313        }
314    }
315
316    /// The pivot table has `created_at` and `updated_at` columns, which
317    /// attaching and [`Pivot::update_pivot`] fill.
318    pub const fn with_timestamps(mut self) -> Self {
319        self.timestamps = true;
320        self
321    }
322
323    /// The same pivot seen from the other side (`tag_id` → `product_id`).
324    pub const fn inverse(self) -> Pivot<R, L> {
325        Pivot {
326            table: self.table,
327            left: self.right,
328            right: self.left,
329            timestamps: self.timestamps,
330            keys: PhantomData,
331        }
332    }
333}
334
335impl<L: ModelKey, R: ModelKey> Pivot<L, R> {
336    /// Links `left` to `right` unless they're linked already; returns
337    /// whether a link was added.
338    async fn insert_link(
339        &self,
340        conn: &mut super::Conn<'_>,
341        left: &L,
342        right: &R,
343        data: PivotData<'_>,
344    ) -> Result<bool> {
345        let mut columns = vec![quote(self.left), quote(self.right)];
346        let mut values = vec![left.to_db_value(), right.to_db_value()];
347        for (column, value) in data {
348            columns.push(quote(column));
349            values.push(value.to_db_value());
350        }
351        if self.timestamps {
352            let at = super::ToDbValue::to_db_value(&super::now());
353            columns.extend([quote("created_at"), quote("updated_at")]);
354            values.extend([at.clone(), at]);
355        }
356        let marks = vec!["?"; columns.len()].join(", ");
357        let added = sql(format!(
358            "INSERT INTO {table} ({columns}) SELECT {marks} \
359             WHERE NOT EXISTS (SELECT 1 FROM {table} WHERE {l} = ? AND {r} = ?)",
360            table = quote(self.table),
361            columns = columns.join(", "),
362            l = quote(self.left),
363            r = quote(self.right),
364        ))
365        .bind_all(values)
366        .bind(left.to_db_value())
367        .bind(right.to_db_value())
368        .execute(conn.reborrow())
369        .await?;
370        Ok(added > 0)
371    }
372
373    /// The right-hand ids linked to `left`.
374    pub async fn ids<'c>(&self, db: impl Executor<'c>, left: L) -> Result<Vec<R>> {
375        Ok(sql(format!(
376            "SELECT {} FROM {} WHERE {} = ? ORDER BY {}",
377            quote(self.right),
378            quote(self.table),
379            quote(self.left),
380            quote(self.right)
381        ))
382        .bind(left)
383        .scalars(db)
384        .await?)
385    }
386
387    /// Links `left` to each of `rights` that isn't linked yet; returns how
388    /// many links were added.
389    pub async fn attach<'c>(
390        &self,
391        db: impl Executor<'c>,
392        left: L,
393        rights: impl IntoIterator<Item = R>,
394    ) -> Result<u64> {
395        let mut conn = db.into_conn();
396        let mut added = 0;
397        for right in rights {
398            added += u64::from(self.insert_link(&mut conn, &left, &right, &[]).await?);
399        }
400        Ok(added)
401    }
402
403    /// Links `left` to `right` with extra pivot columns, unless they're
404    /// linked already (change those with [`Pivot::update_pivot`]); returns
405    /// whether a link was added.
406    ///
407    /// ```
408    /// # use renox::prelude::*;
409    /// # use renox::db::relations::Pivot;
410    /// const MEMBERS: Pivot = Pivot::new("team_user", "team_id", "user_id").with_timestamps();
411    /// # async fn demo(db: Db) -> Result {
412    /// MEMBERS.attach_with(&db, 1, 7, &[("role", &"admin")]).await?;
413    /// MEMBERS.update_pivot(&db, 1, 7, &[("role", &"member")]).await?;
414    /// # Ok(()) }
415    /// ```
416    pub async fn attach_with<'c>(
417        &self,
418        db: impl Executor<'c>,
419        left: L,
420        right: R,
421        data: PivotData<'_>,
422    ) -> Result<bool> {
423        let mut conn = db.into_conn();
424        self.insert_link(&mut conn, &left, &right, data).await
425    }
426
427    /// Changes extra columns of the link between `left` and `right` (and
428    /// `updated_at` with timestamps); returns whether they were linked.
429    pub async fn update_pivot<'c>(
430        &self,
431        db: impl Executor<'c>,
432        left: L,
433        right: R,
434        data: PivotData<'_>,
435    ) -> Result<bool> {
436        let mut sets = Vec::new();
437        let mut values = Vec::new();
438        for (column, value) in data {
439            sets.push(format!("{} = ?", quote(column)));
440            values.push(value.to_db_value());
441        }
442        if self.timestamps {
443            sets.push(format!("{} = ?", quote("updated_at")));
444            values.push(super::ToDbValue::to_db_value(&super::now()));
445        }
446        if sets.is_empty() {
447            return Ok(false);
448        }
449        let changed = sql(format!(
450            "UPDATE {} SET {} WHERE {} = ? AND {} = ?",
451            quote(self.table),
452            sets.join(", "),
453            quote(self.left),
454            quote(self.right)
455        ))
456        .bind_all(values)
457        .bind(left)
458        .bind(right)
459        .execute(db)
460        .await?;
461        Ok(changed > 0)
462    }
463
464    /// Links each of `rights` that isn't linked to `left` and unlinks each
465    /// that is, in one transaction; returns `(attached, detached)`.
466    pub async fn toggle(
467        &self,
468        db: &Db,
469        left: L,
470        rights: impl IntoIterator<Item = R>,
471    ) -> Result<(Vec<R>, Vec<R>)> {
472        let mut tx = db.begin().await?;
473        let current = self.ids(&mut tx, left.clone()).await?;
474        let (detach, attach): (Vec<R>, Vec<R>) =
475            rights.into_iter().partition(|id| current.contains(id));
476        self.detach(&mut tx, left.clone(), detach.iter().cloned())
477            .await?;
478        self.attach(&mut tx, left, attach.iter().cloned()).await?;
479        tx.commit().await?;
480        Ok((attach, detach))
481    }
482
483    /// Unlinks `left` from `rights`; returns how many links were removed.
484    pub async fn detach<'c>(
485        &self,
486        db: impl Executor<'c>,
487        left: L,
488        rights: impl IntoIterator<Item = R>,
489    ) -> Result<u64> {
490        let rights: Vec<R> = rights.into_iter().collect();
491        if rights.is_empty() {
492            return Ok(0);
493        }
494        let marks = vec!["?"; rights.len()].join(", ");
495        Ok(sql(format!(
496            "DELETE FROM {} WHERE {} = ? AND {} IN ({marks})",
497            quote(self.table),
498            quote(self.left),
499            quote(self.right)
500        ))
501        .bind(left)
502        .bind_all(rights.iter().map(ToDbValue::to_db_value))
503        .execute(db)
504        .await?)
505    }
506
507    /// Makes `left` linked to exactly `rights` (e.g. the checked boxes of a
508    /// form), in one transaction.
509    pub async fn sync(&self, db: &Db, left: L, rights: impl IntoIterator<Item = R>) -> Result {
510        let wanted: Vec<R> = rights.into_iter().collect();
511        let mut tx = db.begin().await?;
512        let current = self.ids(&mut tx, left.clone()).await?;
513        let gone: Vec<R> = current
514            .into_iter()
515            .filter(|id| !wanted.contains(id))
516            .collect();
517        self.detach(&mut tx, left.clone(), gone).await?;
518        self.attach(&mut tx, left, wanted).await?;
519        tx.commit().await?;
520        Ok(())
521    }
522
523    /// The right-hand models linked to each left id, in one query per table.
524    pub fn load<'a, T: Model<Key = R> + Clone>(
525        &'a self,
526        db: &'a Db,
527        lefts: impl IntoIterator<Item = L>,
528    ) -> impl Future<Output = Result<HashMap<L, Vec<T>>>> + Send + 'a {
529        let lefts: Vec<L> = lefts.into_iter().collect();
530        self.load_ids(db, lefts)
531    }
532
533    async fn load_ids<T: Model<Key = R> + Clone>(
534        &self,
535        db: &Db,
536        lefts: Vec<L>,
537    ) -> Result<HashMap<L, Vec<T>>> {
538        let mut grouped: HashMap<L, Vec<T>> = HashMap::new();
539        if lefts.is_empty() {
540            return Ok(grouped);
541        }
542        let marks = vec!["?"; lefts.len()].join(", ");
543        let links: Vec<(L, R)> = sql(format!(
544            "SELECT {}, {} FROM {} WHERE {} IN ({marks})",
545            quote(self.left),
546            quote(self.right),
547            quote(self.table),
548            quote(self.left)
549        ))
550        .bind_all(lefts.iter().map(ToDbValue::to_db_value))
551        .fetch_as(db)
552        .await?;
553        let mut rights: Vec<R> = links.iter().map(|(_, right)| right.clone()).collect();
554        rights.sort_unstable();
555        rights.dedup();
556        let models: HashMap<R, T> = T::find_many(db, rights)
557            .await?
558            .into_iter()
559            .map(|model| (model.id(), model))
560            .collect();
561        for (left, right) in links {
562            if let Some(model) = models.get(&right) {
563                grouped.entry(left).or_default().push(model.clone());
564            }
565        }
566        Ok(grouped)
567    }
568
569    /// Like [`Pivot::load`], with each model's pivot row decoded as `D`
570    /// (a `#[derive(FromRow)]` struct naming the pivot columns it wants).
571    ///
572    /// ```
573    /// # use renox::prelude::*;
574    /// # use renox::db::relations::Pivot;
575    /// # const MEMBERS: Pivot = Pivot::new("team_user", "team_id", "user_id");
576    /// #[derive(FromRow)]
577    /// struct Membership { role: String }
578    ///
579    /// # async fn demo(db: Db) -> Result {
580    /// let members = MEMBERS.load_with_pivot::<User, Membership>(&db, [1]).await?;
581    /// for (user, membership) in members.get(&1).map_or(&[][..], Vec::as_slice) {
582    ///     println!("{} is {}", user.name, membership.role);
583    /// }
584    /// # Ok(()) }
585    /// ```
586    pub fn load_with_pivot<'a, T: Model<Key = R> + Clone, D: super::FromRow + Send + 'a>(
587        &'a self,
588        db: &'a Db,
589        lefts: impl IntoIterator<Item = L>,
590    ) -> impl Future<Output = Result<HashMap<L, Vec<(T, D)>>>> + Send + 'a {
591        let lefts: Vec<L> = lefts.into_iter().collect();
592        async move {
593            let mut grouped: HashMap<L, Vec<(T, D)>> = HashMap::new();
594            if lefts.is_empty() {
595                return Ok(grouped);
596            }
597            let marks = vec!["?"; lefts.len()].join(", ");
598            let rows = sql(format!(
599                "SELECT * FROM {} WHERE {} IN ({marks})",
600                quote(self.table),
601                quote(self.left)
602            ))
603            .bind_all(lefts.iter().map(ToDbValue::to_db_value))
604            .fetch_all(db)
605            .await?;
606            let mut links = Vec::with_capacity(rows.len());
607            for row in &rows {
608                let left: L = row.try_get(self.left)?;
609                let right: R = row.try_get(self.right)?;
610                links.push((left, right, D::from_row(row)?));
611            }
612            let mut rights: Vec<R> = links.iter().map(|(_, right, _)| right.clone()).collect();
613            rights.sort_unstable();
614            rights.dedup();
615            let models: HashMap<R, T> = T::find_many(db, rights)
616                .await?
617                .into_iter()
618                .map(|model| (model.id(), model))
619                .collect();
620            for (left, right, data) in links {
621                if let Some(model) = models.get(&right) {
622                    grouped.entry(left).or_default().push((model.clone(), data));
623                }
624            }
625            Ok(grouped)
626        }
627    }
628
629    /// `load` for these parents' ids.
630    pub fn load_for<'a, T: Model<Key = R> + Clone, P: Model<Key = L>>(
631        &'a self,
632        db: &'a Db,
633        parents: &[P],
634    ) -> impl Future<Output = Result<HashMap<L, Vec<T>>>> + Send + 'a {
635        let ids: Vec<L> = parents.iter().map(Model::id).collect();
636        self.load_ids(db, ids)
637    }
638}
639
640/// A polymorphic relation: a child row that belongs to one of several
641/// parent tables, through a type column holding the parent's table name and
642/// an id column, e.g. comments on both posts and videos:
643///
644/// ```
645/// # use renox::prelude::*;
646/// use renox::db::relations::Morph;
647///
648/// # #[derive(Model, serde::Serialize, Default, Clone)] struct Post { id: i64, title: String }
649/// # #[derive(Model, serde::Serialize, Default, Clone)] struct Video { id: i64, title: String }
650/// #[derive(Model, serde::Serialize, Default, Clone)]
651/// struct Comment { id: i64, commentable_type: String, commentable_id: i64, body: String }
652///
653/// const COMMENTABLE: Morph = Morph::new("commentable_type", "commentable_id");
654///
655/// # async fn demo(db: Db, post: Post) -> Result {
656/// Comment::create(&db, Comment {
657///     commentable_type: Post::TABLE.into(), // "posts"
658///     commentable_id: post.id,
659///     body: "Nice".into(),
660///     ..Default::default()
661/// }).await?;
662///
663/// let posts = Post::query().latest().limit(20).get(&db).await?;
664/// let comments = COMMENTABLE.load_many(&db, &posts, Comment::query(), |c| c.commentable_id).await?;
665/// let on_this_post = COMMENTABLE.of(&post, Comment::query()).count(&db).await?;
666///
667/// // The other way: each comment's parent, one query per parent type.
668/// let recent = Comment::query().latest().limit(50).get(&db).await?;
669/// let parent = |c: &Comment| (c.commentable_type.clone(), c.commentable_id);
670/// let post_parents = COMMENTABLE.parents::<Post, _>(&db, &recent, parent).await?;
671/// let video_parents = COMMENTABLE.parents::<Video, _>(&db, &recent, parent).await?;
672/// # let _ = (comments, on_this_post, post_parents, video_parents); Ok(()) }
673/// ```
674///
675/// There's no foreign key to the parents, so deleting a parent doesn't
676/// delete its children: do it in the parent's `deleting` hook
677/// ([`ModelHooks`](super::ModelHooks)) or its delete handler.
678#[derive(Debug, Clone, Copy)]
679pub struct Morph {
680    type_column: &'static str,
681    id_column: &'static str,
682}
683
684impl Morph {
685    /// A polymorphic relation stored in `type_column` (the parent's table) and `id_column`.
686    pub const fn new(type_column: &'static str, id_column: &'static str) -> Self {
687        Self {
688            type_column,
689            id_column,
690        }
691    }
692
693    /// `children` narrowed to those of `parent`.
694    pub fn of<C: Model, P: Model>(&self, parent: &P, children: Query<C>) -> Query<C> {
695        children
696            .where_eq(self.type_column, P::TABLE)
697            .where_eq(self.id_column, parent.id())
698    }
699
700    /// The children of each parent, grouped by the parent's id, in one
701    /// query (`has_many` for a polymorphic relation). `foreign_key` reads
702    /// the id column.
703    pub fn load_many<'a, C: Model, P: Model>(
704        &self,
705        db: &'a Db,
706        parents: &[P],
707        children: Query<C>,
708        foreign_key: impl Fn(&C) -> P::Key + Send + 'a,
709    ) -> impl Future<Output = Result<HashMap<P::Key, Vec<C>>>> + Send + 'a {
710        let children = children.where_eq(self.type_column, P::TABLE);
711        has_many(db, parents, children, self.id_column, foreign_key)
712    }
713
714    /// How many children each parent has, in one `GROUP BY` query (0 for
715    /// parents without any): `withCount` for a polymorphic relation.
716    pub fn count_many<'a, C: Model, P: Model>(
717        &self,
718        db: &'a Db,
719        parents: &[P],
720        children: Query<C>,
721    ) -> impl Future<Output = Result<HashMap<P::Key, i64>>> + Send + 'a {
722        let children = children.where_eq(self.type_column, P::TABLE);
723        count_many(db, parents, children, self.id_column)
724    }
725
726    /// The parents of type `P` of these children, by id, in one query;
727    /// `parent` reads a child's type and id columns. Children of other
728    /// parent types are skipped: call it once per type.
729    pub fn parents<'a, P: Model, C>(
730        &self,
731        db: &'a Db,
732        children: &[C],
733        parent: impl Fn(&C) -> (String, P::Key),
734    ) -> impl Future<Output = Result<HashMap<P::Key, P>>> + Send + 'a {
735        let mut ids: Vec<P::Key> = children
736            .iter()
737            .filter_map(|child| {
738                let (kind, id) = parent(child);
739                (kind == P::TABLE).then_some(id)
740            })
741            .collect();
742        ids.sort_unstable();
743        ids.dedup();
744        async move {
745            if ids.is_empty() {
746                return Ok(HashMap::new());
747            }
748            Ok(P::find_many(db, ids)
749                .await?
750                .into_iter()
751                .map(|parent| (parent.id(), parent))
752                .collect())
753        }
754    }
755}