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