Skip to main content

toasty_driver_integration_suite/tests/
relation_preload_options.rs

1use crate::prelude::*;
2
3/// Tuple and chained ordering apply independently to each parent's relation.
4#[driver_test(
5    requires(sql),
6    scenario(crate::scenarios::has_many_belongs_to_with_flags)
7)]
8pub async fn preload_has_many_ordered_per_parent(test: &mut Test) -> Result<()> {
9    let mut db = setup(test).await;
10
11    toasty::create!(User::[
12        {
13            name: "alice",
14            todos: [
15                { title: "b", complete: false, priority: 2 },
16                { title: "a", complete: false, priority: 2 },
17                { title: "c", complete: false, priority: 1 },
18            ]
19        },
20        {
21            name: "bob",
22            todos: [
23                { title: "z", complete: false, priority: 1 },
24                { title: "y", complete: false, priority: 1 },
25                { title: "x", complete: false, priority: 3 },
26            ]
27        },
28    ])
29    .exec(&mut db)
30    .await?;
31
32    let tuple_ordered: Vec<User> = User::all()
33        .order_by(User::fields().name().asc())
34        .include(User::fields().todos().order_by((
35            Todo::fields().priority().desc(),
36            Todo::fields().title().asc(),
37        )))
38        .exec(&mut db)
39        .await?;
40
41    assert_struct!(tuple_ordered, [
42        { name: "alice", todos.get(): [{ title: "a" }, { title: "b" }, { title: "c" }] },
43        { name: "bob", todos.get(): [{ title: "x" }, { title: "y" }, { title: "z" }] },
44    ]);
45
46    let chained_ordered: Vec<User> = User::all()
47        .order_by(User::fields().name().asc())
48        .include(
49            User::fields()
50                .todos()
51                .order_by(Todo::fields().priority().desc())
52                .order_by(Todo::fields().title().asc()),
53        )
54        .exec(&mut db)
55        .await?;
56
57    assert_struct!(chained_ordered, [
58        { name: "alice", todos.get(): [{ title: "a" }, { title: "b" }, { title: "c" }] },
59        { name: "bob", todos.get(): [{ title: "x" }, { title: "y" }, { title: "z" }] },
60    ]);
61
62    Ok(())
63}
64
65/// Include filters run before ordering.
66#[driver_test(
67    requires(sql),
68    scenario(crate::scenarios::has_many_belongs_to::id_uuid)
69)]
70pub async fn preload_has_many_filter_and_order(test: &mut Test) -> Result<()> {
71    let mut db = setup(test).await;
72
73    let user = toasty::create!(User {
74        name: "alice",
75        todos: [{ title: "a" }, { title: "c" }, { title: "b" }]
76    })
77    .exec(&mut db)
78    .await?;
79
80    let loaded = User::filter_by_id(user.id)
81        .include(
82            User::fields()
83                .todos()
84                .filter(Todo::fields().title().ne("c"))
85                .order_by(Todo::fields().title().desc()),
86        )
87        .get(&mut db)
88        .await?;
89
90    assert_struct!(loaded.todos.get(), [
91        { title: "b" },
92        { title: "a" },
93    ]);
94
95    Ok(())
96}
97
98/// Ordering applies at each terminal relation in a nested include path.
99#[driver_test(requires(sql), scenario(crate::scenarios::user_post_comment))]
100pub async fn preload_nested_relations_ordered_at_each_level(test: &mut Test) -> Result<()> {
101    let mut db = setup(test).await;
102
103    let user = toasty::create!(User {
104        name: "alice",
105        posts: [
106            {
107                title: "b",
108                comments: [{ body: "a" }, { body: "c" }, { body: "b" }]
109            },
110            {
111                title: "a",
112                comments: [{ body: "x" }, { body: "z" }, { body: "y" }]
113            },
114        ]
115    })
116    .exec(&mut db)
117    .await?;
118
119    let loaded = User::filter_by_id(user.id)
120        .include(
121            User::fields()
122                .posts()
123                .order_by(Post::fields().title().asc()),
124        )
125        .include(
126            User::fields()
127                .posts()
128                .comments()
129                .order_by(Comment::fields().body().desc()),
130        )
131        .get(&mut db)
132        .await?;
133
134    assert_struct!(loaded.posts.get(), [
135        { title: "a", comments.get(): [{ body: "z" }, { body: "y" }, { body: "x" }] },
136        { title: "b", comments.get(): [{ body: "c" }, { body: "b" }, { body: "a" }] },
137    ]);
138
139    Ok(())
140}
141
142/// Include modifiers take precedence over fields with the same names.
143#[driver_test(requires(sql))]
144pub async fn preload_modifiers_with_reserved_field_names(test: &mut Test) -> Result<()> {
145    #[derive(Debug, toasty::Model)]
146    struct Parent {
147        #[key]
148        #[auto]
149        id: uuid::Uuid,
150
151        #[has_many]
152        children: toasty::Deferred<Vec<Child>>,
153    }
154
155    #[derive(Debug, toasty::Model)]
156    struct Child {
157        #[key]
158        #[auto]
159        id: uuid::Uuid,
160
161        filter: String,
162        order_by: String,
163        rank: i64,
164
165        #[index]
166        parent_id: uuid::Uuid,
167
168        #[belongs_to(key = parent_id, references = id)]
169        parent: toasty::Deferred<Parent>,
170    }
171
172    let mut db = test.setup_db(models!(Parent, Child)).await;
173
174    let parent = toasty::create!(Parent {
175        children: [
176            { filter: "a", order_by: "a", rank: 1 },
177            { filter: "b", order_by: "b", rank: 3 },
178            { filter: "c", order_by: "c", rank: 2 },
179        ]
180    })
181    .exec(&mut db)
182    .await?;
183
184    let parent = Parent::filter_by_id(parent.id)
185        .include(
186            Parent::fields()
187                .children()
188                .filter(Child::fields().rank().gt(1))
189                .order_by(Child::fields().rank().desc()),
190        )
191        .get(&mut db)
192        .await?;
193
194    assert_struct!(parent.children.get(), [
195        { rank: 3 },
196        { rank: 2 },
197    ]);
198
199    Ok(())
200}
201
202/// The last include supplies the complete ordering for a repeated relation path.
203#[driver_test(
204    requires(sql),
205    scenario(crate::scenarios::has_many_belongs_to::id_uuid)
206)]
207pub async fn preload_has_many_repeated_ordering_last_wins(test: &mut Test) -> Result<()> {
208    use toasty_core::{driver::Operation, stmt::Statement};
209
210    let mut db = setup(test).await;
211
212    let user = toasty::create!(User {
213        name: "alice",
214        todos: [{ title: "a" }, { title: "c" }, { title: "b" }]
215    })
216    .exec(&mut db)
217    .await?;
218
219    let loaded = User::filter_by_id(user.id)
220        .include(
221            User::fields()
222                .todos()
223                .order_by(Todo::fields().title().asc()),
224        )
225        .include(
226            User::fields()
227                .todos()
228                .order_by(Todo::fields().title().desc()),
229        )
230        .get(&mut db)
231        .await?;
232
233    assert_struct!(loaded.todos.get(), [
234        { title: "c" },
235        { title: "b" },
236        { title: "a" },
237    ]);
238
239    test.log().clear();
240
241    let loaded = User::filter_by_id(user.id)
242        .include(
243            User::fields()
244                .todos()
245                .order_by(Todo::fields().title().desc()),
246        )
247        .include(User::fields().todos())
248        .get(&mut db)
249        .await?;
250
251    assert_eq!(loaded.todos.get().len(), 3);
252
253    let _ = test.log().pop_last_op(); // transaction commit
254    assert_struct!(test.log().pop_last_op(), Operation::QuerySql({
255        stmt: Statement::Query({ order_by: None }),
256    }));
257
258    Ok(())
259}
260
261/// A limit on an include applies per parent record, not globally.
262#[driver_test(
263    requires(sql),
264    scenario(crate::scenarios::has_many_belongs_to_with_flags)
265)]
266pub async fn preload_has_many_limit_per_parent(test: &mut Test) -> Result<()> {
267    let mut db = setup(test).await;
268
269    toasty::create!(User::[
270        {
271            name: "alice",
272            todos: [
273                { title: "b", complete: false, priority: 2 },
274                { title: "a", complete: false, priority: 2 },
275                { title: "c", complete: false, priority: 1 },
276            ]
277        },
278        {
279            name: "bob",
280            todos: [
281                { title: "z", complete: false, priority: 1 },
282                { title: "y", complete: false, priority: 1 },
283                { title: "x", complete: false, priority: 3 },
284            ]
285        },
286    ])
287    .exec(&mut db)
288    .await?;
289
290    let users: Vec<User> = User::all()
291        .order_by(User::fields().name().asc())
292        .include(
293            User::fields()
294                .todos()
295                .order_by(Todo::fields().title().asc())
296                .limit(2),
297        )
298        .exec(&mut db)
299        .await?;
300
301    assert_struct!(users, [
302        { name: "alice", todos.get(): [{ title: "a" }, { title: "b" }] },
303        { name: "bob", todos.get(): [{ title: "x" }, { title: "y" }] },
304    ]);
305
306    Ok(())
307}
308
309/// A bare include limit needs no SQL: truncation happens in the engine during
310/// the nested merge, so it runs on every driver. Without an order_by, *which*
311/// rows are kept is driver-dependent — assert only the per-parent counts.
312#[driver_test(id(ID), scenario(crate::scenarios::has_many_belongs_to))]
313pub async fn preload_has_many_limit_without_order(test: &mut Test) -> Result<()> {
314    let mut db = setup(test).await;
315
316    // TODO: seed with a single batch create once MySQL supports returning
317    // generated IDs from a multi-row insert (the per-row fallback deferred in
318    // tokio-rs/toasty#1190).
319    for (name, titles) in [("alice", ["a", "b", "c"]), ("bob", ["x", "y", "z"])] {
320        let user = toasty::create!(User { name }).exec(&mut db).await?;
321
322        for title in titles {
323            user.todos().create().title(title).exec(&mut db).await?;
324        }
325    }
326
327    let users: Vec<User> = User::all()
328        .include(User::fields().todos().limit(2))
329        .exec(&mut db)
330        .await?;
331
332    assert_struct!(users, [
333        { todos.get().len(): 2 },
334        { todos.get().len(): 2 },
335    ]);
336
337    Ok(())
338}
339
340/// Include filters run before the limit is applied.
341#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
342pub async fn preload_has_many_limit_with_filter(test: &mut Test) -> Result<()> {
343    let mut db = setup(test).await;
344
345    // TODO: seed with a single batch create once MySQL supports returning
346    // generated IDs from a multi-row insert (the per-row fallback deferred in
347    // tokio-rs/toasty#1190).
348    let user = toasty::create!(User { name: "alice" })
349        .exec(&mut db)
350        .await?;
351
352    for title in ["a", "c", "b", "d"] {
353        user.todos().create().title(title).exec(&mut db).await?;
354    }
355
356    let loaded = User::filter_by_id(user.id)
357        .include(
358            User::fields()
359                .todos()
360                .filter(Todo::fields().title().ne("c"))
361                .order_by(Todo::fields().title().desc())
362                .limit(2),
363        )
364        .get(&mut db)
365        .await?;
366
367    assert_struct!(loaded.todos.get(), [
368        { title: "d" },
369        { title: "b" },
370    ]);
371
372    Ok(())
373}
374
375/// A limit larger than the number of related rows returns them all; a limit of
376/// zero returns none.
377#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
378pub async fn preload_has_many_limit_bounds(test: &mut Test) -> Result<()> {
379    let mut db = setup(test).await;
380
381    // TODO: seed with a single batch create once MySQL supports returning
382    // generated IDs from a multi-row insert (the per-row fallback deferred in
383    // tokio-rs/toasty#1190).
384    let user = toasty::create!(User { name: "alice" })
385        .exec(&mut db)
386        .await?;
387
388    for title in ["a", "b"] {
389        user.todos().create().title(title).exec(&mut db).await?;
390    }
391
392    let loaded = User::filter_by_id(user.id)
393        .include(
394            User::fields()
395                .todos()
396                .order_by(Todo::fields().title().asc())
397                .limit(10),
398        )
399        .get(&mut db)
400        .await?;
401
402    assert_struct!(loaded.todos.get(), [
403        { title: "a" },
404        { title: "b" },
405    ]);
406
407    let loaded = User::filter_by_id(user.id)
408        .include(User::fields().todos().limit(0))
409        .get(&mut db)
410        .await?;
411
412    assert!(loaded.todos.get().is_empty());
413
414    Ok(())
415}
416
417/// Limits apply independently at each relation in a nested include path.
418#[driver_test(requires(sql), scenario(crate::scenarios::user_post_comment))]
419pub async fn preload_nested_relations_limited_at_each_level(test: &mut Test) -> Result<()> {
420    let mut db = setup(test).await;
421
422    let user = toasty::create!(User {
423        name: "alice",
424        posts: [
425            {
426                title: "a",
427                comments: [{ body: "a" }, { body: "c" }, { body: "b" }]
428            },
429            {
430                title: "b",
431                comments: [{ body: "x" }, { body: "z" }, { body: "y" }]
432            },
433            {
434                title: "c",
435                comments: [{ body: "q" }]
436            },
437        ]
438    })
439    .exec(&mut db)
440    .await?;
441
442    let loaded = User::filter_by_id(user.id)
443        .include(
444            User::fields()
445                .posts()
446                .order_by(Post::fields().title().asc())
447                .limit(2),
448        )
449        .include(
450            User::fields()
451                .posts()
452                .comments()
453                .order_by(Comment::fields().body().desc())
454                .limit(2),
455        )
456        .get(&mut db)
457        .await?;
458
459    assert_struct!(loaded.posts.get(), [
460        { title: "a", comments.get(): [{ body: "c" }, { body: "b" }] },
461        { title: "b", comments.get(): [{ body: "z" }, { body: "y" }] },
462    ]);
463
464    Ok(())
465}
466
467/// An offset on an include skips rows per parent record, not globally.
468#[driver_test(
469    requires(sql),
470    scenario(crate::scenarios::has_many_belongs_to_with_flags)
471)]
472pub async fn preload_has_many_offset_per_parent(test: &mut Test) -> Result<()> {
473    let mut db = setup(test).await;
474
475    toasty::create!(User::[
476        {
477            name: "alice",
478            todos: [
479                { title: "b", complete: false, priority: 2 },
480                { title: "a", complete: false, priority: 2 },
481                { title: "c", complete: false, priority: 1 },
482            ]
483        },
484        {
485            name: "bob",
486            todos: [
487                { title: "z", complete: false, priority: 1 },
488                { title: "y", complete: false, priority: 1 },
489                { title: "x", complete: false, priority: 3 },
490            ]
491        },
492    ])
493    .exec(&mut db)
494    .await?;
495
496    let users: Vec<User> = User::all()
497        .order_by(User::fields().name().asc())
498        .include(
499            User::fields()
500                .todos()
501                .order_by(Todo::fields().title().asc())
502                .limit(2)
503                .offset(1),
504        )
505        .exec(&mut db)
506        .await?;
507
508    assert_struct!(users, [
509        { name: "alice", todos.get(): [{ title: "b" }, { title: "c" }] },
510        { name: "bob", todos.get(): [{ title: "y" }, { title: "z" }] },
511    ]);
512
513    Ok(())
514}
515
516/// A bare include offset needs no SQL: skipping happens in the engine during
517/// the nested merge, so it runs on every driver. Without an order_by, *which*
518/// rows are skipped is driver-dependent — assert only the per-parent counts.
519#[driver_test(id(ID), scenario(crate::scenarios::has_many_belongs_to))]
520pub async fn preload_has_many_offset_without_order(test: &mut Test) -> Result<()> {
521    let mut db = setup(test).await;
522
523    // TODO: seed with a single batch create once MySQL supports returning
524    // generated IDs from a multi-row insert (the per-row fallback deferred in
525    // tokio-rs/toasty#1190).
526    for (name, titles) in [("alice", ["a", "b", "c"]), ("bob", ["x", "y", "z"])] {
527        let user = toasty::create!(User { name }).exec(&mut db).await?;
528
529        for title in titles {
530            user.todos().create().title(title).exec(&mut db).await?;
531        }
532    }
533
534    let users: Vec<User> = User::all()
535        .include(User::fields().todos().limit(2).offset(2))
536        .exec(&mut db)
537        .await?;
538
539    assert_struct!(users, [
540        { todos.get().len(): 1 },
541        { todos.get().len(): 1 },
542    ]);
543
544    Ok(())
545}
546
547/// Include filters run before the offset and limit are applied.
548#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
549pub async fn preload_has_many_offset_with_filter(test: &mut Test) -> Result<()> {
550    let mut db = setup(test).await;
551
552    // TODO: seed with a single batch create once MySQL supports returning
553    // generated IDs from a multi-row insert (the per-row fallback deferred in
554    // tokio-rs/toasty#1190).
555    let user = toasty::create!(User { name: "alice" })
556        .exec(&mut db)
557        .await?;
558
559    for title in ["a", "c", "b", "d"] {
560        user.todos().create().title(title).exec(&mut db).await?;
561    }
562
563    let loaded = User::filter_by_id(user.id)
564        .include(
565            User::fields()
566                .todos()
567                .filter(Todo::fields().title().ne("c"))
568                .order_by(Todo::fields().title().desc())
569                .limit(2)
570                .offset(1),
571        )
572        .get(&mut db)
573        .await?;
574
575    assert_struct!(loaded.todos.get(), [
576        { title: "b" },
577        { title: "a" },
578    ]);
579
580    Ok(())
581}
582
583/// An offset past the end of the related rows returns none; an offset of zero
584/// is a no-op.
585#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
586pub async fn preload_has_many_offset_bounds(test: &mut Test) -> Result<()> {
587    let mut db = setup(test).await;
588
589    // TODO: seed with a single batch create once MySQL supports returning
590    // generated IDs from a multi-row insert (the per-row fallback deferred in
591    // tokio-rs/toasty#1190).
592    let user = toasty::create!(User { name: "alice" })
593        .exec(&mut db)
594        .await?;
595
596    for title in ["a", "b"] {
597        user.todos().create().title(title).exec(&mut db).await?;
598    }
599
600    let loaded = User::filter_by_id(user.id)
601        .include(
602            User::fields()
603                .todos()
604                .order_by(Todo::fields().title().asc())
605                .limit(10)
606                .offset(10),
607        )
608        .get(&mut db)
609        .await?;
610
611    assert!(loaded.todos.get().is_empty());
612
613    let loaded = User::filter_by_id(user.id)
614        .include(
615            User::fields()
616                .todos()
617                .order_by(Todo::fields().title().asc())
618                .limit(10)
619                .offset(0),
620        )
621        .get(&mut db)
622        .await?;
623
624    assert_struct!(loaded.todos.get(), [
625        { title: "a" },
626        { title: "b" },
627    ]);
628
629    Ok(())
630}
631
632/// Offsets apply independently at each relation in a nested include path.
633#[driver_test(requires(sql), scenario(crate::scenarios::user_post_comment))]
634pub async fn preload_nested_relations_offset_at_each_level(test: &mut Test) -> Result<()> {
635    let mut db = setup(test).await;
636
637    let user = toasty::create!(User {
638        name: "alice",
639        posts: [
640            {
641                title: "a",
642                comments: [{ body: "a" }, { body: "c" }, { body: "b" }]
643            },
644            {
645                title: "b",
646                comments: [{ body: "x" }, { body: "z" }, { body: "y" }]
647            },
648            {
649                title: "c",
650                comments: [{ body: "q" }]
651            },
652        ]
653    })
654    .exec(&mut db)
655    .await?;
656
657    let loaded = User::filter_by_id(user.id)
658        .include(
659            User::fields()
660                .posts()
661                .order_by(Post::fields().title().asc())
662                .limit(2)
663                .offset(1),
664        )
665        .include(
666            User::fields()
667                .posts()
668                .comments()
669                .order_by(Comment::fields().body().asc())
670                .limit(2)
671                .offset(1),
672        )
673        .get(&mut db)
674        .await?;
675
676    assert_struct!(loaded.posts.get(), [
677        { title: "b", comments.get(): [{ body: "y" }, { body: "z" }] },
678        { title: "c", comments.get(): [] },
679    ]);
680
681    Ok(())
682}
683
684/// Include ordering is rejected when the terminal relation is singular.
685#[driver_test(scenario(crate::scenarios::has_one_optional_belongs_to::id_uuid))]
686pub async fn preload_has_one_ordering_rejected(test: &mut Test) -> Result<()> {
687    let mut db = setup(test).await;
688
689    let err = assert_err!(
690        User::all()
691            .include(
692                User::fields()
693                    .profile()
694                    .order_by(Profile::fields().bio().asc()),
695            )
696            .exec(&mut db)
697            .await
698    );
699
700    assert!(err.is_invalid_statement(), "unexpected error: {err}");
701
702    Ok(())
703}
704
705/// An include limit is rejected when the terminal relation is singular.
706#[driver_test(id(ID), scenario(crate::scenarios::has_one_optional_belongs_to))]
707pub async fn preload_has_one_limit_rejected(test: &mut Test) -> Result<()> {
708    let mut db = setup(test).await;
709
710    let err = assert_err!(
711        User::all()
712            .include(User::fields().profile().limit(1))
713            .exec(&mut db)
714            .await
715    );
716
717    assert!(err.is_invalid_statement(), "unexpected error: {err}");
718
719    Ok(())
720}