Skip to main content

toasty_driver_integration_suite/tests/
tx_interactive.rs

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