toasty_driver_integration_suite/tests/
one_model_batch_create.rs

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