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<Option<T>> =====
1057// Team has_many Tasks, each Task optionally belongs_to an Assignee
1058#[driver_test(id(ID))]
1059pub async fn nested_has_many_then_belongs_to_optional(test: &mut Test) -> Result<()> {
1060    #[derive(Debug, toasty::Model)]
1061    #[allow(dead_code)]
1062    struct Team {
1063        #[key]
1064        #[auto]
1065        id: ID,
1066
1067        name: String,
1068
1069        #[has_many]
1070        tasks: toasty::HasMany<Task>,
1071    }
1072
1073    #[derive(Debug, toasty::Model)]
1074    #[allow(dead_code)]
1075    struct Assignee {
1076        #[key]
1077        #[auto]
1078        id: ID,
1079
1080        name: String,
1081    }
1082
1083    #[derive(Debug, toasty::Model)]
1084    #[allow(dead_code)]
1085    struct Task {
1086        #[key]
1087        #[auto]
1088        id: ID,
1089
1090        title: String,
1091
1092        #[index]
1093        team_id: ID,
1094
1095        #[belongs_to(key = team_id, references = id)]
1096        team: toasty::BelongsTo<Team>,
1097
1098        #[index]
1099        assignee_id: Option<ID>,
1100
1101        #[belongs_to(key = assignee_id, references = id)]
1102        assignee: toasty::BelongsTo<Option<Assignee>>,
1103    }
1104
1105    let mut db = test.setup_db(models!(Team, Assignee, Task)).await;
1106
1107    let person = Assignee::create().name("Alice").exec(&mut db).await?;
1108
1109    let team = Team::create()
1110        .name("Engineering")
1111        .task(Task::create().title("Assigned").assignee(&person))
1112        .task(Task::create().title("Unassigned"))
1113        .exec(&mut db)
1114        .await?;
1115
1116    let team = Team::filter_by_id(team.id)
1117        .include(Team::fields().tasks().assignee())
1118        .get(&mut db)
1119        .await?;
1120
1121    let tasks = team.tasks.get();
1122    assert_eq!(2, tasks.len());
1123
1124    let mut assigned = 0;
1125    let mut unassigned = 0;
1126    for task in tasks {
1127        match task.assignee.get() {
1128            Some(a) => {
1129                assert_eq!("Alice", a.name);
1130                assigned += 1;
1131            }
1132            None => unassigned += 1,
1133        }
1134    }
1135    assert_eq!(1, assigned);
1136    assert_eq!(1, unassigned);
1137
1138    Ok(())
1139}
1140
1141// ===== HasOne<Option<T>> -> HasMany =====
1142// User has_one optional Profile, Profile has_many Badges
1143#[driver_test(id(ID))]
1144pub async fn nested_has_one_optional_then_has_many(test: &mut Test) -> Result<()> {
1145    #[derive(Debug, toasty::Model)]
1146    #[allow(dead_code)]
1147    struct User {
1148        #[key]
1149        #[auto]
1150        id: ID,
1151
1152        name: String,
1153
1154        #[has_one]
1155        profile: toasty::HasOne<Option<Profile>>,
1156    }
1157
1158    #[derive(Debug, toasty::Model)]
1159    #[allow(dead_code)]
1160    struct Profile {
1161        #[key]
1162        #[auto]
1163        id: ID,
1164
1165        bio: String,
1166
1167        #[unique]
1168        user_id: Option<ID>,
1169
1170        #[belongs_to(key = user_id, references = id)]
1171        user: toasty::BelongsTo<Option<User>>,
1172
1173        #[has_many]
1174        badges: toasty::HasMany<Badge>,
1175    }
1176
1177    #[derive(Debug, toasty::Model)]
1178    #[allow(dead_code)]
1179    struct Badge {
1180        #[key]
1181        #[auto]
1182        id: ID,
1183
1184        label: String,
1185
1186        #[index]
1187        profile_id: ID,
1188
1189        #[belongs_to(key = profile_id, references = id)]
1190        profile: toasty::BelongsTo<Profile>,
1191    }
1192
1193    let mut db = test.setup_db(models!(User, Profile, Badge)).await;
1194
1195    // User with profile and badges
1196    let user = User::create()
1197        .name("Alice")
1198        .profile(
1199            Profile::create()
1200                .bio("hi")
1201                .badge(Badge::create().label("Gold"))
1202                .badge(Badge::create().label("Silver")),
1203        )
1204        .exec(&mut db)
1205        .await?;
1206
1207    let user = User::filter_by_id(user.id)
1208        .include(User::fields().profile().badges())
1209        .get(&mut db)
1210        .await?;
1211
1212    let profile = user.profile.get().as_ref().unwrap();
1213    assert_eq!("hi", profile.bio);
1214    let mut labels: Vec<&str> = profile
1215        .badges
1216        .get()
1217        .iter()
1218        .map(|b| b.label.as_str())
1219        .collect();
1220    labels.sort();
1221    assert_eq!(labels, vec!["Gold", "Silver"]);
1222
1223    // User without profile - nested preload should handle gracefully
1224    let user2 = User::create().name("Bob").exec(&mut db).await?;
1225
1226    let user2 = User::filter_by_id(user2.id)
1227        .include(User::fields().profile().badges())
1228        .get(&mut db)
1229        .await?;
1230
1231    assert!(user2.profile.get().is_none());
1232
1233    Ok(())
1234}
1235
1236// ===== HasOne<T> (required) -> HasMany =====
1237// Order has_one required Invoice, Invoice has_many LineItems
1238#[driver_test(id(ID))]
1239pub async fn nested_has_one_required_then_has_many(test: &mut Test) -> Result<()> {
1240    #[derive(Debug, toasty::Model)]
1241    #[allow(dead_code)]
1242    struct Order {
1243        #[key]
1244        #[auto]
1245        id: ID,
1246
1247        label: String,
1248
1249        #[has_one]
1250        invoice: toasty::HasOne<Invoice>,
1251    }
1252
1253    #[derive(Debug, toasty::Model)]
1254    #[allow(dead_code)]
1255    struct Invoice {
1256        #[key]
1257        #[auto]
1258        id: ID,
1259
1260        code: String,
1261
1262        #[unique]
1263        order_id: Option<ID>,
1264
1265        #[belongs_to(key = order_id, references = id)]
1266        order: toasty::BelongsTo<Option<Order>>,
1267
1268        #[has_many]
1269        line_items: toasty::HasMany<LineItem>,
1270    }
1271
1272    #[derive(Debug, toasty::Model)]
1273    #[allow(dead_code)]
1274    struct LineItem {
1275        #[key]
1276        #[auto]
1277        id: ID,
1278
1279        description: String,
1280
1281        #[index]
1282        invoice_id: ID,
1283
1284        #[belongs_to(key = invoice_id, references = id)]
1285        invoice: toasty::BelongsTo<Invoice>,
1286    }
1287
1288    let mut db = test.setup_db(models!(Order, Invoice, LineItem)).await;
1289
1290    let order = Order::create()
1291        .label("Order1")
1292        .invoice(
1293            Invoice::create()
1294                .code("INV-001")
1295                .line_item(LineItem::create().description("Widget"))
1296                .line_item(LineItem::create().description("Gadget")),
1297        )
1298        .exec(&mut db)
1299        .await?;
1300
1301    let order = Order::filter_by_id(order.id)
1302        .include(Order::fields().invoice().line_items())
1303        .get(&mut db)
1304        .await?;
1305
1306    let invoice = order.invoice.get();
1307    assert_eq!("INV-001", invoice.code);
1308    let mut descs: Vec<&str> = invoice
1309        .line_items
1310        .get()
1311        .iter()
1312        .map(|li| li.description.as_str())
1313        .collect();
1314    descs.sort();
1315    assert_eq!(descs, vec!["Gadget", "Widget"]);
1316
1317    Ok(())
1318}
1319
1320// ===== HasOne<Option<T>> -> HasOne<Option<T>> =====
1321// User has_one optional Profile, Profile has_one optional Avatar
1322#[driver_test(id(ID))]
1323pub async fn nested_has_one_optional_then_has_one_optional(test: &mut Test) -> Result<()> {
1324    #[derive(Debug, toasty::Model)]
1325    #[allow(dead_code)]
1326    struct User {
1327        #[key]
1328        #[auto]
1329        id: ID,
1330
1331        name: String,
1332
1333        #[has_one]
1334        profile: toasty::HasOne<Option<Profile>>,
1335    }
1336
1337    #[derive(Debug, toasty::Model)]
1338    #[allow(dead_code)]
1339    struct Profile {
1340        #[key]
1341        #[auto]
1342        id: ID,
1343
1344        bio: String,
1345
1346        #[unique]
1347        user_id: Option<ID>,
1348
1349        #[belongs_to(key = user_id, references = id)]
1350        user: toasty::BelongsTo<Option<User>>,
1351
1352        #[has_one]
1353        avatar: toasty::HasOne<Option<Avatar>>,
1354    }
1355
1356    #[derive(Debug, toasty::Model)]
1357    #[allow(dead_code)]
1358    struct Avatar {
1359        #[key]
1360        #[auto]
1361        id: ID,
1362
1363        url: String,
1364
1365        #[unique]
1366        profile_id: Option<ID>,
1367
1368        #[belongs_to(key = profile_id, references = id)]
1369        profile: toasty::BelongsTo<Option<Profile>>,
1370    }
1371
1372    let mut db = test.setup_db(models!(User, Profile, Avatar)).await;
1373
1374    // User -> Profile -> Avatar (all present)
1375    let user = User::create()
1376        .name("Alice")
1377        .profile(
1378            Profile::create()
1379                .bio("hi")
1380                .avatar(Avatar::create().url("pic.png")),
1381        )
1382        .exec(&mut db)
1383        .await?;
1384
1385    let user = User::filter_by_id(user.id)
1386        .include(User::fields().profile().avatar())
1387        .get(&mut db)
1388        .await?;
1389
1390    let profile = user.profile.get().as_ref().unwrap();
1391    assert_eq!("hi", profile.bio);
1392    let avatar = profile.avatar.get().as_ref().unwrap();
1393    assert_eq!("pic.png", avatar.url);
1394
1395    // User -> Profile (present) -> Avatar (missing)
1396    let user2 = User::create()
1397        .name("Bob")
1398        .profile(Profile::create().bio("no pic"))
1399        .exec(&mut db)
1400        .await?;
1401
1402    let user2 = User::filter_by_id(user2.id)
1403        .include(User::fields().profile().avatar())
1404        .get(&mut db)
1405        .await?;
1406
1407    let profile2 = user2.profile.get().as_ref().unwrap();
1408    assert_eq!("no pic", profile2.bio);
1409    assert!(profile2.avatar.get().is_none());
1410
1411    // User -> Profile (missing) - nested preload short-circuits
1412    let user3 = User::create().name("Carol").exec(&mut db).await?;
1413
1414    let user3 = User::filter_by_id(user3.id)
1415        .include(User::fields().profile().avatar())
1416        .get(&mut db)
1417        .await?;
1418
1419    assert!(user3.profile.get().is_none());
1420
1421    Ok(())
1422}
1423
1424// ===== HasOne<T> (required) -> HasOne<T> (required) =====
1425// User has_one required Profile, Profile has_one required Avatar
1426#[driver_test(id(ID))]
1427pub async fn nested_has_one_required_then_has_one_required(test: &mut Test) -> Result<()> {
1428    #[derive(Debug, toasty::Model)]
1429    #[allow(dead_code)]
1430    struct User {
1431        #[key]
1432        #[auto]
1433        id: ID,
1434
1435        name: String,
1436
1437        #[has_one]
1438        profile: toasty::HasOne<Profile>,
1439    }
1440
1441    #[derive(Debug, toasty::Model)]
1442    #[allow(dead_code)]
1443    struct Profile {
1444        #[key]
1445        #[auto]
1446        id: ID,
1447
1448        bio: String,
1449
1450        #[unique]
1451        user_id: Option<ID>,
1452
1453        #[belongs_to(key = user_id, references = id)]
1454        user: toasty::BelongsTo<Option<User>>,
1455
1456        #[has_one]
1457        avatar: toasty::HasOne<Avatar>,
1458    }
1459
1460    #[derive(Debug, toasty::Model)]
1461    #[allow(dead_code)]
1462    struct Avatar {
1463        #[key]
1464        #[auto]
1465        id: ID,
1466
1467        url: String,
1468
1469        #[unique]
1470        profile_id: Option<ID>,
1471
1472        #[belongs_to(key = profile_id, references = id)]
1473        profile: toasty::BelongsTo<Option<Profile>>,
1474    }
1475
1476    let mut db = test.setup_db(models!(User, Profile, Avatar)).await;
1477
1478    let user = User::create()
1479        .name("Alice")
1480        .profile(
1481            Profile::create()
1482                .bio("engineer")
1483                .avatar(Avatar::create().url("alice.jpg")),
1484        )
1485        .exec(&mut db)
1486        .await?;
1487
1488    let user = User::filter_by_id(user.id)
1489        .include(User::fields().profile().avatar())
1490        .get(&mut db)
1491        .await?;
1492
1493    let profile = user.profile.get();
1494    assert_eq!("engineer", profile.bio);
1495    let avatar = profile.avatar.get();
1496    assert_eq!("alice.jpg", avatar.url);
1497
1498    Ok(())
1499}
1500
1501// ===== HasOne<Option<T>> -> BelongsTo<T> (required) =====
1502// User has_one optional Review, Review belongs_to a Product
1503#[driver_test(id(ID))]
1504pub async fn nested_has_one_optional_then_belongs_to_required(test: &mut Test) -> Result<()> {
1505    #[derive(Debug, toasty::Model)]
1506    #[allow(dead_code)]
1507    struct User {
1508        #[key]
1509        #[auto]
1510        id: ID,
1511
1512        name: String,
1513
1514        #[has_one]
1515        review: toasty::HasOne<Option<Review>>,
1516    }
1517
1518    #[derive(Debug, toasty::Model)]
1519    #[allow(dead_code)]
1520    struct Product {
1521        #[key]
1522        #[auto]
1523        id: ID,
1524
1525        name: String,
1526    }
1527
1528    #[derive(Debug, toasty::Model)]
1529    #[allow(dead_code)]
1530    struct Review {
1531        #[key]
1532        #[auto]
1533        id: ID,
1534
1535        body: String,
1536
1537        #[unique]
1538        user_id: Option<ID>,
1539
1540        #[belongs_to(key = user_id, references = id)]
1541        user: toasty::BelongsTo<Option<User>>,
1542
1543        #[index]
1544        product_id: ID,
1545
1546        #[belongs_to(key = product_id, references = id)]
1547        product: toasty::BelongsTo<Product>,
1548    }
1549
1550    let mut db = test.setup_db(models!(User, Product, Review)).await;
1551
1552    let product = Product::create().name("Widget").exec(&mut db).await?;
1553
1554    let user = User::create()
1555        .name("Alice")
1556        .review(Review::create().body("Great!").product(&product))
1557        .exec(&mut db)
1558        .await?;
1559
1560    // User with review -> preload nested product
1561    let user = User::filter_by_id(user.id)
1562        .include(User::fields().review().product())
1563        .get(&mut db)
1564        .await?;
1565
1566    let review = user.review.get().as_ref().unwrap();
1567    assert_eq!("Great!", review.body);
1568    assert_eq!("Widget", review.product.get().name);
1569
1570    // User without review
1571    let user2 = User::create().name("Bob").exec(&mut db).await?;
1572
1573    let user2 = User::filter_by_id(user2.id)
1574        .include(User::fields().review().product())
1575        .get(&mut db)
1576        .await?;
1577
1578    assert!(user2.review.get().is_none());
1579
1580    Ok(())
1581}
1582
1583// ===== BelongsTo<T> (required) -> HasMany =====
1584// Comment belongs_to a Post, Post has_many Tags
1585#[driver_test(id(ID))]
1586pub async fn nested_belongs_to_required_then_has_many(test: &mut Test) -> Result<()> {
1587    #[derive(Debug, toasty::Model)]
1588    #[allow(dead_code)]
1589    struct Post {
1590        #[key]
1591        #[auto]
1592        id: ID,
1593
1594        title: String,
1595
1596        #[has_many]
1597        tags: toasty::HasMany<Tag>,
1598    }
1599
1600    #[derive(Debug, toasty::Model)]
1601    #[allow(dead_code)]
1602    struct Tag {
1603        #[key]
1604        #[auto]
1605        id: ID,
1606
1607        label: String,
1608
1609        #[index]
1610        post_id: ID,
1611
1612        #[belongs_to(key = post_id, references = id)]
1613        post: toasty::BelongsTo<Post>,
1614    }
1615
1616    #[derive(Debug, toasty::Model)]
1617    #[allow(dead_code)]
1618    struct Comment {
1619        #[key]
1620        #[auto]
1621        id: ID,
1622
1623        body: String,
1624
1625        #[index]
1626        post_id: ID,
1627
1628        #[belongs_to(key = post_id, references = id)]
1629        post: toasty::BelongsTo<Post>,
1630    }
1631
1632    let mut db = test.setup_db(models!(Post, Tag, Comment)).await;
1633
1634    let post = Post::create()
1635        .title("Hello")
1636        .tag(Tag::create().label("rust"))
1637        .tag(Tag::create().label("orm"))
1638        .exec(&mut db)
1639        .await?;
1640
1641    let comment = Comment::create()
1642        .body("Nice post")
1643        .post(&post)
1644        .exec(&mut db)
1645        .await?;
1646
1647    // From comment, preload post's tags
1648    let comment = Comment::filter_by_id(comment.id)
1649        .include(Comment::fields().post().tags())
1650        .get(&mut db)
1651        .await?;
1652
1653    assert_eq!("Hello", comment.post.get().title);
1654    let mut labels: Vec<&str> = comment
1655        .post
1656        .get()
1657        .tags
1658        .get()
1659        .iter()
1660        .map(|t| t.label.as_str())
1661        .collect();
1662    labels.sort();
1663    assert_eq!(labels, vec!["orm", "rust"]);
1664
1665    Ok(())
1666}
1667
1668// ===== BelongsTo<T> (required) -> HasOne<Option<T>> =====
1669// Todo belongs_to a User, User has_one optional Profile
1670#[driver_test(id(ID))]
1671pub async fn nested_belongs_to_required_then_has_one_optional(test: &mut Test) -> Result<()> {
1672    #[derive(Debug, toasty::Model)]
1673    #[allow(dead_code)]
1674    struct User {
1675        #[key]
1676        #[auto]
1677        id: ID,
1678
1679        name: String,
1680
1681        #[has_one]
1682        profile: toasty::HasOne<Option<Profile>>,
1683
1684        #[has_many]
1685        todos: toasty::HasMany<Todo>,
1686    }
1687
1688    #[derive(Debug, toasty::Model)]
1689    #[allow(dead_code)]
1690    struct Profile {
1691        #[key]
1692        #[auto]
1693        id: ID,
1694
1695        bio: String,
1696
1697        #[unique]
1698        user_id: Option<ID>,
1699
1700        #[belongs_to(key = user_id, references = id)]
1701        user: toasty::BelongsTo<Option<User>>,
1702    }
1703
1704    #[derive(Debug, toasty::Model)]
1705    #[allow(dead_code)]
1706    struct Todo {
1707        #[key]
1708        #[auto]
1709        id: ID,
1710
1711        title: String,
1712
1713        #[index]
1714        user_id: ID,
1715
1716        #[belongs_to(key = user_id, references = id)]
1717        user: toasty::BelongsTo<User>,
1718    }
1719
1720    let mut db = test.setup_db(models!(User, Profile, Todo)).await;
1721
1722    // User with profile
1723    let user = User::create()
1724        .name("Alice")
1725        .profile(Profile::create().bio("developer"))
1726        .todo(Todo::create().title("Task 1"))
1727        .exec(&mut db)
1728        .await?;
1729
1730    let todo_id = Todo::get_by_user_id(&mut db, user.id).await?.id;
1731
1732    let todo = Todo::filter_by_id(todo_id)
1733        .include(Todo::fields().user().profile())
1734        .get(&mut db)
1735        .await?;
1736
1737    assert_eq!("Alice", todo.user.get().name);
1738    let profile = todo.user.get().profile.get().as_ref().unwrap();
1739    assert_eq!("developer", profile.bio);
1740
1741    // User without profile
1742    let user2 = User::create()
1743        .name("Bob")
1744        .todo(Todo::create().title("Task 2"))
1745        .exec(&mut db)
1746        .await?;
1747
1748    let todo2_id = Todo::get_by_user_id(&mut db, user2.id).await?.id;
1749
1750    let todo2 = Todo::filter_by_id(todo2_id)
1751        .include(Todo::fields().user().profile())
1752        .get(&mut db)
1753        .await?;
1754
1755    assert_eq!("Bob", todo2.user.get().name);
1756    assert!(todo2.user.get().profile.get().is_none());
1757
1758    Ok(())
1759}
1760
1761// ===== BelongsTo<T> (required) -> BelongsTo<T> (required) =====
1762// Step belongs_to a Todo, Todo belongs_to a User (chain of belongs_to going up)
1763#[driver_test(id(ID))]
1764pub async fn nested_belongs_to_required_then_belongs_to_required(test: &mut Test) -> Result<()> {
1765    #[derive(Debug, toasty::Model)]
1766    #[allow(dead_code)]
1767    struct User {
1768        #[key]
1769        #[auto]
1770        id: ID,
1771
1772        name: String,
1773
1774        #[has_many]
1775        todos: toasty::HasMany<Todo>,
1776    }
1777
1778    #[derive(Debug, toasty::Model)]
1779    #[allow(dead_code)]
1780    struct Todo {
1781        #[key]
1782        #[auto]
1783        id: ID,
1784
1785        title: String,
1786
1787        #[index]
1788        user_id: ID,
1789
1790        #[belongs_to(key = user_id, references = id)]
1791        user: toasty::BelongsTo<User>,
1792
1793        #[has_many]
1794        steps: toasty::HasMany<Step>,
1795    }
1796
1797    #[derive(Debug, toasty::Model)]
1798    #[allow(dead_code)]
1799    struct Step {
1800        #[key]
1801        #[auto]
1802        id: ID,
1803
1804        description: String,
1805
1806        #[index]
1807        todo_id: ID,
1808
1809        #[belongs_to(key = todo_id, references = id)]
1810        todo: toasty::BelongsTo<Todo>,
1811    }
1812
1813    let mut db = test.setup_db(models!(User, Todo, Step)).await;
1814
1815    let user = User::create()
1816        .name("Alice")
1817        .todo(
1818            Todo::create()
1819                .title("T1")
1820                .step(Step::create().description("S1")),
1821        )
1822        .exec(&mut db)
1823        .await?;
1824
1825    let todo_id = Todo::get_by_user_id(&mut db, user.id).await?.id;
1826    let step_id = Step::get_by_todo_id(&mut db, todo_id).await?.id;
1827
1828    // From step, preload todo and then todo's user
1829    let step = Step::filter_by_id(step_id)
1830        .include(Step::fields().todo().user())
1831        .get(&mut db)
1832        .await?;
1833
1834    assert_eq!("T1", step.todo.get().title);
1835    assert_eq!("Alice", step.todo.get().user.get().name);
1836
1837    Ok(())
1838}
1839
1840// ===== BelongsTo<Option<T>> -> HasMany =====
1841// Task optionally belongs_to a Project, Project has_many Members
1842#[driver_test(id(ID))]
1843pub async fn nested_belongs_to_optional_then_has_many(test: &mut Test) -> Result<()> {
1844    #[derive(Debug, toasty::Model)]
1845    #[allow(dead_code)]
1846    struct Project {
1847        #[key]
1848        #[auto]
1849        id: ID,
1850
1851        name: String,
1852
1853        #[has_many]
1854        members: toasty::HasMany<Member>,
1855    }
1856
1857    #[derive(Debug, toasty::Model)]
1858    #[allow(dead_code)]
1859    struct Member {
1860        #[key]
1861        #[auto]
1862        id: ID,
1863
1864        name: String,
1865
1866        #[index]
1867        project_id: ID,
1868
1869        #[belongs_to(key = project_id, references = id)]
1870        project: toasty::BelongsTo<Project>,
1871    }
1872
1873    #[derive(Debug, toasty::Model)]
1874    #[allow(dead_code)]
1875    struct Task {
1876        #[key]
1877        #[auto]
1878        id: ID,
1879
1880        title: String,
1881
1882        #[index]
1883        project_id: Option<ID>,
1884
1885        #[belongs_to(key = project_id, references = id)]
1886        project: toasty::BelongsTo<Option<Project>>,
1887    }
1888
1889    let mut db = test.setup_db(models!(Project, Member, Task)).await;
1890
1891    let project = Project::create()
1892        .name("Proj1")
1893        .member(Member::create().name("Alice"))
1894        .member(Member::create().name("Bob"))
1895        .exec(&mut db)
1896        .await?;
1897
1898    // Task with project
1899    let task = Task::create()
1900        .title("Linked")
1901        .project(&project)
1902        .exec(&mut db)
1903        .await?;
1904
1905    let task = Task::filter_by_id(task.id)
1906        .include(Task::fields().project().members())
1907        .get(&mut db)
1908        .await?;
1909
1910    let proj = task.project.get().as_ref().unwrap();
1911    assert_eq!("Proj1", proj.name);
1912    let mut names: Vec<&str> = proj.members.get().iter().map(|m| m.name.as_str()).collect();
1913    names.sort();
1914    assert_eq!(names, vec!["Alice", "Bob"]);
1915
1916    // Task without project
1917    let orphan = Task::create().title("Orphan").exec(&mut db).await?;
1918
1919    let orphan = Task::filter_by_id(orphan.id)
1920        .include(Task::fields().project().members())
1921        .get(&mut db)
1922        .await?;
1923
1924    assert!(orphan.project.get().is_none());
1925
1926    Ok(())
1927}
1928
1929// ===== BelongsTo<Option<T>> -> BelongsTo<Option<T>> =====
1930// Comment optionally belongs_to a Post, Post optionally belongs_to a Category
1931#[driver_test(id(ID))]
1932pub async fn nested_belongs_to_optional_then_belongs_to_optional(test: &mut Test) -> Result<()> {
1933    #[derive(Debug, toasty::Model)]
1934    #[allow(dead_code)]
1935    struct Category {
1936        #[key]
1937        #[auto]
1938        id: ID,
1939
1940        name: String,
1941    }
1942
1943    #[derive(Debug, toasty::Model)]
1944    #[allow(dead_code)]
1945    struct Post {
1946        #[key]
1947        #[auto]
1948        id: ID,
1949
1950        title: String,
1951
1952        #[index]
1953        category_id: Option<ID>,
1954
1955        #[belongs_to(key = category_id, references = id)]
1956        category: toasty::BelongsTo<Option<Category>>,
1957    }
1958
1959    #[derive(Debug, toasty::Model)]
1960    #[allow(dead_code)]
1961    struct Comment {
1962        #[key]
1963        #[auto]
1964        id: ID,
1965
1966        body: String,
1967
1968        #[index]
1969        post_id: Option<ID>,
1970
1971        #[belongs_to(key = post_id, references = id)]
1972        post: toasty::BelongsTo<Option<Post>>,
1973    }
1974
1975    let mut db = test.setup_db(models!(Category, Post, Comment)).await;
1976
1977    let cat = Category::create().name("Tech").exec(&mut db).await?;
1978    let post = Post::create()
1979        .title("Hello")
1980        .category(&cat)
1981        .exec(&mut db)
1982        .await?;
1983
1984    // Comment -> Post (present) -> Category (present)
1985    let c1 = Comment::create()
1986        .body("Nice")
1987        .post(&post)
1988        .exec(&mut db)
1989        .await?;
1990
1991    let c1 = Comment::filter_by_id(c1.id)
1992        .include(Comment::fields().post().category())
1993        .get(&mut db)
1994        .await?;
1995
1996    let loaded_post = c1.post.get().as_ref().unwrap();
1997    assert_eq!("Hello", loaded_post.title);
1998    let loaded_cat = loaded_post.category.get().as_ref().unwrap();
1999    assert_eq!("Tech", loaded_cat.name);
2000
2001    // Post without category
2002    let post2 = Post::create().title("Uncategorized").exec(&mut db).await?;
2003    let c2 = Comment::create()
2004        .body("Hmm")
2005        .post(&post2)
2006        .exec(&mut db)
2007        .await?;
2008
2009    let c2 = Comment::filter_by_id(c2.id)
2010        .include(Comment::fields().post().category())
2011        .get(&mut db)
2012        .await?;
2013
2014    let loaded_post2 = c2.post.get().as_ref().unwrap();
2015    assert_eq!("Uncategorized", loaded_post2.title);
2016    assert!(loaded_post2.category.get().is_none());
2017
2018    // Comment without post
2019    let c3 = Comment::create().body("Orphan").exec(&mut db).await?;
2020
2021    let c3 = Comment::filter_by_id(c3.id)
2022        .include(Comment::fields().post().category())
2023        .get(&mut db)
2024        .await?;
2025
2026    assert!(c3.post.get().is_none());
2027
2028    Ok(())
2029}
2030
2031// ===== BelongsTo<T> -> HasOne<T> (required) =====
2032// Todo belongs_to a User, User has_one required Config
2033#[driver_test(id(ID))]
2034pub async fn nested_belongs_to_required_then_has_one_required(test: &mut Test) -> Result<()> {
2035    #[derive(Debug, toasty::Model)]
2036    #[allow(dead_code)]
2037    struct User {
2038        #[key]
2039        #[auto]
2040        id: ID,
2041
2042        name: String,
2043
2044        #[has_one]
2045        config: toasty::HasOne<Config>,
2046
2047        #[has_many]
2048        todos: toasty::HasMany<Todo>,
2049    }
2050
2051    #[derive(Debug, toasty::Model)]
2052    #[allow(dead_code)]
2053    struct Config {
2054        #[key]
2055        #[auto]
2056        id: ID,
2057
2058        theme: String,
2059
2060        #[unique]
2061        user_id: Option<ID>,
2062
2063        #[belongs_to(key = user_id, references = id)]
2064        user: toasty::BelongsTo<Option<User>>,
2065    }
2066
2067    #[derive(Debug, toasty::Model)]
2068    #[allow(dead_code)]
2069    struct Todo {
2070        #[key]
2071        #[auto]
2072        id: ID,
2073
2074        title: String,
2075
2076        #[index]
2077        user_id: ID,
2078
2079        #[belongs_to(key = user_id, references = id)]
2080        user: toasty::BelongsTo<User>,
2081    }
2082
2083    let mut db = test.setup_db(models!(User, Config, Todo)).await;
2084
2085    let user = User::create()
2086        .name("Alice")
2087        .config(Config::create().theme("dark"))
2088        .todo(Todo::create().title("Task"))
2089        .exec(&mut db)
2090        .await?;
2091
2092    let todo_id = Todo::get_by_user_id(&mut db, user.id).await?.id;
2093
2094    let todo = Todo::filter_by_id(todo_id)
2095        .include(Todo::fields().user().config())
2096        .get(&mut db)
2097        .await?;
2098
2099    assert_eq!("Alice", todo.user.get().name);
2100    assert_eq!("dark", todo.user.get().config.get().theme);
2101
2102    Ok(())
2103}
2104
2105// ===== HasMany -> HasMany (with empty nested collections) =====
2106// Ensures that when some parents have children and others don't, nested preload
2107// correctly assigns empty collections rather than panicking.
2108#[driver_test(id(ID))]
2109pub async fn nested_has_many_then_has_many_with_empty_leaves(test: &mut Test) {
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_many]
2120        todos: toasty::HasMany<Todo>,
2121    }
2122
2123    #[derive(Debug, toasty::Model)]
2124    #[allow(dead_code)]
2125    struct Todo {
2126        #[key]
2127        #[auto]
2128        id: ID,
2129
2130        title: String,
2131
2132        #[index]
2133        user_id: ID,
2134
2135        #[belongs_to(key = user_id, references = id)]
2136        user: toasty::BelongsTo<User>,
2137
2138        #[has_many]
2139        steps: toasty::HasMany<Step>,
2140    }
2141
2142    #[derive(Debug, toasty::Model)]
2143    #[allow(dead_code)]
2144    struct Step {
2145        #[key]
2146        #[auto]
2147        id: ID,
2148
2149        description: String,
2150
2151        #[index]
2152        todo_id: ID,
2153
2154        #[belongs_to(key = todo_id, references = id)]
2155        todo: toasty::BelongsTo<Todo>,
2156    }
2157
2158    let mut db = test.setup_db(models!(User, Todo, Step)).await;
2159
2160    let user = User::create()
2161        .name("Alice")
2162        .todo(
2163            Todo::create()
2164                .title("With Steps")
2165                .step(Step::create().description("S1")),
2166        )
2167        .todo(Todo::create().title("No Steps")) // empty nested
2168        .exec(&mut db)
2169        .await
2170        .unwrap();
2171
2172    let user = User::filter_by_id(user.id)
2173        .include(User::fields().todos().steps())
2174        .get(&mut db)
2175        .await
2176        .unwrap();
2177
2178    let todos = user.todos.get();
2179    assert_eq!(2, todos.len());
2180
2181    let mut total_steps = 0;
2182    for todo in todos {
2183        let steps = todo.steps.get();
2184        if todo.title == "With Steps" {
2185            assert_eq!(1, steps.len());
2186            assert_eq!("S1", steps[0].description);
2187        } else {
2188            assert_eq!(0, steps.len());
2189        }
2190        total_steps += steps.len();
2191    }
2192    assert_eq!(1, total_steps);
2193}