toasty_driver_integration_suite/tests/
tx_interactive.rs

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