toasty_driver_integration_suite/tests/
crud_batch_create.rs

1//! Test batch creation of models
2
3use crate::prelude::*;
4
5use toasty_core::driver::{Operation, operation::Transaction};
6
7#[driver_test(id(ID))]
8pub async fn batch_create_empty(test: &mut Test) -> Result<()> {
9    #[derive(Debug, toasty::Model)]
10    struct Todo {
11        #[key]
12        #[auto]
13        id: ID,
14
15        #[allow(dead_code)]
16        title: String,
17    }
18
19    let mut db = test.setup_db(models!(Todo)).await;
20
21    let res = Todo::create_many().exec(&mut db).await?;
22    assert!(res.is_empty());
23    Ok(())
24}
25
26#[driver_test(id(ID))]
27pub async fn batch_create_one(test: &mut Test) -> Result<()> {
28    #[derive(Debug, toasty::Model)]
29    struct Todo {
30        #[key]
31        #[auto]
32        id: ID,
33
34        title: String,
35    }
36
37    let mut db = test.setup_db(models!(Todo)).await;
38
39    test.log().clear();
40    let res = Todo::create_many()
41        .item(Todo::create().title("hello"))
42        .exec(&mut db)
43        .await?;
44
45    assert_eq!(1, res.len());
46    assert_eq!(res[0].title, "hello");
47
48    // Single-row batch: no transaction wrapping needed
49    if test.capability().sql {
50        assert_struct!(test.log().pop_op(), Operation::QuerySql(_));
51        assert!(test.log().is_empty());
52    }
53
54    let reloaded: Vec<_> = Todo::filter_by_id(res[0].id).exec(&mut db).await?;
55    assert_eq!(1, reloaded.len());
56    assert_eq!(reloaded[0].id, res[0].id);
57    Ok(())
58}
59
60#[driver_test(id(ID))]
61pub async fn batch_create_many(test: &mut Test) -> Result<()> {
62    #[derive(Debug, toasty::Model)]
63    struct Todo {
64        #[key]
65        #[auto]
66        id: ID,
67
68        title: String,
69    }
70
71    let mut db = test.setup_db(models!(Todo)).await;
72
73    test.log().clear();
74    let res = Todo::create_many()
75        .item(Todo::create().title("todo 1"))
76        .item(Todo::create().title("todo 2"))
77        .exec(&mut db)
78        .await?;
79
80    assert_eq!(2, res.len());
81    assert_eq!(res[0].title, "todo 1");
82    assert_eq!(res[1].title, "todo 2");
83
84    // Multi-row batch in a single INSERT statement: no transaction wrapping
85    // needed because single SQL statements are inherently atomic.
86    if test.capability().sql {
87        assert_struct!(test.log().pop_op(), Operation::QuerySql(_));
88        assert!(test.log().is_empty());
89    }
90
91    for todo in &res {
92        let reloaded: Vec<_> = Todo::filter_by_id(todo.id).exec(&mut db).await?;
93        assert_eq!(1, reloaded.len());
94        assert_eq!(reloaded[0].id, todo.id);
95    }
96    Ok(())
97}
98
99// TODO: is a batch supposed to be atomic? Probably not.
100#[driver_test(id(ID))]
101#[should_panic]
102pub async fn batch_create_fails_if_any_record_missing_fields(test: &mut Test) -> Result<()> {
103    #[derive(Debug, toasty::Model)]
104    struct User {
105        #[key]
106        email: String,
107
108        #[allow(dead_code)]
109        name: String,
110    }
111
112    let mut db = test.setup_db(models!(User)).await;
113
114    let res = User::create_many()
115        .item(User::create().email("user1@example.com").name("User 1"))
116        .item(User::create().email("user2@example.com"))
117        .exec(&mut db)
118        .await?;
119
120    assert!(res.is_empty());
121
122    let users: Vec<_> = User::filter_by_email("me@carllerche.com")
123        .exec(&mut db)
124        .await?;
125
126    assert!(users.is_empty());
127    Ok(())
128}
129
130#[driver_test(id(ID), scenario(crate::scenarios::user_unique_email))]
131pub async fn batch_create_model_with_unique_field_index_all_unique(test: &mut Test) -> Result<()> {
132    let mut db = setup(test).await;
133
134    let mut res = User::create_many()
135        .item(User::create().email("user1@example.com"))
136        .item(User::create().email("user2@example.com"))
137        .exec(&mut db)
138        .await?;
139
140    assert_eq!(2, res.len());
141
142    res.sort_by_key(|user| user.email.clone());
143
144    assert_eq!(res[0].email, "user1@example.com");
145    assert_eq!(res[1].email, "user2@example.com");
146
147    // We can fetch the user by ID and email
148    for user in &res {
149        let found = User::get_by_id(&mut db, user.id).await?;
150        assert_eq!(found.id, user.id);
151        assert_eq!(found.email, user.email);
152
153        let found = User::get_by_email(&mut db, &user.email).await?;
154        assert_eq!(found.id, user.id);
155        assert_eq!(found.email, user.email);
156    }
157    Ok(())
158}
159
160#[driver_test(id(ID), scenario(crate::scenarios::user_unique_email))]
161#[should_panic]
162pub async fn batch_create_model_with_unique_field_index_all_dups(test: &mut Test) -> Result<()> {
163    let mut db = setup(test).await;
164
165    let _res = User::create_many()
166        .item(User::create().email("user@example.com"))
167        .item(User::create().email("user@example.com"))
168        .exec(&mut db)
169        .await?;
170    Ok(())
171}
172
173/// Unique constraint violation on a multi-row batch is atomic because a single
174/// INSERT statement is inherently atomic in SQL databases.
175#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::user_unique_email))]
176pub async fn batch_create_unique_violation_rolls_back(t: &mut Test) -> Result<()> {
177    let mut db = setup(t).await;
178
179    // Seed the duplicate
180    User::create()
181        .email("taken@example.com")
182        .exec(&mut db)
183        .await?;
184
185    t.log().clear();
186    assert_err!(
187        User::create_many()
188            .item(User::create().email("new@example.com"))
189            .item(User::create().email("taken@example.com"))
190            .exec(&mut db)
191            .await
192    );
193
194    // No transaction wrapper — the single INSERT fails atomically
195    assert!(t.log().is_empty());
196
197    // Only the seeded user remains
198    let users = User::all().exec(&mut db).await?;
199    assert_eq!(1, users.len());
200
201    Ok(())
202}
203
204/// Multi-row batch inside an explicit transaction executes as a single INSERT
205/// without extra savepoint wrapping (the statement is inherently atomic).
206#[driver_test(id(ID), requires(sql))]
207pub async fn batch_create_inside_transaction_uses_savepoints(t: &mut Test) -> Result<()> {
208    #[derive(Debug, toasty::Model)]
209    struct Todo {
210        #[key]
211        #[auto]
212        id: ID,
213        title: String,
214    }
215
216    let mut db = t.setup_db(models!(Todo)).await;
217
218    t.log().clear();
219    let mut tx = db.transaction().await?;
220
221    assert_struct!(
222        t.log().pop_op(),
223        Operation::Transaction(Transaction::Start {
224            isolation: None,
225            read_only: false
226        })
227    );
228
229    Todo::create_many()
230        .item(Todo::create().title("a"))
231        .item(Todo::create().title("b"))
232        .exec(&mut tx)
233        .await?;
234
235    // Single INSERT statement — no savepoint needed
236    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
237
238    tx.commit().await?;
239
240    assert_struct!(
241        t.log().pop_op(),
242        Operation::Transaction(Transaction::Commit)
243    );
244    assert!(t.log().is_empty());
245
246    Ok(())
247}