Skip to main content

toasty_driver_integration_suite/tests/
tx_atomic_stmt.rs

1use crate::prelude::*;
2
3use toasty_core::driver::{Operation, operation::Transaction};
4
5// ===== Transaction wrapping =====
6
7/// A multi-op create (user + associated todo) should be wrapped in
8/// BEGIN ... COMMIT so the driver sees all three transaction operations.
9#[driver_test(
10    requires(sql),
11    scenario(crate::scenarios::has_many_belongs_to::id_uuid)
12)]
13pub async fn multi_op_create_wraps_in_transaction(t: &mut Test) -> Result<()> {
14    let mut db = setup(t).await;
15
16    t.log().clear();
17    let user = User::create()
18        .name("Alice")
19        .todos([Todo::create().title("task")])
20        .exec(&mut db)
21        .await?;
22
23    assert_struct!(
24        t.log().pop_op(),
25        Operation::Transaction(Transaction::Start {
26            isolation: None,
27            read_only: false,
28            ..
29        })
30    );
31    assert_struct!(t.log().pop_op(), Operation::Insert(_)); // INSERT user
32    assert_struct!(t.log().pop_op(), Operation::Insert(_)); // INSERT todo
33    assert_struct!(
34        t.log().pop_op(),
35        Operation::Transaction(Transaction::Commit)
36    );
37    assert!(t.log().is_empty());
38
39    let todos = user.todos().exec(&mut db).await?;
40    assert_eq!(1, todos.len());
41
42    Ok(())
43}
44
45/// A single-op create (no associations) must NOT be wrapped in a transaction —
46/// the engine skips the overhead for plans with only one DB operation.
47#[driver_test(requires(scan), scenario(crate::scenarios::two_models))]
48pub async fn single_op_skips_transaction(t: &mut Test) -> Result<()> {
49    let mut db = setup(t).await;
50
51    t.log().clear();
52    User::create().name("x").exec(&mut db).await?;
53
54    // Only the INSERT — no Transaction::Start { isolation: None, read_only: false } bookending it
55    assert_struct!(t.log().pop_op(), Operation::Insert(_));
56    assert!(t.log().is_empty());
57
58    Ok(())
59}
60
61// ===== Rollback on partial failure =====
62
63/// When the second INSERT in a has_many create plan fails (unique constraint),
64/// the driver should receive Transaction::Rollback and no orphaned user should
65/// remain in the database.
66///
67/// Uses u64 (auto-increment) IDs so that the engine always generates two
68/// separate DB operations (INSERT user then INSERT todo), ensuring the
69/// explicit transaction wrapping is exercised. With uuid::Uuid IDs the engine
70/// reorders execution (INSERT todo before INSERT user due to the Const
71/// optimization), which produces a different but equally valid log pattern.
72#[driver_test(requires(and(sql, auto_increment)))]
73pub async fn create_with_has_many_rolls_back_on_failure(t: &mut Test) -> Result<()> {
74    #[derive(Debug, toasty::Model)]
75    struct User {
76        #[key]
77        #[auto]
78        id: u64,
79
80        #[has_many]
81        todos: toasty::Deferred<Vec<Todo>>,
82    }
83
84    #[derive(Debug, toasty::Model)]
85    struct Todo {
86        #[key]
87        #[auto]
88        id: u64,
89
90        #[index]
91        user_id: u64,
92
93        #[belongs_to(key = user_id, references = id)]
94        user: toasty::Deferred<User>,
95
96        #[unique]
97        title: String,
98    }
99
100    let mut db = t.setup_db(models!(User, Todo)).await;
101
102    // Seed the title that will cause the second INSERT to fail.
103    User::create()
104        .todos([Todo::create().title("taken")])
105        .exec(&mut db)
106        .await?;
107
108    t.log().clear();
109    assert_err!(
110        User::create()
111            .todos([Todo::create().title("taken")])
112            .exec(&mut db)
113            .await
114    );
115
116    // Transaction::Start { isolation: None, read_only: false } → INSERT user (succeeds, logged) →
117    // INSERT todo (fails on unique constraint, NOT logged) → Transaction::Rollback
118    assert_struct!(
119        t.log().pop_op(),
120        Operation::Transaction(Transaction::Start {
121            isolation: None,
122            read_only: false,
123            ..
124        })
125    );
126    assert_struct!(t.log().pop_op(), Operation::Insert(_)); // INSERT user
127    assert_struct!(
128        t.log().pop_op(),
129        Operation::Transaction(Transaction::Rollback)
130    );
131    assert!(t.log().is_empty());
132
133    // No orphaned user — count unchanged from pre-seed
134    let users = User::all().exec(&mut db).await?;
135    assert_eq!(1, users.len());
136
137    Ok(())
138}
139
140/// Same rollback guarantee for a has_one association create.
141///
142/// Uses u64 (auto-increment) IDs so that the engine always generates two
143/// separate DB operations (INSERT user then INSERT profile), ensuring the
144/// explicit transaction wrapping is exercised. With uuid::Uuid IDs the engine
145/// can combine both inserts into a single atomic SQL statement, which provides
146/// atomicity without an explicit transaction.
147#[driver_test(requires(and(sql, auto_increment)))]
148pub async fn create_with_has_one_rolls_back_on_failure(t: &mut Test) -> Result<()> {
149    #[derive(Debug, toasty::Model)]
150    struct User {
151        #[key]
152        #[auto]
153        id: u64,
154
155        #[has_one]
156        profile: toasty::Deferred<Option<Profile>>,
157    }
158
159    #[derive(Debug, toasty::Model)]
160    struct Profile {
161        #[key]
162        #[auto]
163        id: u64,
164
165        #[unique]
166        bio: String,
167
168        #[unique]
169        user_id: u64,
170
171        #[belongs_to(key = user_id, references = id)]
172        user: toasty::Deferred<User>,
173    }
174
175    let mut db = t.setup_db(models!(User, Profile)).await;
176
177    // Seed the bio that will cause the second INSERT to fail.
178    User::create()
179        .profile(Profile::create().bio("taken"))
180        .exec(&mut db)
181        .await?;
182
183    t.log().clear();
184    assert_err!(
185        User::create()
186            .profile(Profile::create().bio("taken"))
187            .exec(&mut db)
188            .await
189    );
190
191    assert_struct!(
192        t.log().pop_op(),
193        Operation::Transaction(Transaction::Start {
194            isolation: None,
195            read_only: false,
196            ..
197        })
198    );
199    assert_struct!(t.log().pop_op(), Operation::Insert(_)); // INSERT user
200    assert_struct!(
201        t.log().pop_op(),
202        Operation::Transaction(Transaction::Rollback)
203    );
204    assert!(t.log().is_empty());
205
206    // No orphaned user — count unchanged from pre-seed
207    let users = User::all().exec(&mut db).await?;
208    assert_eq!(1, users.len());
209
210    Ok(())
211}
212
213/// When an update + new-association plan fails on the UPDATE (after the
214/// INSERT succeeds), the INSERT must also be rolled back.
215///
216/// The engine always executes INSERT before UPDATE in such plans (INSERT is
217/// a dependency of the UPDATE's returning clause). So the collision is placed
218/// on the User's name field (not the Todo), ensuring the INSERT succeeds first
219/// and is then rolled back when the subsequent UPDATE fails.
220#[driver_test(requires(sql))]
221pub async fn update_with_new_association_rolls_back_on_failure(t: &mut Test) -> Result<()> {
222    #[derive(Debug, toasty::Model)]
223    struct User {
224        #[key]
225        #[auto]
226        id: uuid::Uuid,
227
228        #[unique]
229        name: String,
230
231        #[has_many]
232        todos: toasty::Deferred<Vec<Todo>>,
233    }
234
235    #[derive(Debug, toasty::Model)]
236    struct Todo {
237        #[key]
238        #[auto]
239        id: uuid::Uuid,
240
241        #[index]
242        user_id: uuid::Uuid,
243
244        #[belongs_to(key = user_id, references = id)]
245        user: toasty::Deferred<User>,
246
247        title: String,
248    }
249
250    let mut db = t.setup_db(models!(User, Todo)).await;
251
252    let mut user = User::create().name("original").exec(&mut db).await?;
253    // Seed the name collision — this user's name will be duplicated by the failing UPDATE.
254    User::create().name("taken").exec(&mut db).await?;
255
256    t.log().clear();
257    assert_err!(
258        user.update()
259            .name("taken") // UPDATE will fail: unique name
260            .todos(toasty::stmt::insert(Todo::create().title("new-todo"))) // INSERT runs first and succeeds
261            .exec(&mut db)
262            .await
263    );
264
265    // INSERT todo runs first (succeeds, logged), then UPDATE user fails on unique
266    // name → Transaction::Rollback undoes the INSERT.
267    assert_struct!(
268        t.log().pop_op(),
269        Operation::Transaction(Transaction::Start {
270            isolation: None,
271            read_only: false,
272            ..
273        })
274    );
275    assert_struct!(t.log().pop_op(), Operation::Insert(_)); // INSERT todo (rolled back)
276    assert_struct!(
277        t.log().pop_op(),
278        Operation::Transaction(Transaction::Rollback)
279    );
280    assert!(t.log().is_empty());
281
282    // INSERT was rolled back — no orphaned todo
283    let todos = user.todos().exec(&mut db).await?;
284    assert!(todos.is_empty());
285
286    Ok(())
287}
288
289// ===== ReadModifyWrite transaction behavior =====
290
291/// A successful standalone conditional update (link/unlink) wraps itself in
292/// its own BEGIN...COMMIT on drivers that don't support CTE-with-update
293/// (SQLite, MySQL). When nested inside an outer transaction it uses savepoints
294/// instead. On PostgreSQL the same operation is a single CTE-based QuerySql.
295#[driver_test(requires(sql), scenario(crate::scenarios::has_many_nullable_fk))]
296pub async fn rmw_uses_savepoints(t: &mut Test) -> Result<()> {
297    let mut db = setup(t).await;
298
299    let user = User::create()
300        .todos([Todo::create().title("task")])
301        .exec(&mut db)
302        .await?;
303    let todos: Vec<_> = user.todos().exec(&mut db).await?;
304
305    t.log().clear();
306    user.todos().remove(&todos[0]).exec(&mut db).await?;
307
308    if t.capability().cte_with_update {
309        // PostgreSQL: single CTE bundles the condition + update
310        assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
311    } else {
312        // SQLite / MySQL: standalone RMW starts its own transaction
313        assert_struct!(
314            t.log().pop_op(),
315            Operation::Transaction(Transaction::Start {
316                isolation: None,
317                read_only: false,
318                ..
319            })
320        );
321        assert_struct!(t.log().pop_op(), Operation::QuerySql(_)); // read
322        assert_struct!(t.log().pop_op(), Operation::QuerySql(_)); // write
323        assert_struct!(
324            t.log().pop_op(),
325            Operation::Transaction(Transaction::Commit)
326        );
327    }
328    assert!(t.log().is_empty());
329
330    Ok(())
331}
332
333/// When a standalone RMW condition fails (todo doesn't belong to this user),
334/// the driver should receive ROLLBACK on the RMW's own transaction.
335/// On PostgreSQL the CTE handles this in a single statement.
336#[driver_test(requires(sql), scenario(crate::scenarios::has_many_nullable_fk))]
337pub async fn rmw_condition_failure_issues_rollback_to_savepoint(t: &mut Test) -> Result<()> {
338    let mut db = setup(t).await;
339
340    let user1 = User::create().exec(&mut db).await?;
341    let user2 = User::create()
342        .todos([Todo::create().title("task")])
343        .exec(&mut db)
344        .await?;
345    let u2_todos: Vec<_> = user2.todos().exec(&mut db).await?;
346
347    t.log().clear();
348    // Remove u2's todo via user1 — condition (user_id = user1.id) won't match
349    assert_err!(user1.todos().remove(&u2_todos[0]).exec(&mut db).await);
350
351    if t.capability().cte_with_update {
352        // PostgreSQL: a single QuerySql; condition handled inside the CTE
353        assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
354    } else {
355        // SQLite / MySQL: standalone RMW starts its own transaction;
356        // condition failure rolls it back
357        assert_struct!(
358            t.log().pop_op(),
359            Operation::Transaction(Transaction::Start {
360                isolation: None,
361                read_only: false,
362                ..
363            })
364        );
365        assert_struct!(t.log().pop_op(), Operation::QuerySql(_)); // read
366        assert_struct!(
367            t.log().pop_op(),
368            Operation::Transaction(Transaction::Rollback)
369        );
370    }
371    assert!(t.log().is_empty());
372
373    // The todo is untouched — still belongs to user2
374    let reloaded = Todo::get_by_id(&mut db, u2_todos[0].id).await?;
375    assert_struct!(reloaded, { user_id: Some(== user2.id) });
376
377    Ok(())
378}