toasty_driver_integration_suite/tests/
tx_interactive.rs

1use crate::prelude::*;
2
3use toasty::Executor;
4use toasty_core::driver::{operation::Transaction, Operation};
5
6// ===== Basic commit / rollback =====
7
8/// Data created inside a committed transaction is visible afterwards.
9#[driver_test(id(ID), requires(sql))]
10pub async fn commit_persists_data(t: &mut Test) -> Result<()> {
11    #[derive(Debug, toasty::Model)]
12    struct User {
13        #[key]
14        #[auto]
15        id: ID,
16        name: String,
17    }
18
19    let mut db = t.setup_db(models!(User)).await;
20
21    let mut tx = db.transaction().await?;
22    User::create().name("Alice").exec(&mut tx).await?;
23    tx.commit().await?;
24
25    let users = User::all().exec(&mut db).await?;
26    assert_eq!(users.len(), 1);
27    assert_eq!(users[0].name, "Alice");
28
29    Ok(())
30}
31
32/// Data created inside a rolled-back transaction is not visible.
33#[driver_test(id(ID), requires(sql))]
34pub async fn rollback_discards_data(t: &mut Test) -> Result<()> {
35    #[derive(Debug, toasty::Model)]
36    struct User {
37        #[key]
38        #[auto]
39        id: ID,
40        name: String,
41    }
42
43    let mut db = t.setup_db(models!(User)).await;
44
45    let mut tx = db.transaction().await?;
46    User::create().name("Ghost").exec(&mut tx).await?;
47    tx.rollback().await?;
48
49    let users = User::all().exec(&mut db).await?;
50    assert!(users.is_empty());
51
52    Ok(())
53}
54
55/// Dropping a transaction without commit or rollback automatically rolls back.
56#[driver_test(id(ID), requires(sql))]
57pub async fn drop_without_finalize_rolls_back(t: &mut Test) -> Result<()> {
58    #[derive(Debug, toasty::Model)]
59    struct User {
60        #[key]
61        #[auto]
62        id: ID,
63        name: String,
64    }
65
66    let mut db = t.setup_db(models!(User)).await;
67
68    {
69        let mut tx = db.transaction().await?;
70        User::create().name("Ghost").exec(&mut tx).await?;
71        // tx is dropped here without commit/rollback
72    }
73
74    let users = User::all().exec(&mut db).await?;
75    assert!(users.is_empty());
76
77    Ok(())
78}
79
80/// Multiple operations inside a single transaction are all committed together.
81#[driver_test(id(ID), requires(sql))]
82pub async fn multiple_ops_in_transaction(t: &mut Test) -> Result<()> {
83    #[derive(Debug, toasty::Model)]
84    struct User {
85        #[key]
86        #[auto]
87        id: ID,
88        name: String,
89    }
90
91    let mut db = t.setup_db(models!(User)).await;
92
93    let mut tx = db.transaction().await?;
94    User::create().name("Alice").exec(&mut tx).await?;
95    User::create().name("Bob").exec(&mut tx).await?;
96    User::create().name("Carol").exec(&mut tx).await?;
97    tx.commit().await?;
98
99    let users = User::all().exec(&mut db).await?;
100    assert_eq!(users.len(), 3);
101
102    Ok(())
103}
104
105/// Read-your-writes: data created inside a transaction is visible within it
106/// before commit.
107#[driver_test(id(ID), requires(sql))]
108pub async fn read_your_writes(t: &mut Test) -> Result<()> {
109    #[derive(Debug, toasty::Model)]
110    struct User {
111        #[key]
112        #[auto]
113        id: ID,
114        name: String,
115    }
116
117    let mut db = t.setup_db(models!(User)).await;
118
119    let mut tx = db.transaction().await?;
120    User::create().name("Alice").exec(&mut tx).await?;
121
122    let users = User::all().exec(&mut tx).await?;
123    assert_eq!(users.len(), 1);
124    assert_eq!(users[0].name, "Alice");
125
126    tx.commit().await?;
127
128    Ok(())
129}
130
131/// Updates inside a transaction are committed.
132#[driver_test(id(ID), requires(sql))]
133pub async fn update_inside_transaction(t: &mut Test) -> Result<()> {
134    #[derive(Debug, toasty::Model)]
135    struct User {
136        #[key]
137        #[auto]
138        id: ID,
139        name: String,
140    }
141
142    let mut db = t.setup_db(models!(User)).await;
143
144    let mut user = User::create().name("Alice").exec(&mut db).await?;
145
146    let mut tx = db.transaction().await?;
147    user.update().name("Bob").exec(&mut tx).await?;
148    tx.commit().await?;
149
150    let reloaded = User::get_by_id(&mut db, user.id).await?;
151    assert_eq!(reloaded.name, "Bob");
152
153    Ok(())
154}
155
156/// Updates inside a rolled-back transaction are discarded.
157#[driver_test(id(ID), requires(sql))]
158pub async fn update_rolled_back(t: &mut Test) -> Result<()> {
159    #[derive(Debug, toasty::Model)]
160    struct User {
161        #[key]
162        #[auto]
163        id: ID,
164        name: String,
165    }
166
167    let mut db = t.setup_db(models!(User)).await;
168
169    let mut user = User::create().name("Alice").exec(&mut db).await?;
170
171    let mut tx = db.transaction().await?;
172    user.update().name("Bob").exec(&mut tx).await?;
173    tx.rollback().await?;
174
175    let reloaded = User::get_by_id(&mut db, user.id).await?;
176    assert_eq!(reloaded.name, "Alice");
177
178    Ok(())
179}
180
181/// Deletes inside a rolled-back transaction are discarded.
182#[driver_test(id(ID), requires(sql))]
183pub async fn delete_rolled_back(t: &mut Test) -> Result<()> {
184    #[derive(Debug, toasty::Model)]
185    struct User {
186        #[key]
187        #[auto]
188        id: ID,
189        name: String,
190    }
191
192    let mut db = t.setup_db(models!(User)).await;
193
194    let user = User::create().name("Alice").exec(&mut db).await?;
195
196    let mut tx = db.transaction().await?;
197    User::filter_by_id(user.id).delete().exec(&mut tx).await?;
198    tx.rollback().await?;
199
200    let reloaded = User::get_by_id(&mut db, user.id).await?;
201    assert_eq!(reloaded.name, "Alice");
202
203    Ok(())
204}
205
206// ===== Driver operation log =====
207
208/// Verify the driver receives BEGIN, statements, and COMMIT in the right order.
209#[driver_test(id(ID), requires(sql))]
210pub async fn driver_sees_begin_commit(t: &mut Test) -> Result<()> {
211    #[derive(Debug, toasty::Model)]
212    struct User {
213        #[key]
214        #[auto]
215        id: ID,
216        name: String,
217    }
218
219    let mut db = t.setup_db(models!(User)).await;
220
221    t.log().clear();
222
223    let mut tx = db.transaction().await?;
224    User::create().name("Alice").exec(&mut tx).await?;
225    tx.commit().await?;
226
227    assert_struct!(
228        t.log().pop_op(),
229        Operation::Transaction(Transaction::Start {
230            isolation: None,
231            read_only: false
232        })
233    );
234    assert_struct!(t.log().pop_op(), Operation::QuerySql(_)); // INSERT
235    assert_struct!(
236        t.log().pop_op(),
237        Operation::Transaction(Transaction::Commit)
238    );
239    assert!(t.log().is_empty());
240
241    Ok(())
242}
243
244/// Verify the driver receives BEGIN and ROLLBACK when rolled back.
245#[driver_test(id(ID), requires(sql))]
246pub async fn driver_sees_begin_rollback(t: &mut Test) -> Result<()> {
247    #[derive(Debug, toasty::Model)]
248    struct User {
249        #[key]
250        #[auto]
251        id: ID,
252        name: String,
253    }
254
255    let mut db = t.setup_db(models!(User)).await;
256
257    t.log().clear();
258
259    let mut tx = db.transaction().await?;
260    User::create().name("Alice").exec(&mut tx).await?;
261    tx.rollback().await?;
262
263    assert_struct!(
264        t.log().pop_op(),
265        Operation::Transaction(Transaction::Start {
266            isolation: None,
267            read_only: false
268        })
269    );
270    assert_struct!(t.log().pop_op(), Operation::QuerySql(_)); // INSERT
271    assert_struct!(
272        t.log().pop_op(),
273        Operation::Transaction(Transaction::Rollback)
274    );
275    assert!(t.log().is_empty());
276
277    Ok(())
278}
279
280// ===== Nested transactions (savepoints) =====
281
282/// A committed nested transaction (savepoint) persists when the outer
283/// transaction also commits.
284#[driver_test(id(ID), requires(sql))]
285pub async fn nested_commit_both(t: &mut Test) -> Result<()> {
286    #[derive(Debug, toasty::Model)]
287    struct User {
288        #[key]
289        #[auto]
290        id: ID,
291        name: String,
292    }
293
294    let mut db = t.setup_db(models!(User)).await;
295
296    let mut tx = db.transaction().await?;
297    User::create().name("Alice").exec(&mut tx).await?;
298
299    {
300        let mut nested = tx.transaction().await?;
301        User::create().name("Bob").exec(&mut nested).await?;
302        nested.commit().await?;
303    }
304
305    tx.commit().await?;
306
307    let users = User::all().exec(&mut db).await?;
308    assert_eq!(users.len(), 2);
309
310    Ok(())
311}
312
313/// Rolling back a nested transaction discards only its changes; the outer
314/// transaction can still commit its own.
315#[driver_test(id(ID), requires(sql))]
316pub async fn nested_rollback_inner(t: &mut Test) -> Result<()> {
317    #[derive(Debug, toasty::Model)]
318    struct User {
319        #[key]
320        #[auto]
321        id: ID,
322        name: String,
323    }
324
325    let mut db = t.setup_db(models!(User)).await;
326
327    let mut tx = db.transaction().await?;
328    User::create().name("Alice").exec(&mut tx).await?;
329
330    {
331        let mut nested = tx.transaction().await?;
332        User::create().name("Ghost").exec(&mut nested).await?;
333        nested.rollback().await?;
334    }
335
336    tx.commit().await?;
337
338    let users = User::all().exec(&mut db).await?;
339    assert_eq!(users.len(), 1);
340    assert_eq!(users[0].name, "Alice");
341
342    Ok(())
343}
344
345/// Rolling back the outer transaction discards everything, including changes
346/// from an already-committed nested transaction.
347#[driver_test(id(ID), requires(sql))]
348pub async fn nested_rollback_outer(t: &mut Test) -> Result<()> {
349    #[derive(Debug, toasty::Model)]
350    struct User {
351        #[key]
352        #[auto]
353        id: ID,
354        name: String,
355    }
356
357    let mut db = t.setup_db(models!(User)).await;
358
359    let mut tx = db.transaction().await?;
360    User::create().name("Alice").exec(&mut tx).await?;
361
362    {
363        let mut nested = tx.transaction().await?;
364        User::create().name("Bob").exec(&mut nested).await?;
365        nested.commit().await?;
366    }
367
368    tx.rollback().await?;
369
370    let users = User::all().exec(&mut db).await?;
371    assert!(users.is_empty());
372
373    Ok(())
374}
375
376/// Dropping a nested transaction without finalize rolls back just that
377/// savepoint.
378#[driver_test(id(ID), requires(sql))]
379pub async fn nested_drop_rolls_back_savepoint(t: &mut Test) -> Result<()> {
380    #[derive(Debug, toasty::Model)]
381    struct User {
382        #[key]
383        #[auto]
384        id: ID,
385        name: String,
386    }
387
388    let mut db = t.setup_db(models!(User)).await;
389
390    let mut tx = db.transaction().await?;
391    User::create().name("Alice").exec(&mut tx).await?;
392
393    {
394        let mut nested = tx.transaction().await?;
395        User::create().name("Ghost").exec(&mut nested).await?;
396        // dropped without commit/rollback
397    }
398
399    tx.commit().await?;
400
401    let users = User::all().exec(&mut db).await?;
402    assert_eq!(users.len(), 1);
403    assert_eq!(users[0].name, "Alice");
404
405    Ok(())
406}
407
408/// Verify the driver log for a nested transaction shows SAVEPOINT / RELEASE
409/// SAVEPOINT around the inner work.
410#[driver_test(id(ID), requires(sql))]
411pub async fn nested_driver_sees_savepoint_ops(t: &mut Test) -> Result<()> {
412    #[derive(Debug, toasty::Model)]
413    struct User {
414        #[key]
415        #[auto]
416        id: ID,
417        name: String,
418    }
419
420    let mut db = t.setup_db(models!(User)).await;
421
422    t.log().clear();
423
424    let mut tx = db.transaction().await?;
425    User::create().name("Alice").exec(&mut tx).await?;
426
427    let mut nested = tx.transaction().await?;
428    User::create().name("Bob").exec(&mut nested).await?;
429    nested.commit().await?;
430
431    tx.commit().await?;
432
433    // BEGIN
434    assert_struct!(
435        t.log().pop_op(),
436        Operation::Transaction(Transaction::Start {
437            isolation: None,
438            read_only: false
439        })
440    );
441    // INSERT Alice
442    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
443    // SAVEPOINT
444    assert_struct!(
445        t.log().pop_op(),
446        Operation::Transaction(Transaction::Savepoint(_))
447    );
448    // INSERT Bob
449    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
450    // RELEASE SAVEPOINT
451    assert_struct!(
452        t.log().pop_op(),
453        Operation::Transaction(Transaction::ReleaseSavepoint(_))
454    );
455    // COMMIT
456    assert_struct!(
457        t.log().pop_op(),
458        Operation::Transaction(Transaction::Commit)
459    );
460    assert!(t.log().is_empty());
461
462    Ok(())
463}
464
465/// Verify the driver log when a nested transaction is rolled back shows
466/// ROLLBACK TO SAVEPOINT.
467#[driver_test(id(ID), requires(sql))]
468pub async fn nested_driver_sees_rollback_to_savepoint(t: &mut Test) -> Result<()> {
469    #[derive(Debug, toasty::Model)]
470    struct User {
471        #[key]
472        #[auto]
473        id: ID,
474        name: String,
475    }
476
477    let mut db = t.setup_db(models!(User)).await;
478
479    t.log().clear();
480
481    let mut tx = db.transaction().await?;
482
483    let mut nested = tx.transaction().await?;
484    User::create().name("Ghost").exec(&mut nested).await?;
485    nested.rollback().await?;
486
487    tx.commit().await?;
488
489    // BEGIN
490    assert_struct!(
491        t.log().pop_op(),
492        Operation::Transaction(Transaction::Start {
493            isolation: None,
494            read_only: false
495        })
496    );
497    // SAVEPOINT
498    assert_struct!(
499        t.log().pop_op(),
500        Operation::Transaction(Transaction::Savepoint(_))
501    );
502    // INSERT Ghost
503    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
504    // ROLLBACK TO SAVEPOINT
505    assert_struct!(
506        t.log().pop_op(),
507        Operation::Transaction(Transaction::RollbackToSavepoint(_))
508    );
509    // COMMIT
510    assert_struct!(
511        t.log().pop_op(),
512        Operation::Transaction(Transaction::Commit)
513    );
514    assert!(t.log().is_empty());
515
516    Ok(())
517}
518
519/// Two sequential nested transactions: first committed, second rolled back.
520/// Only data from the first survives.
521#[driver_test(id(ID), requires(sql))]
522pub async fn two_sequential_nested_transactions(t: &mut Test) -> Result<()> {
523    #[derive(Debug, toasty::Model)]
524    struct User {
525        #[key]
526        #[auto]
527        id: ID,
528        name: String,
529    }
530
531    let mut db = t.setup_db(models!(User)).await;
532
533    let mut tx = db.transaction().await?;
534
535    {
536        let mut nested1 = tx.transaction().await?;
537        User::create().name("Alice").exec(&mut nested1).await?;
538        nested1.commit().await?;
539    }
540
541    {
542        let mut nested2 = tx.transaction().await?;
543        User::create().name("Ghost").exec(&mut nested2).await?;
544        nested2.rollback().await?;
545    }
546
547    tx.commit().await?;
548
549    let users = User::all().exec(&mut db).await?;
550    assert_eq!(users.len(), 1);
551    assert_eq!(users[0].name, "Alice");
552
553    Ok(())
554}
555
556// ===== Statements inside transactions use savepoints for multi-op plans =====
557
558/// When a multi-op statement (e.g. create with association) runs inside an
559/// interactive transaction, the engine wraps it in SAVEPOINT/RELEASE instead
560/// of BEGIN/COMMIT.
561#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
562pub async fn multi_op_inside_tx_uses_savepoints(t: &mut Test) -> Result<()> {
563    let mut db = setup(t).await;
564
565    t.log().clear();
566
567    let mut tx = db.transaction().await?;
568    let user = User::create()
569        .name("Alice")
570        .todo(Todo::create().title("task"))
571        .exec(&mut tx)
572        .await?;
573    tx.commit().await?;
574
575    // BEGIN (interactive tx)
576    assert_struct!(
577        t.log().pop_op(),
578        Operation::Transaction(Transaction::Start {
579            isolation: None,
580            read_only: false
581        })
582    );
583    // SAVEPOINT (engine wraps the multi-op plan)
584    assert_struct!(
585        t.log().pop_op(),
586        Operation::Transaction(Transaction::Savepoint(_))
587    );
588    // INSERT user
589    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
590    // INSERT todo
591    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
592    // RELEASE SAVEPOINT
593    assert_struct!(
594        t.log().pop_op(),
595        Operation::Transaction(Transaction::ReleaseSavepoint(_))
596    );
597    // COMMIT (interactive tx)
598    assert_struct!(
599        t.log().pop_op(),
600        Operation::Transaction(Transaction::Commit)
601    );
602    assert!(t.log().is_empty());
603
604    // Verify the data landed
605    let todos = user.todos().exec(&mut db).await?;
606    assert_eq!(todos.len(), 1);
607    assert_eq!(todos[0].title, "task");
608
609    Ok(())
610}