Skip to main content

toasty_driver_integration_suite/tests/
relation_preload.rs

1use crate::prelude::*;
2
3/// Tests that preloading a `HasOne<Option<_>>` correctly distinguishes between
4/// "not loaded" and "loaded as None" when the relation does not exist.
5#[driver_test(id(ID), scenario(crate::scenarios::has_one_optional_belongs_to))]
6pub async fn preload_has_one_option_none_then_some(test: &mut Test) -> Result<()> {
7    let mut db = setup(test).await;
8
9    // Create a user WITHOUT a profile
10    let user_no_profile = User::create().name("No Profile").exec(&mut db).await?;
11
12    // Preload the profile — no profile exists, so it should be `None` (loaded)
13    let user_no_profile = User::filter_by_id(user_no_profile.id)
14        .include(User::fields().profile())
15        .get(&mut db)
16        .await?;
17
18    // `.get()` must not panic — the relation was preloaded and is None
19    assert!(user_no_profile.profile.get().is_none());
20
21    // Create a user WITH a profile
22    let user_with_profile = User::create()
23        .name("Has Profile")
24        .profile(Profile::create().bio("A bio"))
25        .exec(&mut db)
26        .await?;
27
28    // Preload the profile — a profile exists, so it should be `Some`
29    let user_with_profile = User::filter_by_id(user_with_profile.id)
30        .include(User::fields().profile())
31        .get(&mut db)
32        .await?;
33
34    let profile = user_with_profile.profile.get().as_ref().unwrap();
35    assert_eq!("A bio", profile.bio);
36    assert_eq!(user_with_profile.id, *profile.user_id.as_ref().unwrap());
37
38    Ok(())
39}
40
41#[driver_test(id(ID), scenario(crate::scenarios::has_many_belongs_to))]
42pub async fn basic_has_many_and_belongs_to_preload(test: &mut Test) -> Result<()> {
43    let mut db = setup(test).await;
44
45    // Create a user with a few todos
46    let user = User::create()
47        .name("Alice")
48        .todo(Todo::create().title("todo 1"))
49        .todo(Todo::create().title("todo 2"))
50        .todo(Todo::create().title("todo 3"))
51        .exec(&mut db)
52        .await?;
53
54    // Find the user, include TODOs
55    let user = User::filter_by_id(user.id)
56        .include(User::fields().todos())
57        .get(&mut db)
58        .await?;
59
60    // This will panic
61    assert_eq!(3, user.todos.get().len());
62
63    let id = user.todos.get()[0].id;
64
65    let todo = Todo::filter_by_id(id)
66        .include(Todo::fields().user())
67        .get(&mut db)
68        .await?;
69
70    assert_eq!(user.id, todo.user.get().id);
71    assert_eq!(user.id, todo.user_id);
72    Ok(())
73}
74
75#[driver_test(id(ID))]
76pub async fn multiple_includes_same_model(test: &mut Test) -> Result<()> {
77    #[derive(Debug, toasty::Model)]
78    struct User {
79        #[key]
80        #[auto]
81        id: ID,
82
83        #[allow(dead_code)]
84        name: String,
85
86        #[has_many]
87        posts: toasty::HasMany<Post>,
88
89        #[has_many]
90        comments: toasty::HasMany<Comment>,
91    }
92
93    #[derive(Debug, toasty::Model)]
94    struct Post {
95        #[key]
96        #[auto]
97        id: ID,
98
99        #[allow(dead_code)]
100        title: String,
101
102        #[index]
103        #[allow(dead_code)]
104        user_id: ID,
105
106        #[belongs_to(key = user_id, references = id)]
107        user: toasty::BelongsTo<User>,
108    }
109
110    #[derive(Debug, toasty::Model)]
111    struct Comment {
112        #[key]
113        #[auto]
114        id: ID,
115
116        #[allow(dead_code)]
117        text: String,
118
119        #[index]
120        #[allow(dead_code)]
121        user_id: ID,
122
123        #[belongs_to(key = user_id, references = id)]
124        user: toasty::BelongsTo<User>,
125    }
126
127    let mut db = test.setup_db(models!(User, Post, Comment)).await;
128
129    // Create a user
130    let user = User::create().name("Test User").exec(&mut db).await?;
131
132    // Create posts associated with the user
133    Post::create()
134        .title("Post 1")
135        .user(&user)
136        .exec(&mut db)
137        .await?;
138
139    Post::create()
140        .title("Post 2")
141        .user(&user)
142        .exec(&mut db)
143        .await?;
144
145    // Create comments associated with the user
146    Comment::create()
147        .text("Comment 1")
148        .user(&user)
149        .exec(&mut db)
150        .await?;
151
152    Comment::create()
153        .text("Comment 2")
154        .user(&user)
155        .exec(&mut db)
156        .await?;
157
158    Comment::create()
159        .text("Comment 3")
160        .user(&user)
161        .exec(&mut db)
162        .await?;
163
164    // Test individual includes work (baseline)
165    let user_with_posts = User::filter_by_id(user.id)
166        .include(User::fields().posts())
167        .get(&mut db)
168        .await?;
169    assert_eq!(2, user_with_posts.posts.get().len());
170
171    let user_with_comments = User::filter_by_id(user.id)
172        .include(User::fields().comments())
173        .get(&mut db)
174        .await?;
175    assert_eq!(3, user_with_comments.comments.get().len());
176
177    // Test multiple includes in one query
178    let loaded_user = User::filter_by_id(user.id)
179        .include(User::fields().posts()) // First include
180        .include(User::fields().comments()) // Second include
181        .get(&mut db)
182        .await?;
183
184    assert_eq!(2, loaded_user.posts.get().len());
185    assert_eq!(3, loaded_user.comments.get().len());
186    Ok(())
187}
188
189#[driver_test(id(ID))]
190pub async fn basic_has_one_and_belongs_to_preload(test: &mut Test) -> Result<()> {
191    #[derive(Debug, toasty::Model)]
192    struct User {
193        #[key]
194        #[auto]
195        id: ID,
196
197        name: String,
198
199        #[has_one]
200        profile: toasty::HasOne<Option<Profile>>,
201    }
202
203    #[derive(Debug, toasty::Model)]
204    struct Profile {
205        #[key]
206        #[auto]
207        id: ID,
208
209        bio: String,
210
211        #[unique]
212        user_id: Option<ID>,
213
214        #[belongs_to(key = user_id, references = id)]
215        user: toasty::BelongsTo<Option<User>>,
216    }
217
218    let mut db = test.setup_db(models!(User, Profile)).await;
219
220    // Create a user with a profile
221    let user = User::create()
222        .name("John Doe")
223        .profile(Profile::create().bio("A person"))
224        .exec(&mut db)
225        .await?;
226
227    // Find the user, include profile
228    let user = User::filter_by_id(user.id)
229        .include(User::fields().profile())
230        .get(&mut db)
231        .await?;
232
233    // Verify the profile is preloaded
234    let profile = user.profile.get().as_ref().unwrap();
235    assert_eq!("A person", profile.bio);
236    assert_eq!(user.id, *profile.user_id.as_ref().unwrap());
237
238    let profile_id = profile.id;
239
240    // Test the reciprocal belongs_to preload
241    let profile = Profile::filter_by_id(profile_id)
242        .include(Profile::fields().user())
243        .get(&mut db)
244        .await?;
245
246    assert_eq!(user.id, profile.user.get().as_ref().unwrap().id);
247    assert_eq!("John Doe", profile.user.get().as_ref().unwrap().name);
248    Ok(())
249}
250
251#[driver_test(id(ID))]
252pub async fn multiple_includes_with_has_one(test: &mut Test) -> Result<()> {
253    #[derive(Debug, toasty::Model)]
254    #[allow(dead_code)]
255    struct User {
256        #[key]
257        #[auto]
258        id: ID,
259
260        name: String,
261
262        #[has_one]
263        profile: toasty::HasOne<Option<Profile>>,
264
265        #[has_one]
266        settings: toasty::HasOne<Option<Settings>>,
267    }
268
269    #[derive(Debug, toasty::Model)]
270    #[allow(dead_code)]
271    struct Profile {
272        #[key]
273        #[auto]
274        id: ID,
275
276        bio: String,
277
278        #[unique]
279        user_id: Option<ID>,
280
281        #[belongs_to(key = user_id, references = id)]
282        user: toasty::BelongsTo<Option<User>>,
283    }
284
285    #[derive(Debug, toasty::Model)]
286    #[allow(dead_code)]
287    struct Settings {
288        #[key]
289        #[auto]
290        id: ID,
291
292        theme: String,
293
294        #[unique]
295        user_id: Option<ID>,
296
297        #[belongs_to(key = user_id, references = id)]
298        user: toasty::BelongsTo<Option<User>>,
299    }
300
301    let mut db = test.setup_db(models!(User, Profile, Settings)).await;
302
303    // Create a user with both profile and settings
304    let user = User::create()
305        .name("Jane Doe")
306        .profile(Profile::create().bio("Software engineer"))
307        .settings(Settings::create().theme("dark"))
308        .exec(&mut db)
309        .await?;
310
311    // Test individual includes work (baseline)
312    let user_with_profile = User::filter_by_id(user.id)
313        .include(User::fields().profile())
314        .get(&mut db)
315        .await?;
316    assert!(user_with_profile.profile.get().is_some());
317    assert_eq!(
318        "Software engineer",
319        user_with_profile.profile.get().as_ref().unwrap().bio
320    );
321
322    let user_with_settings = User::filter_by_id(user.id)
323        .include(User::fields().settings())
324        .get(&mut db)
325        .await?;
326    assert!(user_with_settings.settings.get().is_some());
327    assert_eq!(
328        "dark",
329        user_with_settings.settings.get().as_ref().unwrap().theme
330    );
331
332    // Test multiple includes in one query
333    let loaded_user = User::filter_by_id(user.id)
334        .include(User::fields().profile()) // First include
335        .include(User::fields().settings()) // Second include
336        .get(&mut db)
337        .await?;
338
339    assert!(loaded_user.profile.get().is_some());
340    assert_eq!(
341        "Software engineer",
342        loaded_user.profile.get().as_ref().unwrap().bio
343    );
344    assert!(loaded_user.settings.get().is_some());
345    assert_eq!("dark", loaded_user.settings.get().as_ref().unwrap().theme);
346    Ok(())
347}
348
349#[driver_test(id(ID))]
350pub async fn combined_has_many_and_has_one_preload(test: &mut Test) -> Result<()> {
351    #[derive(Debug, toasty::Model)]
352    #[allow(dead_code)]
353    struct User {
354        #[key]
355        #[auto]
356        id: ID,
357
358        name: String,
359
360        #[has_one]
361        profile: toasty::HasOne<Option<Profile>>,
362
363        #[has_many]
364        todos: toasty::HasMany<Todo>,
365    }
366
367    #[derive(Debug, toasty::Model)]
368    #[allow(dead_code)]
369    struct Profile {
370        #[key]
371        #[auto]
372        id: ID,
373
374        bio: String,
375
376        #[unique]
377        user_id: Option<ID>,
378
379        #[belongs_to(key = user_id, references = id)]
380        user: toasty::BelongsTo<Option<User>>,
381    }
382
383    #[derive(Debug, toasty::Model)]
384    #[allow(dead_code)]
385    struct Todo {
386        #[key]
387        #[auto]
388        id: ID,
389
390        title: String,
391
392        #[index]
393        user_id: ID,
394
395        #[belongs_to(key = user_id, references = id)]
396        user: toasty::BelongsTo<User>,
397    }
398
399    let mut db = test.setup_db(models!(User, Profile, Todo)).await;
400
401    // Create a user with a profile and multiple todos
402    let user = User::create()
403        .name("Bob Smith")
404        .profile(Profile::create().bio("Developer"))
405        .todo(Todo::create().title("Task 1"))
406        .todo(Todo::create().title("Task 2"))
407        .todo(Todo::create().title("Task 3"))
408        .exec(&mut db)
409        .await?;
410
411    // Test combined has_one and has_many preload in a single query
412    let loaded_user = User::filter_by_id(user.id)
413        .include(User::fields().profile()) // has_one include
414        .include(User::fields().todos()) // has_many include
415        .get(&mut db)
416        .await?;
417
418    // Verify has_one association is preloaded
419    assert!(loaded_user.profile.get().is_some());
420    assert_eq!("Developer", loaded_user.profile.get().as_ref().unwrap().bio);
421
422    // Verify has_many association is preloaded
423    assert_eq!(3, loaded_user.todos.get().len());
424    let todo_titles: Vec<&str> = loaded_user
425        .todos
426        .get()
427        .iter()
428        .map(|t| t.title.as_str())
429        .collect();
430    assert!(todo_titles.contains(&"Task 1"));
431    assert!(todo_titles.contains(&"Task 2"));
432    assert!(todo_titles.contains(&"Task 3"));
433    Ok(())
434}
435
436#[driver_test(
437    id(ID),
438    requires(scan),
439    scenario(crate::scenarios::has_many_belongs_to)
440)]
441pub async fn preload_on_empty_table(test: &mut Test) -> Result<()> {
442    let mut db = setup(test).await;
443
444    // Query with include on empty table - should return empty result, not SQL error
445    let users: Vec<User> = User::all()
446        .include(User::fields().todos())
447        .exec(&mut db)
448        .await?;
449
450    assert_eq!(0, users.len());
451    Ok(())
452}
453
454#[driver_test(id(ID))]
455pub async fn preload_on_empty_query(test: &mut Test) -> Result<()> {
456    #[derive(Debug, toasty::Model)]
457    struct User {
458        #[key]
459        #[auto]
460        id: ID,
461
462        #[index]
463        #[allow(dead_code)]
464        name: String,
465
466        #[has_many]
467        #[allow(dead_code)]
468        todos: toasty::HasMany<Todo>,
469    }
470
471    #[derive(Debug, toasty::Model)]
472    struct Todo {
473        #[key]
474        #[auto]
475        id: ID,
476
477        #[index]
478        #[allow(dead_code)]
479        user_id: ID,
480
481        #[belongs_to(key = user_id, references = id)]
482        #[allow(dead_code)]
483        user: toasty::BelongsTo<User>,
484    }
485
486    let mut db = test.setup_db(models!(User, Todo)).await;
487
488    // Query with include on empty table - should return empty result, not SQL error
489    let users: Vec<User> = User::filter_by_name("foo")
490        .include(User::fields().todos())
491        .exec(&mut db)
492        .await?;
493
494    assert_eq!(0, users.len());
495    Ok(())
496}
497
498/// `HasMany<T>` + `BelongsTo<Option<T>>`: nullable FK allows children to
499/// exist without a parent. Tests preloading from both directions.
500#[driver_test(id(ID))]
501pub async fn preload_has_many_with_optional_belongs_to(test: &mut Test) -> Result<()> {
502    #[derive(Debug, toasty::Model)]
503    struct User {
504        #[key]
505        #[auto]
506        id: ID,
507
508        name: String,
509
510        #[has_many]
511        todos: toasty::HasMany<Todo>,
512    }
513
514    #[derive(Debug, toasty::Model)]
515    struct Todo {
516        #[key]
517        #[auto]
518        id: ID,
519
520        #[index]
521        title: String,
522
523        #[index]
524        user_id: Option<ID>,
525
526        #[belongs_to(key = user_id, references = id)]
527        user: toasty::BelongsTo<Option<User>>,
528    }
529
530    let mut db = test.setup_db(models!(User, Todo)).await;
531
532    // Create a user with linked todos
533    let user = User::create()
534        .name("Alice")
535        .todo(Todo::create().title("Task 1"))
536        .todo(Todo::create().title("Task 2"))
537        .exec(&mut db)
538        .await?;
539
540    // Preload HasMany from parent side
541    let user = User::filter_by_id(user.id)
542        .include(User::fields().todos())
543        .get(&mut db)
544        .await?;
545
546    assert_eq!(2, user.todos.get().len());
547
548    let todo_id = user.todos.get()[0].id;
549
550    // Preload BelongsTo<Option<User>> from child side — linked todo
551    let todo = Todo::filter_by_id(todo_id)
552        .include(Todo::fields().user())
553        .get(&mut db)
554        .await?;
555
556    assert_eq!(user.id, todo.user.get().as_ref().unwrap().id);
557
558    // Create an orphan todo (no user)
559    let orphan = Todo::create().title("Orphan").exec(&mut db).await?;
560
561    // Preload BelongsTo<Option<User>> on orphan — should be None
562    let orphan = Todo::filter_by_id(orphan.id)
563        .include(Todo::fields().user())
564        .get(&mut db)
565        .await?;
566
567    assert!(orphan.user.get().is_none());
568
569    Ok(())
570}
571
572/// `HasOne<Option<T>>` + `BelongsTo<T>` (required FK): the child always points
573/// to a parent, but the parent may or may not have a child. Tests preloading
574/// from both directions.
575#[driver_test(id(ID))]
576pub async fn preload_has_one_optional_with_required_belongs_to(test: &mut Test) -> Result<()> {
577    #[derive(Debug, toasty::Model)]
578    struct User {
579        #[key]
580        #[auto]
581        id: ID,
582
583        name: String,
584
585        #[has_one]
586        profile: toasty::HasOne<Option<Profile>>,
587    }
588
589    #[derive(Debug, toasty::Model)]
590    struct Profile {
591        #[key]
592        #[auto]
593        id: ID,
594
595        bio: String,
596
597        #[unique]
598        user_id: ID,
599
600        #[belongs_to(key = user_id, references = id)]
601        user: toasty::BelongsTo<User>,
602    }
603
604    let mut db = test.setup_db(models!(User, Profile)).await;
605
606    // Create a user WITH a profile
607    let user_with = User::create()
608        .name("Has Profile")
609        .profile(Profile::create().bio("hello"))
610        .exec(&mut db)
611        .await?;
612
613    // Create a user WITHOUT a profile
614    let user_without = User::create().name("No Profile").exec(&mut db).await?;
615
616    // Preload HasOne<Option<Profile>> — profile exists
617    let loaded = User::filter_by_id(user_with.id)
618        .include(User::fields().profile())
619        .get(&mut db)
620        .await?;
621
622    let profile = loaded.profile.get().as_ref().unwrap();
623    assert_eq!("hello", profile.bio);
624    assert_eq!(user_with.id, profile.user_id);
625
626    // Preload HasOne<Option<Profile>> — no profile
627    let loaded = User::filter_by_id(user_without.id)
628        .include(User::fields().profile())
629        .get(&mut db)
630        .await?;
631
632    assert!(loaded.profile.get().is_none());
633
634    // Preload BelongsTo<User> (required) from child side
635    let profile = Profile::filter_by_user_id(user_with.id)
636        .include(Profile::fields().user())
637        .get(&mut db)
638        .await?;
639
640    assert_eq!(user_with.id, profile.user.get().id);
641    assert_eq!("Has Profile", profile.user.get().name);
642
643    Ok(())
644}
645
646/// `HasOne<T>` (required) + `BelongsTo<Option<T>>`: creating a parent requires
647/// providing a child, but the child FK is nullable. Tests preloading from both
648/// directions.
649#[driver_test(id(ID))]
650pub async fn preload_has_one_required_with_optional_belongs_to(test: &mut Test) -> Result<()> {
651    #[derive(Debug, toasty::Model)]
652    struct User {
653        #[key]
654        #[auto]
655        id: ID,
656
657        name: String,
658
659        #[has_one]
660        profile: toasty::HasOne<Profile>,
661    }
662
663    #[derive(Debug, toasty::Model)]
664    struct Profile {
665        #[key]
666        #[auto]
667        id: ID,
668
669        bio: String,
670
671        #[unique]
672        user_id: Option<ID>,
673
674        #[belongs_to(key = user_id, references = id)]
675        user: toasty::BelongsTo<Option<User>>,
676    }
677
678    let mut db = test.setup_db(models!(User, Profile)).await;
679
680    // Create a user (must provide a profile since HasOne<T> is required)
681    let user = User::create()
682        .name("Alice")
683        .profile(Profile::create().bio("a bio"))
684        .exec(&mut db)
685        .await?;
686
687    // Preload HasOne<Profile> (required) from parent side
688    let loaded = User::filter_by_id(user.id)
689        .include(User::fields().profile())
690        .get(&mut db)
691        .await?;
692
693    let profile = loaded.profile.get();
694    assert_eq!("a bio", profile.bio);
695    assert_eq!(user.id, *profile.user_id.as_ref().unwrap());
696
697    // Preload BelongsTo<Option<User>> from child side
698    let profile = Profile::filter_by_id(profile.id)
699        .include(Profile::fields().user())
700        .get(&mut db)
701        .await?;
702
703    assert_eq!(user.id, profile.user.get().as_ref().unwrap().id);
704    assert_eq!("Alice", profile.user.get().as_ref().unwrap().name);
705
706    Ok(())
707}
708
709#[driver_test(id(ID))]
710pub async fn nested_has_many_preload(test: &mut Test) {
711    #[derive(Debug, toasty::Model)]
712    #[allow(dead_code)]
713    struct User {
714        #[key]
715        #[auto]
716        id: ID,
717
718        name: String,
719
720        #[has_many]
721        todos: toasty::HasMany<Todo>,
722    }
723
724    #[derive(Debug, toasty::Model)]
725    #[allow(dead_code)]
726    struct Todo {
727        #[key]
728        #[auto]
729        id: ID,
730
731        title: String,
732
733        #[index]
734        user_id: ID,
735
736        #[belongs_to(key = user_id, references = id)]
737        user: toasty::BelongsTo<User>,
738
739        #[has_many]
740        steps: toasty::HasMany<Step>,
741    }
742
743    #[derive(Debug, toasty::Model)]
744    #[allow(dead_code)]
745    struct Step {
746        #[key]
747        #[auto]
748        id: ID,
749
750        description: String,
751
752        #[index]
753        todo_id: ID,
754
755        #[belongs_to(key = todo_id, references = id)]
756        todo: toasty::BelongsTo<Todo>,
757    }
758
759    let mut db = test.setup_db(models!(User, Todo, Step)).await;
760
761    // Create a user with todos, each with steps
762    let user = User::create()
763        .name("Alice")
764        .todo(
765            Todo::create()
766                .title("Todo 1")
767                .step(Step::create().description("Step 1a"))
768                .step(Step::create().description("Step 1b")),
769        )
770        .todo(
771            Todo::create()
772                .title("Todo 2")
773                .step(Step::create().description("Step 2a"))
774                .step(Step::create().description("Step 2b"))
775                .step(Step::create().description("Step 2c")),
776        )
777        .exec(&mut db)
778        .await
779        .unwrap();
780
781    // Load user with nested include: todos AND their steps
782    let user = User::filter_by_id(user.id)
783        .include(User::fields().todos().steps())
784        .get(&mut db)
785        .await
786        .unwrap();
787
788    // Verify todos are loaded
789    let todos = user.todos.get();
790    assert_eq!(2, todos.len());
791
792    // Verify steps are loaded on each todo
793    let mut all_step_descriptions: Vec<&str> = Vec::new();
794    for todo in todos {
795        let steps = todo.steps.get();
796        for step in steps {
797            all_step_descriptions.push(&step.description);
798        }
799    }
800    all_step_descriptions.sort();
801    assert_eq!(
802        all_step_descriptions,
803        vec!["Step 1a", "Step 1b", "Step 2a", "Step 2b", "Step 2c"]
804    );
805}
806
807// ===== HasMany -> HasOne<Option<T>> =====
808// User has_many Posts, each Post has_one optional Detail
809#[driver_test(id(ID))]
810pub async fn nested_has_many_then_has_one_optional(test: &mut Test) -> Result<()> {
811    #[derive(Debug, toasty::Model)]
812    #[allow(dead_code)]
813    struct User {
814        #[key]
815        #[auto]
816        id: ID,
817
818        name: String,
819
820        #[has_many]
821        posts: toasty::HasMany<Post>,
822    }
823
824    #[derive(Debug, toasty::Model)]
825    #[allow(dead_code)]
826    struct Post {
827        #[key]
828        #[auto]
829        id: ID,
830
831        title: String,
832
833        #[index]
834        user_id: ID,
835
836        #[belongs_to(key = user_id, references = id)]
837        user: toasty::BelongsTo<User>,
838
839        #[has_one]
840        detail: toasty::HasOne<Option<Detail>>,
841    }
842
843    #[derive(Debug, toasty::Model)]
844    #[allow(dead_code)]
845    struct Detail {
846        #[key]
847        #[auto]
848        id: ID,
849
850        body: String,
851
852        #[unique]
853        post_id: Option<ID>,
854
855        #[belongs_to(key = post_id, references = id)]
856        post: toasty::BelongsTo<Option<Post>>,
857    }
858
859    let mut db = test.setup_db(models!(User, Post, Detail)).await;
860
861    let user = User::create()
862        .name("Alice")
863        .post(
864            Post::create()
865                .title("P1")
866                .detail(Detail::create().body("D1")),
867        )
868        .post(Post::create().title("P2")) // no detail
869        .exec(&mut db)
870        .await?;
871
872    let user = User::filter_by_id(user.id)
873        .include(User::fields().posts().detail())
874        .get(&mut db)
875        .await?;
876
877    let posts = user.posts.get();
878    assert_eq!(2, posts.len());
879
880    let mut with_detail = 0;
881    let mut without_detail = 0;
882    for post in posts {
883        match post.detail.get() {
884            Some(d) => {
885                assert_eq!("D1", d.body);
886                with_detail += 1;
887            }
888            None => without_detail += 1,
889        }
890    }
891    assert_eq!(1, with_detail);
892    assert_eq!(1, without_detail);
893
894    Ok(())
895}
896
897// ===== HasMany -> HasOne<T> (required) =====
898// User has_many Accounts, each Account has_one required Settings
899#[driver_test(id(ID))]
900pub async fn nested_has_many_then_has_one_required(test: &mut Test) -> Result<()> {
901    #[derive(Debug, toasty::Model)]
902    #[allow(dead_code)]
903    struct User {
904        #[key]
905        #[auto]
906        id: ID,
907
908        name: String,
909
910        #[has_many]
911        accounts: toasty::HasMany<Account>,
912    }
913
914    #[derive(Debug, toasty::Model)]
915    #[allow(dead_code)]
916    struct Account {
917        #[key]
918        #[auto]
919        id: ID,
920
921        label: String,
922
923        #[index]
924        user_id: ID,
925
926        #[belongs_to(key = user_id, references = id)]
927        user: toasty::BelongsTo<User>,
928
929        #[has_one]
930        settings: toasty::HasOne<Settings>,
931    }
932
933    #[derive(Debug, toasty::Model)]
934    #[allow(dead_code)]
935    struct Settings {
936        #[key]
937        #[auto]
938        id: ID,
939
940        theme: String,
941
942        #[unique]
943        account_id: Option<ID>,
944
945        #[belongs_to(key = account_id, references = id)]
946        account: toasty::BelongsTo<Option<Account>>,
947    }
948
949    let mut db = test.setup_db(models!(User, Account, Settings)).await;
950
951    let user = User::create()
952        .name("Bob")
953        .account(
954            Account::create()
955                .label("A1")
956                .settings(Settings::create().theme("dark")),
957        )
958        .account(
959            Account::create()
960                .label("A2")
961                .settings(Settings::create().theme("light")),
962        )
963        .exec(&mut db)
964        .await?;
965
966    let user = User::filter_by_id(user.id)
967        .include(User::fields().accounts().settings())
968        .get(&mut db)
969        .await?;
970
971    let accounts = user.accounts.get();
972    assert_eq!(2, accounts.len());
973
974    let mut themes: Vec<&str> = accounts
975        .iter()
976        .map(|a| a.settings.get().theme.as_str())
977        .collect();
978    themes.sort();
979    assert_eq!(themes, vec!["dark", "light"]);
980
981    Ok(())
982}
983
984// ===== HasMany -> BelongsTo<T> (required) =====
985// Category has_many Items, each Item belongs_to a Brand
986#[driver_test(id(ID))]
987pub async fn nested_has_many_then_belongs_to_required(test: &mut Test) -> Result<()> {
988    #[derive(Debug, toasty::Model)]
989    #[allow(dead_code)]
990    struct Category {
991        #[key]
992        #[auto]
993        id: ID,
994
995        name: String,
996
997        #[has_many]
998        items: toasty::HasMany<Item>,
999    }
1000
1001    #[derive(Debug, toasty::Model)]
1002    #[allow(dead_code)]
1003    struct Brand {
1004        #[key]
1005        #[auto]
1006        id: ID,
1007
1008        name: String,
1009    }
1010
1011    #[derive(Debug, toasty::Model)]
1012    #[allow(dead_code)]
1013    struct Item {
1014        #[key]
1015        #[auto]
1016        id: ID,
1017
1018        title: String,
1019
1020        #[index]
1021        category_id: ID,
1022
1023        #[belongs_to(key = category_id, references = id)]
1024        category: toasty::BelongsTo<Category>,
1025
1026        #[index]
1027        brand_id: ID,
1028
1029        #[belongs_to(key = brand_id, references = id)]
1030        brand: toasty::BelongsTo<Brand>,
1031    }
1032
1033    let mut db = test.setup_db(models!(Category, Brand, Item)).await;
1034
1035    let brand_a = Brand::create().name("BrandA").exec(&mut db).await?;
1036    let brand_b = Brand::create().name("BrandB").exec(&mut db).await?;
1037
1038    let cat = Category::create()
1039        .name("Electronics")
1040        .item(Item::create().title("Phone").brand(&brand_a))
1041        .item(Item::create().title("Laptop").brand(&brand_b))
1042        .exec(&mut db)
1043        .await?;
1044
1045    let cat = Category::filter_by_id(cat.id)
1046        .include(Category::fields().items().brand())
1047        .get(&mut db)
1048        .await?;
1049
1050    let items = cat.items.get();
1051    assert_eq!(2, items.len());
1052
1053    let mut brand_names: Vec<&str> = items.iter().map(|i| i.brand.get().name.as_str()).collect();
1054    brand_names.sort();
1055    assert_eq!(brand_names, vec!["BrandA", "BrandB"]);
1056
1057    Ok(())
1058}
1059
1060// ===== HasMany -> BelongsTo<T> where multiple items share the same target =====
1061// Sibling rows with the same foreign key must not break the nested preload.
1062// Regression for #701: the DDB nested merge used to panic with
1063// "HashIndex: duplicate key detected" when two Items pointed at one Brand.
1064#[driver_test(id(ID))]
1065pub async fn nested_has_many_then_shared_belongs_to(test: &mut Test) -> Result<()> {
1066    #[derive(Debug, toasty::Model)]
1067    #[allow(dead_code)]
1068    struct Category {
1069        #[key]
1070        #[auto]
1071        id: ID,
1072
1073        name: String,
1074
1075        #[has_many]
1076        items: toasty::HasMany<Item>,
1077    }
1078
1079    #[derive(Debug, toasty::Model)]
1080    #[allow(dead_code)]
1081    struct Brand {
1082        #[key]
1083        #[auto]
1084        id: ID,
1085
1086        name: String,
1087    }
1088
1089    #[derive(Debug, toasty::Model)]
1090    #[allow(dead_code)]
1091    struct Item {
1092        #[key]
1093        #[auto]
1094        id: ID,
1095
1096        title: String,
1097
1098        #[index]
1099        category_id: ID,
1100
1101        #[belongs_to(key = category_id, references = id)]
1102        category: toasty::BelongsTo<Category>,
1103
1104        #[index]
1105        brand_id: ID,
1106
1107        #[belongs_to(key = brand_id, references = id)]
1108        brand: toasty::BelongsTo<Brand>,
1109    }
1110
1111    let mut db = test.setup_db(models!(Category, Brand, Item)).await;
1112
1113    let brand = Brand::create().name("BrandA").exec(&mut db).await?;
1114    let cat = Category::create()
1115        .name("Electronics")
1116        .item(Item::create().title("Phone").brand(&brand))
1117        .item(Item::create().title("Laptop").brand(&brand))
1118        .exec(&mut db)
1119        .await?;
1120
1121    let cat = Category::filter_by_id(cat.id)
1122        .include(Category::fields().items().brand())
1123        .get(&mut db)
1124        .await?;
1125
1126    let items = cat.items.get();
1127    assert_eq!(2, items.len());
1128    for item in items {
1129        assert_eq!("BrandA", item.brand.get().name);
1130    }
1131
1132    Ok(())
1133}
1134
1135// ===== HasMany -> BelongsTo<Option<T>> =====
1136// Team has_many Tasks, each Task optionally belongs_to an Assignee
1137#[driver_test(id(ID))]
1138pub async fn nested_has_many_then_belongs_to_optional(test: &mut Test) -> Result<()> {
1139    #[derive(Debug, toasty::Model)]
1140    #[allow(dead_code)]
1141    struct Team {
1142        #[key]
1143        #[auto]
1144        id: ID,
1145
1146        name: String,
1147
1148        #[has_many]
1149        tasks: toasty::HasMany<Task>,
1150    }
1151
1152    #[derive(Debug, toasty::Model)]
1153    #[allow(dead_code)]
1154    struct Assignee {
1155        #[key]
1156        #[auto]
1157        id: ID,
1158
1159        name: String,
1160    }
1161
1162    #[derive(Debug, toasty::Model)]
1163    #[allow(dead_code)]
1164    struct Task {
1165        #[key]
1166        #[auto]
1167        id: ID,
1168
1169        title: String,
1170
1171        #[index]
1172        team_id: ID,
1173
1174        #[belongs_to(key = team_id, references = id)]
1175        team: toasty::BelongsTo<Team>,
1176
1177        #[index]
1178        assignee_id: Option<ID>,
1179
1180        #[belongs_to(key = assignee_id, references = id)]
1181        assignee: toasty::BelongsTo<Option<Assignee>>,
1182    }
1183
1184    let mut db = test.setup_db(models!(Team, Assignee, Task)).await;
1185
1186    let person = Assignee::create().name("Alice").exec(&mut db).await?;
1187
1188    let team = Team::create()
1189        .name("Engineering")
1190        .task(Task::create().title("Assigned").assignee(&person))
1191        .task(Task::create().title("Unassigned"))
1192        .exec(&mut db)
1193        .await?;
1194
1195    let team = Team::filter_by_id(team.id)
1196        .include(Team::fields().tasks().assignee())
1197        .get(&mut db)
1198        .await?;
1199
1200    let tasks = team.tasks.get();
1201    assert_eq!(2, tasks.len());
1202
1203    let mut assigned = 0;
1204    let mut unassigned = 0;
1205    for task in tasks {
1206        match task.assignee.get() {
1207            Some(a) => {
1208                assert_eq!("Alice", a.name);
1209                assigned += 1;
1210            }
1211            None => unassigned += 1,
1212        }
1213    }
1214    assert_eq!(1, assigned);
1215    assert_eq!(1, unassigned);
1216
1217    Ok(())
1218}
1219
1220// ===== HasOne<Option<T>> -> HasMany =====
1221// User has_one optional Profile, Profile has_many Badges
1222#[driver_test(id(ID))]
1223pub async fn nested_has_one_optional_then_has_many(test: &mut Test) -> Result<()> {
1224    #[derive(Debug, toasty::Model)]
1225    #[allow(dead_code)]
1226    struct User {
1227        #[key]
1228        #[auto]
1229        id: ID,
1230
1231        name: String,
1232
1233        #[has_one]
1234        profile: toasty::HasOne<Option<Profile>>,
1235    }
1236
1237    #[derive(Debug, toasty::Model)]
1238    #[allow(dead_code)]
1239    struct Profile {
1240        #[key]
1241        #[auto]
1242        id: ID,
1243
1244        bio: String,
1245
1246        #[unique]
1247        user_id: Option<ID>,
1248
1249        #[belongs_to(key = user_id, references = id)]
1250        user: toasty::BelongsTo<Option<User>>,
1251
1252        #[has_many]
1253        badges: toasty::HasMany<Badge>,
1254    }
1255
1256    #[derive(Debug, toasty::Model)]
1257    #[allow(dead_code)]
1258    struct Badge {
1259        #[key]
1260        #[auto]
1261        id: ID,
1262
1263        label: String,
1264
1265        #[index]
1266        profile_id: ID,
1267
1268        #[belongs_to(key = profile_id, references = id)]
1269        profile: toasty::BelongsTo<Profile>,
1270    }
1271
1272    let mut db = test.setup_db(models!(User, Profile, Badge)).await;
1273
1274    // User with profile and badges
1275    let user = User::create()
1276        .name("Alice")
1277        .profile(
1278            Profile::create()
1279                .bio("hi")
1280                .badge(Badge::create().label("Gold"))
1281                .badge(Badge::create().label("Silver")),
1282        )
1283        .exec(&mut db)
1284        .await?;
1285
1286    let user = User::filter_by_id(user.id)
1287        .include(User::fields().profile().badges())
1288        .get(&mut db)
1289        .await?;
1290
1291    let profile = user.profile.get().as_ref().unwrap();
1292    assert_eq!("hi", profile.bio);
1293    let mut labels: Vec<&str> = profile
1294        .badges
1295        .get()
1296        .iter()
1297        .map(|b| b.label.as_str())
1298        .collect();
1299    labels.sort();
1300    assert_eq!(labels, vec!["Gold", "Silver"]);
1301
1302    // User without profile - nested preload should handle gracefully
1303    let user2 = User::create().name("Bob").exec(&mut db).await?;
1304
1305    let user2 = User::filter_by_id(user2.id)
1306        .include(User::fields().profile().badges())
1307        .get(&mut db)
1308        .await?;
1309
1310    assert!(user2.profile.get().is_none());
1311
1312    Ok(())
1313}
1314
1315// ===== HasOne<T> (required) -> HasMany =====
1316// Order has_one required Invoice, Invoice has_many LineItems
1317#[driver_test(id(ID))]
1318pub async fn nested_has_one_required_then_has_many(test: &mut Test) -> Result<()> {
1319    #[derive(Debug, toasty::Model)]
1320    #[allow(dead_code)]
1321    struct Order {
1322        #[key]
1323        #[auto]
1324        id: ID,
1325
1326        label: String,
1327
1328        #[has_one]
1329        invoice: toasty::HasOne<Invoice>,
1330    }
1331
1332    #[derive(Debug, toasty::Model)]
1333    #[allow(dead_code)]
1334    struct Invoice {
1335        #[key]
1336        #[auto]
1337        id: ID,
1338
1339        code: String,
1340
1341        #[unique]
1342        order_id: Option<ID>,
1343
1344        #[belongs_to(key = order_id, references = id)]
1345        order: toasty::BelongsTo<Option<Order>>,
1346
1347        #[has_many]
1348        line_items: toasty::HasMany<LineItem>,
1349    }
1350
1351    #[derive(Debug, toasty::Model)]
1352    #[allow(dead_code)]
1353    struct LineItem {
1354        #[key]
1355        #[auto]
1356        id: ID,
1357
1358        description: String,
1359
1360        #[index]
1361        invoice_id: ID,
1362
1363        #[belongs_to(key = invoice_id, references = id)]
1364        invoice: toasty::BelongsTo<Invoice>,
1365    }
1366
1367    let mut db = test.setup_db(models!(Order, Invoice, LineItem)).await;
1368
1369    let order = Order::create()
1370        .label("Order1")
1371        .invoice(
1372            Invoice::create()
1373                .code("INV-001")
1374                .line_item(LineItem::create().description("Widget"))
1375                .line_item(LineItem::create().description("Gadget")),
1376        )
1377        .exec(&mut db)
1378        .await?;
1379
1380    let order = Order::filter_by_id(order.id)
1381        .include(Order::fields().invoice().line_items())
1382        .get(&mut db)
1383        .await?;
1384
1385    let invoice = order.invoice.get();
1386    assert_eq!("INV-001", invoice.code);
1387    let mut descs: Vec<&str> = invoice
1388        .line_items
1389        .get()
1390        .iter()
1391        .map(|li| li.description.as_str())
1392        .collect();
1393    descs.sort();
1394    assert_eq!(descs, vec!["Gadget", "Widget"]);
1395
1396    Ok(())
1397}
1398
1399// ===== HasOne<Option<T>> -> HasOne<Option<T>> =====
1400// User has_one optional Profile, Profile has_one optional Avatar
1401#[driver_test(id(ID))]
1402pub async fn nested_has_one_optional_then_has_one_optional(test: &mut Test) -> Result<()> {
1403    #[derive(Debug, toasty::Model)]
1404    #[allow(dead_code)]
1405    struct User {
1406        #[key]
1407        #[auto]
1408        id: ID,
1409
1410        name: String,
1411
1412        #[has_one]
1413        profile: toasty::HasOne<Option<Profile>>,
1414    }
1415
1416    #[derive(Debug, toasty::Model)]
1417    #[allow(dead_code)]
1418    struct Profile {
1419        #[key]
1420        #[auto]
1421        id: ID,
1422
1423        bio: String,
1424
1425        #[unique]
1426        user_id: Option<ID>,
1427
1428        #[belongs_to(key = user_id, references = id)]
1429        user: toasty::BelongsTo<Option<User>>,
1430
1431        #[has_one]
1432        avatar: toasty::HasOne<Option<Avatar>>,
1433    }
1434
1435    #[derive(Debug, toasty::Model)]
1436    #[allow(dead_code)]
1437    struct Avatar {
1438        #[key]
1439        #[auto]
1440        id: ID,
1441
1442        url: String,
1443
1444        #[unique]
1445        profile_id: Option<ID>,
1446
1447        #[belongs_to(key = profile_id, references = id)]
1448        profile: toasty::BelongsTo<Option<Profile>>,
1449    }
1450
1451    let mut db = test.setup_db(models!(User, Profile, Avatar)).await;
1452
1453    // User -> Profile -> Avatar (all present)
1454    let user = User::create()
1455        .name("Alice")
1456        .profile(
1457            Profile::create()
1458                .bio("hi")
1459                .avatar(Avatar::create().url("pic.png")),
1460        )
1461        .exec(&mut db)
1462        .await?;
1463
1464    let user = User::filter_by_id(user.id)
1465        .include(User::fields().profile().avatar())
1466        .get(&mut db)
1467        .await?;
1468
1469    let profile = user.profile.get().as_ref().unwrap();
1470    assert_eq!("hi", profile.bio);
1471    let avatar = profile.avatar.get().as_ref().unwrap();
1472    assert_eq!("pic.png", avatar.url);
1473
1474    // User -> Profile (present) -> Avatar (missing)
1475    let user2 = User::create()
1476        .name("Bob")
1477        .profile(Profile::create().bio("no pic"))
1478        .exec(&mut db)
1479        .await?;
1480
1481    let user2 = User::filter_by_id(user2.id)
1482        .include(User::fields().profile().avatar())
1483        .get(&mut db)
1484        .await?;
1485
1486    let profile2 = user2.profile.get().as_ref().unwrap();
1487    assert_eq!("no pic", profile2.bio);
1488    assert!(profile2.avatar.get().is_none());
1489
1490    // User -> Profile (missing) - nested preload short-circuits
1491    let user3 = User::create().name("Carol").exec(&mut db).await?;
1492
1493    let user3 = User::filter_by_id(user3.id)
1494        .include(User::fields().profile().avatar())
1495        .get(&mut db)
1496        .await?;
1497
1498    assert!(user3.profile.get().is_none());
1499
1500    Ok(())
1501}
1502
1503// ===== HasOne<T> (required) -> HasOne<T> (required) =====
1504// User has_one required Profile, Profile has_one required Avatar
1505#[driver_test(id(ID))]
1506pub async fn nested_has_one_required_then_has_one_required(test: &mut Test) -> Result<()> {
1507    #[derive(Debug, toasty::Model)]
1508    #[allow(dead_code)]
1509    struct User {
1510        #[key]
1511        #[auto]
1512        id: ID,
1513
1514        name: String,
1515
1516        #[has_one]
1517        profile: toasty::HasOne<Profile>,
1518    }
1519
1520    #[derive(Debug, toasty::Model)]
1521    #[allow(dead_code)]
1522    struct Profile {
1523        #[key]
1524        #[auto]
1525        id: ID,
1526
1527        bio: String,
1528
1529        #[unique]
1530        user_id: Option<ID>,
1531
1532        #[belongs_to(key = user_id, references = id)]
1533        user: toasty::BelongsTo<Option<User>>,
1534
1535        #[has_one]
1536        avatar: toasty::HasOne<Avatar>,
1537    }
1538
1539    #[derive(Debug, toasty::Model)]
1540    #[allow(dead_code)]
1541    struct Avatar {
1542        #[key]
1543        #[auto]
1544        id: ID,
1545
1546        url: String,
1547
1548        #[unique]
1549        profile_id: Option<ID>,
1550
1551        #[belongs_to(key = profile_id, references = id)]
1552        profile: toasty::BelongsTo<Option<Profile>>,
1553    }
1554
1555    let mut db = test.setup_db(models!(User, Profile, Avatar)).await;
1556
1557    let user = User::create()
1558        .name("Alice")
1559        .profile(
1560            Profile::create()
1561                .bio("engineer")
1562                .avatar(Avatar::create().url("alice.jpg")),
1563        )
1564        .exec(&mut db)
1565        .await?;
1566
1567    let user = User::filter_by_id(user.id)
1568        .include(User::fields().profile().avatar())
1569        .get(&mut db)
1570        .await?;
1571
1572    let profile = user.profile.get();
1573    assert_eq!("engineer", profile.bio);
1574    let avatar = profile.avatar.get();
1575    assert_eq!("alice.jpg", avatar.url);
1576
1577    Ok(())
1578}
1579
1580// ===== HasOne<Option<T>> -> BelongsTo<T> (required) =====
1581// User has_one optional Review, Review belongs_to a Product
1582#[driver_test(id(ID))]
1583pub async fn nested_has_one_optional_then_belongs_to_required(test: &mut Test) -> Result<()> {
1584    #[derive(Debug, toasty::Model)]
1585    #[allow(dead_code)]
1586    struct User {
1587        #[key]
1588        #[auto]
1589        id: ID,
1590
1591        name: String,
1592
1593        #[has_one]
1594        review: toasty::HasOne<Option<Review>>,
1595    }
1596
1597    #[derive(Debug, toasty::Model)]
1598    #[allow(dead_code)]
1599    struct Product {
1600        #[key]
1601        #[auto]
1602        id: ID,
1603
1604        name: String,
1605    }
1606
1607    #[derive(Debug, toasty::Model)]
1608    #[allow(dead_code)]
1609    struct Review {
1610        #[key]
1611        #[auto]
1612        id: ID,
1613
1614        body: String,
1615
1616        #[unique]
1617        user_id: Option<ID>,
1618
1619        #[belongs_to(key = user_id, references = id)]
1620        user: toasty::BelongsTo<Option<User>>,
1621
1622        #[index]
1623        product_id: ID,
1624
1625        #[belongs_to(key = product_id, references = id)]
1626        product: toasty::BelongsTo<Product>,
1627    }
1628
1629    let mut db = test.setup_db(models!(User, Product, Review)).await;
1630
1631    let product = Product::create().name("Widget").exec(&mut db).await?;
1632
1633    let user = User::create()
1634        .name("Alice")
1635        .review(Review::create().body("Great!").product(&product))
1636        .exec(&mut db)
1637        .await?;
1638
1639    // User with review -> preload nested product
1640    let user = User::filter_by_id(user.id)
1641        .include(User::fields().review().product())
1642        .get(&mut db)
1643        .await?;
1644
1645    let review = user.review.get().as_ref().unwrap();
1646    assert_eq!("Great!", review.body);
1647    assert_eq!("Widget", review.product.get().name);
1648
1649    // User without review
1650    let user2 = User::create().name("Bob").exec(&mut db).await?;
1651
1652    let user2 = User::filter_by_id(user2.id)
1653        .include(User::fields().review().product())
1654        .get(&mut db)
1655        .await?;
1656
1657    assert!(user2.review.get().is_none());
1658
1659    Ok(())
1660}
1661
1662// ===== BelongsTo<T> (required) -> HasMany =====
1663// Comment belongs_to a Post, Post has_many Tags
1664#[driver_test(id(ID))]
1665pub async fn nested_belongs_to_required_then_has_many(test: &mut Test) -> Result<()> {
1666    #[derive(Debug, toasty::Model)]
1667    #[allow(dead_code)]
1668    struct Post {
1669        #[key]
1670        #[auto]
1671        id: ID,
1672
1673        title: String,
1674
1675        #[has_many]
1676        tags: toasty::HasMany<Tag>,
1677    }
1678
1679    #[derive(Debug, toasty::Model)]
1680    #[allow(dead_code)]
1681    struct Tag {
1682        #[key]
1683        #[auto]
1684        id: ID,
1685
1686        label: String,
1687
1688        #[index]
1689        post_id: ID,
1690
1691        #[belongs_to(key = post_id, references = id)]
1692        post: toasty::BelongsTo<Post>,
1693    }
1694
1695    #[derive(Debug, toasty::Model)]
1696    #[allow(dead_code)]
1697    struct Comment {
1698        #[key]
1699        #[auto]
1700        id: ID,
1701
1702        body: String,
1703
1704        #[index]
1705        post_id: ID,
1706
1707        #[belongs_to(key = post_id, references = id)]
1708        post: toasty::BelongsTo<Post>,
1709    }
1710
1711    let mut db = test.setup_db(models!(Post, Tag, Comment)).await;
1712
1713    let post = Post::create()
1714        .title("Hello")
1715        .tag(Tag::create().label("rust"))
1716        .tag(Tag::create().label("orm"))
1717        .exec(&mut db)
1718        .await?;
1719
1720    let comment = Comment::create()
1721        .body("Nice post")
1722        .post(&post)
1723        .exec(&mut db)
1724        .await?;
1725
1726    // From comment, preload post's tags
1727    let comment = Comment::filter_by_id(comment.id)
1728        .include(Comment::fields().post().tags())
1729        .get(&mut db)
1730        .await?;
1731
1732    assert_eq!("Hello", comment.post.get().title);
1733    let mut labels: Vec<&str> = comment
1734        .post
1735        .get()
1736        .tags
1737        .get()
1738        .iter()
1739        .map(|t| t.label.as_str())
1740        .collect();
1741    labels.sort();
1742    assert_eq!(labels, vec!["orm", "rust"]);
1743
1744    Ok(())
1745}
1746
1747// ===== BelongsTo<T> (required) -> HasOne<Option<T>> =====
1748// Todo belongs_to a User, User has_one optional Profile
1749#[driver_test(id(ID))]
1750pub async fn nested_belongs_to_required_then_has_one_optional(test: &mut Test) -> Result<()> {
1751    #[derive(Debug, toasty::Model)]
1752    #[allow(dead_code)]
1753    struct User {
1754        #[key]
1755        #[auto]
1756        id: ID,
1757
1758        name: String,
1759
1760        #[has_one]
1761        profile: toasty::HasOne<Option<Profile>>,
1762
1763        #[has_many]
1764        todos: toasty::HasMany<Todo>,
1765    }
1766
1767    #[derive(Debug, toasty::Model)]
1768    #[allow(dead_code)]
1769    struct Profile {
1770        #[key]
1771        #[auto]
1772        id: ID,
1773
1774        bio: String,
1775
1776        #[unique]
1777        user_id: Option<ID>,
1778
1779        #[belongs_to(key = user_id, references = id)]
1780        user: toasty::BelongsTo<Option<User>>,
1781    }
1782
1783    #[derive(Debug, toasty::Model)]
1784    #[allow(dead_code)]
1785    struct Todo {
1786        #[key]
1787        #[auto]
1788        id: ID,
1789
1790        title: String,
1791
1792        #[index]
1793        user_id: ID,
1794
1795        #[belongs_to(key = user_id, references = id)]
1796        user: toasty::BelongsTo<User>,
1797    }
1798
1799    let mut db = test.setup_db(models!(User, Profile, Todo)).await;
1800
1801    // User with profile
1802    let user = User::create()
1803        .name("Alice")
1804        .profile(Profile::create().bio("developer"))
1805        .todo(Todo::create().title("Task 1"))
1806        .exec(&mut db)
1807        .await?;
1808
1809    let todo_id = Todo::get_by_user_id(&mut db, user.id).await?.id;
1810
1811    let todo = Todo::filter_by_id(todo_id)
1812        .include(Todo::fields().user().profile())
1813        .get(&mut db)
1814        .await?;
1815
1816    assert_eq!("Alice", todo.user.get().name);
1817    let profile = todo.user.get().profile.get().as_ref().unwrap();
1818    assert_eq!("developer", profile.bio);
1819
1820    // User without profile
1821    let user2 = User::create()
1822        .name("Bob")
1823        .todo(Todo::create().title("Task 2"))
1824        .exec(&mut db)
1825        .await?;
1826
1827    let todo2_id = Todo::get_by_user_id(&mut db, user2.id).await?.id;
1828
1829    let todo2 = Todo::filter_by_id(todo2_id)
1830        .include(Todo::fields().user().profile())
1831        .get(&mut db)
1832        .await?;
1833
1834    assert_eq!("Bob", todo2.user.get().name);
1835    assert!(todo2.user.get().profile.get().is_none());
1836
1837    Ok(())
1838}
1839
1840// ===== BelongsTo<T> (required) -> BelongsTo<T> (required) =====
1841// Step belongs_to a Todo, Todo belongs_to a User (chain of belongs_to going up)
1842#[driver_test(id(ID))]
1843pub async fn nested_belongs_to_required_then_belongs_to_required(test: &mut Test) -> Result<()> {
1844    #[derive(Debug, toasty::Model)]
1845    #[allow(dead_code)]
1846    struct User {
1847        #[key]
1848        #[auto]
1849        id: ID,
1850
1851        name: String,
1852
1853        #[has_many]
1854        todos: toasty::HasMany<Todo>,
1855    }
1856
1857    #[derive(Debug, toasty::Model)]
1858    #[allow(dead_code)]
1859    struct Todo {
1860        #[key]
1861        #[auto]
1862        id: ID,
1863
1864        title: String,
1865
1866        #[index]
1867        user_id: ID,
1868
1869        #[belongs_to(key = user_id, references = id)]
1870        user: toasty::BelongsTo<User>,
1871
1872        #[has_many]
1873        steps: toasty::HasMany<Step>,
1874    }
1875
1876    #[derive(Debug, toasty::Model)]
1877    #[allow(dead_code)]
1878    struct Step {
1879        #[key]
1880        #[auto]
1881        id: ID,
1882
1883        description: String,
1884
1885        #[index]
1886        todo_id: ID,
1887
1888        #[belongs_to(key = todo_id, references = id)]
1889        todo: toasty::BelongsTo<Todo>,
1890    }
1891
1892    let mut db = test.setup_db(models!(User, Todo, Step)).await;
1893
1894    let user = User::create()
1895        .name("Alice")
1896        .todo(
1897            Todo::create()
1898                .title("T1")
1899                .step(Step::create().description("S1")),
1900        )
1901        .exec(&mut db)
1902        .await?;
1903
1904    let todo_id = Todo::get_by_user_id(&mut db, user.id).await?.id;
1905    let step_id = Step::get_by_todo_id(&mut db, todo_id).await?.id;
1906
1907    // From step, preload todo and then todo's user
1908    let step = Step::filter_by_id(step_id)
1909        .include(Step::fields().todo().user())
1910        .get(&mut db)
1911        .await?;
1912
1913    assert_eq!("T1", step.todo.get().title);
1914    assert_eq!("Alice", step.todo.get().user.get().name);
1915
1916    Ok(())
1917}
1918
1919// ===== BelongsTo<Option<T>> -> HasMany =====
1920// Task optionally belongs_to a Project, Project has_many Members
1921#[driver_test(id(ID))]
1922pub async fn nested_belongs_to_optional_then_has_many(test: &mut Test) -> Result<()> {
1923    #[derive(Debug, toasty::Model)]
1924    #[allow(dead_code)]
1925    struct Project {
1926        #[key]
1927        #[auto]
1928        id: ID,
1929
1930        name: String,
1931
1932        #[has_many]
1933        members: toasty::HasMany<Member>,
1934    }
1935
1936    #[derive(Debug, toasty::Model)]
1937    #[allow(dead_code)]
1938    struct Member {
1939        #[key]
1940        #[auto]
1941        id: ID,
1942
1943        name: String,
1944
1945        #[index]
1946        project_id: ID,
1947
1948        #[belongs_to(key = project_id, references = id)]
1949        project: toasty::BelongsTo<Project>,
1950    }
1951
1952    #[derive(Debug, toasty::Model)]
1953    #[allow(dead_code)]
1954    struct Task {
1955        #[key]
1956        #[auto]
1957        id: ID,
1958
1959        title: String,
1960
1961        #[index]
1962        project_id: Option<ID>,
1963
1964        #[belongs_to(key = project_id, references = id)]
1965        project: toasty::BelongsTo<Option<Project>>,
1966    }
1967
1968    let mut db = test.setup_db(models!(Project, Member, Task)).await;
1969
1970    let project = Project::create()
1971        .name("Proj1")
1972        .member(Member::create().name("Alice"))
1973        .member(Member::create().name("Bob"))
1974        .exec(&mut db)
1975        .await?;
1976
1977    // Task with project
1978    let task = Task::create()
1979        .title("Linked")
1980        .project(&project)
1981        .exec(&mut db)
1982        .await?;
1983
1984    let task = Task::filter_by_id(task.id)
1985        .include(Task::fields().project().members())
1986        .get(&mut db)
1987        .await?;
1988
1989    let proj = task.project.get().as_ref().unwrap();
1990    assert_eq!("Proj1", proj.name);
1991    let mut names: Vec<&str> = proj.members.get().iter().map(|m| m.name.as_str()).collect();
1992    names.sort();
1993    assert_eq!(names, vec!["Alice", "Bob"]);
1994
1995    // Task without project
1996    let orphan = Task::create().title("Orphan").exec(&mut db).await?;
1997
1998    let orphan = Task::filter_by_id(orphan.id)
1999        .include(Task::fields().project().members())
2000        .get(&mut db)
2001        .await?;
2002
2003    assert!(orphan.project.get().is_none());
2004
2005    Ok(())
2006}
2007
2008// ===== BelongsTo<Option<T>> -> BelongsTo<Option<T>> =====
2009// Comment optionally belongs_to a Post, Post optionally belongs_to a Category
2010#[driver_test(id(ID))]
2011pub async fn nested_belongs_to_optional_then_belongs_to_optional(test: &mut Test) -> Result<()> {
2012    #[derive(Debug, toasty::Model)]
2013    #[allow(dead_code)]
2014    struct Category {
2015        #[key]
2016        #[auto]
2017        id: ID,
2018
2019        name: String,
2020    }
2021
2022    #[derive(Debug, toasty::Model)]
2023    #[allow(dead_code)]
2024    struct Post {
2025        #[key]
2026        #[auto]
2027        id: ID,
2028
2029        title: String,
2030
2031        #[index]
2032        category_id: Option<ID>,
2033
2034        #[belongs_to(key = category_id, references = id)]
2035        category: toasty::BelongsTo<Option<Category>>,
2036    }
2037
2038    #[derive(Debug, toasty::Model)]
2039    #[allow(dead_code)]
2040    struct Comment {
2041        #[key]
2042        #[auto]
2043        id: ID,
2044
2045        body: String,
2046
2047        #[index]
2048        post_id: Option<ID>,
2049
2050        #[belongs_to(key = post_id, references = id)]
2051        post: toasty::BelongsTo<Option<Post>>,
2052    }
2053
2054    let mut db = test.setup_db(models!(Category, Post, Comment)).await;
2055
2056    let cat = Category::create().name("Tech").exec(&mut db).await?;
2057    let post = Post::create()
2058        .title("Hello")
2059        .category(&cat)
2060        .exec(&mut db)
2061        .await?;
2062
2063    // Comment -> Post (present) -> Category (present)
2064    let c1 = Comment::create()
2065        .body("Nice")
2066        .post(&post)
2067        .exec(&mut db)
2068        .await?;
2069
2070    let c1 = Comment::filter_by_id(c1.id)
2071        .include(Comment::fields().post().category())
2072        .get(&mut db)
2073        .await?;
2074
2075    let loaded_post = c1.post.get().as_ref().unwrap();
2076    assert_eq!("Hello", loaded_post.title);
2077    let loaded_cat = loaded_post.category.get().as_ref().unwrap();
2078    assert_eq!("Tech", loaded_cat.name);
2079
2080    // Post without category
2081    let post2 = Post::create().title("Uncategorized").exec(&mut db).await?;
2082    let c2 = Comment::create()
2083        .body("Hmm")
2084        .post(&post2)
2085        .exec(&mut db)
2086        .await?;
2087
2088    let c2 = Comment::filter_by_id(c2.id)
2089        .include(Comment::fields().post().category())
2090        .get(&mut db)
2091        .await?;
2092
2093    let loaded_post2 = c2.post.get().as_ref().unwrap();
2094    assert_eq!("Uncategorized", loaded_post2.title);
2095    assert!(loaded_post2.category.get().is_none());
2096
2097    // Comment without post
2098    let c3 = Comment::create().body("Orphan").exec(&mut db).await?;
2099
2100    let c3 = Comment::filter_by_id(c3.id)
2101        .include(Comment::fields().post().category())
2102        .get(&mut db)
2103        .await?;
2104
2105    assert!(c3.post.get().is_none());
2106
2107    Ok(())
2108}
2109
2110// ===== BelongsTo<T> -> HasOne<T> (required) =====
2111// Todo belongs_to a User, User has_one required Config
2112#[driver_test(id(ID))]
2113pub async fn nested_belongs_to_required_then_has_one_required(test: &mut Test) -> Result<()> {
2114    #[derive(Debug, toasty::Model)]
2115    #[allow(dead_code)]
2116    struct User {
2117        #[key]
2118        #[auto]
2119        id: ID,
2120
2121        name: String,
2122
2123        #[has_one]
2124        config: toasty::HasOne<Config>,
2125
2126        #[has_many]
2127        todos: toasty::HasMany<Todo>,
2128    }
2129
2130    #[derive(Debug, toasty::Model)]
2131    #[allow(dead_code)]
2132    struct Config {
2133        #[key]
2134        #[auto]
2135        id: ID,
2136
2137        theme: String,
2138
2139        #[unique]
2140        user_id: Option<ID>,
2141
2142        #[belongs_to(key = user_id, references = id)]
2143        user: toasty::BelongsTo<Option<User>>,
2144    }
2145
2146    #[derive(Debug, toasty::Model)]
2147    #[allow(dead_code)]
2148    struct Todo {
2149        #[key]
2150        #[auto]
2151        id: ID,
2152
2153        title: String,
2154
2155        #[index]
2156        user_id: ID,
2157
2158        #[belongs_to(key = user_id, references = id)]
2159        user: toasty::BelongsTo<User>,
2160    }
2161
2162    let mut db = test.setup_db(models!(User, Config, Todo)).await;
2163
2164    let user = User::create()
2165        .name("Alice")
2166        .config(Config::create().theme("dark"))
2167        .todo(Todo::create().title("Task"))
2168        .exec(&mut db)
2169        .await?;
2170
2171    let todo_id = Todo::get_by_user_id(&mut db, user.id).await?.id;
2172
2173    let todo = Todo::filter_by_id(todo_id)
2174        .include(Todo::fields().user().config())
2175        .get(&mut db)
2176        .await?;
2177
2178    assert_eq!("Alice", todo.user.get().name);
2179    assert_eq!("dark", todo.user.get().config.get().theme);
2180
2181    Ok(())
2182}
2183
2184// ===== HasMany -> HasMany (with empty nested collections) =====
2185// Ensures that when some parents have children and others don't, nested preload
2186// correctly assigns empty collections rather than panicking.
2187#[driver_test(id(ID))]
2188pub async fn nested_has_many_then_has_many_with_empty_leaves(test: &mut Test) {
2189    #[derive(Debug, toasty::Model)]
2190    #[allow(dead_code)]
2191    struct User {
2192        #[key]
2193        #[auto]
2194        id: ID,
2195
2196        name: String,
2197
2198        #[has_many]
2199        todos: toasty::HasMany<Todo>,
2200    }
2201
2202    #[derive(Debug, toasty::Model)]
2203    #[allow(dead_code)]
2204    struct Todo {
2205        #[key]
2206        #[auto]
2207        id: ID,
2208
2209        title: String,
2210
2211        #[index]
2212        user_id: ID,
2213
2214        #[belongs_to(key = user_id, references = id)]
2215        user: toasty::BelongsTo<User>,
2216
2217        #[has_many]
2218        steps: toasty::HasMany<Step>,
2219    }
2220
2221    #[derive(Debug, toasty::Model)]
2222    #[allow(dead_code)]
2223    struct Step {
2224        #[key]
2225        #[auto]
2226        id: ID,
2227
2228        description: String,
2229
2230        #[index]
2231        todo_id: ID,
2232
2233        #[belongs_to(key = todo_id, references = id)]
2234        todo: toasty::BelongsTo<Todo>,
2235    }
2236
2237    let mut db = test.setup_db(models!(User, Todo, Step)).await;
2238
2239    let user = User::create()
2240        .name("Alice")
2241        .todo(
2242            Todo::create()
2243                .title("With Steps")
2244                .step(Step::create().description("S1")),
2245        )
2246        .todo(Todo::create().title("No Steps")) // empty nested
2247        .exec(&mut db)
2248        .await
2249        .unwrap();
2250
2251    let user = User::filter_by_id(user.id)
2252        .include(User::fields().todos().steps())
2253        .get(&mut db)
2254        .await
2255        .unwrap();
2256
2257    let todos = user.todos.get();
2258    assert_eq!(2, todos.len());
2259
2260    let mut total_steps = 0;
2261    for todo in todos {
2262        let steps = todo.steps.get();
2263        if todo.title == "With Steps" {
2264            assert_eq!(1, steps.len());
2265            assert_eq!("S1", steps[0].description);
2266        } else {
2267            assert_eq!(0, steps.len());
2268        }
2269        total_steps += steps.len();
2270    }
2271    assert_eq!(1, total_steps);
2272}
2273
2274// ===== Issue #691: multiple nested includes sharing a prefix =====
2275// When several `.include()` calls share a common prefix (e.g. `todos()`), each
2276// sibling nested include must be preserved — previously the second overwrote
2277// the first at the shared field slot.
2278#[driver_test(
2279    id(ID),
2280    requires(sql),
2281    scenario(crate::scenarios::has_many_multi_relation)
2282)]
2283pub async fn sibling_nested_includes_on_shared_prefix(test: &mut Test) -> Result<()> {
2284    let mut db = setup(test).await;
2285
2286    let category = toasty::create!(Category { name: "Food" })
2287        .exec(&mut db)
2288        .await?;
2289    let user = toasty::create!(User {
2290        name: "Alice",
2291        todos: [
2292            { title: "T1", category: &category },
2293            { title: "T2", category: &category },
2294        ],
2295    })
2296    .exec(&mut db)
2297    .await?;
2298
2299    // Two sibling nested includes under the `todos()` prefix. Both must be
2300    // preloaded — neither should be silently clobbered by the other.
2301    let loaded = User::filter_by_id(user.id)
2302        .include(User::fields().todos().user())
2303        .include(User::fields().todos().category())
2304        .get(&mut db)
2305        .await?;
2306
2307    let todos = loaded.todos.get();
2308    assert_eq!(2, todos.len());
2309    for todo in todos {
2310        assert_eq!("Alice", todo.user.get().name);
2311        assert_eq!("Food", todo.category.get().name);
2312    }
2313
2314    Ok(())
2315}
2316
2317// Mirrors the exact pattern from issue #691: a bare top-level include plus
2318// two sibling nested includes sharing that same top-level prefix. All three
2319// paths must be honored.
2320#[driver_test(
2321    id(ID),
2322    requires(sql),
2323    scenario(crate::scenarios::has_many_multi_relation)
2324)]
2325pub async fn bare_and_nested_includes_on_shared_prefix(test: &mut Test) -> Result<()> {
2326    let mut db = setup(test).await;
2327
2328    let category = toasty::create!(Category { name: "Food" })
2329        .exec(&mut db)
2330        .await?;
2331    let user = toasty::create!(User {
2332        name: "Alice",
2333        todos: [{ title: "T1", category: &category }],
2334    })
2335    .exec(&mut db)
2336    .await?;
2337
2338    let loaded = User::filter_by_id(user.id)
2339        .include(User::fields().todos()) // bare
2340        .include(User::fields().todos().user()) // sibling 1
2341        .include(User::fields().todos().category()) // sibling 2
2342        .get(&mut db)
2343        .await?;
2344
2345    let todos = loaded.todos.get();
2346    assert_eq!(1, todos.len());
2347    assert_eq!("Alice", todos[0].user.get().name);
2348    assert_eq!("Food", todos[0].category.get().name);
2349
2350    Ok(())
2351}
2352
2353// DDB-compatible variant of the issue #691 repro. Two sibling nested includes
2354// under `items()` — each item has a distinct brand and supplier so the
2355// per-item belongs_to batches stay unique (DDB's nested merge uses a HashIndex
2356// that requires unique keys).
2357#[driver_test(id(ID))]
2358pub async fn sibling_nested_includes_on_shared_prefix_non_sql(test: &mut Test) -> Result<()> {
2359    #[derive(Debug, toasty::Model)]
2360    #[allow(dead_code)]
2361    struct Category {
2362        #[key]
2363        #[auto]
2364        id: ID,
2365
2366        name: String,
2367
2368        #[has_many]
2369        items: toasty::HasMany<Item>,
2370    }
2371
2372    #[derive(Debug, toasty::Model)]
2373    #[allow(dead_code)]
2374    struct Brand {
2375        #[key]
2376        #[auto]
2377        id: ID,
2378
2379        name: String,
2380    }
2381
2382    #[derive(Debug, toasty::Model)]
2383    #[allow(dead_code)]
2384    struct Supplier {
2385        #[key]
2386        #[auto]
2387        id: ID,
2388
2389        name: String,
2390    }
2391
2392    #[derive(Debug, toasty::Model)]
2393    #[allow(dead_code)]
2394    struct Item {
2395        #[key]
2396        #[auto]
2397        id: ID,
2398
2399        title: String,
2400
2401        #[index]
2402        category_id: ID,
2403
2404        #[belongs_to(key = category_id, references = id)]
2405        category: toasty::BelongsTo<Category>,
2406
2407        #[index]
2408        brand_id: ID,
2409
2410        #[belongs_to(key = brand_id, references = id)]
2411        brand: toasty::BelongsTo<Brand>,
2412
2413        #[index]
2414        supplier_id: ID,
2415
2416        #[belongs_to(key = supplier_id, references = id)]
2417        supplier: toasty::BelongsTo<Supplier>,
2418    }
2419
2420    let mut db = test
2421        .setup_db(models!(Category, Brand, Supplier, Item))
2422        .await;
2423
2424    let brand_a = toasty::create!(Brand { name: "BrandA" })
2425        .exec(&mut db)
2426        .await?;
2427    let brand_b = toasty::create!(Brand { name: "BrandB" })
2428        .exec(&mut db)
2429        .await?;
2430    let sup_a = toasty::create!(Supplier { name: "SupA" })
2431        .exec(&mut db)
2432        .await?;
2433    let sup_b = toasty::create!(Supplier { name: "SupB" })
2434        .exec(&mut db)
2435        .await?;
2436
2437    let cat = toasty::create!(Category {
2438        name: "Electronics",
2439        items: [
2440            { title: "Phone", brand: &brand_a, supplier: &sup_a },
2441            { title: "Laptop", brand: &brand_b, supplier: &sup_b },
2442        ],
2443    })
2444    .exec(&mut db)
2445    .await?;
2446
2447    // Two sibling nested includes under the `items()` prefix. Without the
2448    // fix, the second would overwrite the first.
2449    let loaded = Category::filter_by_id(cat.id)
2450        .include(Category::fields().items().brand())
2451        .include(Category::fields().items().supplier())
2452        .get(&mut db)
2453        .await?;
2454
2455    let items = loaded.items.get();
2456    assert_eq!(2, items.len());
2457    let mut pairs: Vec<(&str, &str)> = items
2458        .iter()
2459        .map(|i| (i.brand.get().name.as_str(), i.supplier.get().name.as_str()))
2460        .collect();
2461    pairs.sort();
2462    assert_eq!(pairs, vec![("BrandA", "SupA"), ("BrandB", "SupB")]);
2463
2464    Ok(())
2465}