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