Skip to main content

toasty_driver_integration_suite/tests/
crud_batch_create.rs

1//! Test batch creation of models
2
3use crate::prelude::*;
4
5use toasty::Json;
6use toasty_core::{
7    driver::{Operation, operation::Transaction},
8    schema::db,
9    stmt::{Expr, ExprFunc, ExprSet, Statement, TableRef, Value},
10};
11
12#[driver_test(scenario(crate::scenarios::two_models))]
13pub async fn batch_create_empty(test: &mut Test) -> Result<()> {
14    let mut db = setup(test).await;
15
16    let res = Post::create_many().exec(&mut db).await?;
17    assert!(res.is_empty());
18    Ok(())
19}
20
21#[driver_test(scenario(crate::scenarios::two_models))]
22pub async fn batch_create_one(test: &mut Test) -> Result<()> {
23    let mut db = setup(test).await;
24
25    test.log().clear();
26    let res = Post::create_many()
27        .item(Post::create().title("hello"))
28        .exec(&mut db)
29        .await?;
30
31    assert_eq!(1, res.len());
32    assert_eq!(res[0].title, "hello");
33
34    // Single-row batch: no transaction wrapping needed
35    if test.capability().sql() {
36        assert_struct!(test.log().pop_op(), Operation::Insert(_));
37        assert!(test.log().is_empty());
38    }
39
40    let reloaded: Vec<_> = Post::filter_by_id(res[0].id).exec(&mut db).await?;
41    assert_eq!(1, reloaded.len());
42    assert_eq!(reloaded[0].id, res[0].id);
43    Ok(())
44}
45
46#[driver_test(scenario(crate::scenarios::two_models))]
47pub async fn batch_create_many(test: &mut Test) -> Result<()> {
48    let mut db = setup(test).await;
49
50    test.log().clear();
51    let res = Post::create_many()
52        .item(Post::create().title("todo 1"))
53        .item(Post::create().title("todo 2"))
54        .exec(&mut db)
55        .await?;
56
57    assert_eq!(2, res.len());
58    assert_eq!(res[0].title, "todo 1");
59    assert_eq!(res[1].title, "todo 2");
60
61    // Multi-row batch in a single INSERT statement: no transaction wrapping
62    // needed because single SQL statements are inherently atomic.
63    if test.capability().sql() {
64        assert_struct!(test.log().pop_op(), Operation::Insert(_));
65        assert!(test.log().is_empty());
66    }
67
68    for post in &res {
69        let reloaded: Vec<_> = Post::filter_by_id(post.id).exec(&mut db).await?;
70        assert_eq!(1, reloaded.len());
71        assert_eq!(reloaded[0].id, post.id);
72    }
73    Ok(())
74}
75
76#[driver_test(requires(and(auto_increment, returning_from_insert)))]
77pub async fn batch_create_many_auto_increment(test: &mut Test) -> Result<()> {
78    #[derive(Debug, toasty::Model)]
79    struct Item {
80        #[key]
81        #[auto]
82        id: u64,
83
84        name: String,
85    }
86
87    let mut db = test.setup_db(models!(Item)).await;
88    let items = Item::create_many()
89        .item(Item::create().name("one"))
90        .item(Item::create().name("two"))
91        .exec(&mut db)
92        .await?;
93
94    assert_struct!(items, [{ id: 1, name: "one" }, { id: 2, name: "two" }]);
95
96    Ok(())
97}
98
99#[driver_test(requires(and(auto_increment, not(returning_from_insert))))]
100pub async fn batch_create_many_auto_increment_requires_returning(test: &mut Test) -> Result<()> {
101    #[derive(Debug, toasty::Model)]
102    struct Generated {
103        #[key]
104        #[auto]
105        id: u64,
106
107        name: String,
108    }
109
110    #[derive(Debug, toasty::Model)]
111    struct Manual {
112        #[key]
113        id: u64,
114
115        name: String,
116    }
117
118    let mut db = test.setup_db(models!(Generated, Manual)).await;
119
120    test.log().clear();
121    let err = assert_err!(
122        Generated::create_many()
123            .item(Generated::create().name("one"))
124            .item(Generated::create().name("two"))
125            .exec(&mut db)
126            .await
127    );
128
129    assert!(err.is_unsupported_feature());
130    assert!(err.to_string().contains(test.capability().driver_name));
131    assert!(err.to_string().contains("generateds.id"));
132    assert!(test.log().is_empty());
133
134    let items = Manual::create_many()
135        .item(Manual::create().id(10).name("one"))
136        .item(Manual::create().id(20).name("two"))
137        .exec(&mut db)
138        .await?;
139
140    assert_struct!(items, [{ id: 10, name: "one" }, { id: 20, name: "two" }]);
141    assert_struct!(test.log().pop_op(), Operation::Insert({ ret: None }));
142    assert!(test.log().is_empty());
143
144    Ok(())
145}
146
147/// A NULL cell in an optional column survives a multi-row create with a
148/// generated key, under both ID strategies. On PostgreSQL this exercises the
149/// INSERT → `unnest` transpose with a NULL cell inside a column array bind.
150///
151/// Gated on `returning_from_insert`: the `id(ID)` expansion gates its
152/// `id_u64` variant on `auto_increment` alone, so without this the test also
153/// runs on MySQL, where a multi-row insert with a generated key needs the
154/// `RETURNING` clause MySQL lacks.
155#[driver_test(id(ID), requires(returning_from_insert))]
156pub async fn batch_create_with_null_field(test: &mut Test) -> Result<()> {
157    #[derive(Debug, toasty::Model)]
158    struct Item {
159        #[key]
160        #[auto]
161        id: ID,
162        name: Option<String>,
163    }
164
165    let mut db = test.setup_db(models!(Item)).await;
166
167    let res = Item::create_many()
168        .item(Item::create().name("n1"))
169        .item(Item::create())
170        .item(Item::create().name("n3"))
171        .exec(&mut db)
172        .await?;
173
174    assert_eq!(3, res.len());
175    assert_eq!(res[0].name.as_deref(), Some("n1"));
176    assert_eq!(res[1].name, None);
177    assert_eq!(res[2].name.as_deref(), Some("n3"));
178
179    let reloaded = Item::get_by_id(&mut db, &res[1].id).await?;
180    assert_eq!(reloaded.name, None);
181    Ok(())
182}
183
184#[driver_test(requires(insert_values_unnest))]
185pub async fn batch_create_uses_unnest_array_params(test: &mut Test) -> Result<()> {
186    #[derive(Debug, toasty::Model)]
187    struct Item {
188        #[key]
189        id: String,
190        name: Option<String>,
191    }
192
193    let mut db = test.setup_db(models!(Item)).await;
194
195    test.log().clear();
196    Item::create_many()
197        .item(Item::create().id("item-1").name("n1"))
198        .item(Item::create().id("item-2"))
199        .exec(&mut db)
200        .await?;
201
202    let Operation::Insert(op) = test.log().pop_op() else {
203        panic!("expected Insert operation");
204    };
205    let Statement::Insert(insert) = op.stmt else {
206        panic!("expected Insert statement");
207    };
208    let ExprSet::Select(select) = insert.source.body else {
209        panic!("expected Select insert source");
210    };
211    let [TableRef::RowsFrom(funcs)] = select.source.as_table_unwrap().tables.as_slice() else {
212        panic!("expected ROWS FROM table source");
213    };
214    let [ExprFunc::Unnest(ids), ExprFunc::Unnest(names)] = funcs.as_slice() else {
215        panic!("expected one unnest function per inserted column");
216    };
217
218    assert_eq!(ids.arg.as_ref(), &Expr::arg(0));
219    assert_eq!(names.arg.as_ref(), &Expr::arg(1));
220    assert_eq!(op.params.len(), 2);
221    assert_eq!(
222        op.params[0].value,
223        Value::List(vec![Value::from("item-1"), Value::from("item-2")])
224    );
225    assert_eq!(op.params[0].ty, db::Type::list(db::Type::Text));
226    assert_eq!(
227        op.params[1].value,
228        Value::List(vec![Value::from("n1"), Value::Null])
229    );
230    assert_eq!(op.params[1].ty, db::Type::list(db::Type::Text));
231    assert!(test.log().is_empty());
232
233    // The plan assertions above can't catch a swapped or misaligned column
234    // array — read the rows back to confirm each cell landed on its own row.
235    let first = Item::get_by_id(&mut db, "item-1").await?;
236    assert_eq!(first.name.as_deref(), Some("n1"));
237
238    let second = Item::get_by_id(&mut db, "item-2").await?;
239    assert_eq!(second.name, None);
240
241    Ok(())
242}
243
244/// Native `json`/`jsonb` columns hold a document per row, so they must not be
245/// transposed into a per-column array bind: PostgreSQL has no array type
246/// registered for either, and binding one panics the driver.
247#[driver_test(requires(and(native_json, native_jsonb)))]
248pub async fn batch_create_document_columns(test: &mut Test) -> Result<()> {
249    #[derive(Debug, toasty::Model)]
250    struct Doc {
251        #[key]
252        id: String,
253        #[column(type = "json")]
254        text_encoded: Json<String>,
255        #[column(type = "jsonb")]
256        binary_encoded: Json<String>,
257    }
258
259    let mut db = test.setup_db(models!(Doc)).await;
260
261    let res = Doc::create_many()
262        .item(
263            Doc::create()
264                .id("a")
265                .text_encoded("one".to_string())
266                .binary_encoded("two".to_string()),
267        )
268        .item(
269            Doc::create()
270                .id("b")
271                .text_encoded("three".to_string())
272                .binary_encoded("four".to_string()),
273        )
274        .exec(&mut db)
275        .await?;
276
277    assert_eq!(2, res.len());
278
279    let reloaded = Doc::get_by_id(&mut db, "b").await?;
280    assert_struct!(reloaded, {
281        text_encoded: Json(== "three"),
282        binary_encoded: Json(== "four"),
283    });
284
285    Ok(())
286}
287
288// TODO: is a batch supposed to be atomic? Probably not.
289#[driver_test]
290#[should_panic]
291pub async fn batch_create_fails_if_any_record_missing_fields(test: &mut Test) -> Result<()> {
292    #[derive(Debug, toasty::Model)]
293    struct User {
294        #[key]
295        email: String,
296
297        #[allow(dead_code)]
298        name: String,
299    }
300
301    let mut db = test.setup_db(models!(User)).await;
302
303    let res = User::create_many()
304        .item(User::create().email("user1@example.com").name("User 1"))
305        .item(User::create().email("user2@example.com"))
306        .exec(&mut db)
307        .await?;
308
309    assert!(res.is_empty());
310
311    let users: Vec<_> = User::filter_by_email("me@carllerche.com")
312        .exec(&mut db)
313        .await?;
314
315    assert!(users.is_empty());
316    Ok(())
317}
318
319#[driver_test(scenario(crate::scenarios::user_unique_email))]
320pub async fn batch_create_model_with_unique_field_index_all_unique(test: &mut Test) -> Result<()> {
321    let mut db = setup(test).await;
322
323    let mut res = User::create_many()
324        .item(User::create().email("user1@example.com"))
325        .item(User::create().email("user2@example.com"))
326        .exec(&mut db)
327        .await?;
328
329    assert_eq!(2, res.len());
330
331    res.sort_by_key(|user| user.email.clone());
332
333    assert_eq!(res[0].email, "user1@example.com");
334    assert_eq!(res[1].email, "user2@example.com");
335
336    // We can fetch the user by ID and email
337    for user in &res {
338        let found = User::get_by_id(&mut db, user.id).await?;
339        assert_eq!(found.id, user.id);
340        assert_eq!(found.email, user.email);
341
342        let found = User::get_by_email(&mut db, &user.email).await?;
343        assert_eq!(found.id, user.id);
344        assert_eq!(found.email, user.email);
345    }
346    Ok(())
347}
348
349#[driver_test(scenario(crate::scenarios::user_unique_email))]
350#[should_panic]
351pub async fn batch_create_model_with_unique_field_index_all_dups(test: &mut Test) -> Result<()> {
352    let mut db = setup(test).await;
353
354    let _res = User::create_many()
355        .item(User::create().email("user@example.com"))
356        .item(User::create().email("user@example.com"))
357        .exec(&mut db)
358        .await?;
359    Ok(())
360}
361
362/// Unique constraint violation on a multi-row batch is atomic because a single
363/// INSERT statement is inherently atomic in SQL databases.
364#[driver_test(requires(sql), scenario(crate::scenarios::user_unique_email))]
365pub async fn batch_create_unique_violation_rolls_back(t: &mut Test) -> Result<()> {
366    let mut db = setup(t).await;
367
368    // Seed the duplicate
369    User::create()
370        .email("taken@example.com")
371        .exec(&mut db)
372        .await?;
373
374    t.log().clear();
375    assert_err!(
376        User::create_many()
377            .item(User::create().email("new@example.com"))
378            .item(User::create().email("taken@example.com"))
379            .exec(&mut db)
380            .await
381    );
382
383    // No transaction wrapper — the single INSERT fails atomically
384    assert!(t.log().is_empty());
385
386    // Only the seeded user remains
387    let users = User::all().exec(&mut db).await?;
388    assert_eq!(1, users.len());
389
390    Ok(())
391}
392
393/// Multi-row batch inside an explicit transaction executes as a single INSERT
394/// without extra savepoint wrapping (the statement is inherently atomic).
395#[driver_test(requires(sql), scenario(crate::scenarios::two_models))]
396pub async fn batch_create_inside_transaction_uses_savepoints(t: &mut Test) -> Result<()> {
397    let mut db = setup(t).await;
398
399    t.log().clear();
400    let mut tx = db.transaction().await?;
401
402    assert_struct!(
403        t.log().pop_op(),
404        Operation::Transaction(Transaction::Start {
405            isolation: None,
406            read_only: false,
407            ..
408        })
409    );
410
411    Post::create_many()
412        .item(Post::create().title("a"))
413        .item(Post::create().title("b"))
414        .exec(&mut tx)
415        .await?;
416
417    // Single INSERT statement — no savepoint needed
418    assert_struct!(t.log().pop_op(), Operation::Insert(_));
419
420    tx.commit().await?;
421
422    assert_struct!(
423        t.log().pop_op(),
424        Operation::Transaction(Transaction::Commit)
425    );
426    assert!(t.log().is_empty());
427
428    Ok(())
429}