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