#![allow(unused_imports, dead_code)] pub mod common; pub use common::{TestContext, bakery_chain::*, setup::*}; use pretty_assertions::assert_eq; use sea_orm::{ AccessMode, DatabaseTransaction, IsolationLevel, Set, SqliteTransactionMode, TransactionOptions, TransactionTrait, prelude::*, }; fn seaside_bakery() -> bakery::ActiveModel { bakery::ActiveModel { name: Set("SeaSide Bakery".to_owned()), profit_margin: Set(10.4), ..Default::default() } } fn top_bakery() -> bakery::ActiveModel { bakery::ActiveModel { name: Set("Top Bakery".to_owned()), profit_margin: Set(15.0), ..Default::default() } } #[sea_orm_macros::test] pub async fn transaction() { let ctx = TestContext::new("transaction_test").await; create_bakery_table(&ctx.db).await.unwrap(); ctx.db .transaction_async::<_, _, DbErr>(async |txn| { let _ = seaside_bakery().save(txn).await?; let _ = top_bakery().save(txn).await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 2); Ok(()) }) .await .unwrap(); ctx.delete().await; } #[sea_orm_macros::test] #[cfg(feature = "rbac")] pub async fn rbac_transaction() { use sea_orm::rbac::{RbacEngine, RbacSnapshot, RbacUserId}; let ctx = TestContext::new("rbac_transaction_test").await; create_bakery_table(&ctx.db).await.unwrap(); ctx.db.replace_rbac(RbacEngine::from_snapshot( RbacSnapshot::danger_unrestricted(), )); let db = ctx.db.restricted_for(RbacUserId(0)).unwrap(); db.transaction_async::<_, _, DbErr>(async |txn| { let _ = seaside_bakery().save(txn).await?; let _ = top_bakery().save(txn).await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 2); Ok(()) }) .await .unwrap(); ctx.delete().await; } #[sea_orm_macros::test] pub async fn transaction_with_reference() { let ctx = TestContext::new("transaction_with_reference_test").await; create_bakery_table(&ctx.db).await.unwrap(); let name1 = "SeaSide Bakery"; let name2 = "Top Bakery"; let search_name = "Bakery"; ctx.db .transaction_async(async |txn| { _transaction_with_reference(txn, name1, name2, search_name).await }) .await .unwrap(); ctx.delete().await; } async fn _transaction_with_reference( txn: &DatabaseTransaction, name1: &str, name2: &str, search_name: &str, ) -> Result<(), DbErr> { let _ = bakery::ActiveModel { name: Set(name1.to_owned()), profit_margin: Set(10.4), ..Default::default() } .save(txn) .await?; let _ = bakery::ActiveModel { name: Set(name2.to_owned()), profit_margin: Set(15.0), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains(search_name)) .all(txn) .await?; assert_eq!(bakeries.len(), 2); Ok(()) } #[sea_orm_macros::test] pub async fn transaction_begin_out_of_scope() -> Result<(), DbErr> { let ctx = TestContext::new("transaction_begin_out_of_scope_test").await; create_bakery_table(&ctx.db).await?; assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 0); { // Transaction begin in this scope let txn = ctx.db.begin().await?; seaside_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 1); top_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 2); // The scope ended and transaction is dropped without commit } assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 0); ctx.delete().await; Ok(()) } #[sea_orm_macros::test] #[cfg(feature = "rbac")] pub async fn rbac_transaction_begin_out_of_scope() -> Result<(), DbErr> { use sea_orm::rbac::{RbacEngine, RbacSnapshot, RbacUserId}; let ctx = TestContext::new("rbac_transaction_begin_out_of_scope_test").await; create_bakery_table(&ctx.db).await?; ctx.db.replace_rbac(RbacEngine::from_snapshot( RbacSnapshot::danger_unrestricted(), )); let db = ctx.db.restricted_for(RbacUserId(0)).unwrap(); assert_eq!(bakery::Entity::find().all(&db).await?.len(), 0); { // Transaction begin in this scope let txn = db.begin().await?; seaside_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 1); top_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 2); // The scope ended and transaction is dropped without commit } assert_eq!(bakery::Entity::find().all(&db).await?.len(), 0); ctx.delete().await; Ok(()) } #[sea_orm_macros::test] pub async fn transaction_begin_commit() -> Result<(), DbErr> { let ctx = TestContext::new("transaction_begin_commit_test").await; create_bakery_table(&ctx.db).await?; assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 0); { // Transaction begin in this scope let txn = ctx.db.begin().await?; seaside_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 1); top_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 2); // Commit changes before the end of scope txn.commit().await?; } assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 2); ctx.delete().await; Ok(()) } #[sea_orm_macros::test] #[cfg(feature = "rbac")] pub async fn rbac_transaction_begin_commit() -> Result<(), DbErr> { use sea_orm::rbac::{RbacEngine, RbacSnapshot, RbacUserId}; let ctx = TestContext::new("rbac_transaction_begin_commit_test").await; create_bakery_table(&ctx.db).await?; ctx.db.replace_rbac(RbacEngine::from_snapshot( RbacSnapshot::danger_unrestricted(), )); let db = ctx.db.restricted_for(RbacUserId(0)).unwrap(); assert_eq!(bakery::Entity::find().all(&db).await?.len(), 0); { // Transaction begin in this scope let txn = db.begin().await?; seaside_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 1); top_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 2); // Commit changes before the end of scope txn.commit().await?; } assert_eq!(bakery::Entity::find().all(&db).await?.len(), 2); ctx.delete().await; Ok(()) } #[sea_orm_macros::test] pub async fn transaction_begin_rollback() -> Result<(), DbErr> { let ctx = TestContext::new("transaction_begin_rollback_test").await; create_bakery_table(&ctx.db).await?; assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 0); { // Transaction begin in this scope let txn = ctx.db.begin().await?; seaside_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 1); top_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 2); // Rollback changes before the end of scope txn.rollback().await?; } assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 0); ctx.delete().await; Ok(()) } #[sea_orm_macros::test] pub async fn transaction_closure_commit() -> Result<(), DbErr> { let ctx = TestContext::new("transaction_closure_commit_test").await; create_bakery_table(&ctx.db).await?; assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 0); let res = ctx .db .transaction_async::<_, _, DbErr>(async |txn| { seaside_bakery().save(txn).await?; assert_eq!(bakery::Entity::find().all(txn).await?.len(), 1); top_bakery().save(txn).await?; assert_eq!(bakery::Entity::find().all(txn).await?.len(), 2); Ok(()) }) .await; assert!(res.is_ok()); assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 2); ctx.delete().await; Ok(()) } #[sea_orm_macros::test] pub async fn transaction_closure_rollback() -> Result<(), DbErr> { let ctx = TestContext::new("transaction_closure_rollback_test").await; create_bakery_table(&ctx.db).await?; assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 0); let res = ctx .db .transaction_async::<_, _, DbErr>(async |txn| { seaside_bakery().save(txn).await?; assert_eq!(bakery::Entity::find().all(txn).await?.len(), 1); top_bakery().save(txn).await?; assert_eq!(bakery::Entity::find().all(txn).await?.len(), 2); bakery::ActiveModel { id: Set(1), name: Set("Duplicated primary key".to_owned()), profit_margin: Set(20.0), } .insert(txn) .await?; // Throw error and rollback // This line won't be reached unreachable!(); #[allow(unreachable_code)] Ok(()) }) .await; assert!(res.is_err()); assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 0); ctx.delete().await; Ok(()) } #[sea_orm_macros::test] pub async fn transaction_with_active_model_behaviour() -> Result<(), DbErr> { let ctx = TestContext::new("transaction_with_active_model_behaviour_test").await; create_bakery_table(&ctx.db).await?; create_cake_table(&ctx.db).await?; if let Ok(txn) = ctx.db.begin().await { assert_eq!( cake::ActiveModel { name: Set("Cake with invalid price".to_owned()), price: Set(rust_dec(0)), gluten_free: Set(false), ..Default::default() } .save(&txn) .await, Err(DbErr::Custom( "[before_save] Invalid Price, insert: true".to_owned() )) ); assert_eq!(cake::Entity::find().all(&txn).await?.len(), 0); assert_eq!( cake::ActiveModel { name: Set("Cake with invalid price".to_owned()), price: Set(rust_dec(-10)), gluten_free: Set(false), ..Default::default() } .save(&txn) .await, Err(DbErr::Custom( "[after_save] Invalid Price, insert: true".to_owned() )) ); assert_eq!(cake::Entity::find().all(&txn).await?.len(), 1); let readonly_cake_1 = cake::ActiveModel { name: Set("Readonly cake (err_on_before_delete)".to_owned()), price: Set(rust_dec(10)), gluten_free: Set(true), ..Default::default() } .save(&txn) .await?; assert_eq!(cake::Entity::find().all(&txn).await?.len(), 2); assert_eq!( readonly_cake_1.delete(&txn).await.err(), Some(DbErr::Custom( "[before_delete] Cannot be deleted".to_owned() )) ); assert_eq!(cake::Entity::find().all(&txn).await?.len(), 2); let readonly_cake_2 = cake::ActiveModel { name: Set("Readonly cake (err_on_after_delete)".to_owned()), price: Set(rust_dec(10)), gluten_free: Set(true), ..Default::default() } .save(&txn) .await?; assert_eq!(cake::Entity::find().all(&txn).await?.len(), 3); assert_eq!( readonly_cake_2.delete(&txn).await.err(), Some(DbErr::Custom("[after_delete] Cannot be deleted".to_owned())) ); assert_eq!(cake::Entity::find().all(&txn).await?.len(), 2); } assert_eq!(cake::Entity::find().all(&ctx.db).await?.len(), 0); ctx.delete().await; Ok(()) } #[sea_orm_macros::test] pub async fn transaction_nested() { let ctx = TestContext::new("transaction_nested_test").await; create_bakery_table(&ctx.db).await.unwrap(); ctx.db .transaction_async::<_, _, DbErr>(async |txn| { let _ = seaside_bakery().save(txn).await?; let _ = top_bakery().save(txn).await?; // Try nested transaction committed txn.transaction_async::<_, _, DbErr>(async |txn| { let _ = bakery::ActiveModel { name: Set("Nested Bakery".to_owned()), profit_margin: Set(88.88), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 3); // Try nested-nested transaction rollbacked let is_err = txn .transaction_async::<_, _, DbErr>(async |txn| { let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 4); if true { Err(DbErr::Query(RuntimeErr::Internal( "Force Rollback!".to_owned(), ))) } else { Ok(()) } }) .await .is_err(); assert!(is_err); let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 3); // Try nested-nested transaction committed txn.transaction_async::<_, _, DbErr>(async |txn| { let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 4); Ok(()) }) .await .unwrap(); let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 4); Ok(()) }) .await .unwrap(); // Try nested transaction rollbacked let is_err = txn .transaction_async::<_, _, DbErr>(async |txn| { let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 5); // Try nested-nested transaction committed txn.transaction_async::<_, _, DbErr>(async |txn| { let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 6); Ok(()) }) .await .unwrap(); let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 6); // Try nested-nested transaction rollbacked let is_err = txn .transaction_async::<_, _, DbErr>(async |txn| { let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 7); if true { Err(DbErr::Query(RuntimeErr::Internal( "Force Rollback!".to_owned(), ))) } else { Ok(()) } }) .await .is_err(); assert!(is_err); let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 6); if true { Err(DbErr::Query(RuntimeErr::Internal( "Force Rollback!".to_owned(), ))) } else { Ok(()) } }) .await .is_err(); assert!(is_err); let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 4); Ok(()) }) .await .unwrap(); let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&ctx.db) .await .unwrap(); assert_eq!(bakeries.len(), 4); ctx.delete().await; } #[sea_orm_macros::test] pub async fn transaction_manager_nested() -> Result<(), sea_orm::DbErr> { let ctx = TestContext::new("transaction_manager_nested").await; create_bakery_table(&ctx.db).await.unwrap(); let txn = ctx.db.begin().await?; let _ = seaside_bakery().save(&txn).await?; let _ = top_bakery().save(&txn).await?; // Try nested transaction committed { let txn = txn.begin().await?; let _ = bakery::ActiveModel { name: Set("Nested Bakery".to_owned()), profit_margin: Set(88.88), ..Default::default() } .save(&txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&txn) .await?; assert_eq!(bakeries.len(), 3); // Try nested-nested transaction rollbacked { let txn = txn.begin().await?; let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(&txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&txn) .await?; assert_eq!(bakeries.len(), 4); } let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&txn) .await?; assert_eq!(bakeries.len(), 3); // Try nested-nested transaction committed { let txn = txn.begin().await?; let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(&txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&txn) .await?; assert_eq!(bakeries.len(), 4); txn.commit().await?; } txn.commit().await?; } // Try nested transaction rollbacked { let txn = txn.begin().await?; let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(&txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&txn) .await?; assert_eq!(bakeries.len(), 5); // Try nested-nested transaction committed { let txn = txn.begin().await?; let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(&txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&txn) .await?; assert_eq!(bakeries.len(), 6); txn.commit().await?; } let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&txn) .await?; assert_eq!(bakeries.len(), 6); } let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&txn) .await?; assert_eq!(bakeries.len(), 4); txn.commit().await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&ctx.db) .await .unwrap(); assert_eq!(bakeries.len(), 4); ctx.delete().await; Ok(()) } #[sea_orm_macros::test] #[cfg(feature = "rbac")] pub async fn rbac_transaction_nested() { use sea_orm::rbac::{RbacEngine, RbacSnapshot, RbacUserId}; let ctx = TestContext::new("rbac_transaction_nested_test").await; create_bakery_table(&ctx.db).await.unwrap(); ctx.db.replace_rbac(RbacEngine::from_snapshot( RbacSnapshot::danger_unrestricted(), )); let db = ctx.db.restricted_for(RbacUserId(0)).unwrap(); db.transaction_async::<_, _, DbErr>(async |txn| { let _ = seaside_bakery().save(txn).await?; let _ = top_bakery().save(txn).await?; // Try nested transaction committed txn.transaction_async::<_, _, DbErr>(async |txn| { let _ = bakery::ActiveModel { name: Set("Nested Bakery".to_owned()), profit_margin: Set(88.88), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 3); // Try nested-nested transaction committed txn.transaction_async::<_, _, DbErr>(async |txn| { let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), profit_margin: Set(28.8), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 4); Ok(()) }) .await .unwrap(); let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 4); Ok(()) }) .await .unwrap(); let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 4); Ok(()) }) .await .unwrap(); let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(&ctx.db) .await .unwrap(); assert_eq!(bakeries.len(), 4); ctx.delete().await; } #[sea_orm_macros::test] pub async fn transaction_with_config() { let ctx = TestContext::new("transaction_with_config").await; create_bakery_table(&ctx.db).await.unwrap(); for (i, (isolation_level, access_mode)) in [ (IsolationLevel::RepeatableRead, None), (IsolationLevel::ReadCommitted, None), (IsolationLevel::ReadUncommitted, Some(AccessMode::ReadWrite)), (IsolationLevel::Serializable, Some(AccessMode::ReadWrite)), ] .into_iter() .enumerate() { let name1 = format!("SeaSide Bakery {}", i); let name2 = format!("Top Bakery {}", i); let search_name = format!("Bakery {}", i); ctx.db .transaction_with_config_async( async |txn| _transaction_with_config(txn, name1, name2, search_name).await, Some(isolation_level), access_mode, ) .await .unwrap(); } ctx.db .transaction_with_config_async::<_, _, DbErr>( async |txn| { let bakeries = Bakery::find() .filter(bakery::Column::Name.contains("Bakery")) .all(txn) .await?; assert_eq!(bakeries.len(), 8); Ok(()) }, None, Some(AccessMode::ReadOnly), ) .await .unwrap(); ctx.delete().await; } async fn _transaction_with_config( txn: &DatabaseTransaction, name1: String, name2: String, search_name: String, ) -> Result<(), DbErr> { let _ = bakery::ActiveModel { name: Set(name1), profit_margin: Set(10.4), ..Default::default() } .save(txn) .await?; let _ = bakery::ActiveModel { name: Set(name2), profit_margin: Set(15.0), ..Default::default() } .save(txn) .await?; let bakeries = Bakery::find() .filter(bakery::Column::Name.contains(&search_name)) .all(txn) .await?; assert_eq!(bakeries.len(), 2); Ok(()) } #[sea_orm_macros::test] pub async fn transaction_begin_with_options() -> Result<(), DbErr> { let ctx = TestContext::new("transaction_begin_with_options_test").await; create_tables(&ctx.db).await?; assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 0); { // Transaction begin in this scope let txn = ctx .db .begin_with_options(TransactionOptions { sqlite_transaction_mode: Some(SqliteTransactionMode::Immediate), ..Default::default() }) .await?; seaside_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 1); top_bakery().save(&txn).await?; assert_eq!(bakery::Entity::find().all(&txn).await?.len(), 2); // Commit changes before the end of scope txn.commit().await?; } assert_eq!(bakery::Entity::find().all(&ctx.db).await?.len(), 2); ctx.delete().await; Ok(()) }