Skip to main content

toasty_driver_integration_suite/tests/
relation_chain.rs

1//! Chain relation methods on a `Many` handle to traverse multi-step
2//! associations without declaring a `via` relation on the schema.
3//!
4//! `user.todos().category()` produces an `Association` whose path is two
5//! steps long (`User → todos → category`). The query engine lowers this by
6//! unfolding into nested IN-subqueries against the outermost relation.
7
8use crate::prelude::*;
9
10/// Happy path: HasMany → BelongsTo chain returns the distinct set of
11/// categories the user's todos belong to, with no duplicates even when the
12/// user has multiple todos in the same category.
13#[driver_test(id(ID), scenario(crate::scenarios::has_many_multi_relation))]
14pub async fn user_todos_category(test: &mut Test) -> Result<()> {
15    let mut db = setup(test).await;
16
17    let user = toasty::create!(User { name: "Anchovy" })
18        .exec(&mut db)
19        .await?;
20    let other_user = toasty::create!(User { name: "Other" })
21        .exec(&mut db)
22        .await?;
23
24    let food = toasty::create!(Category { name: "Food" })
25        .exec(&mut db)
26        .await?;
27    let drink = toasty::create!(Category { name: "Drink" })
28        .exec(&mut db)
29        .await?;
30    let unused = toasty::create!(Category { name: "Unused" })
31        .exec(&mut db)
32        .await?;
33
34    toasty::create!(Todo::[
35        { title: "salad", user: &user, category: &food },
36        { title: "tea",   user: &user, category: &drink },
37        { title: "sushi", user: &user, category: &food },
38        { title: "wine",  user: &other_user, category: &unused },
39    ])
40    .exec(&mut db)
41    .await?;
42
43    let mut categories = user.todos().category().exec(&mut db).await?;
44    categories.sort_by_key(|c| c.name.clone());
45
46    let ids: Vec<_> = categories.iter().map(|c| c.id).collect();
47    assert_unique!(ids);
48    assert_eq!(categories.len(), 2);
49    assert_eq!(categories[0].id, drink.id);
50    assert_eq!(categories[1].id, food.id);
51    Ok(())
52}
53
54/// Empty source: a user with no todos produces an empty chain result.
55#[driver_test(id(ID), scenario(crate::scenarios::has_many_multi_relation))]
56pub async fn chain_from_empty_source_is_empty(test: &mut Test) -> Result<()> {
57    let mut db = setup(test).await;
58
59    let user = toasty::create!(User { name: "Lonely" })
60        .exec(&mut db)
61        .await?;
62
63    // Another user with todos in some category, to ensure the data is non-empty
64    // overall but isolated from `user`.
65    let other = toasty::create!(User { name: "Busy" }).exec(&mut db).await?;
66    let food = toasty::create!(Category { name: "Food" })
67        .exec(&mut db)
68        .await?;
69    toasty::create!(Todo {
70        title: "salad",
71        user: &other,
72        category: &food
73    })
74    .exec(&mut db)
75    .await?;
76
77    let categories = user.todos().category().exec(&mut db).await?;
78    assert!(categories.is_empty());
79    Ok(())
80}
81
82/// Many todos sharing a single category yield exactly one category row in the
83/// chain result — IN dedupes against the outermost relation.
84#[driver_test(id(ID), scenario(crate::scenarios::has_many_multi_relation))]
85pub async fn chain_dedupes_when_todos_share_category(test: &mut Test) -> Result<()> {
86    let mut db = setup(test).await;
87
88    let user = toasty::create!(User { name: "Cooky" })
89        .exec(&mut db)
90        .await?;
91    let food = toasty::create!(Category { name: "Food" })
92        .exec(&mut db)
93        .await?;
94
95    for i in 0..5 {
96        let title = format!("todo {i}");
97        toasty::create!(Todo {
98            title,
99            user: &user,
100            category: &food
101        })
102        .exec(&mut db)
103        .await?;
104    }
105
106    let categories = user.todos().category().exec(&mut db).await?;
107    assert_eq!(categories.len(), 1);
108    assert_eq!(categories[0].id, food.id);
109    Ok(())
110}
111
112/// The chain respects the starting source: each user's chain returns only the
113/// categories their own todos belong to, even when the data sets overlap.
114#[driver_test(id(ID), scenario(crate::scenarios::has_many_multi_relation))]
115pub async fn chain_scopes_per_starting_user(test: &mut Test) -> Result<()> {
116    let mut db = setup(test).await;
117
118    let alice = toasty::create!(User { name: "Alice" })
119        .exec(&mut db)
120        .await?;
121    let bob = toasty::create!(User { name: "Bob" }).exec(&mut db).await?;
122
123    let a = toasty::create!(Category { name: "A" })
124        .exec(&mut db)
125        .await?;
126    let b = toasty::create!(Category { name: "B" })
127        .exec(&mut db)
128        .await?;
129    let c = toasty::create!(Category { name: "C" })
130        .exec(&mut db)
131        .await?;
132
133    toasty::create!(Todo::[
134        { title: "a1", user: &alice, category: &a },
135        { title: "a2", user: &alice, category: &b },
136        { title: "b1", user: &bob, category: &b },
137        { title: "b2", user: &bob, category: &c },
138    ])
139    .exec(&mut db)
140    .await?;
141
142    let mut alice_cats = alice.todos().category().exec(&mut db).await?;
143    alice_cats.sort_by_key(|c| c.name.clone());
144    let alice_ids: Vec<_> = alice_cats.iter().map(|c| c.id).collect();
145    assert_eq!(alice_ids, vec![a.id, b.id]);
146
147    let mut bob_cats = bob.todos().category().exec(&mut db).await?;
148    bob_cats.sort_by_key(|c| c.name.clone());
149    let bob_ids: Vec<_> = bob_cats.iter().map(|c| c.id).collect();
150    assert_eq!(bob_ids, vec![b.id, c.id]);
151    Ok(())
152}
153
154/// `Many::filter(expr)` after a chain applies a filter to the final
155/// relation. The result is the chain's category set narrowed by the filter.
156#[driver_test(id(ID), scenario(crate::scenarios::has_many_multi_relation))]
157pub async fn chain_then_filter(test: &mut Test) -> Result<()> {
158    let mut db = setup(test).await;
159
160    let user = toasty::create!(User { name: "Filty" })
161        .exec(&mut db)
162        .await?;
163    let food = toasty::create!(Category { name: "Food" })
164        .exec(&mut db)
165        .await?;
166    let drink = toasty::create!(Category { name: "Drink" })
167        .exec(&mut db)
168        .await?;
169
170    toasty::create!(Todo::[
171        { title: "salad", user: &user, category: &food },
172        { title: "tea",   user: &user, category: &drink },
173    ])
174    .exec(&mut db)
175    .await?;
176
177    let only_food = user
178        .todos()
179        .category()
180        .filter(Category::fields().name().eq("Food"))
181        .exec(&mut db)
182        .await?;
183    assert_eq!(only_food.len(), 1);
184    assert_eq!(only_food[0].id, food.id);
185    Ok(())
186}
187
188/// Two HasMany hops in succession (`Author → posts → comments`). The lowering
189/// unfolds into nested IN-subqueries on each `BelongsTo` pair.
190#[driver_test(id(ID), scenario(crate::scenarios::user_post_comment))]
191pub async fn has_many_through_has_many(test: &mut Test) -> Result<()> {
192    let mut db = setup(test).await;
193
194    let alice = toasty::create!(User { name: "Alice" })
195        .exec(&mut db)
196        .await?;
197    let bob = toasty::create!(User { name: "Bob" }).exec(&mut db).await?;
198
199    let p1 = toasty::create!(Post {
200        title: "p1",
201        user: &alice
202    })
203    .exec(&mut db)
204    .await?;
205    let p2 = toasty::create!(Post {
206        title: "p2",
207        user: &alice
208    })
209    .exec(&mut db)
210    .await?;
211    let p3 = toasty::create!(Post {
212        title: "p3",
213        user: &bob
214    })
215    .exec(&mut db)
216    .await?;
217
218    toasty::create!(Comment::[
219        { body: "c1", post: &p1 },
220        { body: "c2", post: &p1 },
221        { body: "c3", post: &p2 },
222        { body: "c4", post: &p3 },
223    ])
224    .exec(&mut db)
225    .await?;
226
227    let mut alice_comments = alice.posts().comments().exec(&mut db).await?;
228    alice_comments.sort_by_key(|c| c.body.clone());
229    let bodies: Vec<_> = alice_comments.iter().map(|c| c.body.clone()).collect();
230    assert_eq!(bodies, vec!["c1", "c2", "c3"]);
231
232    let bob_comments = bob.posts().comments().exec(&mut db).await?;
233    assert_eq!(bob_comments.len(), 1);
234    assert_eq!(bob_comments[0].body, "c4");
235    Ok(())
236}
237
238/// A 3-step chain (`User → Project → Task → Tag`) walks the planner's
239/// unfolder more than once. Verifies the recursive nesting and the chain of
240/// `BelongsTo` rewrites at each hop.
241#[driver_test]
242pub async fn three_step_chain(test: &mut Test) -> Result<()> {
243    #[derive(Debug, toasty::Model)]
244    struct User {
245        #[key]
246        #[auto]
247        id: uuid::Uuid,
248        name: String,
249        #[has_many]
250        projects: toasty::Deferred<Vec<Project>>,
251    }
252
253    #[derive(Debug, toasty::Model)]
254    struct Project {
255        #[key]
256        #[auto]
257        id: uuid::Uuid,
258        #[index]
259        user_id: uuid::Uuid,
260        #[belongs_to(key = user_id, references = id)]
261        user: toasty::Deferred<User>,
262        name: String,
263        #[has_many]
264        tasks: toasty::Deferred<Vec<Task>>,
265    }
266
267    #[derive(Debug, toasty::Model)]
268    struct Task {
269        #[key]
270        #[auto]
271        id: uuid::Uuid,
272        #[index]
273        project_id: uuid::Uuid,
274        #[belongs_to(key = project_id, references = id)]
275        project: toasty::Deferred<Project>,
276        title: String,
277        #[index]
278        tag_id: uuid::Uuid,
279        #[belongs_to(key = tag_id, references = id)]
280        tag: toasty::Deferred<Tag>,
281    }
282
283    #[derive(Debug, toasty::Model)]
284    struct Tag {
285        #[key]
286        #[auto]
287        id: uuid::Uuid,
288        name: String,
289        #[has_many]
290        tasks: toasty::Deferred<Vec<Task>>,
291    }
292
293    let mut db = test.setup_db(models!(User, Project, Task, Tag)).await;
294
295    let user = toasty::create!(User { name: "Owner" })
296        .exec(&mut db)
297        .await?;
298    let other = toasty::create!(User { name: "Other" })
299        .exec(&mut db)
300        .await?;
301
302    let backend = toasty::create!(Project {
303        name: "Backend",
304        user: &user
305    })
306    .exec(&mut db)
307    .await?;
308    let frontend = toasty::create!(Project {
309        name: "Frontend",
310        user: &user
311    })
312    .exec(&mut db)
313    .await?;
314    let unrelated = toasty::create!(Project {
315        name: "Unrelated",
316        user: &other
317    })
318    .exec(&mut db)
319    .await?;
320
321    let bug = toasty::create!(Tag { name: "bug" }).exec(&mut db).await?;
322    let feat = toasty::create!(Tag { name: "feature" })
323        .exec(&mut db)
324        .await?;
325    let chore = toasty::create!(Tag { name: "chore" }).exec(&mut db).await?;
326
327    toasty::create!(Task::[
328        { title: "fix login", project: &backend, tag: &bug },
329        { title: "add dark mode", project: &frontend, tag: &feat },
330        { title: "rotate keys", project: &backend, tag: &chore },
331        { title: "different user", project: &unrelated, tag: &bug },
332    ])
333    .exec(&mut db)
334    .await?;
335
336    let mut tags = user.projects().tasks().tag().exec(&mut db).await?;
337    tags.sort_by_key(|t| t.name.clone());
338    let ids: Vec<_> = tags.iter().map(|t| t.id).collect();
339    assert_unique!(ids);
340    let names: Vec<_> = tags.iter().map(|t| t.name.clone()).collect();
341    assert_eq!(names, vec!["bug", "chore", "feature"]);
342    Ok(())
343}
344
345/// A 4-step chain (`Org → Team → Project → Issue → Tag`) drives the
346/// `peel_first_step` loop through three iterations before reducing to a
347/// single-step rewrite. Guards against regressions in the depth-independent
348/// part of the unfolder.
349#[driver_test]
350pub async fn four_step_chain(test: &mut Test) -> Result<()> {
351    #[derive(Debug, toasty::Model)]
352    struct Org {
353        #[key]
354        #[auto]
355        id: uuid::Uuid,
356        name: String,
357        #[has_many]
358        teams: toasty::Deferred<Vec<Team>>,
359    }
360
361    #[derive(Debug, toasty::Model)]
362    struct Team {
363        #[key]
364        #[auto]
365        id: uuid::Uuid,
366        #[index]
367        org_id: uuid::Uuid,
368        #[belongs_to(key = org_id, references = id)]
369        org: toasty::Deferred<Org>,
370        name: String,
371        #[has_many]
372        projects: toasty::Deferred<Vec<Project>>,
373    }
374
375    #[derive(Debug, toasty::Model)]
376    struct Project {
377        #[key]
378        #[auto]
379        id: uuid::Uuid,
380        #[index]
381        team_id: uuid::Uuid,
382        #[belongs_to(key = team_id, references = id)]
383        team: toasty::Deferred<Team>,
384        name: String,
385        #[has_many]
386        issues: toasty::Deferred<Vec<Issue>>,
387    }
388
389    #[derive(Debug, toasty::Model)]
390    struct Issue {
391        #[key]
392        #[auto]
393        id: uuid::Uuid,
394        #[index]
395        project_id: uuid::Uuid,
396        #[belongs_to(key = project_id, references = id)]
397        project: toasty::Deferred<Project>,
398        title: String,
399        #[index]
400        tag_id: uuid::Uuid,
401        #[belongs_to(key = tag_id, references = id)]
402        tag: toasty::Deferred<Tag>,
403    }
404
405    #[derive(Debug, toasty::Model)]
406    struct Tag {
407        #[key]
408        #[auto]
409        id: uuid::Uuid,
410        name: String,
411        #[has_many]
412        issues: toasty::Deferred<Vec<Issue>>,
413    }
414
415    let mut db = test.setup_db(models!(Org, Team, Project, Issue, Tag)).await;
416
417    let mine = toasty::create!(Org { name: "Mine" }).exec(&mut db).await?;
418    let theirs = toasty::create!(Org { name: "Theirs" })
419        .exec(&mut db)
420        .await?;
421
422    let core = toasty::create!(Team {
423        name: "core",
424        org: &mine
425    })
426    .exec(&mut db)
427    .await?;
428    let ops = toasty::create!(Team {
429        name: "ops",
430        org: &mine
431    })
432    .exec(&mut db)
433    .await?;
434    let outside = toasty::create!(Team {
435        name: "outside",
436        org: &theirs
437    })
438    .exec(&mut db)
439    .await?;
440
441    let backend = toasty::create!(Project {
442        name: "backend",
443        team: &core
444    })
445    .exec(&mut db)
446    .await?;
447    let frontend = toasty::create!(Project {
448        name: "frontend",
449        team: &core
450    })
451    .exec(&mut db)
452    .await?;
453    let infra = toasty::create!(Project {
454        name: "infra",
455        team: &ops
456    })
457    .exec(&mut db)
458    .await?;
459    let unrelated = toasty::create!(Project {
460        name: "unrelated",
461        team: &outside
462    })
463    .exec(&mut db)
464    .await?;
465
466    let bug = toasty::create!(Tag { name: "bug" }).exec(&mut db).await?;
467    let feat = toasty::create!(Tag { name: "feature" })
468        .exec(&mut db)
469        .await?;
470    let chore = toasty::create!(Tag { name: "chore" }).exec(&mut db).await?;
471    let unused = toasty::create!(Tag { name: "unused" })
472        .exec(&mut db)
473        .await?;
474
475    toasty::create!(Issue::[
476        { title: "fix login", project: &backend, tag: &bug },
477        { title: "dark mode", project: &frontend, tag: &feat },
478        { title: "rotate keys", project: &infra, tag: &chore },
479        { title: "duplicate", project: &backend, tag: &bug },
480        { title: "their issue", project: &unrelated, tag: &unused },
481    ])
482    .exec(&mut db)
483    .await?;
484
485    let mut tags = mine.teams().projects().issues().tag().exec(&mut db).await?;
486    tags.sort_by_key(|t| t.name.clone());
487
488    let ids: Vec<_> = tags.iter().map(|t| t.id).collect();
489    assert_unique!(ids);
490    let names: Vec<_> = tags.iter().map(|t| t.name.clone()).collect();
491    assert_eq!(names, vec!["bug", "chore", "feature"]);
492    Ok(())
493}
494
495/// `Deferred<Option<_>>` in the chain skips `NULL` foreign keys. Todos with
496/// no category contribute nothing to the chain.
497#[driver_test]
498pub async fn chain_skips_null_belongs_to(test: &mut Test) -> Result<()> {
499    #[derive(Debug, toasty::Model)]
500    struct User {
501        #[key]
502        #[auto]
503        id: uuid::Uuid,
504        name: String,
505        #[has_many]
506        todos: toasty::Deferred<Vec<Todo>>,
507    }
508
509    #[derive(Debug, toasty::Model)]
510    struct Todo {
511        #[key]
512        #[auto]
513        id: uuid::Uuid,
514        #[index]
515        user_id: uuid::Uuid,
516        #[belongs_to(key = user_id, references = id)]
517        user: toasty::Deferred<User>,
518        title: String,
519        #[index]
520        category_id: Option<uuid::Uuid>,
521        #[belongs_to(key = category_id, references = id)]
522        category: toasty::Deferred<Option<Category>>,
523    }
524
525    #[derive(Debug, toasty::Model)]
526    struct Category {
527        #[key]
528        #[auto]
529        id: uuid::Uuid,
530        name: String,
531    }
532
533    let mut db = test.setup_db(models!(User, Todo, Category)).await;
534
535    let user = toasty::create!(User { name: "Tester" })
536        .exec(&mut db)
537        .await?;
538    let cat = toasty::create!(Category { name: "Only" })
539        .exec(&mut db)
540        .await?;
541
542    toasty::create!(Todo::[
543        { title: "with cat", user: &user, category: &cat },
544        { title: "no cat 1", user: &user },
545        { title: "no cat 2", user: &user },
546    ])
547    .exec(&mut db)
548    .await?;
549
550    let cats = user.todos().category().exec(&mut db).await?;
551    assert_eq!(cats.len(), 1);
552    assert_eq!(cats[0].id, cat.id);
553    Ok(())
554}