toasty_driver_integration_suite/tests/
tx_atomic_stmt.rs

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