toasty_driver_integration_suite/tests/
batch_associations.rs

1//! Test batching association-scoped statements (create, query, update, delete)
2//! through `toasty::batch()`.
3
4use crate::prelude::*;
5
6/// Batch two association-scoped creates on the same relation.
7#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
8pub async fn batch_two_scoped_creates_same_relation(t: &mut Test) -> Result<()> {
9    let mut db = setup(t).await;
10    let user = User::create().name("Alice").exec(&mut db).await?;
11
12    let (t1, t2): (Todo, Todo) = toasty::batch((
13        user.todos().create().title("first"),
14        user.todos().create().title("second"),
15    ))
16    .exec(&mut db)
17    .await?;
18
19    assert_eq!(t1.title, "first");
20    assert_eq!(t2.title, "second");
21    assert_eq!(t1.user_id, user.id);
22    assert_eq!(t2.user_id, user.id);
23
24    let all: Vec<Todo> = user.todos().exec(&mut db).await?;
25    assert_eq!(all.len(), 2);
26
27    Ok(())
28}
29
30/// Batch two association-scoped queries on the same relation.
31#[driver_test(id(ID), requires(sql))]
32pub async fn batch_two_scoped_queries_same_relation(t: &mut Test) -> Result<()> {
33    #[derive(Debug, toasty::Model)]
34    struct User {
35        #[key]
36        #[auto]
37        id: ID,
38        #[has_many]
39        todos: toasty::HasMany<Todo>,
40    }
41
42    #[derive(Debug, toasty::Model)]
43    struct Todo {
44        #[key]
45        #[auto]
46        id: ID,
47        #[index]
48        user_id: ID,
49        #[belongs_to(key = user_id, references = id)]
50        user: toasty::BelongsTo<User>,
51        #[index]
52        title: String,
53    }
54
55    let mut db = t.setup_db(models!(User, Todo)).await;
56
57    let u1 = User::create().exec(&mut db).await?;
58    let u2 = User::create().exec(&mut db).await?;
59    u1.todos().create().title("u1 todo").exec(&mut db).await?;
60    u2.todos().create().title("u2 todo").exec(&mut db).await?;
61
62    let (u1_todos, u2_todos): (Vec<Todo>, Vec<Todo>) = toasty::batch((u1.todos(), u2.todos()))
63        .exec(&mut db)
64        .await?;
65
66    assert_struct!(u1_todos, [_ { title: "u1 todo" }]);
67    assert_struct!(u2_todos, [_ { title: "u2 todo" }]);
68
69    Ok(())
70}
71
72/// Batch association-scoped update and delete on the same relation.
73#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
74pub async fn batch_scoped_update_and_delete_same_relation(t: &mut Test) -> Result<()> {
75    let mut db = setup(t).await;
76    let user = User::create().name("Alice").exec(&mut db).await?;
77    let todo_keep = user.todos().create().title("keep").exec(&mut db).await?;
78    let todo_drop = user.todos().create().title("drop").exec(&mut db).await?;
79
80    let ((), ()): ((), ()) = toasty::batch((
81        user.todos()
82            .filter_by_id(todo_keep.id)
83            .update()
84            .title("kept"),
85        user.todos().filter_by_id(todo_drop.id).delete(),
86    ))
87    .exec(&mut db)
88    .await?;
89
90    let remaining: Vec<Todo> = user.todos().exec(&mut db).await?;
91    assert_eq!(remaining.len(), 1);
92    assert_eq!(remaining[0].title, "kept");
93
94    Ok(())
95}
96
97/// Batch all four CRUD operations through association scope.
98#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
99pub async fn batch_scoped_all_four_crud(t: &mut Test) -> Result<()> {
100    let mut db = setup(t).await;
101    let user = User::create().name("Alice").exec(&mut db).await?;
102    let existing = user
103        .todos()
104        .create()
105        .title("existing")
106        .exec(&mut db)
107        .await?;
108    let doomed = user.todos().create().title("doomed").exec(&mut db).await?;
109
110    let (queried, created, (), ()): (Vec<Todo>, Todo, (), ()) = toasty::batch((
111        user.todos(),
112        user.todos().create().title("new"),
113        user.todos()
114            .filter_by_id(existing.id)
115            .update()
116            .title("updated"),
117        user.todos().filter_by_id(doomed.id).delete(),
118    ))
119    .exec(&mut db)
120    .await?;
121
122    // Query ran before the update/delete in this batch, so sees original state
123    assert_eq!(queried.len(), 2);
124    assert_eq!(created.title, "new");
125
126    // Verify final state
127    let final_todos: Vec<Todo> = user.todos().exec(&mut db).await?;
128    assert_eq!(final_todos.len(), 2); // "updated" + "new", "doomed" deleted
129
130    let titles: Vec<&str> = final_todos.iter().map(|t| t.title.as_str()).collect();
131    assert!(titles.contains(&"updated"));
132    assert!(titles.contains(&"new"));
133
134    Ok(())
135}
136
137/// Batch association-scoped statements mixed with root-level statements.
138#[driver_test(id(ID), requires(sql))]
139pub async fn batch_scoped_with_root_statements(t: &mut Test) -> Result<()> {
140    #[derive(Debug, toasty::Model)]
141    struct User {
142        #[key]
143        #[auto]
144        id: ID,
145        #[index]
146        name: String,
147        #[has_many]
148        todos: toasty::HasMany<Todo>,
149    }
150
151    #[derive(Debug, toasty::Model)]
152    struct Todo {
153        #[key]
154        #[auto]
155        id: ID,
156        #[index]
157        user_id: ID,
158        #[belongs_to(key = user_id, references = id)]
159        user: toasty::BelongsTo<User>,
160        title: String,
161    }
162
163    let mut db = t.setup_db(models!(User, Todo)).await;
164    let user = User::create().name("Alice").exec(&mut db).await?;
165
166    // Mix: root-level query + scoped create + root-level create
167    let (users, todo, new_user): (Vec<User>, Todo, User) = toasty::batch((
168        User::filter_by_name("Alice"),
169        user.todos().create().title("from batch"),
170        User::create().name("Bob"),
171    ))
172    .exec(&mut db)
173    .await?;
174
175    assert_struct!(users, [_ { name: "Alice" }]);
176    assert_eq!(todo.title, "from batch");
177    assert_eq!(todo.user_id, user.id);
178    assert_eq!(new_user.name, "Bob");
179
180    Ok(())
181}
182
183/// Batch association statements across different relations of the same parent.
184#[driver_test(id(ID), requires(sql))]
185pub async fn batch_scoped_across_relations(t: &mut Test) -> Result<()> {
186    #[derive(Debug, toasty::Model)]
187    struct User {
188        #[key]
189        #[auto]
190        id: ID,
191        #[has_many]
192        todos: toasty::HasMany<Todo>,
193        #[has_many]
194        posts: toasty::HasMany<Post>,
195    }
196
197    #[derive(Debug, toasty::Model)]
198    struct Todo {
199        #[key]
200        #[auto]
201        id: ID,
202        #[index]
203        user_id: ID,
204        #[belongs_to(key = user_id, references = id)]
205        user: toasty::BelongsTo<User>,
206        title: String,
207    }
208
209    #[derive(Debug, toasty::Model)]
210    struct Post {
211        #[key]
212        #[auto]
213        id: ID,
214        #[index]
215        user_id: ID,
216        #[belongs_to(key = user_id, references = id)]
217        user: toasty::BelongsTo<User>,
218        body: String,
219    }
220
221    let mut db = t.setup_db(models!(User, Todo, Post)).await;
222    let user = User::create().exec(&mut db).await?;
223
224    // Create across two different relations in one batch
225    let (todo, post): (Todo, Post) = toasty::batch((
226        user.todos().create().title("my todo"),
227        user.posts().create().body("my post"),
228    ))
229    .exec(&mut db)
230    .await?;
231
232    assert_eq!(todo.title, "my todo");
233    assert_eq!(todo.user_id, user.id);
234    assert_eq!(post.body, "my post");
235    assert_eq!(post.user_id, user.id);
236
237    Ok(())
238}
239
240/// Batch queries across different relations of the same parent.
241#[driver_test(id(ID), requires(sql))]
242pub async fn batch_query_across_relations(t: &mut Test) -> Result<()> {
243    #[derive(Debug, toasty::Model)]
244    struct User {
245        #[key]
246        #[auto]
247        id: ID,
248        #[has_many]
249        todos: toasty::HasMany<Todo>,
250        #[has_many]
251        posts: toasty::HasMany<Post>,
252    }
253
254    #[derive(Debug, toasty::Model)]
255    struct Todo {
256        #[key]
257        #[auto]
258        id: ID,
259        #[index]
260        user_id: ID,
261        #[belongs_to(key = user_id, references = id)]
262        user: toasty::BelongsTo<User>,
263        title: String,
264    }
265
266    #[derive(Debug, toasty::Model)]
267    struct Post {
268        #[key]
269        #[auto]
270        id: ID,
271        #[index]
272        user_id: ID,
273        #[belongs_to(key = user_id, references = id)]
274        user: toasty::BelongsTo<User>,
275        body: String,
276    }
277
278    let mut db = t.setup_db(models!(User, Todo, Post)).await;
279    let user = User::create().exec(&mut db).await?;
280    user.todos().create().title("t1").exec(&mut db).await?;
281    user.todos().create().title("t2").exec(&mut db).await?;
282    user.posts().create().body("p1").exec(&mut db).await?;
283
284    let (todos, posts): (Vec<Todo>, Vec<Post>) = toasty::batch((user.todos(), user.posts()))
285        .exec(&mut db)
286        .await?;
287
288    assert_eq!(todos.len(), 2);
289    assert_eq!(posts.len(), 1);
290    assert_eq!(posts[0].body, "p1");
291
292    Ok(())
293}
294
295/// Batch scoped operations from different parents.
296#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
297pub async fn batch_scoped_different_parents(t: &mut Test) -> Result<()> {
298    let mut db = setup(t).await;
299    let alice = User::create().name("Alice").exec(&mut db).await?;
300    let bob = User::create().name("Bob").exec(&mut db).await?;
301
302    // Create todos for different users in one batch
303    let (alice_todo, bob_todo): (Todo, Todo) = toasty::batch((
304        alice.todos().create().title("alice task"),
305        bob.todos().create().title("bob task"),
306    ))
307    .exec(&mut db)
308    .await?;
309
310    assert_eq!(alice_todo.user_id, alice.id);
311    assert_eq!(bob_todo.user_id, bob.id);
312
313    // Query both scopes in one batch
314    let (alice_todos, bob_todos): (Vec<Todo>, Vec<Todo>) =
315        toasty::batch((alice.todos(), bob.todos()))
316            .exec(&mut db)
317            .await?;
318
319    assert_struct!(alice_todos, [_ { title: "alice task" }]);
320    assert_struct!(bob_todos, [_ { title: "bob task" }]);
321
322    Ok(())
323}
324
325/// Batch a scoped delete together with a root-level update.
326#[driver_test(id(ID), requires(sql))]
327pub async fn batch_scoped_delete_with_root_update(t: &mut Test) -> Result<()> {
328    #[derive(Debug, toasty::Model)]
329    struct User {
330        #[key]
331        #[auto]
332        id: ID,
333        #[index]
334        name: String,
335        #[has_many]
336        todos: toasty::HasMany<Todo>,
337    }
338
339    #[derive(Debug, toasty::Model)]
340    struct Todo {
341        #[key]
342        #[auto]
343        id: ID,
344        #[index]
345        user_id: ID,
346        #[belongs_to(key = user_id, references = id)]
347        user: toasty::BelongsTo<User>,
348        title: String,
349    }
350
351    let mut db = t.setup_db(models!(User, Todo)).await;
352    let user = User::create().name("Alice").exec(&mut db).await?;
353    let todo = user.todos().create().title("done").exec(&mut db).await?;
354
355    let ((), ()): ((), ()) = toasty::batch((
356        user.todos().filter_by_id(todo.id).delete(),
357        User::filter_by_name("Alice").update().name("Alice2"),
358    ))
359    .exec(&mut db)
360    .await?;
361
362    // Todo deleted
363    let remaining: Vec<Todo> = user.todos().exec(&mut db).await?;
364    assert!(remaining.is_empty());
365
366    // User updated
367    let updated: Vec<User> = User::filter_by_name("Alice2").exec(&mut db).await?;
368    assert_eq!(updated.len(), 1);
369
370    Ok(())
371}