Skip to main content

rws_server/model/
relation.rs

1//! Async relationship helpers: `HasMany`, `HasOne`, `BelongsTo`, and
2//! `BelongsToMany` (18.30.0) through a join table.
3//!
4//! These are explicit-load helpers — no hidden N+1 queries, no lazy loading.
5//! Call `.load(&pool)` when you want the related records. Soft-deleted
6//! related rows (`#[soft_delete]`) are left out. To load a relation for a
7//! whole result set, see [`Relation`](super::Relation) and
8//! [`QueryBuilder::with`](super::QueryBuilder::with).
9
10use std::collections::HashSet;
11use std::marker::PhantomData;
12
13use super::eager::{distinct_keys, key_of, IN_BATCH};
14use super::pool::{DbPool, DbTransaction};
15use super::repository::{live_condition, placeholder};
16use super::{DbError, Model, ToColumn, Value};
17
18// ── HasMany ───────────────────────────────────────────────────────────────────
19
20/// Represents a one-to-many relationship from an owner to a collection of `T`.
21///
22/// The `fk_col` column on the child table holds the owner's PK.
23///
24/// ```ignore
25/// let posts: Vec<Post> = HasMany::new(Value::Int(user.id), "user_id")
26///     .load(&pool).await?;
27/// ```
28#[derive(Debug, Clone)]
29pub struct HasMany<T: Model> {
30    owner_pk: Value,
31    fk_col: &'static str,
32    _phantom: PhantomData<T>,
33}
34
35impl<T: Model> HasMany<T> {
36    pub fn new(owner_pk: Value, fk_col: &'static str) -> Self {
37        HasMany { owner_pk, fk_col, _phantom: PhantomData }
38    }
39
40    /// Load all related records from the database.
41    pub async fn load(&self, pool: &DbPool) -> Result<Vec<T>, DbError> {
42        let sql = format!(
43            "SELECT * FROM {} WHERE {} = {}{}",
44            T::table_name(), self.fk_col, placeholder(pool.backend(), 1), and_live::<T>(),
45        );
46        let rows = pool.query_rows(&sql, &[self.owner_pk.clone()]).await?;
47        rows.iter().map(|r| T::from_row(r)).collect()
48    }
49}
50
51// ── HasOne ────────────────────────────────────────────────────────────────────
52
53/// Represents a one-to-one relationship (owner → child).
54///
55/// The `fk_col` column on the child table holds the owner's PK.
56#[derive(Debug, Clone)]
57pub struct HasOne<O: Model> {
58    owner_pk: Value,
59    fk_col: &'static str,
60    _phantom: PhantomData<O>,
61}
62
63impl<O: Model> HasOne<O> {
64    pub fn new(owner_pk: Value, fk_col: &'static str) -> Self {
65        HasOne { owner_pk, fk_col, _phantom: PhantomData }
66    }
67
68    /// Load the related record from the database.
69    pub async fn load(&self, pool: &DbPool) -> Result<Option<O>, DbError> {
70        let sql = format!(
71            "SELECT * FROM {} WHERE {} = {}{} LIMIT 1",
72            O::table_name(), self.fk_col, placeholder(pool.backend(), 1), and_live::<O>(),
73        );
74        let rows = pool.query_rows(&sql, &[self.owner_pk.clone()]).await?;
75        match rows.into_iter().next() {
76            Some(row) => Ok(Some(O::from_row(&row)?)),
77            None => Ok(None),
78        }
79    }
80}
81
82// ── BelongsTo ─────────────────────────────────────────────────────────────────
83
84/// Represents the inverse side of a relationship (child → owner).
85///
86/// `fk_value` is the foreign key stored on this record pointing to the owner's PK.
87#[derive(Debug, Clone)]
88pub struct BelongsTo<O: Model> {
89    fk_value: Value,
90    _phantom: PhantomData<O>,
91}
92
93impl<O: Model> BelongsTo<O> {
94    pub fn new(fk_value: Value) -> Self {
95        BelongsTo { fk_value, _phantom: PhantomData }
96    }
97
98    /// Load the owner record from the database.
99    pub async fn load(&self, pool: &DbPool) -> Result<Option<O>, DbError> {
100        let sql = format!(
101            "SELECT * FROM {} WHERE {} = {}{} LIMIT 1",
102            O::table_name(), O::primary_key_name(), placeholder(pool.backend(), 1), and_live::<O>(),
103        );
104        let rows = pool.query_rows(&sql, &[self.fk_value.clone()]).await?;
105        match rows.into_iter().next() {
106            Some(row) => Ok(Some(O::from_row(&row)?)),
107            None => Ok(None),
108        }
109    }
110}
111
112fn and_live<T: Model>() -> String {
113    live_condition::<T>().map(|c| format!(" AND {c}")).unwrap_or_default()
114}
115
116// ── BelongsToMany ─────────────────────────────────────────────────────────────
117
118/// Many-to-many through a join table (18.30.0): the rows of `R` one owner
119/// is linked to, and the links themselves — [`attach`](Self::attach),
120/// [`detach`](Self::detach), [`sync`](Self::sync).
121///
122/// The join table holds the owner's key in `owner_key` and `R`'s in
123/// `related_key`; give it a primary key or unique index over both (the
124/// methods never insert a link twice, but two processes attaching at once
125/// could). Other columns need defaults: the methods insert the two keys
126/// only. [`Relation::belongs_to_many`](super::Relation::belongs_to_many)
127/// loads the same links for a whole result set.
128///
129/// ```ignore
130/// impl Post {
131///     pub fn tags(&self) -> BelongsToMany<Tag> {
132///         BelongsToMany::new(Value::Int(self.id), "post_tags", "post_id", "tag_id")
133///     }
134/// }
135/// post.tags().attach(&pool, [1, 2]).await?;
136/// post.tags().sync(&pool, [1, 3]).await?; // detaches 2, attaches 3
137/// let tags: Vec<Tag> = post.tags().load(&pool).await?;
138/// ```
139pub struct BelongsToMany<R: Model> {
140    owner_pk: Value,
141    join_table: &'static str,
142    owner_key: &'static str,
143    related_key: &'static str,
144    _phantom: PhantomData<fn() -> R>,
145}
146
147impl<R: Model> Clone for BelongsToMany<R> {
148    fn clone(&self) -> Self {
149        BelongsToMany { owner_pk: self.owner_pk.clone(), ..*self }
150    }
151}
152
153impl<R: Model> std::fmt::Debug for BelongsToMany<R> {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        f.debug_struct("BelongsToMany")
156            .field("owner_pk", &self.owner_pk)
157            .field("join_table", &self.join_table)
158            .field("owner_key", &self.owner_key)
159            .field("related_key", &self.related_key)
160            .finish()
161    }
162}
163
164/// What [`BelongsToMany::sync`] changed: the related keys it linked and
165/// unlinked.
166#[derive(Debug, Clone, Default, PartialEq)]
167#[non_exhaustive]
168pub struct SyncChanges {
169    pub attached: Vec<Value>,
170    pub detached: Vec<Value>,
171}
172
173/// `values` as their to-column values, `NULL`s and repeats left out.
174fn keys<I>(ids: I) -> Vec<Value>
175where
176    I: IntoIterator,
177    I::Item: ToColumn,
178{
179    distinct_keys(&ids.into_iter().map(|id| id.to_column()).collect::<Vec<_>>())
180}
181
182fn first_column(row: super::ModelRow) -> Option<Value> {
183    row.columns.into_iter().next().map(|(_, v)| v)
184}
185
186impl<R: Model> BelongsToMany<R> {
187    /// The links of the owner with key `owner_pk` in `join_table`.
188    pub fn new(owner_pk: Value, join_table: &'static str, owner_key: &'static str, related_key: &'static str) -> Self {
189        BelongsToMany { owner_pk, join_table, owner_key, related_key, _phantom: PhantomData }
190    }
191
192    /// The linked rows of `R` (soft-deleted ones left out), by primary key.
193    pub async fn load(&self, pool: &DbPool) -> Result<Vec<R>, DbError> {
194        let pk = R::primary_key_name();
195        let live = R::soft_delete_column().map(|c| format!(" AND r.{c} IS NULL")).unwrap_or_default();
196        let sql = format!(
197            "SELECT r.* FROM {} r INNER JOIN {} j ON j.{} = r.{pk} WHERE j.{} = {}{live} ORDER BY r.{pk}",
198            R::table_name(),
199            self.join_table,
200            self.related_key,
201            self.owner_key,
202            placeholder(pool.backend(), 1),
203        );
204        let rows = pool.query_rows(&sql, &[self.owner_pk.clone()]).await?;
205        rows.iter().map(|r| R::from_row(r)).collect()
206    }
207
208    /// The related keys linked to the owner, as the join table holds them.
209    pub async fn ids(&self, pool: &DbPool) -> Result<Vec<Value>, DbError> {
210        let rows = pool.query_rows(&self.ids_sql(pool), &[self.owner_pk.clone()]).await?;
211        Ok(rows.into_iter().filter_map(first_column).collect())
212    }
213
214    fn ids_sql(&self, pool: &DbPool) -> String {
215        format!("SELECT {} FROM {} WHERE {} = {}", self.related_key, self.join_table, self.owner_key, placeholder(pool.backend(), 1))
216    }
217
218    /// Link the owner to each of `ids` it isn't linked to yet, in one
219    /// transaction; answers how many links were added.
220    pub async fn attach<I>(&self, pool: &DbPool, ids: I) -> Result<u64, DbError>
221    where
222        I: IntoIterator,
223        I::Item: ToColumn,
224    {
225        let wanted = keys(ids);
226        if wanted.is_empty() {
227            return Ok(0);
228        }
229        let mut tx = pool.begin().await?;
230        let existing = self.linked(&mut tx, pool).await?.1;
231        let mut added = 0;
232        for id in wanted {
233            if key_of(id.clone()).is_some_and(|k| !existing.contains(&k)) {
234                self.insert_link(&mut tx, pool, id).await?;
235                added += 1;
236            }
237        }
238        tx.commit().await?;
239        Ok(added)
240    }
241
242    /// Unlink the owner from each of `ids`; answers how many links were
243    /// removed.
244    pub async fn detach<I>(&self, pool: &DbPool, ids: I) -> Result<u64, DbError>
245    where
246        I: IntoIterator,
247        I::Item: ToColumn,
248    {
249        let backend = pool.backend();
250        let mut removed = 0;
251        for batch in keys(ids).chunks(IN_BATCH) {
252            let list = (2..=batch.len() + 1).map(|i| placeholder(backend, i)).collect::<Vec<_>>().join(", ");
253            let sql = format!(
254                "DELETE FROM {} WHERE {} = {} AND {} IN ({list})",
255                self.join_table,
256                self.owner_key,
257                placeholder(backend, 1),
258                self.related_key
259            );
260            let mut params = vec![self.owner_pk.clone()];
261            params.extend(batch.iter().cloned());
262            removed += pool.execute(&sql, &params).await?;
263        }
264        Ok(removed)
265    }
266
267    /// Unlink the owner from every row; answers how many links were
268    /// removed.
269    pub async fn detach_all(&self, pool: &DbPool) -> Result<u64, DbError> {
270        let sql = format!("DELETE FROM {} WHERE {} = {}", self.join_table, self.owner_key, placeholder(pool.backend(), 1));
271        pool.execute(&sql, &[self.owner_pk.clone()]).await
272    }
273
274    /// Make `ids` the owner's links, exactly: unlink the others and link
275    /// the missing ones, in one transaction.
276    pub async fn sync<I>(&self, pool: &DbPool, ids: I) -> Result<SyncChanges, DbError>
277    where
278        I: IntoIterator,
279        I::Item: ToColumn,
280    {
281        let wanted = keys(ids);
282        let wanted_keys: HashSet<String> = wanted.iter().filter_map(|v| key_of(v.clone())).collect();
283        let backend = pool.backend();
284        let mut tx = pool.begin().await?;
285        let (existing, existing_keys) = self.linked(&mut tx, pool).await?;
286        let mut changes = SyncChanges::default();
287        for id in existing {
288            if key_of(id.clone()).is_some_and(|k| !wanted_keys.contains(&k)) {
289                let sql = format!(
290                    "DELETE FROM {} WHERE {} = {} AND {} = {}",
291                    self.join_table,
292                    self.owner_key,
293                    placeholder(backend, 1),
294                    self.related_key,
295                    placeholder(backend, 2)
296                );
297                tx.execute(&sql, &[self.owner_pk.clone(), id.clone()]).await?;
298                changes.detached.push(id);
299            }
300        }
301        for id in wanted {
302            if key_of(id.clone()).is_some_and(|k| !existing_keys.contains(&k)) {
303                self.insert_link(&mut tx, pool, id.clone()).await?;
304                changes.attached.push(id);
305            }
306        }
307        tx.commit().await?;
308        Ok(changes)
309    }
310
311    /// The linked keys, and their map keys, read in `tx`.
312    async fn linked(&self, tx: &mut DbTransaction, pool: &DbPool) -> Result<(Vec<Value>, HashSet<String>), DbError> {
313        let rows = tx.query_rows(&self.ids_sql(pool), &[self.owner_pk.clone()]).await?;
314        let values: Vec<Value> = rows.into_iter().filter_map(first_column).collect();
315        let keys = values.iter().filter_map(|v| key_of(v.clone())).collect();
316        Ok((values, keys))
317    }
318
319    async fn insert_link(&self, tx: &mut DbTransaction, pool: &DbPool, id: Value) -> Result<(), DbError> {
320        let backend = pool.backend();
321        let sql = format!(
322            "INSERT INTO {} ({}, {}) VALUES ({}, {})",
323            self.join_table,
324            self.owner_key,
325            self.related_key,
326            placeholder(backend, 1),
327            placeholder(backend, 2)
328        );
329        tx.execute(&sql, &[self.owner_pk.clone(), id]).await?;
330        Ok(())
331    }
332}