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]
191pub async fn has_many_through_has_many(test: &mut Test) -> Result<()> {
192    #[derive(Debug, toasty::Model)]
193    struct Author {
194        #[key]
195        #[auto]
196        id: uuid::Uuid,
197        name: String,
198        #[has_many]
199        posts: toasty::HasMany<Post>,
200    }
201
202    #[derive(Debug, toasty::Model)]
203    struct Post {
204        #[key]
205        #[auto]
206        id: uuid::Uuid,
207        #[index]
208        author_id: uuid::Uuid,
209        #[belongs_to(key = author_id, references = id)]
210        author: toasty::BelongsTo<Author>,
211        title: String,
212        #[has_many]
213        comments: toasty::HasMany<Comment>,
214    }
215
216    #[derive(Debug, toasty::Model)]
217    struct Comment {
218        #[key]
219        #[auto]
220        id: uuid::Uuid,
221        #[index]
222        post_id: uuid::Uuid,
223        #[belongs_to(key = post_id, references = id)]
224        post: toasty::BelongsTo<Post>,
225        body: String,
226    }
227
228    let mut db = test.setup_db(models!(Author, Post, Comment)).await;
229
230    let alice = toasty::create!(Author { name: "Alice" })
231        .exec(&mut db)
232        .await?;
233    let bob = toasty::create!(Author { name: "Bob" })
234        .exec(&mut db)
235        .await?;
236
237    let p1 = toasty::create!(Post {
238        title: "p1",
239        author: &alice
240    })
241    .exec(&mut db)
242    .await?;
243    let p2 = toasty::create!(Post {
244        title: "p2",
245        author: &alice
246    })
247    .exec(&mut db)
248    .await?;
249    let p3 = toasty::create!(Post {
250        title: "p3",
251        author: &bob
252    })
253    .exec(&mut db)
254    .await?;
255
256    toasty::create!(Comment::[
257        { body: "c1", post: &p1 },
258        { body: "c2", post: &p1 },
259        { body: "c3", post: &p2 },
260        { body: "c4", post: &p3 },
261    ])
262    .exec(&mut db)
263    .await?;
264
265    let mut alice_comments = alice.posts().comments().exec(&mut db).await?;
266    alice_comments.sort_by_key(|c| c.body.clone());
267    let bodies: Vec<_> = alice_comments.iter().map(|c| c.body.clone()).collect();
268    assert_eq!(bodies, vec!["c1", "c2", "c3"]);
269
270    let bob_comments = bob.posts().comments().exec(&mut db).await?;
271    assert_eq!(bob_comments.len(), 1);
272    assert_eq!(bob_comments[0].body, "c4");
273    Ok(())
274}
275
276/// A 3-step chain (`User → Project → Task → Tag`) walks the planner's
277/// unfolder more than once. Verifies the recursive nesting and the chain of
278/// `BelongsTo` rewrites at each hop.
279#[driver_test]
280pub async fn three_step_chain(test: &mut Test) -> Result<()> {
281    #[derive(Debug, toasty::Model)]
282    struct User {
283        #[key]
284        #[auto]
285        id: uuid::Uuid,
286        name: String,
287        #[has_many]
288        projects: toasty::HasMany<Project>,
289    }
290
291    #[derive(Debug, toasty::Model)]
292    struct Project {
293        #[key]
294        #[auto]
295        id: uuid::Uuid,
296        #[index]
297        user_id: uuid::Uuid,
298        #[belongs_to(key = user_id, references = id)]
299        user: toasty::BelongsTo<User>,
300        name: String,
301        #[has_many]
302        tasks: toasty::HasMany<Task>,
303    }
304
305    #[derive(Debug, toasty::Model)]
306    struct Task {
307        #[key]
308        #[auto]
309        id: uuid::Uuid,
310        #[index]
311        project_id: uuid::Uuid,
312        #[belongs_to(key = project_id, references = id)]
313        project: toasty::BelongsTo<Project>,
314        title: String,
315        #[index]
316        tag_id: uuid::Uuid,
317        #[belongs_to(key = tag_id, references = id)]
318        tag: toasty::BelongsTo<Tag>,
319    }
320
321    #[derive(Debug, toasty::Model)]
322    struct Tag {
323        #[key]
324        #[auto]
325        id: uuid::Uuid,
326        name: String,
327        #[has_many]
328        tasks: toasty::HasMany<Task>,
329    }
330
331    let mut db = test.setup_db(models!(User, Project, Task, Tag)).await;
332
333    let user = toasty::create!(User { name: "Owner" })
334        .exec(&mut db)
335        .await?;
336    let other = toasty::create!(User { name: "Other" })
337        .exec(&mut db)
338        .await?;
339
340    let backend = toasty::create!(Project {
341        name: "Backend",
342        user: &user
343    })
344    .exec(&mut db)
345    .await?;
346    let frontend = toasty::create!(Project {
347        name: "Frontend",
348        user: &user
349    })
350    .exec(&mut db)
351    .await?;
352    let unrelated = toasty::create!(Project {
353        name: "Unrelated",
354        user: &other
355    })
356    .exec(&mut db)
357    .await?;
358
359    let bug = toasty::create!(Tag { name: "bug" }).exec(&mut db).await?;
360    let feat = toasty::create!(Tag { name: "feature" })
361        .exec(&mut db)
362        .await?;
363    let chore = toasty::create!(Tag { name: "chore" }).exec(&mut db).await?;
364
365    toasty::create!(Task::[
366        { title: "fix login", project: &backend, tag: &bug },
367        { title: "add dark mode", project: &frontend, tag: &feat },
368        { title: "rotate keys", project: &backend, tag: &chore },
369        { title: "different user", project: &unrelated, tag: &bug },
370    ])
371    .exec(&mut db)
372    .await?;
373
374    let mut tags = user.projects().tasks().tag().exec(&mut db).await?;
375    tags.sort_by_key(|t| t.name.clone());
376    let ids: Vec<_> = tags.iter().map(|t| t.id).collect();
377    assert_unique!(ids);
378    let names: Vec<_> = tags.iter().map(|t| t.name.clone()).collect();
379    assert_eq!(names, vec!["bug", "chore", "feature"]);
380    Ok(())
381}
382
383/// A 4-step chain (`Org → Team → Project → Issue → Tag`) drives the
384/// `peel_first_step` loop through three iterations before reducing to a
385/// single-step rewrite. Guards against regressions in the depth-independent
386/// part of the unfolder.
387#[driver_test]
388pub async fn four_step_chain(test: &mut Test) -> Result<()> {
389    #[derive(Debug, toasty::Model)]
390    struct Org {
391        #[key]
392        #[auto]
393        id: uuid::Uuid,
394        name: String,
395        #[has_many]
396        teams: toasty::HasMany<Team>,
397    }
398
399    #[derive(Debug, toasty::Model)]
400    struct Team {
401        #[key]
402        #[auto]
403        id: uuid::Uuid,
404        #[index]
405        org_id: uuid::Uuid,
406        #[belongs_to(key = org_id, references = id)]
407        org: toasty::BelongsTo<Org>,
408        name: String,
409        #[has_many]
410        projects: toasty::HasMany<Project>,
411    }
412
413    #[derive(Debug, toasty::Model)]
414    struct Project {
415        #[key]
416        #[auto]
417        id: uuid::Uuid,
418        #[index]
419        team_id: uuid::Uuid,
420        #[belongs_to(key = team_id, references = id)]
421        team: toasty::BelongsTo<Team>,
422        name: String,
423        #[has_many]
424        issues: toasty::HasMany<Issue>,
425    }
426
427    #[derive(Debug, toasty::Model)]
428    struct Issue {
429        #[key]
430        #[auto]
431        id: uuid::Uuid,
432        #[index]
433        project_id: uuid::Uuid,
434        #[belongs_to(key = project_id, references = id)]
435        project: toasty::BelongsTo<Project>,
436        title: String,
437        #[index]
438        tag_id: uuid::Uuid,
439        #[belongs_to(key = tag_id, references = id)]
440        tag: toasty::BelongsTo<Tag>,
441    }
442
443    #[derive(Debug, toasty::Model)]
444    struct Tag {
445        #[key]
446        #[auto]
447        id: uuid::Uuid,
448        name: String,
449        #[has_many]
450        issues: toasty::HasMany<Issue>,
451    }
452
453    let mut db = test.setup_db(models!(Org, Team, Project, Issue, Tag)).await;
454
455    let mine = toasty::create!(Org { name: "Mine" }).exec(&mut db).await?;
456    let theirs = toasty::create!(Org { name: "Theirs" })
457        .exec(&mut db)
458        .await?;
459
460    let core = toasty::create!(Team {
461        name: "core",
462        org: &mine
463    })
464    .exec(&mut db)
465    .await?;
466    let ops = toasty::create!(Team {
467        name: "ops",
468        org: &mine
469    })
470    .exec(&mut db)
471    .await?;
472    let outside = toasty::create!(Team {
473        name: "outside",
474        org: &theirs
475    })
476    .exec(&mut db)
477    .await?;
478
479    let backend = toasty::create!(Project {
480        name: "backend",
481        team: &core
482    })
483    .exec(&mut db)
484    .await?;
485    let frontend = toasty::create!(Project {
486        name: "frontend",
487        team: &core
488    })
489    .exec(&mut db)
490    .await?;
491    let infra = toasty::create!(Project {
492        name: "infra",
493        team: &ops
494    })
495    .exec(&mut db)
496    .await?;
497    let unrelated = toasty::create!(Project {
498        name: "unrelated",
499        team: &outside
500    })
501    .exec(&mut db)
502    .await?;
503
504    let bug = toasty::create!(Tag { name: "bug" }).exec(&mut db).await?;
505    let feat = toasty::create!(Tag { name: "feature" })
506        .exec(&mut db)
507        .await?;
508    let chore = toasty::create!(Tag { name: "chore" }).exec(&mut db).await?;
509    let unused = toasty::create!(Tag { name: "unused" })
510        .exec(&mut db)
511        .await?;
512
513    toasty::create!(Issue::[
514        { title: "fix login", project: &backend, tag: &bug },
515        { title: "dark mode", project: &frontend, tag: &feat },
516        { title: "rotate keys", project: &infra, tag: &chore },
517        { title: "duplicate", project: &backend, tag: &bug },
518        { title: "their issue", project: &unrelated, tag: &unused },
519    ])
520    .exec(&mut db)
521    .await?;
522
523    let mut tags = mine.teams().projects().issues().tag().exec(&mut db).await?;
524    tags.sort_by_key(|t| t.name.clone());
525
526    let ids: Vec<_> = tags.iter().map(|t| t.id).collect();
527    assert_unique!(ids);
528    let names: Vec<_> = tags.iter().map(|t| t.name.clone()).collect();
529    assert_eq!(names, vec!["bug", "chore", "feature"]);
530    Ok(())
531}
532
533/// `BelongsTo<Option<_>>` in the chain skips `NULL` foreign keys. Todos with
534/// no category contribute nothing to the chain.
535#[driver_test]
536pub async fn chain_skips_null_belongs_to(test: &mut Test) -> Result<()> {
537    #[derive(Debug, toasty::Model)]
538    struct User {
539        #[key]
540        #[auto]
541        id: uuid::Uuid,
542        name: String,
543        #[has_many]
544        todos: toasty::HasMany<Todo>,
545    }
546
547    #[derive(Debug, toasty::Model)]
548    struct Todo {
549        #[key]
550        #[auto]
551        id: uuid::Uuid,
552        #[index]
553        user_id: uuid::Uuid,
554        #[belongs_to(key = user_id, references = id)]
555        user: toasty::BelongsTo<User>,
556        title: String,
557        #[index]
558        category_id: Option<uuid::Uuid>,
559        #[belongs_to(key = category_id, references = id)]
560        category: toasty::BelongsTo<Option<Category>>,
561    }
562
563    #[derive(Debug, toasty::Model)]
564    struct Category {
565        #[key]
566        #[auto]
567        id: uuid::Uuid,
568        name: String,
569    }
570
571    let mut db = test.setup_db(models!(User, Todo, Category)).await;
572
573    let user = toasty::create!(User { name: "Tester" })
574        .exec(&mut db)
575        .await?;
576    let cat = toasty::create!(Category { name: "Only" })
577        .exec(&mut db)
578        .await?;
579
580    toasty::create!(Todo::[
581        { title: "with cat", user: &user, category: &cat },
582        { title: "no cat 1", user: &user },
583        { title: "no cat 2", user: &user },
584    ])
585    .exec(&mut db)
586    .await?;
587
588    let cats = user.todos().category().exec(&mut db).await?;
589    assert_eq!(cats.len(), 1);
590    assert_eq!(cats[0].id, cat.id);
591    Ok(())
592}