Skip to main content

toasty_driver_integration_suite/tests/
relation_has_many_via.rs

1//! Multi-step (`via`) has_many relations: a `has_many` reached by following a
2//! path of existing relations rather than a single foreign key.
3//!
4//! The shape under test is `User` → `Comment` → `Article`: a user has many
5//! comments, each comment belongs to an article, so a user has many
6//! `commented_articles` via `comments.article`.
7
8use crate::prelude::*;
9
10/// A nullable one-field newtype foreign key uses its leaf column rather than
11/// its presence-guarded embed expression when preloading a via relation.
12#[driver_test(requires(and(sql, auto_increment)))]
13pub async fn include_with_newtype_foreign_key(test: &mut Test) -> Result<()> {
14    #[derive(Debug, toasty::Embed)]
15    struct UserId(u64);
16
17    #[derive(Debug, toasty::Model)]
18    struct User {
19        #[key]
20        #[auto]
21        id: UserId,
22
23        #[has_many]
24        comments: toasty::Deferred<Vec<Comment>>,
25
26        #[has_many(via = comments.article)]
27        commented_articles: toasty::Deferred<Vec<Article>>,
28    }
29
30    #[derive(Debug, toasty::Model)]
31    struct Comment {
32        #[key]
33        #[auto]
34        id: u64,
35
36        #[index]
37        user_id: Option<UserId>,
38
39        #[belongs_to(key = user_id, references = id)]
40        user: toasty::Deferred<Option<User>>,
41
42        #[index]
43        article_id: u64,
44
45        #[belongs_to(key = article_id, references = id)]
46        article: toasty::Deferred<Article>,
47    }
48
49    #[derive(Debug, toasty::Model)]
50    struct Article {
51        #[key]
52        #[auto]
53        id: u64,
54
55        #[has_many]
56        comments: toasty::Deferred<Vec<Comment>>,
57    }
58
59    let mut db = test.setup_db(models!(User, Comment, Article)).await;
60    let user = toasty::create!(User {}).exec(&mut db).await?;
61    let article = toasty::create!(Article {}).exec(&mut db).await?;
62    toasty::create!(Comment {
63        user: &user,
64        article: &article,
65    })
66    .exec(&mut db)
67    .await?;
68
69    let users: Vec<User> = User::all()
70        .include(User::fields().commented_articles())
71        .exec(&mut db)
72        .await?;
73
74    assert_struct!(users, [{
75        commented_articles.get().len(): 1,
76    }]);
77
78    Ok(())
79}
80
81/// A unit enum key and foreign key link through the enum's discriminant column
82/// when preloading a via relation.
83#[driver_test(requires(sql))]
84pub async fn include_with_unit_enum_foreign_key(test: &mut Test) -> Result<()> {
85    #[derive(Debug, toasty::Embed)]
86    enum UserId {
87        #[column(variant = 1)]
88        Alice,
89        #[column(variant = 2)]
90        Bob,
91    }
92
93    #[derive(Debug, toasty::Model)]
94    struct User {
95        #[key]
96        id: UserId,
97
98        #[has_many]
99        comments: toasty::Deferred<Vec<Comment>>,
100
101        #[has_many(via = comments.article)]
102        commented_articles: toasty::Deferred<Vec<Article>>,
103    }
104
105    #[derive(Debug, toasty::Model)]
106    struct Comment {
107        #[key]
108        id: u64,
109
110        #[index]
111        user_id: UserId,
112
113        #[belongs_to(key = user_id, references = id)]
114        user: toasty::Deferred<User>,
115
116        #[index]
117        article_id: u64,
118
119        #[belongs_to(key = article_id, references = id)]
120        article: toasty::Deferred<Article>,
121    }
122
123    #[derive(Debug, toasty::Model)]
124    struct Article {
125        #[key]
126        id: u64,
127
128        #[has_many]
129        comments: toasty::Deferred<Vec<Comment>>,
130    }
131
132    let mut db = test.setup_db(models!(User, Comment, Article)).await;
133    let user = toasty::create!(User { id: UserId::Alice })
134        .exec(&mut db)
135        .await?;
136    let article = toasty::create!(Article { id: 1 }).exec(&mut db).await?;
137    toasty::create!(Comment {
138        id: 1,
139        user: &user,
140        article: &article,
141    })
142    .exec(&mut db)
143    .await?;
144
145    let users: Vec<User> = User::all()
146        .include(User::fields().commented_articles())
147        .exec(&mut db)
148        .await?;
149
150    assert_struct!(users, [{
151        commented_articles.get().len(): 1,
152    }]);
153
154    Ok(())
155}
156
157/// Querying and including a two-step `via` returns distinct target models and
158/// scalar terminal values, grouped by parent for includes.
159#[driver_test(requires(sql), scenario(crate::scenarios::user_comment_article))]
160pub async fn query_and_include_two_step_targets_and_values(test: &mut Test) -> Result<()> {
161    let mut db = setup(test).await;
162
163    let users = toasty::create!(User::[
164        { name: "Alice" },
165        { name: "Bob" },
166        { name: "Charlie" },
167    ])
168    .exec(&mut db)
169    .await?;
170    let (alice, bob) = (&users[0], &users[1]);
171
172    let articles = toasty::create!(Article::[
173        { title: "Rust" },
174        { title: "Toasty" },
175        { title: "SQL" },
176    ])
177    .exec(&mut db)
178    .await?;
179    let (rust, toasty_article, sql) = (&articles[0], &articles[1], &articles[2]);
180
181    // Alice comments on Rust twice and Toasty once; Bob comments on SQL.
182    toasty::create!(Comment::[
183        { body: "a1", user: alice, article: rust },
184        { body: "a2", user: alice, article: rust },
185        { body: "a3", user: alice, article: toasty_article },
186        { body: "b1", user: bob, article: sql },
187    ])
188    .exec(&mut db)
189    .await?;
190
191    // Alice has commented on Rust and Toasty. Rust appears once even though
192    // she commented on it twice — `via` yields distinct targets.
193    let commented = alice.commented_articles().exec(&mut db).await?;
194    assert_eq_unordered!(commented.iter().map(|a| &a.title[..]), ["Rust", "Toasty"]);
195
196    // Bob has commented only on SQL.
197    let commented = bob.commented_articles().exec(&mut db).await?;
198    assert_eq_unordered!(commented.iter().map(|a| &a.title[..]), ["SQL"]);
199
200    let loaded: Vec<User> = User::all()
201        .include(User::fields().commented_articles())
202        .exec(&mut db)
203        .await?;
204    assert_eq!(3, loaded.len());
205    for user in &loaded {
206        let titles: Vec<&str> = user
207            .commented_articles
208            .get()
209            .iter()
210            .map(|a| &a.title[..])
211            .collect();
212        match &user.name[..] {
213            "Alice" => {
214                assert_eq_unordered!(titles, ["Rust", "Toasty"]);
215            }
216            "Bob" => {
217                assert_eq_unordered!(titles, ["SQL"]);
218            }
219            "Charlie" => assert!(titles.is_empty(), "Charlie has no comments; got {titles:?}"),
220            other => panic!("unexpected user {other}"),
221        }
222    }
223
224    let titles = alice.commented_article_titles().exec(&mut db).await?;
225    assert_eq_unordered!(titles.iter().map(|t| &t[..]), ["Rust", "Toasty"]);
226    let titles = bob.commented_article_titles().exec(&mut db).await?;
227    assert_eq_unordered!(titles.iter().map(|t| &t[..]), ["SQL"]);
228
229    let loaded: Vec<User> = User::all()
230        .include(User::fields().commented_article_titles())
231        .exec(&mut db)
232        .await?;
233    assert_eq!(3, loaded.len());
234    for user in &loaded {
235        let titles: Vec<&str> = user
236            .commented_article_titles
237            .get()
238            .iter()
239            .map(|t| &t[..])
240            .collect();
241        match &user.name[..] {
242            "Alice" => {
243                assert_eq_unordered!(titles, ["Rust", "Toasty"]);
244            }
245            "Bob" => {
246                assert_eq_unordered!(titles, ["SQL"]);
247            }
248            "Charlie" => assert!(titles.is_empty(), "Charlie has no comments; got {titles:?}"),
249            other => panic!("unexpected user {other}"),
250        }
251    }
252
253    Ok(())
254}
255
256/// A user with no comments reaches no articles — an empty result, no error.
257#[driver_test(requires(sql), scenario(crate::scenarios::user_comment_article))]
258pub async fn query_with_no_intermediates_is_empty(test: &mut Test) -> Result<()> {
259    let mut db = setup(test).await;
260
261    let user = toasty::create!(User { name: "Alice" })
262        .exec(&mut db)
263        .await?;
264    toasty::create!(Article { title: "Rust" })
265        .exec(&mut db)
266        .await?;
267
268    let commented = user.commented_articles().exec(&mut db).await?;
269    assert!(commented.is_empty());
270
271    Ok(())
272}
273
274/// A `via` relation query can be further filtered, like any other relation
275/// query.
276#[driver_test(requires(sql), scenario(crate::scenarios::user_comment_article))]
277pub async fn via_relation_query_can_be_filtered(test: &mut Test) -> Result<()> {
278    let mut db = setup(test).await;
279
280    let alice = toasty::create!(User { name: "Alice" })
281        .exec(&mut db)
282        .await?;
283
284    let articles = toasty::create!(Article::[
285        { title: "Rust" },
286        { title: "Toasty" },
287        { title: "SQL" },
288    ])
289    .exec(&mut db)
290    .await?;
291    let (rust, toasty_article, sql) = (&articles[0], &articles[1], &articles[2]);
292
293    toasty::create!(Comment::[
294        { body: "a1", user: &alice, article: rust },
295        { body: "a2", user: &alice, article: toasty_article },
296        { body: "a3", user: &alice, article: sql },
297    ])
298    .exec(&mut db)
299    .await?;
300
301    let filtered: Vec<_> = alice
302        .commented_articles()
303        .filter(Article::fields().title().eq("Toasty"))
304        .exec(&mut db)
305        .await?;
306    assert_eq_unordered!(filtered.iter().map(|a| &a.title[..]), ["Toasty"]);
307
308    Ok(())
309}
310
311/// `.any()` on a `via` field filters parent records through the expanded
312/// relation path. The same predicate works when the path contains another
313/// `via` field.
314#[driver_test(requires(sql), scenario(crate::scenarios::user_org_project_todo))]
315pub async fn filter_parent_by_via_any(test: &mut Test) -> Result<()> {
316    let mut db = setup(test).await;
317
318    let users = toasty::create!(User::[{ name: "Alice" }, { name: "Bob" }])
319        .exec(&mut db)
320        .await?;
321    let alice_org = toasty::create!(Organization {
322        name: "Alice Org",
323        user: &users[0]
324    })
325    .exec(&mut db)
326    .await?;
327    let bob_org = toasty::create!(Organization {
328        name: "Bob Org",
329        user: &users[1]
330    })
331    .exec(&mut db)
332    .await?;
333    let alice_project = toasty::create!(Project {
334        name: "Alice Project",
335        organization: &alice_org
336    })
337    .exec(&mut db)
338    .await?;
339    let bob_project = toasty::create!(Project {
340        name: "Bob Project",
341        organization: &bob_org
342    })
343    .exec(&mut db)
344    .await?;
345
346    toasty::create!(Todo::[
347        { title: "match", project: &alice_project },
348        { title: "other", project: &bob_project },
349    ])
350    .exec(&mut db)
351    .await?;
352
353    let users: Vec<User> = User::filter(
354        User::fields()
355            .todos()
356            .any(Todo::fields().title().eq("match")),
357    )
358    .exec(&mut db)
359    .await?;
360    assert_eq!(users.len(), 1);
361    assert_eq!(users[0].name, "Alice");
362
363    let users: Vec<User> = User::filter(
364        User::fields()
365            .nested_todos()
366            .any(Todo::fields().title().eq("match")),
367    )
368    .exec(&mut db)
369    .await?;
370    assert_eq!(users.len(), 1);
371    assert_eq!(users[0].name, "Alice");
372
373    Ok(())
374}
375
376// ===== `.include()` / `.select()` of multi-step `via` relations =====
377//
378// The scenarios below cover via paths of different lengths and shapes:
379//
380//   - `user_comment_article`        — 2 steps (HasMany → BelongsTo)
381//   - `user_org_project_todo`       — 3 steps (HasMany → HasMany → HasMany),
382//                                     plus a via-of-via whose path step is
383//                                     itself a via.
384//   - `user_account_subscription`   — 2 steps (HasOne → HasOne); a
385//                                     single-result via.
386//
387// The engine should fetch parents once, then issue a single child query that
388// `INNER JOIN`s each intermediate model and groups results by the parent FK.
389// `.include()` splices that child query into a record slot; `.select()` uses it
390// as the whole projection.
391
392/// `.include()` over a 3-step `via`: User → Organization → Project → Todo,
393/// all `HasMany` steps. Verifies that the child query joins every
394/// intermediate and groups todos by the root user.
395///
396/// The data shape (Alice has two orgs, one with two projects; Bob one org with
397/// one project; each project has a couple of todos) is shared with
398/// [`include_via_nested_via`] so the two can be compared directly. It can't be
399/// hoisted into a helper. The macro imports the scenario types inside each test
400/// function, so a helper cannot name them.
401#[driver_test(requires(sql), scenario(crate::scenarios::user_org_project_todo))]
402pub async fn include_via_three_step(test: &mut Test) -> Result<()> {
403    let mut db = setup(test).await;
404
405    let users = toasty::create!(User::[
406        { name: "Alice" },
407        { name: "Bob" },
408    ])
409    .exec(&mut db)
410    .await?;
411    let (alice, bob) = (&users[0], &users[1]);
412
413    let alice_org_a = toasty::create!(Organization {
414        name: "A-Co",
415        user: alice
416    })
417    .exec(&mut db)
418    .await?;
419    let alice_org_b = toasty::create!(Organization {
420        name: "B-Co",
421        user: alice
422    })
423    .exec(&mut db)
424    .await?;
425    let bob_org = toasty::create!(Organization {
426        name: "Bob-Inc",
427        user: bob
428    })
429    .exec(&mut db)
430    .await?;
431
432    let alice_proj_1 = toasty::create!(Project {
433        name: "p1",
434        organization: &alice_org_a
435    })
436    .exec(&mut db)
437    .await?;
438    let alice_proj_2 = toasty::create!(Project {
439        name: "p2",
440        organization: &alice_org_a
441    })
442    .exec(&mut db)
443    .await?;
444    let alice_proj_3 = toasty::create!(Project {
445        name: "p3",
446        organization: &alice_org_b
447    })
448    .exec(&mut db)
449    .await?;
450    let bob_proj = toasty::create!(Project {
451        name: "bp",
452        organization: &bob_org
453    })
454    .exec(&mut db)
455    .await?;
456
457    toasty::create!(Todo::[
458        { title: "a-1", project: &alice_proj_1 },
459        { title: "a-2", project: &alice_proj_1 },
460        { title: "a-3", project: &alice_proj_2 },
461        { title: "a-4", project: &alice_proj_3 },
462        { title: "b-1", project: &bob_proj },
463        { title: "b-2", project: &bob_proj },
464    ])
465    .exec(&mut db)
466    .await?;
467
468    let loaded: Vec<User> = User::all()
469        .include(User::fields().todos())
470        .exec(&mut db)
471        .await?;
472    assert_eq!(2, loaded.len());
473
474    for user in &loaded {
475        let titles: Vec<&str> = user.todos.get().iter().map(|t| &t.title[..]).collect();
476        match &user.name[..] {
477            "Alice" => {
478                assert_eq_unordered!(titles, ["a-1", "a-2", "a-3", "a-4"]);
479            }
480            "Bob" => {
481                assert_eq_unordered!(titles, ["b-1", "b-2"]);
482            }
483            other => panic!("unexpected user {other}"),
484        }
485    }
486
487    Ok(())
488}
489
490/// `.include()` over a via-of-via: `User::nested_todos` reaches todos through
491/// `organizations.todos`, where `Organization::todos` is itself a via. The
492/// outer path's second step expands into a nested via during lowering, so this
493/// exercises recursive via flattening. The result must match the flat 3-step
494/// `User::todos` include in [`include_via_three_step`] exactly — same data
495/// shape, same expected grouping.
496#[driver_test(requires(sql), scenario(crate::scenarios::user_org_project_todo))]
497pub async fn include_via_nested_via(test: &mut Test) -> Result<()> {
498    let mut db = setup(test).await;
499
500    let users = toasty::create!(User::[
501        { name: "Alice" },
502        { name: "Bob" },
503    ])
504    .exec(&mut db)
505    .await?;
506    let (alice, bob) = (&users[0], &users[1]);
507
508    let alice_org_a = toasty::create!(Organization {
509        name: "A-Co",
510        user: alice
511    })
512    .exec(&mut db)
513    .await?;
514    let alice_org_b = toasty::create!(Organization {
515        name: "B-Co",
516        user: alice
517    })
518    .exec(&mut db)
519    .await?;
520    let bob_org = toasty::create!(Organization {
521        name: "Bob-Inc",
522        user: bob
523    })
524    .exec(&mut db)
525    .await?;
526
527    let alice_proj_1 = toasty::create!(Project {
528        name: "p1",
529        organization: &alice_org_a
530    })
531    .exec(&mut db)
532    .await?;
533    let alice_proj_2 = toasty::create!(Project {
534        name: "p2",
535        organization: &alice_org_a
536    })
537    .exec(&mut db)
538    .await?;
539    let alice_proj_3 = toasty::create!(Project {
540        name: "p3",
541        organization: &alice_org_b
542    })
543    .exec(&mut db)
544    .await?;
545    let bob_proj = toasty::create!(Project {
546        name: "bp",
547        organization: &bob_org
548    })
549    .exec(&mut db)
550    .await?;
551
552    toasty::create!(Todo::[
553        { title: "a-1", project: &alice_proj_1 },
554        { title: "a-2", project: &alice_proj_1 },
555        { title: "a-3", project: &alice_proj_2 },
556        { title: "a-4", project: &alice_proj_3 },
557        { title: "b-1", project: &bob_proj },
558        { title: "b-2", project: &bob_proj },
559    ])
560    .exec(&mut db)
561    .await?;
562
563    let loaded: Vec<User> = User::all()
564        .include(User::fields().nested_todos())
565        .exec(&mut db)
566        .await?;
567    assert_eq!(2, loaded.len());
568
569    for user in &loaded {
570        let titles: Vec<&str> = user
571            .nested_todos
572            .get()
573            .iter()
574            .map(|t| &t.title[..])
575            .collect();
576        match &user.name[..] {
577            "Alice" => {
578                assert_eq_unordered!(titles, ["a-1", "a-2", "a-3", "a-4"]);
579            }
580            "Bob" => {
581                assert_eq_unordered!(titles, ["b-1", "b-2"]);
582            }
583            other => panic!("unexpected user {other}"),
584        }
585    }
586
587    Ok(())
588}
589
590/// A via-of-via with a **scalar terminal**: `nested_todo_titles` routes through
591/// `organizations.todos` — where `Organization::todos` is itself a via — then
592/// projects `Todo::title`. This drives the nested-via splice
593/// (`flatten_via_steps` / `RewriteVia`) on the *scalar*-terminal code paths,
594/// which the model via-of-via ([`include_via_nested_via`]) leaves untested.
595/// Distinct values still apply, so a title shared by todos in different orgs
596/// collapses to one. Navigation and `.include()` must agree.
597#[driver_test(requires(sql), scenario(crate::scenarios::user_org_project_todo))]
598pub async fn scalar_via_of_via(test: &mut Test) -> Result<()> {
599    let mut db = setup(test).await;
600
601    let alice = toasty::create!(User { name: "Alice" })
602        .exec(&mut db)
603        .await?;
604    let org_a = toasty::create!(Organization {
605        name: "A-Co",
606        user: &alice
607    })
608    .exec(&mut db)
609    .await?;
610    let org_b = toasty::create!(Organization {
611        name: "B-Co",
612        user: &alice
613    })
614    .exec(&mut db)
615    .await?;
616    let proj_a = toasty::create!(Project {
617        name: "p-a",
618        organization: &org_a
619    })
620    .exec(&mut db)
621    .await?;
622    let proj_b = toasty::create!(Project {
623        name: "p-b",
624        organization: &org_b
625    })
626    .exec(&mut db)
627    .await?;
628
629    // "y" appears under todos in *both* orgs — distinct values collapse it.
630    toasty::create!(Todo::[
631        { title: "x", project: &proj_a },
632        { title: "y", project: &proj_a },
633        { title: "y", project: &proj_b },
634        { title: "z", project: &proj_b },
635    ])
636    .exec(&mut db)
637    .await?;
638
639    // Navigation projects the titles through the nested via.
640    let titles = alice.nested_todo_titles().exec(&mut db).await?;
641    assert_eq_unordered!(titles.iter().map(|t| &t[..]), ["x", "y", "z"]);
642
643    // `.include()` agrees.
644    let loaded = User::filter_by_id(alice.id)
645        .include(User::fields().nested_todo_titles())
646        .get(&mut db)
647        .await?;
648    let titles: Vec<&str> = loaded
649        .nested_todo_titles
650        .get()
651        .iter()
652        .map(|t| &t[..])
653        .collect();
654    assert_eq_unordered!(titles, ["x", "y", "z"]);
655
656    Ok(())
657}
658
659/// A user with no intermediates yields an empty included set — the
660/// `INNER JOIN` excludes them but the parent row is still returned.
661#[driver_test(requires(sql), scenario(crate::scenarios::user_org_project_todo))]
662pub async fn include_via_three_step_no_intermediates(test: &mut Test) -> Result<()> {
663    let mut db = setup(test).await;
664
665    let alice = toasty::create!(User { name: "Alice" })
666        .exec(&mut db)
667        .await?;
668
669    let loaded = User::filter_by_id(alice.id)
670        .include(User::fields().todos())
671        .get(&mut db)
672        .await?;
673    assert!(loaded.todos.get().is_empty());
674
675    Ok(())
676}
677
678/// `.select()` of a multi-step `via` relation. `.select()` and `.include()`
679/// share the via-JOIN child query (`build_relation_subquery`); the difference
680/// is that `.select()` uses the subquery as the whole projection (each parent
681/// row decodes to its own `Vec<Article>`) rather than splicing it into a record
682/// slot. Distinct targets still apply, so Rust appears once though commented
683/// twice.
684#[driver_test(requires(sql), scenario(crate::scenarios::user_comment_article))]
685pub async fn select_via_two_step(test: &mut Test) -> Result<()> {
686    let mut db = setup(test).await;
687
688    let alice = toasty::create!(User { name: "Alice" })
689        .exec(&mut db)
690        .await?;
691
692    let articles = toasty::create!(Article::[
693        { title: "Rust" },
694        { title: "Toasty" },
695    ])
696    .exec(&mut db)
697    .await?;
698    let (rust, toasty_article) = (&articles[0], &articles[1]);
699
700    toasty::create!(Comment::[
701        { body: "a1", user: &alice, article: rust },
702        { body: "a2", user: &alice, article: rust },
703        { body: "a3", user: &alice, article: toasty_article },
704    ])
705    .exec(&mut db)
706    .await?;
707
708    let articles_per_user: Vec<Vec<Article>> = User::all()
709        .select(User::fields().commented_articles())
710        .exec(&mut db)
711        .await?;
712
713    assert_eq!(1, articles_per_user.len());
714    let titles: Vec<&str> = articles_per_user[0].iter().map(|a| &a.title[..]).collect();
715    assert_eq_unordered!(titles, ["Rust", "Toasty"]);
716
717    Ok(())
718}
719
720/// `.include()` of a `has_one` (single-result) `via` relation: `User` →
721/// `Account` → `Subscription`, both steps `has_one`. The via target is a single
722/// record, so this exercises the `query.single` branch of via-include lowering
723/// that the all-`has_many` scenarios never reach. The `INNER JOIN` drops a
724/// parent whose chain is incomplete at *either* step, so a missing leaf and a
725/// missing intermediate both surface as `None`.
726#[driver_test(requires(sql), scenario(crate::scenarios::user_account_subscription))]
727pub async fn include_via_has_one(test: &mut Test) -> Result<()> {
728    let mut db = setup(test).await;
729
730    // Alice: account → subscription. Bob: account, no subscription.
731    // Carol: no account at all.
732    toasty::create!(User {
733        name: "Alice",
734        account: Account::create().subscription(Subscription::create().plan("pro")),
735    })
736    .exec(&mut db)
737    .await?;
738    toasty::create!(User {
739        name: "Bob",
740        account: Account::create(),
741    })
742    .exec(&mut db)
743    .await?;
744    toasty::create!(User { name: "Carol" })
745        .exec(&mut db)
746        .await?;
747
748    let loaded: Vec<User> = User::all()
749        .include(User::fields().subscription())
750        .exec(&mut db)
751        .await?;
752    assert_eq!(3, loaded.len());
753
754    for user in &loaded {
755        let plan = user.subscription.get().as_ref().map(|s| &s.plan[..]);
756        match &user.name[..] {
757            "Alice" => assert_eq!(plan, Some("pro")),
758            "Bob" => assert_eq!(plan, None, "Bob has an account but no subscription"),
759            "Carol" => assert_eq!(plan, None, "Carol has no account"),
760            other => panic!("unexpected user {other}"),
761        }
762    }
763
764    Ok(())
765}
766
767/// `.select()` of a single (`has_one`) `via` relation. Like
768/// [`include_via_has_one`] this drives the `query.single` via path, but through
769/// `.select()`, which projects each parent straight to its target rather than
770/// into a record slot. The missing-row path is already covered by the include
771/// test, so this focuses on a matched chain returning the target.
772#[driver_test(requires(sql), scenario(crate::scenarios::user_account_subscription))]
773pub async fn select_via_has_one(test: &mut Test) -> Result<()> {
774    let mut db = setup(test).await;
775
776    toasty::create!(User {
777        name: "Alice",
778        account: Account::create().subscription(Subscription::create().plan("pro")),
779    })
780    .exec(&mut db)
781    .await?;
782
783    let subscriptions: Vec<Subscription> = User::filter(User::fields().name().eq("Alice"))
784        .select(User::fields().subscription())
785        .exec(&mut db)
786        .await?;
787
788    assert_eq!(1, subscriptions.len());
789    assert_eq!(subscriptions[0].plan, "pro");
790
791    Ok(())
792}
793
794// ===== Scalar-terminal `via`: the path ends in a field, not a relation =====
795//
796// `#[has_many(via = comments.article.title)] commented_article_titles:
797// Vec<String>` projects the `title` of every article a user has commented on.
798// The relation chain (`comments.article`) is the same as `commented_articles`;
799// the extra `.title` step makes the field a `Vec<String>` of distinct titles.
800
801/// Pins the **distinct *values*** decision against the alternative (distinct
802/// *targets*). The case that tells them apart is a user commenting on two
803/// *different* articles that happen to share a title: the model-via reaches two
804/// distinct targets, but the scalar via collapses their equal terminal values
805/// to one — `["Rust"]`, not `["Rust", "Rust"]`.
806///
807/// [`query_and_include_two_step_targets_and_values`] can't distinguish the two
808/// semantics: it dedups a single target reached through several comments, which
809/// both semantics collapse identically. Navigation and `.include()` must agree.
810#[driver_test(requires(sql), scenario(crate::scenarios::user_comment_article))]
811pub async fn scalar_via_distinct_values_across_distinct_targets(test: &mut Test) -> Result<()> {
812    let mut db = setup(test).await;
813
814    let alice = toasty::create!(User { name: "Alice" })
815        .exec(&mut db)
816        .await?;
817
818    // Two *different* articles that happen to share the title "Rust".
819    let articles = toasty::create!(Article::[{ title: "Rust" }, { title: "Rust" }])
820        .exec(&mut db)
821        .await?;
822    let (rust_a, rust_b) = (&articles[0], &articles[1]);
823
824    toasty::create!(Comment::[
825        { body: "a1", user: &alice, article: rust_a },
826        { body: "a2", user: &alice, article: rust_b },
827    ])
828    .exec(&mut db)
829    .await?;
830
831    // The targets are genuinely distinct: the model-via reaches both articles.
832    let commented = alice.commented_articles().exec(&mut db).await?;
833    assert_eq!(commented.len(), 2);
834
835    // ...but the scalar via yields distinct *values*: the shared title collapses
836    // to one. Distinct *targets* would instead give ["Rust", "Rust"].
837    let titles = alice.commented_article_titles().exec(&mut db).await?;
838    assert_eq_unordered!(titles.iter().map(|t| &t[..]), ["Rust"]);
839
840    // `.include()` agrees with navigation.
841    let loaded = User::filter_by_id(alice.id)
842        .include(User::fields().commented_article_titles())
843        .get(&mut db)
844        .await?;
845    let titles: Vec<&str> = loaded
846        .commented_article_titles
847        .get()
848        .iter()
849        .map(|t| &t[..])
850        .collect();
851    assert_eq_unordered!(titles, ["Rust"]);
852
853    Ok(())
854}
855
856/// A 2-step scalar via (`comments.body`): the terminal field sits directly on
857/// the first relation's target, so the relation chain is a single step
858/// (`[comments]`) — the minimal scalar-via walk, distinct from the 3-step
859/// `comments.article.title`. Distinct values still apply, so a body repeated
860/// across comments appears once. Navigation and `.include()` must agree.
861#[driver_test(requires(sql), scenario(crate::scenarios::user_comment_article))]
862pub async fn query_scalar_via_two_step(test: &mut Test) -> Result<()> {
863    let mut db = setup(test).await;
864
865    let alice = toasty::create!(User { name: "Alice" })
866        .exec(&mut db)
867        .await?;
868    let article = toasty::create!(Article { title: "Rust" })
869        .exec(&mut db)
870        .await?;
871
872    // "a1" twice, "a2" once — distinct *values* collapse "a1".
873    toasty::create!(Comment::[
874        { body: "a1", user: &alice, article: &article },
875        { body: "a1", user: &alice, article: &article },
876        { body: "a2", user: &alice, article: &article },
877    ])
878    .exec(&mut db)
879    .await?;
880
881    let bodies = alice.comment_bodies().exec(&mut db).await?;
882    assert_eq_unordered!(bodies.iter().map(|b| &b[..]), ["a1", "a2"]);
883
884    let loaded = User::filter_by_id(alice.id)
885        .include(User::fields().comment_bodies())
886        .get(&mut db)
887        .await?;
888    let bodies: Vec<&str> = loaded.comment_bodies.get().iter().map(|b| &b[..]).collect();
889    assert_eq_unordered!(bodies, ["a1", "a2"]);
890
891    Ok(())
892}
893
894/// A scalar-terminal `via` can also be navigated off a query (not just a
895/// loaded instance): `User::filter(…).article_titles()` yields the distinct
896/// titles reachable from the matched users.
897#[driver_test(requires(sql), scenario(crate::scenarios::user_comment_article))]
898pub async fn query_chain_scalar_via(test: &mut Test) -> Result<()> {
899    let mut db = setup(test).await;
900
901    let alice = toasty::create!(User { name: "Alice" })
902        .exec(&mut db)
903        .await?;
904
905    let articles = toasty::create!(Article::[{ title: "Rust" }, { title: "Toasty" }])
906        .exec(&mut db)
907        .await?;
908    let (rust, toasty_article) = (&articles[0], &articles[1]);
909
910    toasty::create!(Comment::[
911        { body: "a1", user: &alice, article: rust },
912        { body: "a2", user: &alice, article: rust },
913        { body: "a3", user: &alice, article: toasty_article },
914    ])
915    .exec(&mut db)
916    .await?;
917
918    let titles = User::filter(User::fields().name().eq("Alice"))
919        .commented_article_titles()
920        .exec(&mut db)
921        .await?;
922    assert_eq_unordered!(titles.iter().map(|t| &t[..]), ["Rust", "Toasty"]);
923
924    Ok(())
925}
926
927/// `.select()` of a scalar-terminal `via` returns the projected titles per
928/// parent row.
929#[driver_test(requires(sql), scenario(crate::scenarios::user_comment_article))]
930pub async fn select_scalar_via(test: &mut Test) -> Result<()> {
931    let mut db = setup(test).await;
932
933    let alice = toasty::create!(User { name: "Alice" })
934        .exec(&mut db)
935        .await?;
936
937    let articles = toasty::create!(Article::[{ title: "Rust" }, { title: "Toasty" }])
938        .exec(&mut db)
939        .await?;
940    let (rust, toasty_article) = (&articles[0], &articles[1]);
941
942    toasty::create!(Comment::[
943        { body: "a1", user: &alice, article: rust },
944        { body: "a2", user: &alice, article: rust },
945        { body: "a3", user: &alice, article: toasty_article },
946    ])
947    .exec(&mut db)
948    .await?;
949
950    let titles_per_user: Vec<Vec<String>> = User::all()
951        .select(User::fields().commented_article_titles())
952        .exec(&mut db)
953        .await?;
954
955    assert_eq!(1, titles_per_user.len());
956    let titles: Vec<&str> = titles_per_user[0].iter().map(|t| &t[..]).collect();
957    assert_eq_unordered!(titles, ["Rust", "Toasty"]);
958
959    Ok(())
960}
961
962/// A **non-deferred** scalar via (`tag_names: Vec<String>`, no `Deferred`) is an
963/// eager relation edge: querying the parent auto-loads the projected terminal
964/// values without an explicit `.include()`. Every other via scenario wraps the
965/// field in `Deferred`, so this is the only test exercising the
966/// `ViaManyField for Vec<E>` (`DEFERRED = false`) impl and via auto-loading. The
967/// load groups per user and collapses duplicate values, like the explicit
968/// `.include()` paths.
969#[driver_test(requires(sql), scenario(crate::scenarios::user_tag_names))]
970pub async fn eager_scalar_via_auto_loads(test: &mut Test) -> Result<()> {
971    let mut db = setup(test).await;
972
973    let users = toasty::create!(User::[{ name: "Alice" }, { name: "Bob" }])
974        .exec(&mut db)
975        .await?;
976    let (alice, bob) = (&users[0], &users[1]);
977
978    toasty::create!(Tag::[
979        { name: "rust", user: alice },
980        { name: "rust", user: alice },
981        { name: "db", user: alice },
982        { name: "sql", user: bob },
983    ])
984    .exec(&mut db)
985    .await?;
986
987    // No `.include()`: the eager via loads on a plain query.
988    let loaded: Vec<User> = User::all().exec(&mut db).await?;
989    assert_eq!(2, loaded.len());
990
991    for user in &loaded {
992        let names: Vec<&str> = user.tag_names.iter().map(|n| &n[..]).collect();
993        match &user.name[..] {
994            // "rust" was tagged twice but the via yields distinct values.
995            "Alice" => {
996                assert_eq_unordered!(names, ["rust", "db"]);
997            }
998            "Bob" => {
999                assert_eq_unordered!(names, ["sql"]);
1000            }
1001            other => panic!("unexpected user {other}"),
1002        }
1003    }
1004
1005    Ok(())
1006}