Skip to main content

rws_server/model/
relation.rs

1//! Async relationship helpers: `HasMany`, `HasOne`, `BelongsTo`.
2//!
3//! These are explicit-load helpers — no hidden N+1 queries, no lazy loading.
4//! Call `.load(&pool)` when you want the related records. Soft-deleted
5//! related rows (`#[soft_delete]`) are left out.
6
7use std::marker::PhantomData;
8
9use super::pool::DbPool;
10use super::repository::{live_condition, placeholder};
11use super::{DbError, Model, Value};
12
13// ── HasMany ───────────────────────────────────────────────────────────────────
14
15/// Represents a one-to-many relationship from an owner to a collection of `T`.
16///
17/// The `fk_col` column on the child table holds the owner's PK.
18///
19/// ```ignore
20/// let posts: Vec<Post> = HasMany::new(Value::Int(user.id), "user_id")
21///     .load(&pool).await?;
22/// ```
23#[derive(Debug, Clone)]
24pub struct HasMany<T: Model> {
25    owner_pk: Value,
26    fk_col: &'static str,
27    _phantom: PhantomData<T>,
28}
29
30impl<T: Model> HasMany<T> {
31    pub fn new(owner_pk: Value, fk_col: &'static str) -> Self {
32        HasMany { owner_pk, fk_col, _phantom: PhantomData }
33    }
34
35    /// Load all related records from the database.
36    pub async fn load(&self, pool: &DbPool) -> Result<Vec<T>, DbError> {
37        let sql = format!(
38            "SELECT * FROM {} WHERE {} = {}{}",
39            T::table_name(), self.fk_col, placeholder(pool.backend(), 1), and_live::<T>(),
40        );
41        let rows = pool.query_rows(&sql, &[self.owner_pk.clone()]).await?;
42        rows.iter().map(|r| T::from_row(r)).collect()
43    }
44}
45
46// ── HasOne ────────────────────────────────────────────────────────────────────
47
48/// Represents a one-to-one relationship (owner → child).
49///
50/// The `fk_col` column on the child table holds the owner's PK.
51#[derive(Debug, Clone)]
52pub struct HasOne<O: Model> {
53    owner_pk: Value,
54    fk_col: &'static str,
55    _phantom: PhantomData<O>,
56}
57
58impl<O: Model> HasOne<O> {
59    pub fn new(owner_pk: Value, fk_col: &'static str) -> Self {
60        HasOne { owner_pk, fk_col, _phantom: PhantomData }
61    }
62
63    /// Load the related record from the database.
64    pub async fn load(&self, pool: &DbPool) -> Result<Option<O>, DbError> {
65        let sql = format!(
66            "SELECT * FROM {} WHERE {} = {}{} LIMIT 1",
67            O::table_name(), self.fk_col, placeholder(pool.backend(), 1), and_live::<O>(),
68        );
69        let rows = pool.query_rows(&sql, &[self.owner_pk.clone()]).await?;
70        match rows.into_iter().next() {
71            Some(row) => Ok(Some(O::from_row(&row)?)),
72            None => Ok(None),
73        }
74    }
75}
76
77// ── BelongsTo ─────────────────────────────────────────────────────────────────
78
79/// Represents the inverse side of a relationship (child → owner).
80///
81/// `fk_value` is the foreign key stored on this record pointing to the owner's PK.
82#[derive(Debug, Clone)]
83pub struct BelongsTo<O: Model> {
84    fk_value: Value,
85    _phantom: PhantomData<O>,
86}
87
88impl<O: Model> BelongsTo<O> {
89    pub fn new(fk_value: Value) -> Self {
90        BelongsTo { fk_value, _phantom: PhantomData }
91    }
92
93    /// Load the owner record from the database.
94    pub async fn load(&self, pool: &DbPool) -> Result<Option<O>, DbError> {
95        let sql = format!(
96            "SELECT * FROM {} WHERE {} = {}{} LIMIT 1",
97            O::table_name(), O::primary_key_name(), placeholder(pool.backend(), 1), and_live::<O>(),
98        );
99        let rows = pool.query_rows(&sql, &[self.fk_value.clone()]).await?;
100        match rows.into_iter().next() {
101            Some(row) => Ok(Some(O::from_row(&row)?)),
102            None => Ok(None),
103        }
104    }
105}
106
107fn and_live<T: Model>() -> String {
108    live_condition::<T>().map(|c| format!(" AND {c}")).unwrap_or_default()
109}