Skip to main content

rainier_database/
relation.rs

1//! Relationships — Eloquent's `hasOne`, `hasMany`, `belongsTo` and
2//! `belongsToMany`, as **values you load** rather than properties that query
3//! themselves.
4//!
5//! ```ignore
6//! impl Post {
7//!     /// The author. Eloquent's `belongsTo(User::class)`.
8//!     pub fn author() -> BelongsTo<Post, User> {
9//!         BelongsTo::new()
10//!     }
11//! }
12//!
13//! let posts = post_repo.all().await?;
14//! let authors = Post::author().load(&posts, &*user_repo).await?;   // ONE query
15//!
16//! for post in &posts {
17//!     println!("{} by {}", post.title, authors.one(post).unwrap().name);
18//! }
19//! ```
20//!
21//! # Why loading is explicit
22//!
23//! Eloquent gives you `$post->author`, and the price is that the same
24//! expression is one query inside a loop and none after `with('author')`. You
25//! cannot tell which by reading it. PHP can do that because `__get` can run a
26//! query; Rust has no such hook, and forging one — a `RefCell`, a handle to the
27//! connection on every model, a blocking call inside `Deref` — would buy the
28//! syntax by making every model carry a database.
29//!
30//! So Rainier keeps the two operations apart. **Declaring** a relationship is a
31//! value; **loading** it is a call that takes the whole slice of parents and
32//! issues one query for all of them. The N+1 problem is not mitigated here, it
33//! is unrepresentable: there is no per-model load to put in a loop.
34//!
35//! # What one query means across backends
36//!
37//! Loading is a `WHERE key IN (…)` against the *other side's own repository*,
38//! never a `JOIN`. That is what lets the two sides live in different databases,
39//! or on different shards, and it is the same strategy Eloquent's eager loading
40//! uses. [`Criteria::join`](crate::Criteria) is still there when both sides are
41//! genuinely in one place and you want the database to do the work.
42
43use std::collections::HashMap;
44use std::marker::PhantomData;
45
46use rainier_orm::sea_query::Value;
47use rainier_support::{str, Result};
48
49use crate::criteria::Criteria;
50use crate::model::Model;
51use crate::repository::Repository;
52
53/// A related-key lookup value.
54///
55/// Keys are normalised to text so that a `u64` primary key and the `u64`
56/// foreign key pointing at it agree, whichever integer width each side's driver
57/// hands back. Two *different* columns are never compared, so the normalisation
58/// cannot conflate unrelated rows.
59#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
60pub struct RelationKey(String);
61
62impl RelationKey {
63    /// The key for a column value.
64    pub fn new(value: &Value) -> Self {
65        Self(normalise(value))
66    }
67
68    /// The key for a value read back out of a row.
69    pub fn from_cell(cell: &crate::row::Cell) -> Self {
70        match cell.to_value() {
71            Some(value) => Self::new(&value),
72            None => Self(null()),
73        }
74    }
75
76    /// The key as text.
77    pub fn as_str(&self) -> &str {
78        &self.0
79    }
80}
81
82impl std::fmt::Display for RelationKey {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.write_str(&self.0)
85    }
86}
87
88/// Normalise a value to its lookup key.
89///
90/// Every integer width collapses to one decimal rendering, so `Int(7)` from one
91/// driver and `BigUnsigned(7)` from another are the same key.
92fn normalise(value: &Value) -> String {
93    macro_rules! int {
94        ($v:expr) => {
95            $v.map(|n| (n as i128).to_string()).unwrap_or_else(null)
96        };
97    }
98
99    match value {
100        Value::TinyInt(v) => int!(*v),
101        Value::SmallInt(v) => int!(*v),
102        Value::Int(v) => int!(*v),
103        Value::BigInt(v) => int!(*v),
104        Value::TinyUnsigned(v) => int!(*v),
105        Value::SmallUnsigned(v) => int!(*v),
106        Value::Unsigned(v) => int!(*v),
107        Value::BigUnsigned(v) => int!(*v),
108        Value::Bool(v) => v.map(|b| b.to_string()).unwrap_or_else(null),
109        Value::String(v) => v.as_ref().map(|s| s.to_string()).unwrap_or_else(null),
110        Value::Char(v) => v.map(|c| c.to_string()).unwrap_or_else(null),
111        Value::Bytes(v) => v.as_ref().map(|b| hex(b)).unwrap_or_else(null),
112        // Floats, dates and anything a driver adds later. Debug is stable
113        // enough for a lookup key, and a float key is a mistake anyway.
114        other => format!("{other:?}"),
115    }
116}
117
118fn null() -> String {
119    "\0null".to_string()
120}
121
122fn hex(bytes: &[u8]) -> String {
123    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
124}
125
126/// Rows loaded for a relationship, grouped by the near side's key.
127///
128/// Returned by every relation's `load`. Look a parent's related rows up by
129/// handing it the parent — the container knows which column to read.
130#[derive(Debug, Clone)]
131pub struct Related<C> {
132    /// The column on the **near** side whose value keys this map: a parent's
133    /// `id` for `has_many`, a child's `author_id` for `belongs_to`.
134    near_key: String,
135    groups: HashMap<RelationKey, Vec<C>>,
136}
137
138impl<C> Related<C> {
139    /// An empty result — what loading no parents gives you.
140    pub fn empty(near_key: impl Into<String>) -> Self {
141        Self { near_key: near_key.into(), groups: HashMap::new() }
142    }
143
144    /// Every related row for `near`, in the order the query returned them.
145    pub fn of<N: Model>(&self, near: &N) -> &[C] {
146        self.key_of(near).and_then(|key| self.groups.get(&key)).map_or(&[], Vec::as_slice)
147    }
148
149    /// The first related row for `near` — what a `has_one` or `belongs_to`
150    /// wants.
151    pub fn one<N: Model>(&self, near: &N) -> Option<&C> {
152        self.of(near).first()
153    }
154
155    /// How many related rows `near` has **among those loaded**.
156    ///
157    /// Not the same as [`Relation::count`], which asks the database and is not
158    /// limited by any `limit` on the relation.
159    pub fn count_of<N: Model>(&self, near: &N) -> usize {
160        self.of(near).len()
161    }
162
163    /// Whether anything was loaded at all.
164    pub fn is_empty(&self) -> bool {
165        self.groups.values().all(Vec::is_empty)
166    }
167
168    /// How many rows were loaded, across every parent.
169    pub fn len(&self) -> usize {
170        self.groups.values().map(Vec::len).sum()
171    }
172
173    /// The column the map is keyed by.
174    pub fn near_key(&self) -> &str {
175        &self.near_key
176    }
177
178    /// Pair each of `near` with its related rows, consuming both.
179    ///
180    /// For handing a controller something to serialise: a `Vec<(Post, Vec<Comment>)>`
181    /// rather than two collections and a lookup.
182    pub fn zip<N: Model>(mut self, near: Vec<N>) -> Vec<(N, Vec<C>)> {
183        near.into_iter()
184            .map(|model| {
185                let related = model
186                    .value_of(&self.near_key)
187                    .and_then(|value| self.groups.remove(&RelationKey::new(&value)))
188                    .unwrap_or_default();
189                (model, related)
190            })
191            .collect()
192    }
193
194    fn key_of<N: Model>(&self, near: &N) -> Option<RelationKey> {
195        near.value_of(&self.near_key).map(|value| RelationKey::new(&value))
196    }
197
198    /// Group rows by the value of `far_key`, to be looked up by `near_key`.
199    fn group_by(near_key: &str, far_key: &str, rows: Vec<C>) -> Self
200    where
201        C: Model,
202    {
203        let mut groups: HashMap<RelationKey, Vec<C>> = HashMap::new();
204        for row in rows {
205            let Some(value) = row.value_of(far_key) else { continue };
206            groups.entry(RelationKey::new(&value)).or_default().push(row);
207        }
208        Self { near_key: near_key.to_string(), groups }
209    }
210}
211
212/// Counts loaded for a relationship — Eloquent's `withCount`.
213#[derive(Debug, Clone, Default)]
214pub struct RelatedCounts {
215    near_key: String,
216    counts: HashMap<RelationKey, u64>,
217}
218
219impl RelatedCounts {
220    /// How many related rows `near` has. `0` when it has none: a parent with
221    /// no children produces no group, not a zero row.
222    pub fn of<N: Model>(&self, near: &N) -> u64 {
223        near.value_of(&self.near_key)
224            .and_then(|value| self.counts.get(&RelationKey::new(&value)).copied())
225            .unwrap_or(0)
226    }
227
228    /// The total across every parent.
229    pub fn total(&self) -> u64 {
230        self.counts.values().sum()
231    }
232}
233
234/// What every relationship can do.
235///
236/// Object-safe on purpose: a function can take `&dyn Relation<Post, Related = Comment>`
237/// and not care which kind it was given.
238#[async_trait::async_trait]
239pub trait Relation<P: Model>: Send + Sync {
240    /// The model on the other side.
241    type Related: Model;
242
243    /// The column on `P` that identifies it in this relationship.
244    fn near_key(&self) -> &str;
245
246    /// The column on the other side that this relationship matches against.
247    fn far_key(&self) -> &str;
248
249    /// Load the related rows for every parent, in **one** query.
250    async fn load(
251        &self,
252        parents: &[P],
253        related: &dyn Repository<Self::Related>,
254    ) -> Result<Related<Self::Related>>;
255
256    /// Count the related rows for every parent, in one query, without
257    /// transferring them.
258    async fn count(
259        &self,
260        parents: &[P],
261        related: &dyn Repository<Self::Related>,
262    ) -> Result<RelatedCounts>;
263
264    /// Load for a single parent.
265    ///
266    /// The one-off case — a show page for one post. Inside a loop this is the
267    /// N+1 that [`load`](Self::load) exists to make impossible, so reach for it
268    /// only when there genuinely is one parent.
269    async fn for_one(
270        &self,
271        parent: &P,
272        related: &dyn Repository<Self::Related>,
273    ) -> Result<Vec<Self::Related>>
274    where
275        Self: Sized,
276    {
277        let loaded = self.load(std::slice::from_ref(parent), related).await?;
278        Ok(loaded.of(parent).to_vec())
279    }
280}
281
282/// The keys of `parents`, deduplicated, in first-seen order.
283fn keys_of<P: Model>(parents: &[P], column: &str) -> Vec<Value> {
284    let mut seen = std::collections::HashSet::new();
285    let mut keys = Vec::with_capacity(parents.len());
286
287    for parent in parents {
288        let Some(value) = parent.value_of(column) else { continue };
289        if seen.insert(RelationKey::new(&value)) {
290            keys.push(value);
291        }
292    }
293    keys
294}
295
296/// The conventional foreign key pointing at `M`: `User` → `user_id`.
297///
298/// Eloquent's convention, and overridable the same way.
299fn conventional_foreign_key<M: Model>() -> String {
300    format!("{}_{}", str::snake(M::model_name()), M::primary_key())
301}
302
303// ---------------------------------------------------------------------------
304// has_many
305// ---------------------------------------------------------------------------
306
307/// One parent, many children — Eloquent's `hasMany`.
308///
309/// ```ignore
310/// // A user has many posts, found by `posts.user_id`.
311/// HasMany::<User, Post>::new()
312/// ```
313pub struct HasMany<P, C> {
314    foreign_key: String,
315    local_key: String,
316    criteria: Criteria,
317    _sides: PhantomData<fn() -> (P, C)>,
318}
319
320impl<P: Model, C: Model> HasMany<P, C> {
321    /// With the conventional keys: `posts.user_id` matched against `users.id`.
322    pub fn new() -> Self {
323        Self {
324            foreign_key: conventional_foreign_key::<P>(),
325            local_key: P::primary_key().to_string(),
326            criteria: Criteria::new(),
327            _sides: PhantomData,
328        }
329    }
330
331    /// The column on the child pointing back at the parent.
332    pub fn foreign_key(mut self, column: impl Into<String>) -> Self {
333        self.foreign_key = column.into();
334        self
335    }
336
337    /// The column on the parent the child points at. Defaults to its key.
338    pub fn local_key(mut self, column: impl Into<String>) -> Self {
339        self.local_key = column.into();
340        self
341    }
342
343    /// Narrow, order or limit the children — a constrained eager load.
344    ///
345    /// A `limit` here limits the **whole** query, not each parent's share: one
346    /// query cannot take the newest three per parent without a window function,
347    /// and pretending otherwise would silently drop rows.
348    pub fn matching(mut self, criteria: Criteria) -> Self {
349        self.criteria = criteria;
350        self
351    }
352}
353
354impl<P: Model, C: Model> Default for HasMany<P, C> {
355    fn default() -> Self {
356        Self::new()
357    }
358}
359
360#[async_trait::async_trait]
361impl<P: Model, C: Model> Relation<P> for HasMany<P, C> {
362    type Related = C;
363
364    fn near_key(&self) -> &str {
365        &self.local_key
366    }
367
368    fn far_key(&self) -> &str {
369        &self.foreign_key
370    }
371
372    async fn load(&self, parents: &[P], related: &dyn Repository<C>) -> Result<Related<C>> {
373        let keys = keys_of(parents, &self.local_key);
374        if keys.is_empty() {
375            return Ok(Related::empty(&self.local_key));
376        }
377
378        let rows = related.matching(self.scoped(keys)).await?;
379        Ok(Related::group_by(&self.local_key, &self.foreign_key, rows))
380    }
381
382    async fn count(&self, parents: &[P], related: &dyn Repository<C>) -> Result<RelatedCounts> {
383        let keys = keys_of(parents, &self.local_key);
384        if keys.is_empty() {
385            return Ok(RelatedCounts::default());
386        }
387
388        let counts = related.count_grouped(&self.foreign_key, self.scoped(keys)).await?;
389        Ok(RelatedCounts { near_key: self.local_key.clone(), counts: counts.into_iter().collect() })
390    }
391}
392
393impl<P: Model, C: Model> HasMany<P, C> {
394    fn scoped(&self, keys: Vec<Value>) -> Criteria {
395        self.criteria.clone().where_in(&self.foreign_key, keys)
396    }
397}
398
399// ---------------------------------------------------------------------------
400// has_one
401// ---------------------------------------------------------------------------
402
403/// One parent, one child — Eloquent's `hasOne`.
404///
405/// The same query as [`HasMany`]; the difference is that you read it with
406/// [`Related::one`]. A second matching row is not an error here, because the
407/// database is where uniqueness belongs: put a unique index on the foreign key
408/// and the question cannot arise.
409pub struct HasOne<P, C>(HasMany<P, C>);
410
411impl<P: Model, C: Model> HasOne<P, C> {
412    /// With the conventional keys.
413    pub fn new() -> Self {
414        Self(HasMany::new())
415    }
416
417    /// The column on the child pointing back at the parent.
418    pub fn foreign_key(mut self, column: impl Into<String>) -> Self {
419        self.0 = self.0.foreign_key(column);
420        self
421    }
422
423    /// The column on the parent the child points at.
424    pub fn local_key(mut self, column: impl Into<String>) -> Self {
425        self.0 = self.0.local_key(column);
426        self
427    }
428
429    /// Narrow or order the children.
430    pub fn matching(mut self, criteria: Criteria) -> Self {
431        self.0 = self.0.matching(criteria);
432        self
433    }
434}
435
436impl<P: Model, C: Model> Default for HasOne<P, C> {
437    fn default() -> Self {
438        Self::new()
439    }
440}
441
442#[async_trait::async_trait]
443impl<P: Model, C: Model> Relation<P> for HasOne<P, C> {
444    type Related = C;
445
446    fn near_key(&self) -> &str {
447        self.0.near_key()
448    }
449
450    fn far_key(&self) -> &str {
451        self.0.far_key()
452    }
453
454    async fn load(&self, parents: &[P], related: &dyn Repository<C>) -> Result<Related<C>> {
455        self.0.load(parents, related).await
456    }
457
458    async fn count(&self, parents: &[P], related: &dyn Repository<C>) -> Result<RelatedCounts> {
459        self.0.count(parents, related).await
460    }
461}
462
463// ---------------------------------------------------------------------------
464// belongs_to
465// ---------------------------------------------------------------------------
466
467/// The inverse — Eloquent's `belongsTo`.
468///
469/// ```ignore
470/// // A post belongs to a user, through `posts.user_id`.
471/// BelongsTo::<Post, User>::new()
472/// ```
473///
474/// The near side is the **child** here, so [`Related::one`] takes the post and
475/// gives you its author.
476pub struct BelongsTo<C, P> {
477    foreign_key: String,
478    owner_key: String,
479    criteria: Criteria,
480    _sides: PhantomData<fn() -> (C, P)>,
481}
482
483impl<C: Model, P: Model> BelongsTo<C, P> {
484    /// With the conventional keys: `posts.user_id` matched against `users.id`.
485    pub fn new() -> Self {
486        Self {
487            foreign_key: conventional_foreign_key::<P>(),
488            owner_key: P::primary_key().to_string(),
489            criteria: Criteria::new(),
490            _sides: PhantomData,
491        }
492    }
493
494    /// The column on this model that points at the owner.
495    pub fn foreign_key(mut self, column: impl Into<String>) -> Self {
496        self.foreign_key = column.into();
497        self
498    }
499
500    /// The column on the owner being pointed at. Defaults to its key.
501    pub fn owner_key(mut self, column: impl Into<String>) -> Self {
502        self.owner_key = column.into();
503        self
504    }
505
506    /// Narrow the owners — rare, but a soft-delete scope belongs here.
507    pub fn matching(mut self, criteria: Criteria) -> Self {
508        self.criteria = criteria;
509        self
510    }
511}
512
513impl<C: Model, P: Model> Default for BelongsTo<C, P> {
514    fn default() -> Self {
515        Self::new()
516    }
517}
518
519#[async_trait::async_trait]
520impl<C: Model, P: Model> Relation<C> for BelongsTo<C, P> {
521    type Related = P;
522
523    fn near_key(&self) -> &str {
524        &self.foreign_key
525    }
526
527    fn far_key(&self) -> &str {
528        &self.owner_key
529    }
530
531    async fn load(&self, children: &[C], related: &dyn Repository<P>) -> Result<Related<P>> {
532        let keys = keys_of(children, &self.foreign_key);
533        if keys.is_empty() {
534            return Ok(Related::empty(&self.foreign_key));
535        }
536
537        let criteria = self.criteria.clone().where_in(&self.owner_key, keys);
538        let rows = related.matching(criteria).await?;
539
540        Ok(Related::group_by(&self.foreign_key, &self.owner_key, rows))
541    }
542
543    async fn count(&self, children: &[C], related: &dyn Repository<P>) -> Result<RelatedCounts> {
544        let keys = keys_of(children, &self.foreign_key);
545        if keys.is_empty() {
546            return Ok(RelatedCounts::default());
547        }
548
549        let criteria = self.criteria.clone().where_in(&self.owner_key, keys);
550        let counts = related.count_grouped(&self.owner_key, criteria).await?;
551
552        Ok(RelatedCounts {
553            near_key: self.foreign_key.clone(),
554            counts: counts.into_iter().collect(),
555        })
556    }
557}
558
559// ---------------------------------------------------------------------------
560// belongs_to_many
561// ---------------------------------------------------------------------------
562
563/// Many to many, through a pivot table — Eloquent's `belongsToMany`.
564///
565/// ```ignore
566/// // posts ↔ tags, through `post_tag(post_id, tag_id)`.
567/// BelongsToMany::<Post, Tag>::new("post_tag")
568/// ```
569///
570/// **Two** queries, not one: the pivot rows, then the related rows. There is no
571/// third for the parents, and no per-parent query — the count does not grow
572/// with the number of parents, which is the property that matters.
573///
574/// The pivot needs no model and no `Entity`. It is read as two columns, which
575/// is all a pivot is; a pivot carrying its own data (`role`, `created_at`) is a
576/// model in its own right, and two `has_many`s through it say so more clearly
577/// than a pivot with attributes.
578pub struct BelongsToMany<P, C> {
579    pivot: String,
580    parent_pivot_key: String,
581    related_pivot_key: String,
582    parent_key: String,
583    related_key: String,
584    criteria: Criteria,
585    _sides: PhantomData<fn() -> (P, C)>,
586}
587
588impl<P: Model, C: Model> BelongsToMany<P, C> {
589    /// Through `pivot`, with the conventional column names —
590    /// `post_id` and `tag_id`.
591    pub fn new(pivot: impl Into<String>) -> Self {
592        Self {
593            pivot: pivot.into(),
594            parent_pivot_key: conventional_foreign_key::<P>(),
595            related_pivot_key: conventional_foreign_key::<C>(),
596            parent_key: P::primary_key().to_string(),
597            related_key: C::primary_key().to_string(),
598            criteria: Criteria::new(),
599            _sides: PhantomData,
600        }
601    }
602
603    /// The pivot's conventional name: the two tables' singulars, in
604    /// alphabetical order — `post_tag`. Eloquent's rule.
605    pub fn conventional_pivot() -> String {
606        let mut sides =
607            [str::snake(P::model_name()), str::snake(C::model_name())].map(|s| str::singular(&s));
608        sides.sort();
609        sides.join("_")
610    }
611
612    /// The pivot column pointing at the parent.
613    pub fn parent_pivot_key(mut self, column: impl Into<String>) -> Self {
614        self.parent_pivot_key = column.into();
615        self
616    }
617
618    /// The pivot column pointing at the related model.
619    pub fn related_pivot_key(mut self, column: impl Into<String>) -> Self {
620        self.related_pivot_key = column.into();
621        self
622    }
623
624    /// The parent column the pivot points at. Defaults to its key.
625    pub fn parent_key(mut self, column: impl Into<String>) -> Self {
626        self.parent_key = column.into();
627        self
628    }
629
630    /// The related column the pivot points at. Defaults to its key.
631    pub fn related_key(mut self, column: impl Into<String>) -> Self {
632        self.related_key = column.into();
633        self
634    }
635
636    /// Narrow or order the related rows.
637    pub fn matching(mut self, criteria: Criteria) -> Self {
638        self.criteria = criteria;
639        self
640    }
641
642    /// The pivot table's name.
643    pub fn pivot(&self) -> &str {
644        &self.pivot
645    }
646}
647
648#[async_trait::async_trait]
649impl<P: Model, C: Model> Relation<P> for BelongsToMany<P, C> {
650    type Related = C;
651
652    fn near_key(&self) -> &str {
653        &self.parent_key
654    }
655
656    fn far_key(&self) -> &str {
657        &self.related_key
658    }
659
660    async fn load(&self, parents: &[P], related: &dyn Repository<C>) -> Result<Related<C>> {
661        let keys = keys_of(parents, &self.parent_key);
662        if keys.is_empty() {
663            return Ok(Related::empty(&self.parent_key));
664        }
665
666        let links = related.pivot_links(self.links_for(keys)).await?;
667        if links.is_empty() {
668            return Ok(Related::empty(&self.parent_key));
669        }
670
671        // The related rows, once, however many parents point at each.
672        let wanted: Vec<Value> = dedup(links.iter().map(|(_, related)| related.clone()));
673        let criteria = self.criteria.clone().where_in(&self.related_key, wanted);
674        let rows = related.matching(criteria).await?;
675
676        // Index them by their own key, then fan out along the pivot: a row
677        // linked from three parents appears under all three.
678        let by_key: HashMap<RelationKey, C> = rows
679            .into_iter()
680            .filter_map(|row| {
681                row.value_of(&self.related_key).map(|key| (RelationKey::new(&key), row))
682            })
683            .collect();
684
685        let mut groups: HashMap<RelationKey, Vec<C>> = HashMap::new();
686        for (parent, related) in &links {
687            if let Some(row) = by_key.get(&RelationKey::new(related)) {
688                groups.entry(RelationKey::new(parent)).or_default().push(row.clone());
689            }
690        }
691
692        Ok(Related { near_key: self.parent_key.clone(), groups })
693    }
694
695    async fn count(&self, parents: &[P], related: &dyn Repository<C>) -> Result<RelatedCounts> {
696        let keys = keys_of(parents, &self.parent_key);
697        if keys.is_empty() {
698            return Ok(RelatedCounts::default());
699        }
700
701        // Counted from the pivot, so no related rows cross the wire. It counts
702        // links rather than rows, which differs only if the pivot has
703        // duplicates or a `matching` filter would have excluded some.
704        let links = related.pivot_links(self.links_for(keys)).await?;
705
706        let mut counts: HashMap<RelationKey, u64> = HashMap::new();
707        for (parent, _) in &links {
708            *counts.entry(RelationKey::new(parent)).or_insert(0) += 1;
709        }
710
711        Ok(RelatedCounts { near_key: self.parent_key.clone(), counts })
712    }
713}
714
715impl<P: Model, C: Model> BelongsToMany<P, C> {
716    fn links_for(&self, keys: Vec<Value>) -> PivotQuery {
717        PivotQuery {
718            table: self.pivot.clone(),
719            parent_column: self.parent_pivot_key.clone(),
720            related_column: self.related_pivot_key.clone(),
721            parent_keys: keys,
722        }
723    }
724}
725
726/// A read of a pivot table: the links from these parents.
727///
728/// A pivot is two columns, so it needs no entity. This is the shape a
729/// repository is asked for them in.
730#[derive(Debug, Clone)]
731pub struct PivotQuery {
732    /// The pivot table.
733    pub table: String,
734    /// Its column pointing at the parent.
735    pub parent_column: String,
736    /// Its column pointing at the related model.
737    pub related_column: String,
738    /// The parent keys to fetch links for.
739    pub parent_keys: Vec<Value>,
740}
741
742fn dedup(values: impl Iterator<Item = Value>) -> Vec<Value> {
743    let mut seen = std::collections::HashSet::new();
744    let mut unique = Vec::new();
745    for value in values {
746        if seen.insert(RelationKey::new(&value)) {
747            unique.push(value);
748        }
749    }
750    unique
751}
752
753#[cfg(test)]
754mod tests {
755    use super::*;
756    use crate::testing::{fake_database, MemoryConnection};
757    use crate::{Dialect, EntityRepository};
758    use rainier_orm::Entity;
759
760    #[derive(Debug, Clone, PartialEq, Entity)]
761    #[orm(table = "users")]
762    struct User {
763        #[orm(pk, auto_increment)]
764        id: u64,
765        name: String,
766    }
767
768    impl Model for User {}
769
770    #[derive(Debug, Clone, PartialEq, Entity)]
771    #[orm(table = "posts")]
772    struct Post {
773        #[orm(pk, auto_increment)]
774        id: u64,
775        title: String,
776        user_id: u64,
777    }
778
779    impl Model for Post {}
780
781    #[derive(Debug, Clone, PartialEq, Entity)]
782    #[orm(table = "tags")]
783    struct Tag {
784        #[orm(pk, auto_increment)]
785        id: u64,
786        name: String,
787    }
788
789    impl Model for Tag {}
790
791    fn user(id: u64) -> User {
792        User { id, name: format!("user-{id}") }
793    }
794
795    fn post(id: u64, user_id: u64) -> Post {
796        Post { id, title: format!("post-{id}"), user_id }
797    }
798
799    #[test]
800    fn the_conventional_keys_come_from_the_model_names() {
801        let has_many = HasMany::<User, Post>::new();
802        assert_eq!(has_many.far_key(), "user_id");
803        assert_eq!(has_many.near_key(), "id");
804
805        let belongs_to = BelongsTo::<Post, User>::new();
806        assert_eq!(belongs_to.near_key(), "user_id", "the column on the post");
807        assert_eq!(belongs_to.far_key(), "id", "the column on the user");
808
809        assert_eq!(BelongsToMany::<Post, Tag>::conventional_pivot(), "post_tag");
810    }
811
812    #[test]
813    fn keys_are_normalised_across_integer_widths() {
814        // The parent's `id: u64` and the child's `user_id: u64` can come back
815        // from different drivers as different widths.
816        assert_eq!(
817            RelationKey::new(&Value::Int(Some(7))),
818            RelationKey::new(&Value::BigUnsigned(Some(7)))
819        );
820        assert_ne!(RelationKey::new(&Value::Int(Some(7))), RelationKey::new(&Value::Int(Some(8))));
821
822        // A NULL foreign key must not join to a NULL primary key.
823        let null_key = RelationKey::new(&Value::BigUnsigned(None));
824        assert_ne!(null_key, RelationKey::new(&Value::BigUnsigned(Some(0))));
825    }
826
827    #[test]
828    fn related_rows_are_grouped_by_the_foreign_key() {
829        let related = Related::group_by("id", "user_id", vec![post(1, 7), post(2, 7), post(3, 8)]);
830
831        assert_eq!(related.of(&user(7)).len(), 2);
832        assert_eq!(related.of(&user(8)).len(), 1);
833        assert_eq!(related.of(&user(9)), &[], "a parent with no children");
834        assert_eq!(related.one(&user(8)).unwrap().id, 3);
835        assert_eq!(related.len(), 3);
836    }
837
838    #[test]
839    fn zipping_pairs_each_parent_with_its_own() {
840        let related = Related::group_by("id", "user_id", vec![post(1, 7), post(2, 8), post(3, 7)]);
841        let pairs = related.zip(vec![user(7), user(8), user(9)]);
842
843        assert_eq!(pairs[0].1.len(), 2);
844        assert_eq!(pairs[1].1.len(), 1);
845        assert!(pairs[2].1.is_empty(), "still present, with nothing");
846    }
847
848    #[test]
849    fn duplicate_parent_keys_are_asked_for_once() {
850        // Ten posts by one author must not put that author in the `IN` clause
851        // ten times.
852        let posts = vec![post(1, 7), post(2, 7), post(3, 8)];
853        assert_eq!(keys_of(&posts, "user_id").len(), 2);
854    }
855
856    #[tokio::test]
857    async fn loading_many_parents_is_one_query() {
858        let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
859        let posts = EntityRepository::<Post>::new(db);
860
861        let users: Vec<User> = (1..=50).map(user).collect();
862        HasMany::<User, Post>::new().load(&users, &posts).await.unwrap();
863
864        assert_eq!(connection.statement_count(), 1, "fifty parents, one query");
865        let sql = connection.last_statement().unwrap();
866        assert!(sql.contains("IN"), "{sql}");
867    }
868
869    #[tokio::test]
870    async fn loading_no_parents_asks_nothing() {
871        let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
872        let posts = EntityRepository::<Post>::new(db);
873
874        let loaded = HasMany::<User, Post>::new().load(&[], &posts).await.unwrap();
875
876        assert_eq!(connection.statement_count(), 0, "an empty IN () is not a query worth running");
877        assert!(loaded.is_empty());
878    }
879
880    #[tokio::test]
881    async fn a_constrained_load_keeps_its_filters() {
882        let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
883        let posts = EntityRepository::<Post>::new(db);
884
885        HasMany::<User, Post>::new()
886            .matching(Criteria::new().order_by_desc("id"))
887            .load(&[user(1)], &posts)
888            .await
889            .unwrap();
890
891        let sql = connection.last_statement().unwrap();
892        assert!(sql.contains("ORDER BY"), "{sql}");
893    }
894
895    #[tokio::test]
896    async fn belongs_to_many_reads_the_pivot_then_the_rows() {
897        let (db, connection) = fake_database(MemoryConnection::new(Dialect::Sqlite));
898        let tags = EntityRepository::<Tag>::new(db);
899
900        BelongsToMany::<Post, Tag>::new("post_tag")
901            .load(&[post(1, 7), post(2, 7)], &tags)
902            .await
903            .unwrap();
904
905        // The pivot came back empty from the fake, so there is nothing to
906        // fetch — the second query is skipped rather than asking for `IN ()`.
907        assert_eq!(connection.statement_count(), 1);
908        assert!(connection.last_statement().unwrap().contains("post_tag"));
909    }
910}