Skip to main content

siderite_orm/expr/
related.rs

1//! Fields reached through foreign keys: `Book::author.join(Author::name)`.
2//!
3//! A [`Joined`] handle behaves like a [`Field`](super::Field) of the *root*
4//! model. Using it in a filter or ordering makes the queryset add the needed
5//! `LEFT JOIN`s (deduplicated, aliased `author`, `author__team`, ...).
6
7use super::{Expr, Field, Ident, pk_column};
8use crate::model::Model;
9use crate::plan::SelectExpr;
10use crate::relations::ForeignKey;
11use std::marker::PhantomData;
12
13/// One foreign-key hop: `<parent>.<fk_column> = <table>.<pk_column>`.
14#[derive(Debug, Clone, PartialEq)]
15pub struct RelHop {
16    /// Foreign-key column on the parent side.
17    pub fk_column: Ident,
18    /// Table the foreign key points at.
19    pub table: Ident,
20    /// Primary-key column of that table.
21    pub pk_column: Ident,
22}
23
24impl RelHop {
25    /// Alias of the joined table: the column without its `_id` suffix.
26    pub fn alias_part(&self) -> &str {
27        self.fk_column
28            .strip_suffix("_id")
29            .filter(|s| !s.is_empty())
30            .unwrap_or(&self.fk_column)
31    }
32}
33
34/// A column of a related model, addressed by its foreign-key path.
35#[derive(Debug, Clone, PartialEq)]
36pub struct RelatedColumn {
37    /// Hops from the root model to the model owning `column`.
38    pub path: Vec<RelHop>,
39    /// Column name on the last model of the path.
40    pub column: Ident,
41}
42
43impl RelatedColumn {
44    /// Alias of the join table holding the column (`author__team`).
45    pub fn alias(&self) -> String {
46        path_alias_of(&self.path)
47    }
48
49    /// Result-column name: `author__name`.
50    pub fn output_name(&self) -> String {
51        format!("{}__{}", self.alias(), self.column)
52    }
53}
54
55/// Alias of the table reached by `path`.
56pub fn path_alias_of(path: &[RelHop]) -> String {
57    path.iter()
58        .map(RelHop::alias_part)
59        .collect::<Vec<_>>()
60        .join("__")
61}
62
63/// A foreign-key slot of a model struct: `ForeignKey<T>` or `Option<ForeignKey<T>>`.
64pub trait FkSlot<T: Model> {
65    /// The foreign key, if set.
66    fn fk(&mut self) -> Option<&mut ForeignKey<T>>;
67}
68
69impl<T: Model> FkSlot<T> for ForeignKey<T> {
70    fn fk(&mut self) -> Option<&mut ForeignKey<T>> {
71        Some(self)
72    }
73}
74
75impl<T: Model> FkSlot<T> for Option<ForeignKey<T>> {
76    fn fk(&mut self) -> Option<&mut ForeignKey<T>> {
77        self.as_mut()
78    }
79}
80
81/// Column of related model `T` that can be reached through more hops:
82/// a [`Field`] of `T`, or a [`Joined`] handle already rooted at `T`.
83pub trait Related<T, X> {
84    /// Hops (relative to `T`) and the column at their end.
85    fn into_parts(self) -> (Vec<RelHop>, &'static str);
86}
87
88impl<T, X> Related<T, X> for Field<T, X> {
89    fn into_parts(self) -> (Vec<RelHop>, &'static str) {
90        (Vec::new(), self.name())
91    }
92}
93
94impl<T, X> Related<T, X> for Joined<T, X> {
95    fn into_parts(self) -> (Vec<RelHop>, &'static str) {
96        (self.path, self.column)
97    }
98}
99
100/// A column of a related model, usable like a field of the root model `M`.
101pub struct Joined<M, T> {
102    path: Vec<RelHop>,
103    column: &'static str,
104    _marker: PhantomData<fn() -> (M, T)>,
105}
106
107impl<M, T> Clone for Joined<M, T> {
108    fn clone(&self) -> Self {
109        Self {
110            path: self.path.clone(),
111            column: self.column,
112            _marker: PhantomData,
113        }
114    }
115}
116
117impl<M, T> std::fmt::Debug for Joined<M, T> {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("Joined")
120            .field("path", &self.path)
121            .field("column", &self.column)
122            .finish()
123    }
124}
125
126impl<M, T> Joined<M, T> {
127    fn related_column(self) -> RelatedColumn {
128        RelatedColumn {
129            path: self.path,
130            column: self.column.into(),
131        }
132    }
133
134    /// Project this column in `values(..)` as `author__name`.
135    pub fn select(self) -> SelectExpr {
136        let related = self.related_column();
137        SelectExpr::new(
138            Expr::Related(related.clone()),
139            Some(related.output_name().into()),
140        )
141    }
142}
143
144impl<M, T> From<Joined<M, T>> for Expr {
145    fn from(j: Joined<M, T>) -> Self {
146        Expr::Related(j.related_column())
147    }
148}
149
150impl<M, T> From<Joined<M, T>> for SelectExpr {
151    fn from(j: Joined<M, T>) -> Self {
152        j.select()
153    }
154}
155
156impl<M, S> Field<M, S> {
157    /// Reach `inner`, a field (or further joined field) of the model this
158    /// foreign key points at: `Book::author.join(Author::name)`.
159    pub fn join<T: Model, X>(self, inner: impl Related<T, X>) -> Joined<M, X>
160    where
161        S: FkSlot<T>,
162    {
163        let (mut path, column) = inner.into_parts();
164        path.insert(
165            0,
166            RelHop {
167                fk_column: self.name().into(),
168                table: T::META.table.into(),
169                pk_column: pk_column::<T>(),
170            },
171        );
172        Joined {
173            path,
174            column,
175            _marker: PhantomData,
176        }
177    }
178}