Skip to main content

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    );
162    assert_struct!(t.log().pop_op(), Operation::QuerySql(_)); // INSERT
163    assert_struct!(
164        t.log().pop_op(),
165        Operation::Transaction(Transaction::Commit)
166    );
167    assert!(t.log().is_empty());
168
169    Ok(())
170}
171
172/// Verify the driver receives BEGIN and ROLLBACK when rolled back.
173#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
174pub async fn driver_sees_begin_rollback(t: &mut Test) -> Result<()> {
175    let mut db = setup(t).await;
176
177    t.log().clear();
178
179    let mut tx = db.transaction().await?;
180    User::create().name("Alice").exec(&mut tx).await?;
181    tx.rollback().await?;
182
183    assert_struct!(
184        t.log().pop_op(),
185        Operation::Transaction(Transaction::Start {
186            isolation: None,
187            read_only: false,
188            ..
189        })
190    );
191    assert_struct!(t.log().pop_op(), Operation::QuerySql(_)); // INSERT
192    assert_struct!(
193        t.log().pop_op(),
194        Operation::Transaction(Transaction::Rollback)
195    );
196    assert!(t.log().is_empty());
197
198    Ok(())
199}
200
201// ===== Nested transactions (savepoints) =====
202
203/// A committed nested transaction (savepoint) persists when the outer
204/// transaction also commits.
205#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
206pub async fn nested_commit_both(t: &mut Test) -> Result<()> {
207    let mut db = setup(t).await;
208
209    let mut tx = db.transaction().await?;
210    User::create().name("Alice").exec(&mut tx).await?;
211
212    {
213        let mut nested = tx.transaction().await?;
214        User::create().name("Bob").exec(&mut nested).await?;
215        nested.commit().await?;
216    }
217
218    tx.commit().await?;
219
220    let users = User::all().exec(&mut db).await?;
221    assert_eq!(users.len(), 2);
222
223    Ok(())
224}
225
226/// Rolling back a nested transaction discards only its changes; the outer
227/// transaction can still commit its own.
228#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
229pub async fn nested_rollback_inner(t: &mut Test) -> Result<()> {
230    let mut db = setup(t).await;
231
232    let mut tx = db.transaction().await?;
233    User::create().name("Alice").exec(&mut tx).await?;
234
235    {
236        let mut nested = tx.transaction().await?;
237        User::create().name("Ghost").exec(&mut nested).await?;
238        nested.rollback().await?;
239    }
240
241    tx.commit().await?;
242
243    let users = User::all().exec(&mut db).await?;
244    assert_eq!(users.len(), 1);
245    assert_eq!(users[0].name, "Alice");
246
247    Ok(())
248}
249
250/// Rolling back the outer transaction discards everything, including changes
251/// from an already-committed nested transaction.
252#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
253pub async fn nested_rollback_outer(t: &mut Test) -> Result<()> {
254    let mut db = setup(t).await;
255
256    let mut tx = db.transaction().await?;
257    User::create().name("Alice").exec(&mut tx).await?;
258
259    {
260        let mut nested = tx.transaction().await?;
261        User::create().name("Bob").exec(&mut nested).await?;
262        nested.commit().await?;
263    }
264
265    tx.rollback().await?;
266
267    let users = User::all().exec(&mut db).await?;
268    assert!(users.is_empty());
269
270    Ok(())
271}
272
273/// Dropping a nested transaction without finalize rolls back just that
274/// savepoint.
275#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
276pub async fn nested_drop_rolls_back_savepoint(t: &mut Test) -> Result<()> {
277    let mut db = setup(t).await;
278
279    let mut tx = db.transaction().await?;
280    User::create().name("Alice").exec(&mut tx).await?;
281
282    {
283        let mut nested = tx.transaction().await?;
284        User::create().name("Ghost").exec(&mut nested).await?;
285        // dropped without commit/rollback
286    }
287
288    tx.commit().await?;
289
290    let users = User::all().exec(&mut db).await?;
291    assert_eq!(users.len(), 1);
292    assert_eq!(users[0].name, "Alice");
293
294    Ok(())
295}
296
297/// Verify the driver log for a nested transaction shows SAVEPOINT / RELEASE
298/// SAVEPOINT around the inner work.
299#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
300pub async fn nested_driver_sees_savepoint_ops(t: &mut Test) -> Result<()> {
301    let mut db = setup(t).await;
302
303    t.log().clear();
304
305    let mut tx = db.transaction().await?;
306    User::create().name("Alice").exec(&mut tx).await?;
307
308    let mut nested = tx.transaction().await?;
309    User::create().name("Bob").exec(&mut nested).await?;
310    nested.commit().await?;
311
312    tx.commit().await?;
313
314    // BEGIN
315    assert_struct!(
316        t.log().pop_op(),
317        Operation::Transaction(Transaction::Start {
318            isolation: None,
319            read_only: false,
320            ..
321        })
322    );
323    // INSERT Alice
324    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
325    // SAVEPOINT
326    assert_struct!(
327        t.log().pop_op(),
328        Operation::Transaction(Transaction::Savepoint(_))
329    );
330    // INSERT Bob
331    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
332    // RELEASE SAVEPOINT
333    assert_struct!(
334        t.log().pop_op(),
335        Operation::Transaction(Transaction::ReleaseSavepoint(_))
336    );
337    // COMMIT
338    assert_struct!(
339        t.log().pop_op(),
340        Operation::Transaction(Transaction::Commit)
341    );
342    assert!(t.log().is_empty());
343
344    Ok(())
345}
346
347/// Verify the driver log when a nested transaction is rolled back shows
348/// ROLLBACK TO SAVEPOINT.
349#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
350pub async fn nested_driver_sees_rollback_to_savepoint(t: &mut Test) -> Result<()> {
351    let mut db = setup(t).await;
352
353    t.log().clear();
354
355    let mut tx = db.transaction().await?;
356
357    let mut nested = tx.transaction().await?;
358    User::create().name("Ghost").exec(&mut nested).await?;
359    nested.rollback().await?;
360
361    tx.commit().await?;
362
363    // BEGIN
364    assert_struct!(
365        t.log().pop_op(),
366        Operation::Transaction(Transaction::Start {
367            isolation: None,
368            read_only: false,
369            ..
370        })
371    );
372    // SAVEPOINT
373    assert_struct!(
374        t.log().pop_op(),
375        Operation::Transaction(Transaction::Savepoint(_))
376    );
377    // INSERT Ghost
378    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
379    // ROLLBACK TO SAVEPOINT
380    assert_struct!(
381        t.log().pop_op(),
382        Operation::Transaction(Transaction::RollbackToSavepoint(_))
383    );
384    // COMMIT
385    assert_struct!(
386        t.log().pop_op(),
387        Operation::Transaction(Transaction::Commit)
388    );
389    assert!(t.log().is_empty());
390
391    Ok(())
392}
393
394/// Two sequential nested transactions: first committed, second rolled back.
395/// Only data from the first survives.
396#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
397pub async fn two_sequential_nested_transactions(t: &mut Test) -> Result<()> {
398    let mut db = setup(t).await;
399
400    let mut tx = db.transaction().await?;
401
402    {
403        let mut nested1 = tx.transaction().await?;
404        User::create().name("Alice").exec(&mut nested1).await?;
405        nested1.commit().await?;
406    }
407
408    {
409        let mut nested2 = tx.transaction().await?;
410        User::create().name("Ghost").exec(&mut nested2).await?;
411        nested2.rollback().await?;
412    }
413
414    tx.commit().await?;
415
416    let users = User::all().exec(&mut db).await?;
417    assert_eq!(users.len(), 1);
418    assert_eq!(users[0].name, "Alice");
419
420    Ok(())
421}
422
423// ===== Statements inside transactions use savepoints for multi-op plans =====
424
425/// When a multi-op statement (e.g. create with association) runs inside an
426/// interactive transaction, the engine wraps it in SAVEPOINT/RELEASE instead
427/// of BEGIN/COMMIT.
428#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::has_many_belongs_to))]
429pub async fn multi_op_inside_tx_uses_savepoints(t: &mut Test) -> Result<()> {
430    let mut db = setup(t).await;
431
432    t.log().clear();
433
434    let mut tx = db.transaction().await?;
435    let user = User::create()
436        .name("Alice")
437        .todo(Todo::create().title("task"))
438        .exec(&mut tx)
439        .await?;
440    tx.commit().await?;
441
442    // BEGIN (interactive tx)
443    assert_struct!(
444        t.log().pop_op(),
445        Operation::Transaction(Transaction::Start {
446            isolation: None,
447            read_only: false,
448            ..
449        })
450    );
451    // SAVEPOINT (engine wraps the multi-op plan)
452    assert_struct!(
453        t.log().pop_op(),
454        Operation::Transaction(Transaction::Savepoint(_))
455    );
456    // INSERT user
457    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
458    // INSERT todo
459    assert_struct!(t.log().pop_op(), Operation::QuerySql(_));
460    // RELEASE SAVEPOINT
461    assert_struct!(
462        t.log().pop_op(),
463        Operation::Transaction(Transaction::ReleaseSavepoint(_))
464    );
465    // COMMIT (interactive tx)
466    assert_struct!(
467        t.log().pop_op(),
468        Operation::Transaction(Transaction::Commit)
469    );
470    assert!(t.log().is_empty());
471
472    // Verify the data landed
473    let todos = user.todos().exec(&mut db).await?;
474    assert_eq!(todos.len(), 1);
475    assert_eq!(todos[0].title, "task");
476
477    Ok(())
478}
479
480// ===== TransactionBuilder API =====
481
482/// TransactionBuilder from Db commits data like a regular transaction.
483#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
484pub async fn builder_on_db_commit(t: &mut Test) -> Result<()> {
485    let mut db = setup(t).await;
486
487    let mut tx = db.transaction_builder().begin().await?;
488    User::create().name("Alice").exec(&mut tx).await?;
489    tx.commit().await?;
490
491    let users = User::all().exec(&mut db).await?;
492    assert_eq!(users.len(), 1);
493    assert_eq!(users[0].name, "Alice");
494
495    Ok(())
496}
497
498/// TransactionBuilder from Connection commits data like a regular transaction.
499#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
500pub async fn builder_on_connection_commit(t: &mut Test) -> Result<()> {
501    let db = setup(t).await;
502    let mut conn = db.connection().await?;
503
504    let mut tx = conn.transaction_builder().begin().await?;
505    User::create().name("Alice").exec(&mut tx).await?;
506    tx.commit().await?;
507
508    let users = User::all().exec(&mut conn).await?;
509    assert_eq!(users.len(), 1);
510    assert_eq!(users[0].name, "Alice");
511
512    Ok(())
513}
514
515/// TransactionBuilder with isolation level sends the correct option to the driver.
516#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
517pub async fn builder_with_isolation_level(t: &mut Test) -> Result<()> {
518    let mut db = setup(t).await;
519
520    t.log().clear();
521
522    let mut tx = db
523        .transaction_builder()
524        .isolation(IsolationLevel::Serializable)
525        .begin()
526        .await?;
527    User::create().name("Alice").exec(&mut tx).await?;
528    tx.commit().await?;
529
530    assert_struct!(
531        t.log().pop_op(),
532        Operation::Transaction(Transaction::Start {
533            isolation: Some(IsolationLevel::Serializable),
534            read_only: false,
535            ..
536        })
537    );
538
539    Ok(())
540}
541
542/// TransactionBuilder with read_only sends the correct option to the driver.
543#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
544pub async fn builder_with_read_only(t: &mut Test) -> Result<()> {
545    let mut db = setup(t).await;
546
547    t.log().clear();
548
549    let tx = db.transaction_builder().read_only(true).begin().await?;
550    tx.commit().await?;
551
552    assert_struct!(
553        t.log().pop_op(),
554        Operation::Transaction(Transaction::Start {
555            isolation: None,
556            read_only: true,
557            ..
558        })
559    );
560
561    Ok(())
562}
563
564/// TransactionBuilder with both isolation and read_only sends both options.
565#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
566pub async fn builder_with_all_options(t: &mut Test) -> Result<()> {
567    let mut db = setup(t).await;
568
569    t.log().clear();
570
571    let tx = db
572        .transaction_builder()
573        .isolation(IsolationLevel::Serializable)
574        .read_only(true)
575        .begin()
576        .await?;
577    tx.commit().await?;
578
579    assert_struct!(
580        t.log().pop_op(),
581        Operation::Transaction(Transaction::Start {
582            isolation: Some(IsolationLevel::Serializable),
583            read_only: true,
584            ..
585        })
586    );
587
588    Ok(())
589}
590
591/// TransactionBuilder auto-rolls back on drop just like a regular transaction.
592#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
593pub async fn builder_drop_rolls_back(t: &mut Test) -> Result<()> {
594    let mut db = setup(t).await;
595
596    {
597        let mut tx = db.transaction_builder().begin().await?;
598        User::create().name("Ghost").exec(&mut tx).await?;
599    }
600
601    let users = User::all().exec(&mut db).await?;
602    assert!(users.is_empty());
603
604    Ok(())
605}
606
607/// Calling `.transaction()` through `&mut dyn Executor` works.
608#[driver_test(id(ID), requires(sql), scenario(crate::scenarios::two_models))]
609pub async fn transaction_via_dyn_executor(t: &mut Test) -> Result<()> {
610    let mut db = setup(t).await;
611
612    let executor: &mut dyn toasty::Executor = &mut db;
613    let mut tx = executor.transaction().await?;
614    User::create().name("Alice").exec(&mut tx).await?;
615    tx.commit().await?;
616
617    let users = User::all().exec(&mut db).await?;
618    assert_eq!(users.len(), 1);
619    assert_eq!(users[0].name, "Alice");
620
621    Ok(())
622}