Skip to main content

umbral_core/orm/
reverse_accessor.rs

1//! gaps2 #45 (accessor half) — the zero-declaration instance
2//! reverse-relation accessor: `post.comment_set.all()` as a
3//! generic runtime method available on EVERY model instance, with no
4//! `ReverseSet` field declared on the parent.
5//!
6//! Why a runtime accessor (not a derive-emitted method): Rust
7//! proc-macros can't enumerate a parent's children at the parent's
8//! derive site — the children live in other modules / crates and the
9//! macro only sees the struct it's expanding. So the child type `C` is
10//! named at the CALL site, and the FK column on `C` that points back at
11//! `Self` is discovered at runtime from `C::FIELDS`:
12//!
13//! ```ignore
14//! let kids = parent.reverse::<Comment>()?.fetch().await?;
15//! let recent = parent.reverse::<Comment>()?
16//!     .filter(comment::CREATED.gt(cutoff))
17//!     .order_by(comment::CREATED.desc())
18//!     .fetch().await?;
19//! // Disambiguate when C has more than one FK to this parent:
20//! let via = parent.reverse_via::<Comment>("author")?.fetch().await?;
21//! ```
22//!
23//! The return type is a real chainable [`QuerySet<C>`] — every
24//! `.filter()/.order_by()/.exclude()/.fetch()/.count()/.exists()`
25//! terminal works on it, exactly like `C::objects().filter(...)`. The
26//! discovery + parent-PK read are synchronous and fallible, so the
27//! accessor returns `Result<QuerySet<C>, ReverseError>` up front; the
28//! QuerySet itself stays lazy and awaitable.
29//!
30//! Parent PKs are bound through the same JSON-to-SQL coercion as the
31//! rest of the ORM relation machinery, so i64, String, and UUID PKs all
32//! work.
33
34use sea_query::{Alias, Expr};
35
36use super::Predicate;
37use super::model::{HydrateRelated, Model};
38use super::queryset::{Manager, QuerySet};
39
40/// Why an instance reverse accessor couldn't build its QuerySet. All
41/// variants carry the names involved so the message is actionable
42/// (which models, which columns, which path to take instead).
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum ReverseError {
45    /// `C` declares no `ForeignKey<_>` whose target table is this
46    /// parent's table — there's nothing to reverse from.
47    NoForeignKey {
48        child: &'static str,
49        parent_table: &'static str,
50    },
51    /// `C` declares MORE THAN ONE FK to this parent. The call is
52    /// ambiguous; `reverse_via::<C>("<col>")` picks one explicitly.
53    Ambiguous {
54        child: &'static str,
55        parent_table: &'static str,
56        candidates: Vec<&'static str>,
57    },
58    /// `reverse_via` was given a column that doesn't exist on `C`.
59    UnknownColumn { child: &'static str, column: String },
60    /// `reverse_via` was given a column that exists but isn't a FK to
61    /// this parent's table.
62    NotAForeignKey {
63        child: &'static str,
64        column: String,
65        parent_table: &'static str,
66    },
67    /// This instance's PK could not be read or bound into the child FK
68    /// predicate.
69    NonI64Pk { parent: &'static str },
70}
71
72impl std::fmt::Display for ReverseError {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            ReverseError::NoForeignKey {
76                child,
77                parent_table,
78            } => write!(
79                f,
80                "umbral::orm::reverse: `{child}` has no foreign key to `{parent_table}` \
81                 — there is no reverse relation to follow"
82            ),
83            ReverseError::Ambiguous {
84                child,
85                parent_table,
86                candidates,
87            } => write!(
88                f,
89                "umbral::orm::reverse: `{child}` has multiple foreign keys to `{parent_table}` \
90                 ({}). Disambiguate with `reverse_via::<{child}>(\"<column>\")`",
91                candidates.join(", ")
92            ),
93            ReverseError::UnknownColumn { child, column } => write!(
94                f,
95                "umbral::orm::reverse_via: `{child}` has no column `{column}`"
96            ),
97            ReverseError::NotAForeignKey {
98                child,
99                column,
100                parent_table,
101            } => write!(
102                f,
103                "umbral::orm::reverse_via: column `{column}` on `{child}` is not a foreign key \
104                 to `{parent_table}`"
105            ),
106            ReverseError::NonI64Pk { parent } => write!(
107                f,
108                "umbral::orm::reverse: `{parent}` primary key could not be bound into the \
109                 reverse relation predicate"
110            ),
111        }
112    }
113}
114
115impl std::error::Error for ReverseError {}
116
117/// Generic instance reverse-relation accessors. Blanket-implemented for
118/// every model (anything that is both [`Model`] and [`HydrateRelated`],
119/// which is every `#[derive(Model)]` type), so a fetched instance of
120/// any model gets `.reverse::<C>()` / `.reverse_via::<C>(col)` for free.
121pub trait ReverseRelations: Model + HydrateRelated {
122    /// Build a chainable [`QuerySet<C>`] of the children of type `C`
123    /// whose foreign key points at THIS instance. The FK column is
124    /// discovered from `C::FIELDS` (the field whose `fk_target ==
125    /// Self::TABLE`):
126    ///
127    /// - exactly one → that column is used,
128    /// - zero → [`ReverseError::NoForeignKey`],
129    /// - two or more → [`ReverseError::Ambiguous`] (use [`reverse_via`]).
130    ///
131    /// [`reverse_via`]: ReverseRelations::reverse_via
132    fn reverse<C: Model + HydrateRelated>(&self) -> Result<QuerySet<C>, ReverseError> {
133        let fk_col = discover_single_fk::<Self, C>()?;
134        self.reverse_on::<C>(fk_col)
135    }
136
137    /// Like [`reverse`], but with the FK column on `C` named explicitly
138    /// — the escape hatch for when `C` has more than one FK to this
139    /// parent. The column is validated to exist AND to be an FK to
140    /// `Self::TABLE`.
141    ///
142    /// [`reverse`]: ReverseRelations::reverse
143    fn reverse_via<C: Model + HydrateRelated>(
144        &self,
145        fk_col: &str,
146    ) -> Result<QuerySet<C>, ReverseError> {
147        let spec = C::FIELDS.iter().find(|f| f.name == fk_col).ok_or_else(|| {
148            ReverseError::UnknownColumn {
149                child: C::NAME,
150                column: fk_col.to_string(),
151            }
152        })?;
153        if spec.fk_target != Some(Self::TABLE) {
154            return Err(ReverseError::NotAForeignKey {
155                child: C::NAME,
156                column: fk_col.to_string(),
157                parent_table: Self::TABLE,
158            });
159        }
160        self.reverse_on::<C>(spec.name)
161    }
162
163    /// Shared tail: read this instance's PK and build
164    /// `C::objects().filter(<fk_col> = pk)`.
165    #[doc(hidden)]
166    fn reverse_on<C: Model + HydrateRelated>(
167        &self,
168        fk_col: &'static str,
169    ) -> Result<QuerySet<C>, ReverseError> {
170        let pk = self
171            .pk_as_json()
172            .ok_or(ReverseError::NonI64Pk { parent: Self::NAME })?;
173        let spec = C::FIELDS.iter().find(|f| f.name == fk_col).ok_or_else(|| {
174            ReverseError::UnknownColumn {
175                child: C::NAME,
176                column: fk_col.to_string(),
177            }
178        })?;
179        let parent_pk_ty = Self::FIELDS.iter().find(|f| f.primary_key).map(|f| f.ty);
180        let pk_value =
181            crate::orm::write::json_to_sea_value(spec.ty, &pk, false, fk_col, parent_pk_ty)
182                .map_err(|_| ReverseError::NonI64Pk { parent: Self::NAME })?;
183        // Build the predicate from a runtime column name + the parent
184        // PK. `Predicate::new` is crate-internal, which is exactly why
185        // this accessor lives in umbral-core rather than in a plugin:
186        // turning a runtime column name into a typed `Predicate<C>`
187        // needs the crate-private constructor.
188        let predicate: Predicate<C> = Predicate::new(Expr::col(Alias::new(fk_col)).eq(pk_value));
189        Ok(Manager::<C>::new().filter(predicate))
190    }
191}
192
193impl<T: Model + HydrateRelated> ReverseRelations for T {}
194
195/// Scan `C::FIELDS` for the single FK whose target table is `P::TABLE`.
196/// Zero → `NoForeignKey`; two+ → `Ambiguous` (names every candidate).
197fn discover_single_fk<P: Model, C: Model>() -> Result<&'static str, ReverseError> {
198    let candidates: Vec<&'static str> = C::FIELDS
199        .iter()
200        .filter(|f| f.fk_target == Some(P::TABLE))
201        .map(|f| f.name)
202        .collect();
203    match candidates.len() {
204        1 => Ok(candidates[0]),
205        0 => Err(ReverseError::NoForeignKey {
206            child: C::NAME,
207            parent_table: P::TABLE,
208        }),
209        _ => Err(ReverseError::Ambiguous {
210            child: C::NAME,
211            parent_table: P::TABLE,
212            candidates,
213        }),
214    }
215}