Skip to main content

toasty_driver_integration_suite/tests/
relation_preload.rs

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