Skip to main content

toasty_driver_integration_suite/tests/
relation_eager.rs

1use crate::prelude::*;
2
3use toasty::schema::Model;
4use toasty_core::stmt;
5
6#[driver_test]
7pub async fn eager_has_many_and_has_one_load_without_include(t: &mut Test) -> Result<()> {
8    #[derive(Debug, toasty::Model)]
9    struct User {
10        #[key]
11        id: uuid::Uuid,
12        name: String,
13
14        #[has_many]
15        posts: Vec<Post>,
16
17        #[has_one]
18        profile: Option<Profile>,
19    }
20
21    #[derive(Debug, toasty::Model)]
22    struct Post {
23        #[key]
24        #[auto]
25        id: uuid::Uuid,
26        title: String,
27
28        #[index]
29        user_id: uuid::Uuid,
30
31        #[belongs_to(key = user_id, references = id)]
32        user: toasty::Deferred<User>,
33    }
34
35    #[derive(Debug, toasty::Model)]
36    struct Profile {
37        #[key]
38        id: uuid::Uuid,
39        bio: String,
40
41        #[unique]
42        user_id: uuid::Uuid,
43
44        #[belongs_to(key = user_id, references = id)]
45        user: toasty::Deferred<Option<User>>,
46    }
47
48    let mut db = t.setup_db(models!(User, Post, Profile)).await;
49    let user_id = uuid::Uuid::from_u128(1);
50
51    insert_row::<User>(
52        &mut db,
53        vec![
54            stmt::Value::Uuid(user_id).into(),
55            stmt::Value::from("Alice").into(),
56            stmt::Value::Null.into(),
57            stmt::Value::Null.into(),
58        ],
59    )
60    .await?;
61    insert_row::<Post>(
62        &mut db,
63        vec![
64            stmt::Value::Uuid(uuid::Uuid::from_u128(2)).into(),
65            stmt::Value::from("hello").into(),
66            stmt::Value::Uuid(user_id).into(),
67            stmt::Value::Null.into(),
68        ],
69    )
70    .await?;
71    insert_row::<Profile>(
72        &mut db,
73        vec![
74            stmt::Value::Uuid(uuid::Uuid::from_u128(3)).into(),
75            stmt::Value::from("writer").into(),
76            stmt::Value::Uuid(user_id).into(),
77            stmt::Value::Null.into(),
78        ],
79    )
80    .await?;
81
82    let user = User::filter_by_id(user_id).get(&mut db).await?;
83
84    assert_eq!(user.posts.len(), 1);
85    assert_eq!(user.posts[0].title, "hello");
86    assert_eq!(user.profile.as_ref().unwrap().bio, "writer");
87
88    Ok(())
89}
90
91#[driver_test]
92pub async fn eager_belongs_to_loads_without_include(t: &mut Test) -> Result<()> {
93    #[derive(Debug, toasty::Model)]
94    struct User {
95        #[key]
96        id: uuid::Uuid,
97        name: String,
98    }
99
100    #[derive(Debug, toasty::Model)]
101    struct Post {
102        #[key]
103        #[auto]
104        id: uuid::Uuid,
105        title: String,
106
107        #[index]
108        user_id: uuid::Uuid,
109
110        #[belongs_to(key = user_id, references = id)]
111        user: User,
112    }
113
114    let mut db = t.setup_db(models!(User, Post)).await;
115    let user_id = uuid::Uuid::from_u128(4);
116    let post_id = uuid::Uuid::from_u128(5);
117
118    insert_row::<User>(
119        &mut db,
120        vec![
121            stmt::Value::Uuid(user_id).into(),
122            stmt::Value::from("Alice").into(),
123        ],
124    )
125    .await?;
126    insert_row::<Post>(
127        &mut db,
128        vec![
129            stmt::Value::Uuid(post_id).into(),
130            stmt::Value::from("hello").into(),
131            stmt::Value::Uuid(user_id).into(),
132            stmt::Value::Null.into(),
133        ],
134    )
135    .await?;
136
137    let post = Post::filter_by_id(post_id).get(&mut db).await?;
138
139    assert_eq!(post.title, "hello");
140    assert_eq!(post.user.name, "Alice");
141
142    Ok(())
143}
144
145#[driver_test]
146pub async fn eager_belongs_to_create_by_fk_returning_loads_relation(t: &mut Test) -> Result<()> {
147    #[derive(Debug, toasty::Model)]
148    struct User {
149        #[key]
150        #[auto]
151        id: uuid::Uuid,
152        name: String,
153    }
154
155    #[derive(Debug, toasty::Model)]
156    struct Post {
157        #[key]
158        #[auto]
159        id: uuid::Uuid,
160
161        #[index]
162        user_id: uuid::Uuid,
163
164        #[belongs_to(key = user_id, references = id)]
165        user: User,
166    }
167
168    let mut db = t.setup_db(models!(User, Post)).await;
169
170    let user = toasty::create!(User { name: "Alice" })
171        .exec(&mut db)
172        .await?;
173
174    // Set the FK scalar instead of the relation. The eager `user` load still
175    // has to back-reference the inserted `user_id` column.
176    let post = toasty::create!(Post { user_id: user.id })
177        .exec(&mut db)
178        .await?;
179
180    assert_eq!(post.user.id, user.id);
181    assert_eq!(post.user.name, "Alice");
182
183    Ok(())
184}
185
186#[driver_test]
187pub async fn eager_has_many_create_returning_loads_relations(t: &mut Test) -> Result<()> {
188    #[derive(Debug, toasty::Model)]
189    struct User {
190        #[key]
191        #[auto]
192        id: uuid::Uuid,
193        name: String,
194
195        #[has_many]
196        posts: Vec<Post>,
197    }
198
199    #[derive(Debug, toasty::Model)]
200    struct Post {
201        #[key]
202        #[auto]
203        id: uuid::Uuid,
204        title: String,
205
206        #[index]
207        user_id: uuid::Uuid,
208
209        #[belongs_to(key = user_id, references = id)]
210        user: toasty::Deferred<User>,
211    }
212
213    let mut db = t.setup_db(models!(User, Post)).await;
214
215    let user = User::create()
216        .name("Alice")
217        .posts([Post::create().title("hello")])
218        .exec(&mut db)
219        .await?;
220
221    assert_eq!(user.name, "Alice");
222    assert_eq!(user.posts.len(), 1);
223    assert_eq!(user.posts[0].title, "hello");
224
225    Ok(())
226}
227
228#[driver_test]
229pub async fn eager_belongs_to_create_returning_loads_relation(t: &mut Test) -> Result<()> {
230    #[derive(Debug, toasty::Model)]
231    struct User {
232        #[key]
233        #[auto]
234        id: uuid::Uuid,
235        name: String,
236    }
237
238    #[derive(Debug, toasty::Model)]
239    struct Post {
240        #[key]
241        #[auto]
242        id: uuid::Uuid,
243        title: String,
244
245        #[index]
246        user_id: uuid::Uuid,
247
248        #[belongs_to(key = user_id, references = id)]
249        user: User,
250    }
251
252    let mut db = t.setup_db(models!(User, Post)).await;
253
254    let user = toasty::create!(User { name: "Alice" })
255        .exec(&mut db)
256        .await?;
257    let post = toasty::create!(Post {
258        title: "hello",
259        user: &user
260    })
261    .exec(&mut db)
262    .await?;
263
264    assert_eq!(post.title, "hello");
265    assert_eq!(post.user_id, user.id);
266    assert_eq!(post.user.id, user.id);
267    assert_eq!(post.user.name, "Alice");
268
269    Ok(())
270}
271
272#[driver_test]
273pub async fn eager_belongs_to_batch_create_returning_loads_relation(t: &mut Test) -> Result<()> {
274    #[derive(Debug, toasty::Model)]
275    struct User {
276        #[key]
277        #[auto]
278        id: uuid::Uuid,
279        name: String,
280    }
281
282    #[derive(Debug, toasty::Model)]
283    struct Post {
284        #[key]
285        #[auto]
286        id: uuid::Uuid,
287        title: String,
288
289        #[index]
290        user_id: uuid::Uuid,
291
292        #[belongs_to(key = user_id, references = id)]
293        user: User,
294    }
295
296    let mut db = t.setup_db(models!(User, Post)).await;
297
298    let alice = toasty::create!(User { name: "Alice" })
299        .exec(&mut db)
300        .await?;
301    let bob = toasty::create!(User { name: "Bob" }).exec(&mut db).await?;
302
303    let posts = toasty::create!(Post::[
304        { title: "a", user: &alice },
305        { title: "b", user: &bob },
306    ])
307    .exec(&mut db)
308    .await?;
309
310    assert_eq!(posts.len(), 2);
311    assert_eq!(posts[0].user.name, "Alice");
312    assert_eq!(posts[1].user.name, "Bob");
313
314    Ok(())
315}
316
317#[driver_test(requires(upsert_targeted_ignore))]
318pub async fn eager_belongs_to_upsert_or_ignore_returning_loads_relation(
319    t: &mut Test,
320) -> Result<()> {
321    #[derive(Debug, toasty::Model)]
322    struct User {
323        #[key]
324        #[auto]
325        id: uuid::Uuid,
326        name: String,
327    }
328
329    #[derive(Debug, toasty::Model)]
330    struct Post {
331        #[key]
332        id: uuid::Uuid,
333        title: String,
334
335        #[index]
336        user_id: uuid::Uuid,
337
338        #[belongs_to(key = user_id, references = id)]
339        user: User,
340    }
341
342    let mut db = t.setup_db(models!(User, Post)).await;
343
344    let user = toasty::create!(User { name: "Alice" })
345        .exec(&mut db)
346        .await?;
347
348    let post_id = uuid::Uuid::from_u128(7);
349    let created = Post::upsert_by_id(post_id)
350        .title("hello")
351        .user_id(user.id)
352        .or_ignore()
353        .exec(&mut db)
354        .await?;
355
356    let created = created.expect("insert succeeded");
357    assert_eq!(created.title, "hello");
358    assert_struct!(created.user, { id: == user.id, name: "Alice" });
359
360    // On conflict the insert is ignored and the upsert returns `None`.
361    let ignored = Post::upsert_by_id(post_id)
362        .title("other")
363        .user_id(user.id)
364        .or_ignore()
365        .exec(&mut db)
366        .await?;
367    assert_none!(ignored);
368
369    Ok(())
370}
371
372#[driver_test(requires(upsert_targeted_ignore))]
373pub async fn eager_belongs_to_upsert_or_ignore_conflict_issues_no_load(t: &mut Test) -> Result<()> {
374    use toasty_core::driver::Operation;
375
376    #[derive(Debug, toasty::Model)]
377    struct User {
378        #[key]
379        #[auto]
380        id: uuid::Uuid,
381        name: String,
382    }
383
384    #[derive(Debug, toasty::Model)]
385    struct Post {
386        #[key]
387        id: uuid::Uuid,
388        title: String,
389
390        #[index]
391        user_id: uuid::Uuid,
392
393        #[belongs_to(key = user_id, references = id)]
394        user: User,
395    }
396
397    let mut db = t.setup_db(models!(User, Post)).await;
398
399    let user = toasty::create!(User { name: "Alice" })
400        .exec(&mut db)
401        .await?;
402
403    let post_id = uuid::Uuid::from_u128(7);
404    Post::upsert_by_id(post_id)
405        .title("hello")
406        .user_id(user.id)
407        .or_ignore()
408        .exec(&mut db)
409        .await?
410        .expect("insert succeeded");
411
412    // On conflict the upsert's RETURNING is empty, so the relation-load read
413    // is skipped: its block is guarded on the upsert producing a row.
414    t.log().clear();
415    let ignored = Post::upsert_by_id(post_id)
416        .title("hello")
417        .user_id(user.id)
418        .or_ignore()
419        .exec(&mut db)
420        .await?;
421    assert_none!(ignored);
422
423    while !t.log().is_empty() {
424        let op = t.log().pop_op();
425        let is_read = match &op {
426            Operation::QuerySql(op) => op.stmt.is_query(),
427            Operation::GetByKey(_)
428            | Operation::QueryPk(_)
429            | Operation::FindPkByIndex(_)
430            | Operation::Scan(_) => true,
431            _ => false,
432        };
433        assert!(!is_read, "conflict path issued a read: {op:#?}");
434    }
435
436    Ok(())
437}
438
439#[driver_test]
440pub async fn eager_belongs_to_nested_create_returning_loads_relation(t: &mut Test) -> Result<()> {
441    #[derive(Debug, toasty::Model)]
442    struct User {
443        #[key]
444        #[auto]
445        id: uuid::Uuid,
446        name: String,
447
448        #[has_many]
449        posts: toasty::Deferred<Vec<Post>>,
450    }
451
452    #[derive(Debug, toasty::Model)]
453    struct Post {
454        #[key]
455        #[auto]
456        id: uuid::Uuid,
457        title: String,
458
459        #[index]
460        user_id: uuid::Uuid,
461
462        #[belongs_to(key = user_id, references = id)]
463        user: User,
464    }
465
466    let mut db = t.setup_db(models!(User, Post)).await;
467
468    // The nested post insert's RETURNING includes its eager `user`, whose FK
469    // value comes from the parent user insert.
470    let user = User::create()
471        .name("Alice")
472        .posts([Post::create().title("hello")])
473        .exec(&mut db)
474        .await?;
475
476    let posts: Vec<_> = user.posts().exec(&mut db).await?;
477    assert_eq!(posts.len(), 1);
478    assert_eq!(posts[0].user.name, "Alice");
479
480    Ok(())
481}
482
483#[driver_test]
484pub async fn eager_nested_relations_load_without_include(t: &mut Test) -> Result<()> {
485    #[derive(Debug, toasty::Model)]
486    struct User {
487        #[key]
488        id: uuid::Uuid,
489        name: String,
490
491        #[has_many]
492        posts: Vec<Post>,
493    }
494
495    #[derive(Debug, toasty::Model)]
496    struct Post {
497        #[key]
498        id: uuid::Uuid,
499        title: String,
500
501        #[index]
502        user_id: uuid::Uuid,
503
504        #[belongs_to(key = user_id, references = id)]
505        user: toasty::Deferred<User>,
506
507        #[has_many]
508        comments: Vec<Comment>,
509    }
510
511    #[derive(Debug, toasty::Model)]
512    struct Comment {
513        #[key]
514        id: uuid::Uuid,
515        body: String,
516
517        #[index]
518        post_id: uuid::Uuid,
519
520        #[belongs_to(key = post_id, references = id)]
521        post: toasty::Deferred<Post>,
522    }
523
524    let mut db = t.setup_db(models!(User, Post, Comment)).await;
525    let user_id = uuid::Uuid::from_u128(10);
526    let post_id = uuid::Uuid::from_u128(11);
527
528    insert_row::<User>(
529        &mut db,
530        vec![
531            stmt::Value::Uuid(user_id).into(),
532            stmt::Value::from("Alice").into(),
533            stmt::Value::Null.into(),
534        ],
535    )
536    .await?;
537    insert_row::<Post>(
538        &mut db,
539        vec![
540            stmt::Value::Uuid(post_id).into(),
541            stmt::Value::from("hello").into(),
542            stmt::Value::Uuid(user_id).into(),
543            stmt::Value::Null.into(),
544            stmt::Value::Null.into(),
545        ],
546    )
547    .await?;
548    insert_row::<Comment>(
549        &mut db,
550        vec![
551            stmt::Value::Uuid(uuid::Uuid::from_u128(12)).into(),
552            stmt::Value::from("first").into(),
553            stmt::Value::Uuid(post_id).into(),
554            stmt::Value::Null.into(),
555        ],
556    )
557    .await?;
558
559    let user = User::filter_by_id(user_id).get(&mut db).await?;
560
561    assert_eq!(user.posts.len(), 1);
562    assert_eq!(user.posts[0].title, "hello");
563    assert_eq!(user.posts[0].comments.len(), 1);
564    assert_eq!(user.posts[0].comments[0].body, "first");
565
566    Ok(())
567}
568
569#[driver_test]
570pub async fn eager_relations_reload_after_update(t: &mut Test) -> Result<()> {
571    #[derive(Debug, toasty::Model)]
572    struct User {
573        #[key]
574        id: uuid::Uuid,
575        name: String,
576
577        #[has_many]
578        posts: Vec<Post>,
579    }
580
581    #[derive(Debug, toasty::Model)]
582    struct Post {
583        #[key]
584        #[auto]
585        id: uuid::Uuid,
586        title: String,
587
588        #[index]
589        user_id: uuid::Uuid,
590
591        #[belongs_to(key = user_id, references = id)]
592        user: toasty::Deferred<User>,
593    }
594
595    let mut db = t.setup_db(models!(User, Post)).await;
596    let user_id = uuid::Uuid::from_u128(20);
597
598    insert_row::<User>(
599        &mut db,
600        vec![
601            stmt::Value::Uuid(user_id).into(),
602            stmt::Value::from("Alice").into(),
603            stmt::Value::Null.into(),
604        ],
605    )
606    .await?;
607
608    let mut user = User::filter_by_id(user_id).get(&mut db).await?;
609    assert!(user.posts.is_empty());
610
611    user.update()
612        .name("Alice Updated")
613        .posts(toasty::stmt::insert(Post::create().title("first")))
614        .exec(&mut db)
615        .await?;
616
617    let mut titles = user
618        .posts
619        .iter()
620        .map(|post| post.title.as_str())
621        .collect::<Vec<_>>();
622    titles.sort_unstable();
623
624    assert_eq!(user.name, "Alice Updated");
625    assert_eq!(titles, vec!["first"]);
626
627    Ok(())
628}
629
630#[driver_test]
631pub async fn eager_relation_cycle_is_rejected(t: &mut Test) -> Result<()> {
632    #[derive(Debug, toasty::Model)]
633    struct User {
634        #[key]
635        #[auto]
636        id: uuid::Uuid,
637
638        #[has_many]
639        posts: Vec<Post>,
640    }
641
642    #[derive(Debug, toasty::Model)]
643    struct Post {
644        #[key]
645        #[auto]
646        id: uuid::Uuid,
647
648        #[index]
649        user_id: uuid::Uuid,
650
651        #[belongs_to(key = user_id, references = id)]
652        user: User,
653    }
654
655    let err = t.try_setup_db(models!(User, Post)).await.unwrap_err();
656    let msg = err.to_string();
657
658    assert!(
659        msg.contains("eager relation cycle"),
660        "expected eager relation cycle error, got: {msg}"
661    );
662
663    Ok(())
664}
665
666#[driver_test]
667pub async fn eager_relation_self_cycle_is_rejected(t: &mut Test) -> Result<()> {
668    #[derive(Debug, toasty::Model)]
669    struct Node {
670        #[key]
671        #[auto]
672        id: uuid::Uuid,
673
674        #[index]
675        parent_id: Option<uuid::Uuid>,
676
677        #[belongs_to(key = parent_id, references = id)]
678        parent: toasty::Deferred<Option<Node>>,
679
680        #[has_many(pair = parent)]
681        children: Vec<Node>,
682    }
683
684    let err = t.try_setup_db(models!(Node)).await.unwrap_err();
685    let msg = err.to_string();
686
687    assert!(
688        msg.contains("eager relation cycle"),
689        "expected eager relation cycle error, got: {msg}"
690    );
691
692    Ok(())
693}
694
695#[driver_test]
696pub async fn eager_relation_long_cycle_is_rejected(t: &mut Test) -> Result<()> {
697    #[derive(Debug, toasty::Model)]
698    struct User {
699        #[key]
700        #[auto]
701        id: uuid::Uuid,
702
703        #[has_many]
704        posts: Vec<Post>,
705    }
706
707    #[derive(Debug, toasty::Model)]
708    struct Post {
709        #[key]
710        #[auto]
711        id: uuid::Uuid,
712
713        #[index]
714        user_id: uuid::Uuid,
715
716        #[belongs_to(key = user_id, references = id)]
717        user: toasty::Deferred<User>,
718
719        #[has_one]
720        detail: Option<Detail>,
721    }
722
723    #[derive(Debug, toasty::Model)]
724    struct Detail {
725        #[key]
726        #[auto]
727        id: uuid::Uuid,
728
729        #[unique]
730        post_id: uuid::Uuid,
731
732        #[belongs_to(key = post_id, references = id)]
733        post: toasty::Deferred<Post>,
734
735        #[index]
736        user_id: uuid::Uuid,
737
738        #[belongs_to(key = user_id, references = id)]
739        user: User,
740    }
741
742    let err = t
743        .try_setup_db(models!(User, Post, Detail))
744        .await
745        .unwrap_err();
746    let msg = err.to_string();
747
748    assert!(
749        msg.contains("eager relation cycle"),
750        "expected eager relation cycle error, got: {msg}"
751    );
752
753    Ok(())
754}
755
756async fn insert_row<M: Model>(db: &mut toasty::Db, fields: Vec<stmt::Expr>) -> Result<()> {
757    let insert = stmt::Insert {
758        target: stmt::InsertTarget::Model(<M as toasty::schema::Model>::id()),
759        source: stmt::Query::new_single(vec![stmt::Expr::record(fields)]),
760        upsert: None,
761        returning: None,
762    };
763
764    toasty::Statement::<()>::from_untyped_stmt(insert.into())
765        .exec(db)
766        .await
767}