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