#![allow(non_snake_case)]
use std::{
collections::HashMap,
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering as AtomicOrdering},
},
};
use rustrails_support::{database, ignored_rails_test, runtime};
use serde_json::{Value, json};
use crate::{
Persistence, Querying, Record, RecordError,
base::test_support::{TestUser, with_sync_test_user_db as with_sync_db},
persistence::AsyncPersistence,
querying::AsyncQuerying,
transactions::{Transactional, transaction_sync},
};
fn user_attrs(name: &str, email: &str) -> HashMap<String, Value> {
HashMap::from([
("name".to_owned(), json!(name)),
("email".to_owned(), json!(email)),
])
}
#[test]
fn test_transaction_open_question_mark() {
with_sync_db(|| {
assert!(!crate::transactions::transaction_open());
transaction_sync(|txn| {
let txn = txn.clone();
async move {
assert!(crate::transactions::transaction_open());
assert_eq!(crate::transactions::open_transactions(), 1);
crate::transactions::transaction(&txn, |_| async move {
assert!(crate::transactions::transaction_open());
assert_eq!(crate::transactions::open_transactions(), 2);
Ok::<(), RecordError>(())
})
.await?;
assert!(crate::transactions::transaction_open());
Ok::<(), RecordError>(())
}
})
.expect("transaction state should be visible while scopes are open");
assert!(!crate::transactions::transaction_open());
});
}
#[test]
fn test_after_all_transactions_commit() {
with_sync_db(|| {
let calls = Arc::new(AtomicUsize::new(0));
let transaction_calls = Arc::clone(&calls);
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, move |txn| {
let txn = txn.clone();
let calls = Arc::clone(&transaction_calls);
let outer_calls = Arc::clone(&calls);
async move {
crate::transactions::after_commit(move || {
outer_calls.fetch_add(1, AtomicOrdering::SeqCst);
});
let nested_calls = Arc::clone(&calls);
crate::transactions::transaction(&txn, move |inner_txn| {
let inner_txn = inner_txn.clone();
let calls = Arc::clone(&nested_calls);
let inner_calls = Arc::clone(&calls);
async move {
TestUser::create(user_attrs("Inner", "inner@example.com"), &inner_txn)
.await?;
crate::transactions::after_commit(move || {
inner_calls.fetch_add(1, AtomicOrdering::SeqCst);
});
assert_eq!(calls.load(AtomicOrdering::SeqCst), 0);
Ok::<(), RecordError>(())
}
})
.await?;
assert_eq!(calls.load(AtomicOrdering::SeqCst), 0);
Ok::<(), RecordError>(())
}
})
.await
.expect("outer transaction should commit");
});
assert_eq!(calls.load(AtomicOrdering::SeqCst), 2);
});
}
ignored_rails_test!(
test_after_current_transaction_commit_multidb_nested_transactions,
"Rails-specific: multi-database after_commit callbacks are not implemented"
);
#[test]
fn test_transaction_after_commit_callback() {
with_sync_db(|| {
let calls = Arc::new(AtomicUsize::new(0));
let callback_calls = Arc::clone(&calls);
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, move |_txn| {
let callback_calls = Arc::clone(&callback_calls);
async move {
crate::transactions::after_commit(move || {
callback_calls.fetch_add(1, AtomicOrdering::SeqCst);
});
Ok::<(), RecordError>(())
}
})
.await
.expect("transaction should commit");
});
assert_eq!(calls.load(AtomicOrdering::SeqCst), 1);
});
}
#[test]
fn test_transaction_after_rollback_callback() {
with_sync_db(|| {
let calls = Arc::new(AtomicUsize::new(0));
let callback_calls = Arc::clone(&calls);
runtime::block_on(async {
let db = database::db();
let error = crate::transactions::transaction(&db, move |_txn| {
let callback_calls = Arc::clone(&callback_calls);
async move {
crate::transactions::after_rollback(move || {
callback_calls.fetch_add(1, AtomicOrdering::SeqCst);
});
Err::<(), RecordError>(RecordError::Invalid("rollback outer".to_owned()))
}
})
.await
.expect_err("transaction should roll back");
assert!(matches!(error, RecordError::Invalid(message) if message == "rollback outer"));
});
assert_eq!(calls.load(AtomicOrdering::SeqCst), 1);
});
}
#[test]
fn test_after_commit_runs_immediately_outside_transaction() {
with_sync_db(|| {
let calls = Arc::new(AtomicUsize::new(0));
let callback_calls = Arc::clone(&calls);
crate::transactions::after_commit(move || {
callback_calls.fetch_add(1, AtomicOrdering::SeqCst);
});
assert_eq!(calls.load(AtomicOrdering::SeqCst), 1);
assert_eq!(crate::transactions::open_transactions(), 0);
assert_eq!(crate::transactions::current_transaction_id(), None);
});
}
#[test]
fn test_after_rollback_does_nothing_outside_transaction() {
with_sync_db(|| {
let calls = Arc::new(AtomicUsize::new(0));
let callback_calls = Arc::clone(&calls);
crate::transactions::after_rollback(move || {
callback_calls.fetch_add(1, AtomicOrdering::SeqCst);
});
assert_eq!(calls.load(AtomicOrdering::SeqCst), 0);
assert_eq!(crate::transactions::open_transactions(), 0);
assert_eq!(crate::transactions::current_transaction_id(), None);
});
}
ignored_rails_test!(
test_blank_question_mark,
"Rails-specific: rustrails-record does not expose transaction-manager null objects"
);
ignored_rails_test!(
test_rollback_dirty_changes,
"Rails-specific: in-memory dirty-state restoration after rollback is not implemented"
);
ignored_rails_test!(
test_transaction_does_not_apply_default_scope,
"Rails-specific: default scopes are not implemented"
);
ignored_rails_test!(
test_rollback_dirty_changes_even_with_raise_during_rollback_removes_from_pool,
"Rails-specific: connection-pool failure injection is not exposed"
);
ignored_rails_test!(
test_rollback_dirty_changes_even_with_raise_during_rollback_doesnt_commit_transaction,
"Rails-specific: connection-pool failure injection is not exposed"
);
ignored_rails_test!(
test_connection_removed_from_pool_when_commit_raises_and_rollback_raises,
"Rails-specific: connection-pool failure injection is not exposed"
);
ignored_rails_test!(
test_connection_removed_from_pool_when_begin_raises_after_successfully_beginning_a_transaction,
"Rails-specific: connection-pool failure injection is not exposed"
);
ignored_rails_test!(
test_connection_removed_from_pool_when_thread_killed_in_begin_after_successfully_beginning_a_transaction,
"Rails-specific: thread-kill begin-transaction failure handling is not exposed"
);
ignored_rails_test!(
test_rollback_dirty_changes_multiple_saves,
"Rails-specific: in-memory dirty-state restoration after rollback is not implemented"
);
ignored_rails_test!(
test_rollback_dirty_changes_then_retry_save,
"Rails-specific: in-memory dirty-state restoration after rollback is not implemented"
);
ignored_rails_test!(
test_rollback_dirty_changes_then_retry_save_on_new_record,
"Rails-specific: in-memory dirty-state restoration after rollback is not implemented"
);
ignored_rails_test!(
test_rollback_dirty_changes_then_retry_save_on_new_record_with_autosave_association,
"Rails-specific: autosave association rollback state is not implemented"
);
ignored_rails_test!(
test_persisted_in_a_model_with_custom_primary_key_after_failed_save,
"Rails-specific: TestUser uses a simple integer primary key"
);
#[test]
fn test_raise_after_destroy() {
with_sync_db(|| {
let user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("seed insert should succeed");
let id = user.id().expect("seed row should have an id");
let error = transaction_sync(|txn| {
let txn = txn.clone();
async move {
let mut to_destroy = TestUser::find(id, &txn).await?;
to_destroy.destroy(&txn).await?;
Err::<(), RecordError>(RecordError::Invalid("raise after destroy".to_owned()))
}
})
.expect_err("destroy then error should roll back");
assert!(matches!(error, RecordError::Invalid(message) if message == "raise after destroy"));
let reloaded = TestUser::find_sync(id).expect("row should still exist after rollback");
assert_eq!(reloaded.name, "Alice");
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
1
);
});
}
#[test]
fn test_successful() {
with_sync_db(|| {
transaction_sync(|txn| {
let txn = txn.clone();
async move {
TestUser::create(user_attrs("Alice", "alice@example.com"), &txn).await?;
TestUser::create(user_attrs("Bob", "bob@example.com"), &txn).await?;
Ok(())
}
})
.expect("successful transaction should commit");
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
2
);
assert_eq!(
TestUser::find_sync(1).expect("first row should exist").name,
"Alice"
);
assert_eq!(
TestUser::find_sync(2)
.expect("second row should exist")
.name,
"Bob"
);
});
}
ignored_rails_test!(
test_add_to_null_transaction,
"Rails-specific: upstream covers a private add_to_transaction null-transaction helper; rustrails-record only exposes outside-transaction callback behavior, covered by the dedicated callback tests"
);
ignored_rails_test!(
test_successful_with_return_outside_inner_transaction,
"Rails-specific: Ruby return semantics do not map to Rust transaction closures"
);
ignored_rails_test!(
test_deprecation_on_ruby_timeout_outside_inner_transaction,
"Rails-specific: Ruby catch/throw timeout behavior does not map to Rust transaction closures"
);
ignored_rails_test!(
test_break_from_transaction_commits,
"Rails-specific: Ruby break semantics do not map to Rust transaction closures"
);
ignored_rails_test!(
test_throw_from_transaction_commits,
"Rails-specific: Ruby throw semantics do not map to Rust transaction closures"
);
ignored_rails_test!(
test_return_from_transaction_commits,
"Rails-specific: Ruby return semantics do not map to Rust transaction closures"
);
#[test]
fn test_number_of_transactions_in_commit() {
with_sync_db(|| {
let observed_state = Arc::new(Mutex::new(None));
let callback_state = Arc::clone(&observed_state);
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, move |_txn| {
let callback_state = Arc::clone(&callback_state);
async move {
assert_eq!(crate::transactions::open_transactions(), 1);
assert!(crate::transactions::transaction_open());
assert!(crate::transactions::current_transaction_id().is_some());
crate::transactions::after_commit(move || {
*callback_state.lock().unwrap() = Some((
crate::transactions::open_transactions(),
crate::transactions::transaction_open(),
crate::transactions::current_transaction_id(),
));
});
Ok::<(), RecordError>(())
}
})
.await
.expect("transaction should commit");
});
assert_eq!(*observed_state.lock().unwrap(), Some((0, false, None)));
});
}
#[test]
fn test_update_should_rollback_on_failure() {
with_sync_db(|| {
let user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("seed insert should succeed");
let id = user.id().expect("seed row should have an id");
let error = transaction_sync(|txn| {
let txn = txn.clone();
async move {
let mut in_txn = TestUser::find(id, &txn).await?;
in_txn
.update_attributes(
HashMap::from([("name".to_owned(), json!("Updated Alice"))]),
&txn,
)
.await?;
Err::<(), RecordError>(RecordError::Invalid("update failed".to_owned()))
}
})
.expect_err("failing update should roll back");
assert!(matches!(error, RecordError::Invalid(message) if message == "update failed"));
let reloaded = TestUser::find_sync(id).expect("row should still exist after rollback");
assert_eq!(reloaded.name, "Alice");
assert_eq!(reloaded.email, "alice@example.com");
});
}
ignored_rails_test!(
test_update_should_rollback_on_failure_bang,
"Rails-specific: rustrails-record does not expose bang update validation helpers"
);
ignored_rails_test!(
test_cancellation_from_before_destroy_rollbacks_in_destroy,
"Rails-specific: before_destroy callbacks are not implemented"
);
ignored_rails_test!(
test_callback_rollback_in_create,
"Rails-specific: transactional create callbacks are not implemented"
);
ignored_rails_test!(
test_callback_rollback_in_create_with_record_invalid_exception,
"Rails-specific: transactional create callbacks are not implemented"
);
ignored_rails_test!(
test_callback_rollback_in_create_with_rollback_exception,
"Rails-specific: transactional create callbacks are not implemented"
);
#[test]
fn test_transaction_state_is_cleared_when_record_is_persisted() {
with_sync_db(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create should succeed");
let id = user.id().expect("persisted user should have an id");
let error = runtime::block_on(async {
let db = database::db();
user.update_attributes(HashMap::from([("id".to_owned(), json!("oops"))]), &db)
.await
})
.expect_err("type-mismatched update should fail");
assert!(matches!(error, RecordError::Invalid(_)));
assert!(user.persisted());
assert_eq!(user.id(), Some(id));
assert_eq!(user.name, "Alice");
assert_eq!(user.email, "alice@example.com");
assert_eq!(
TestUser::find_sync(id)
.expect("persisted row should still reload after the failed update")
.name,
"Alice"
);
});
}
#[test]
fn test_nested_explicit_transactions() {
with_sync_db(|| {
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |txn| {
let txn = txn.clone();
async move {
TestUser::create(user_attrs("Outer", "outer@example.com"), &txn).await?;
crate::transactions::transaction(&txn, |inner_txn| {
let inner_txn = inner_txn.clone();
async move {
TestUser::create(user_attrs("Inner", "inner@example.com"), &inner_txn)
.await?;
Ok::<(), RecordError>(())
}
})
.await?;
Ok::<(), RecordError>(())
}
})
.await
.expect("nested transactions should commit on the same connection");
});
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
2
);
assert_eq!(
TestUser::find_sync(1)
.expect("outer row should persist")
.name,
"Outer"
);
assert_eq!(
TestUser::find_sync(2)
.expect("inner row should persist")
.name,
"Inner"
);
});
}
#[test]
fn test_nested_transaction_with_new_transaction_applies_parent_state_on_rollback() {
with_sync_db(|| {
let error = runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |txn| {
let txn = txn.clone();
async move {
TestUser::create(user_attrs("Outer", "outer@example.com"), &txn).await?;
let outer_id = crate::transactions::current_transaction_id()
.expect("outer transaction id should exist");
let inner_id = crate::transactions::transaction(&txn, |inner_txn| {
let inner_txn = inner_txn.clone();
async move {
assert_eq!(crate::transactions::open_transactions(), 2);
TestUser::create(user_attrs("Inner", "inner@example.com"), &inner_txn)
.await?;
Ok::<String, RecordError>(
crate::transactions::current_transaction_id()
.expect("nested transaction should reuse the outer id"),
)
}
})
.await?;
assert_eq!(inner_id, outer_id);
assert_eq!(TestUser::count(&txn).await?, 2);
Err::<(), RecordError>(RecordError::Invalid("rollback outer".to_owned()))
}
})
.await
})
.expect_err("outer transaction should roll back both scopes");
assert!(matches!(error, RecordError::Invalid(message) if message == "rollback outer"));
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
0
);
assert_eq!(crate::transactions::open_transactions(), 0);
assert!(!crate::transactions::transaction_open());
assert_eq!(crate::transactions::current_transaction_id(), None);
});
}
ignored_rails_test!(
test_nested_transaction_without_new_transaction_applies_parent_state_on_rollback,
"Rails-specific: nested transaction helpers always use savepoints; there is no join-parent mode to exercise"
);
#[test]
fn test_double_nested_transaction_applies_parent_state_on_rollback() {
with_sync_db(|| {
let error = runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |txn| {
let txn = txn.clone();
async move {
TestUser::create(user_attrs("Outer", "outer@example.com"), &txn).await?;
let outer_id = crate::transactions::current_transaction_id()
.expect("outer transaction id should exist");
let (middle_id, inner_id) =
crate::transactions::transaction(&txn, |nested_txn| {
let nested_txn = nested_txn.clone();
async move {
TestUser::create(
user_attrs("Middle", "middle@example.com"),
&nested_txn,
)
.await?;
let middle_id = crate::transactions::current_transaction_id()
.expect("middle transaction should reuse the outer id");
let inner_id =
crate::transactions::transaction(&nested_txn, |inner_txn| {
let inner_txn = inner_txn.clone();
async move {
assert_eq!(crate::transactions::open_transactions(), 3);
TestUser::create(
user_attrs("Inner", "inner@example.com"),
&inner_txn,
)
.await?;
Ok::<String, RecordError>(
crate::transactions::current_transaction_id().expect(
"double-nested transaction should reuse the outer id",
),
)
}
})
.await?;
assert_eq!(TestUser::count(&nested_txn).await?, 3);
Ok::<(String, String), RecordError>((middle_id, inner_id))
}
})
.await?;
assert_eq!(middle_id, outer_id);
assert_eq!(inner_id, outer_id);
assert_eq!(TestUser::count(&txn).await?, 3);
Err::<(), RecordError>(RecordError::Invalid("rollback outer".to_owned()))
}
})
.await
})
.expect_err("outer rollback should discard all nested writes");
assert!(matches!(error, RecordError::Invalid(message) if message == "rollback outer"));
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
0
);
assert_eq!(crate::transactions::open_transactions(), 0);
assert_eq!(crate::transactions::current_transaction_id(), None);
});
}
#[test]
fn test_manually_rolling_back_a_transaction() {
with_sync_db(|| {
let error = transaction_sync(|txn| {
let txn = txn.clone();
async move {
TestUser::create(user_attrs("Alice", "alice@example.com"), &txn).await?;
Err::<(), RecordError>(RecordError::NotSaved)
}
})
.expect_err("closure error should trigger rollback");
assert!(matches!(error, RecordError::NotSaved));
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
0
);
});
}
ignored_rails_test!(
test_invalid_keys_for_transaction,
"Rails-specific: rustrails-record does not accept transaction option hashes"
);
#[test]
fn test_force_savepoint_in_nested_transaction() {
with_sync_db(|| {
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |txn| {
let txn = txn.clone();
async move {
TestUser::create(user_attrs("Outer", "outer@example.com"), &txn).await?;
let error = crate::transactions::transaction(&txn, |inner_txn| {
let inner_txn = inner_txn.clone();
async move {
TestUser::create(user_attrs("Inner", "inner@example.com"), &inner_txn)
.await?;
Err::<(), RecordError>(RecordError::Invalid(
"rollback inner".to_owned(),
))
}
})
.await
.expect_err("inner transaction should roll back to its savepoint");
assert!(matches!(error, RecordError::Invalid(message) if message == "rollback inner"));
assert_eq!(TestUser::count(&txn).await?, 1);
TestUser::create(user_attrs("AfterInner", "after@example.com"), &txn).await?;
Ok::<(), RecordError>(())
}
})
.await
.expect("outer transaction should still commit");
});
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
2
);
assert_eq!(
TestUser::find_sync(1)
.expect("outer row should persist")
.name,
"Outer"
);
assert_eq!(
TestUser::find_sync(2)
.expect("post-rollback outer row should persist")
.name,
"AfterInner"
);
});
}
ignored_rails_test!(
test_no_savepoint_in_nested_transaction_without_force,
"Rails-specific: nested transaction helpers always use savepoints; there is no opt-out mode"
);
#[test]
fn test_force_savepoint_on_instance() {
with_sync_db(|| {
let first = TestUser::create_sync(user_attrs("First", "first@example.com"))
.expect("seed insert should succeed");
let second = TestUser::create_sync(user_attrs("Second", "second@example.com"))
.expect("seed insert should succeed");
let first_id = first.id().expect("first seed row should have an id");
let second_id = second.id().expect("second seed row should have an id");
runtime::block_on(async {
let db = database::db();
TestUser::transaction(&db, |txn| {
let txn = txn.clone();
async move {
let mut outer_first = TestUser::find(first_id, &txn).await?;
outer_first
.update_attributes(
HashMap::from([("name".to_owned(), json!("Outer First"))]),
&txn,
)
.await?;
let mut outer_second = TestUser::find(second_id, &txn).await?;
outer_second
.update_attributes(
HashMap::from([("name".to_owned(), json!("Outer Second"))]),
&txn,
)
.await?;
let error = TestUser::transaction(&txn, |inner_txn| {
let inner_txn = inner_txn.clone();
async move {
let mut rolled_back = TestUser::find(first_id, &inner_txn).await?;
rolled_back
.update_attributes(
HashMap::from([("name".to_owned(), json!("Rolled Back First"))]),
&inner_txn,
)
.await?;
Err::<(), RecordError>(RecordError::Invalid("rollback inner".to_owned()))
}
})
.await
.expect_err("inner transaction should roll back to its savepoint");
assert!(matches!(error, RecordError::Invalid(message) if message == "rollback inner"));
assert_eq!(
TestUser::find(first_id, &txn)
.await
.expect("outer row should remain visible")
.name,
"Outer First"
);
assert_eq!(
TestUser::find(second_id, &txn)
.await
.expect("second row should remain visible")
.name,
"Outer Second"
);
Ok::<(), RecordError>(())
}
})
.await
.expect("outer transaction should commit");
});
assert_eq!(
TestUser::find_sync(first_id)
.expect("first row should persist after commit")
.name,
"Outer First"
);
assert_eq!(
TestUser::find_sync(second_id)
.expect("second row should persist after commit")
.name,
"Outer Second"
);
});
}
ignored_rails_test!(
test_using_named_savepoints,
"Rails-specific: named savepoints are not implemented"
);
ignored_rails_test!(
test_releasing_named_savepoints,
"Rails-specific: named savepoints are not implemented"
);
ignored_rails_test!(
test_savepoints_name,
"Rails-specific: savepoint naming internals are not exposed"
);
ignored_rails_test!(
test_rollback_when_commit_raises,
"Rails-specific: commit failure injection is not exposed"
);
ignored_rails_test!(
test_rollback_when_saving_a_frozen_record,
"Rails-specific: Rust records do not model Ruby object freezing"
);
ignored_rails_test!(
test_rollback_when_thread_killed,
"Rails-specific: thread-kill rollback behavior is not exposed"
);
ignored_rails_test!(
test_restore_active_record_state_for_all_records_in_a_transaction,
"Rails-specific: in-memory record-state restoration after rollback is not implemented"
);
ignored_rails_test!(
test_restore_frozen_state_after_double_destroy,
"Rails-specific: Rust records do not model Ruby object freezing"
);
ignored_rails_test!(
test_restore_new_record_after_double_save,
"Rails-specific: in-memory new-record restoration after rollback is not implemented"
);
ignored_rails_test!(
test_dont_restore_new_record_in_subsequent_transaction,
"Rails-specific: in-memory new-record restoration after rollback is not implemented"
);
ignored_rails_test!(
test_restore_previously_new_record_after_double_save,
"Rails-specific: previously_new_record rollback semantics are not implemented"
);
ignored_rails_test!(
test_restore_composite_id_after_rollback,
"Rails-specific: TestUser uses a simple integer primary key"
);
ignored_rails_test!(
test_rollback_on_composite_key_model,
"Rails-specific: TestUser uses a simple integer primary key"
);
ignored_rails_test!(
test_restore_id_after_rollback,
"Rails-specific: in-memory primary-key restoration after rollback is not implemented"
);
ignored_rails_test!(
test_restore_custom_primary_key_after_rollback,
"Rails-specific: TestUser uses a simple integer primary key"
);
ignored_rails_test!(
test_assign_id_after_rollback,
"Rails-specific: in-memory primary-key restoration after rollback is not implemented"
);
ignored_rails_test!(
test_assign_custom_primary_key_after_rollback,
"Rails-specific: TestUser uses a simple integer primary key"
);
ignored_rails_test!(
test_read_attribute_after_rollback,
"Rails-specific: attribute restoration after rollback is not implemented"
);
ignored_rails_test!(
test_read_attribute_with_custom_primary_key_after_rollback,
"Rails-specific: TestUser uses a simple integer primary key"
);
ignored_rails_test!(
test_write_attribute_after_rollback,
"Rails-specific: attribute restoration after rollback is not implemented"
);
ignored_rails_test!(
test_write_attribute_with_custom_primary_key_after_rollback,
"Rails-specific: TestUser uses a simple integer primary key"
);
ignored_rails_test!(
test_rollback_of_frozen_records,
"Rails-specific: Rust records do not model Ruby object freezing"
);
ignored_rails_test!(
test_rollback_for_freshly_persisted_records,
"Rails-specific: in-memory persisted-state restoration after rollback is not implemented"
);
ignored_rails_test!(
test_sqlite_add_column_in_transaction,
"Rails-specific: adapter-level DDL transaction behavior is not exposed"
);
ignored_rails_test!(
test_sqlite_default_transaction_mode_is_immediate,
"Rails-specific: adapter-level SQLite transaction mode is not exposed"
);
ignored_rails_test!(
test_transactions_state_from_rollback,
"Rails-specific: rustrails-record does not expose transaction state objects"
);
ignored_rails_test!(
test_transactions_state_from_commit,
"Rails-specific: rustrails-record does not expose transaction state objects"
);
ignored_rails_test!(
test_mark_transaction_state_as_committed,
"Rails-specific: rustrails-record does not expose transaction state objects"
);
ignored_rails_test!(
test_mark_transaction_state_as_rolledback,
"Rails-specific: rustrails-record does not expose transaction state objects"
);
ignored_rails_test!(
test_mark_transaction_state_as_nil,
"Rails-specific: rustrails-record does not expose transaction state objects"
);
ignored_rails_test!(
test_transaction_rollback_with_primarykeyless_tables,
"Rails-specific: primary-keyless table callbacks are not modeled by TestUser"
);
ignored_rails_test!(
test_empty_transaction_is_not_materialized,
"Rails-specific: lazy transaction materialization is not implemented"
);
ignored_rails_test!(
test_unprepared_statement_materializes_transaction,
"Rails-specific: lazy transaction materialization is not implemented"
);
ignored_rails_test!(
test_nested_transactions_skip_excess_savepoints,
"Rails-specific: savepoints are not implemented"
);
ignored_rails_test!(
test_nested_transactions_after_disable_lazy_transactions,
"Rails-specific: lazy transaction materialization is not implemented"
);
ignored_rails_test!(
test_prepared_statement_materializes_transaction,
"Rails-specific: lazy transaction materialization is not implemented"
);
ignored_rails_test!(
test_savepoint_does_not_materialize_transaction,
"Rails-specific: savepoints and lazy transaction materialization are not implemented"
);
ignored_rails_test!(
test_raising_does_not_materialize_transaction,
"Rails-specific: lazy transaction materialization is not implemented"
);
ignored_rails_test!(
test_accessing_raw_connection_materializes_transaction,
"Rails-specific: raw connection materialization hooks are not exposed"
);
ignored_rails_test!(
test_accessing_raw_connection_disables_lazy_transactions,
"Rails-specific: lazy transaction materialization is not implemented"
);
ignored_rails_test!(
test_checking_in_connection_reenables_lazy_transactions,
"Rails-specific: connection-pool lazy transaction toggles are not exposed"
);
ignored_rails_test!(
test_transactions_can_be_manually_materialized,
"Rails-specific: manual transaction materialization is not exposed"
);
ignored_rails_test!(
test_locally_rejected_query_does_not_materialize_or_dirty_transaction,
"Rails-specific: write-prevention dirty-transaction internals are not exposed"
);
ignored_rails_test!(
test_failed_materialization_does_not_dirty_transaction,
"Rails-specific: transaction materialization failure hooks are not exposed"
);
ignored_rails_test!(
test_automatic_savepoint_in_outer_transaction,
"Rails-specific: automatic savepoints are not implemented"
);
ignored_rails_test!(
test_no_automatic_savepoint_for_inner_transaction,
"Rails-specific: automatic savepoints are not implemented"
);
ignored_rails_test!(
test_the_uuid_is_lazily_computed,
"Rails-specific: transaction UUID internals are not exposed"
);
ignored_rails_test!(
test_the_uuid_for_regular_transactions_is_generated_and_memoized,
"Rails-specific: transaction UUID internals are not exposed"
);
ignored_rails_test!(
test_the_uuid_for_null_transactions_is_nil,
"Rails-specific: transaction UUID internals are not exposed"
);
#[test]
fn test_transaction_per_thread() {
with_sync_db(|| {
let (main_id, worker_id) = runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |_| async move {
assert_eq!(crate::transactions::open_transactions(), 1);
assert!(crate::transactions::transaction_open());
let main_id = crate::transactions::current_transaction_id()
.expect("main transaction id should exist");
let (tx, rx) = std::sync::mpsc::channel();
let worker = std::thread::spawn(move || {
let _rt = runtime::init_runtime();
runtime::block_on(async move {
assert_eq!(crate::transactions::open_transactions(), 0);
assert!(!crate::transactions::transaction_open());
assert_eq!(crate::transactions::current_transaction_id(), None);
let worker_db = crate::base::test_support::setup_db().await;
let worker_id =
crate::transactions::transaction(&worker_db, |_| async move {
assert_eq!(crate::transactions::open_transactions(), 1);
assert!(crate::transactions::transaction_open());
Ok::<String, RecordError>(
crate::transactions::current_transaction_id()
.expect("worker transaction id should exist"),
)
})
.await
.expect("worker transaction should commit");
assert_eq!(crate::transactions::open_transactions(), 0);
assert!(!crate::transactions::transaction_open());
assert_eq!(crate::transactions::current_transaction_id(), None);
tx.send(worker_id)
.expect("worker should send its transaction id");
});
});
let worker_id = rx.recv().expect("worker should report its transaction id");
worker.join().expect("worker thread should complete");
Ok::<(String, String), RecordError>((main_id, worker_id))
})
.await
.expect("main transaction should commit")
});
assert_ne!(main_id, worker_id);
assert_eq!(crate::transactions::open_transactions(), 0);
assert!(!crate::transactions::transaction_open());
assert_eq!(crate::transactions::current_transaction_id(), None);
});
}
ignored_rails_test!(
test_transaction_isolation__read_committed,
"Rails-specific: transaction isolation options are not exposed"
);
ignored_rails_test!(
test_implicit_persistence_transaction_is_called,
"Rails-specific: persistence helpers do not expose implicit transaction hooks"
);
ignored_rails_test!(
test_implicit_persistence_transaction_is_called_when_in_nested_transaction,
"Rails-specific: persistence helpers do not expose implicit transaction hooks"
);
#[test]
fn test_after_commit_callbacks_fire_in_registration_order_across_nested_transactions() {
with_sync_db(|| {
let events = Arc::new(Mutex::new(Vec::new()));
let transaction_events = Arc::clone(&events);
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, move |txn| {
let txn = txn.clone();
let events = Arc::clone(&transaction_events);
let outer_events = Arc::clone(&events);
async move {
crate::transactions::after_commit(move || {
outer_events.lock().unwrap().push("outer-1".to_owned());
});
let nested_events = Arc::clone(&events);
crate::transactions::transaction(&txn, move |inner_txn| {
let inner_txn = inner_txn.clone();
let inner_events = Arc::clone(&nested_events);
async move {
crate::transactions::after_commit(move || {
inner_events.lock().unwrap().push("inner-1".to_owned());
});
TestUser::create(user_attrs("Inner", "inner@example.com"), &inner_txn)
.await?;
Ok::<(), RecordError>(())
}
})
.await?;
let trailing_events = Arc::clone(&events);
crate::transactions::after_commit(move || {
trailing_events.lock().unwrap().push("outer-2".to_owned());
});
Ok::<(), RecordError>(())
}
})
.await
.expect("callback ordering transaction should commit");
});
assert_eq!(
*events.lock().unwrap(),
vec![
"outer-1".to_owned(),
"inner-1".to_owned(),
"outer-2".to_owned(),
]
);
});
}
#[test]
fn test_after_commit_callbacks_do_not_fire_when_outer_transaction_rolls_back() {
with_sync_db(|| {
let calls = Arc::new(AtomicUsize::new(0));
let transaction_calls = Arc::clone(&calls);
runtime::block_on(async {
let db = database::db();
let error = crate::transactions::transaction(&db, move |txn| {
let txn = txn.clone();
let calls = Arc::clone(&transaction_calls);
let outer_calls = Arc::clone(&calls);
async move {
crate::transactions::after_commit(move || {
outer_calls.fetch_add(1, AtomicOrdering::SeqCst);
});
let nested_calls = Arc::clone(&calls);
crate::transactions::transaction(&txn, move |inner_txn| {
let inner_txn = inner_txn.clone();
let calls = Arc::clone(&nested_calls);
let inner_calls = Arc::clone(&calls);
async move {
crate::transactions::after_commit(move || {
inner_calls.fetch_add(1, AtomicOrdering::SeqCst);
});
TestUser::create(user_attrs("Inner", "inner@example.com"), &inner_txn)
.await?;
Ok::<(), RecordError>(())
}
})
.await?;
Err::<(), RecordError>(RecordError::Invalid("rollback outer".to_owned()))
}
})
.await
.expect_err("outer transaction should roll back");
assert!(matches!(error, RecordError::Invalid(message) if message == "rollback outer"));
});
assert_eq!(calls.load(AtomicOrdering::SeqCst), 0);
});
}
#[test]
fn test_after_commit_callbacks_are_cleared_after_commit() {
with_sync_db(|| {
let calls = Arc::new(AtomicUsize::new(0));
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |_| {
let calls = Arc::clone(&calls);
async move {
crate::transactions::after_commit(move || {
calls.fetch_add(1, AtomicOrdering::SeqCst);
});
Ok::<(), RecordError>(())
}
})
.await
.expect("first transaction should commit");
crate::transactions::transaction(&db, |_| async move { Ok::<(), RecordError>(()) })
.await
.expect("second transaction should commit");
});
assert_eq!(calls.load(AtomicOrdering::SeqCst), 1);
});
}
#[test]
fn test_nested_rollback_fires_only_inner_after_rollback_callbacks() {
with_sync_db(|| {
let calls = Arc::new(Mutex::new(Vec::new()));
let transaction_calls = Arc::clone(&calls);
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, move |txn| {
let txn = txn.clone();
let calls = Arc::clone(&transaction_calls);
let outer_calls = Arc::clone(&calls);
async move {
crate::transactions::after_rollback(move || {
outer_calls.lock().unwrap().push("outer".to_owned());
});
let nested_calls = Arc::clone(&calls);
let error = crate::transactions::transaction(&txn, move |inner_txn| {
let inner_txn = inner_txn.clone();
let calls = Arc::clone(&nested_calls);
let inner_calls = Arc::clone(&calls);
async move {
crate::transactions::after_rollback(move || {
inner_calls.lock().unwrap().push("inner".to_owned());
});
TestUser::create(user_attrs("Inner", "inner@example.com"), &inner_txn)
.await?;
Err::<(), RecordError>(RecordError::Invalid("rollback inner".to_owned()))
}
})
.await
.expect_err("inner transaction should roll back");
assert!(matches!(error, RecordError::Invalid(message) if message == "rollback inner"));
assert_eq!(*calls.lock().unwrap(), vec!["inner".to_owned()]);
Ok::<(), RecordError>(())
}
})
.await
.expect("outer transaction should commit");
});
assert_eq!(*calls.lock().unwrap(), vec!["inner".to_owned()]);
});
}
#[test]
fn test_nested_successful_after_rollback_callbacks_fire_if_outer_transaction_rolls_back() {
with_sync_db(|| {
let calls = Arc::new(Mutex::new(Vec::new()));
let transaction_calls = Arc::clone(&calls);
runtime::block_on(async {
let db = database::db();
let error = crate::transactions::transaction(&db, move |txn| {
let txn = txn.clone();
let calls = Arc::clone(&transaction_calls);
let outer_calls = Arc::clone(&calls);
async move {
crate::transactions::after_rollback(move || {
outer_calls.lock().unwrap().push("outer".to_owned());
});
let nested_calls = Arc::clone(&calls);
crate::transactions::transaction(&txn, move |inner_txn| {
let inner_txn = inner_txn.clone();
let calls = Arc::clone(&nested_calls);
let inner_calls = Arc::clone(&calls);
async move {
crate::transactions::after_rollback(move || {
inner_calls.lock().unwrap().push("inner".to_owned());
});
TestUser::create(user_attrs("Inner", "inner@example.com"), &inner_txn)
.await?;
Ok::<(), RecordError>(())
}
})
.await?;
Err::<(), RecordError>(RecordError::Invalid("rollback outer".to_owned()))
}
})
.await
.expect_err("outer transaction should roll back");
assert!(matches!(error, RecordError::Invalid(message) if message == "rollback outer"));
});
assert_eq!(
*calls.lock().unwrap(),
vec!["outer".to_owned(), "inner".to_owned()]
);
});
}
#[test]
fn test_after_rollback_callbacks_are_cleared_after_rollback() {
with_sync_db(|| {
let calls = Arc::new(AtomicUsize::new(0));
let callback_calls = Arc::clone(&calls);
runtime::block_on(async {
let db = database::db();
let _ = crate::transactions::transaction(&db, move |_txn| {
let outer_calls = Arc::clone(&callback_calls);
async move {
crate::transactions::after_rollback(move || {
outer_calls.fetch_add(1, AtomicOrdering::SeqCst);
});
Err::<(), RecordError>(RecordError::Invalid("rollback once".to_owned()))
}
})
.await;
crate::transactions::transaction(&db, |_| async move { Ok::<(), RecordError>(()) })
.await
.expect("later transaction should commit");
});
assert_eq!(calls.load(AtomicOrdering::SeqCst), 1);
});
}
#[test]
fn test_transaction_state_clears_after_outer_rollback() {
with_sync_db(|| {
let error = runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |_| async move {
assert_eq!(crate::transactions::open_transactions(), 1);
assert!(crate::transactions::transaction_open());
assert!(crate::transactions::current_transaction_id().is_some());
Err::<(), RecordError>(RecordError::Invalid("rollback outer".to_owned()))
})
.await
})
.expect_err("transaction should roll back");
assert!(matches!(error, RecordError::Invalid(message) if message == "rollback outer"));
assert_eq!(crate::transactions::open_transactions(), 0);
assert!(!crate::transactions::transaction_open());
assert_eq!(crate::transactions::current_transaction_id(), None);
});
}
#[test]
fn test_nested_rollback_restores_outer_transaction_state() {
with_sync_db(|| {
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |txn| {
let txn = txn.clone();
async move {
let outer_id = crate::transactions::current_transaction_id()
.expect("outer transaction id should exist");
let _ = crate::transactions::transaction(&txn, |_| async move {
assert_eq!(crate::transactions::open_transactions(), 2);
Err::<(), RecordError>(RecordError::Invalid("rollback inner".to_owned()))
})
.await
.expect_err("inner transaction should roll back");
assert_eq!(crate::transactions::open_transactions(), 1);
assert!(crate::transactions::transaction_open());
assert_eq!(
crate::transactions::current_transaction_id()
.expect("outer transaction id should remain"),
outer_id
);
Ok::<(), RecordError>(())
}
})
.await
.expect("outer transaction should commit");
});
assert_eq!(crate::transactions::open_transactions(), 0);
assert_eq!(crate::transactions::current_transaction_id(), None);
});
}
#[test]
fn test_current_transaction_id_is_none_outside_transactions() {
with_sync_db(|| {
assert_eq!(crate::transactions::current_transaction_id(), None);
runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |_| async move { Ok::<(), RecordError>(()) })
.await
.expect("transaction should commit");
});
assert_eq!(crate::transactions::current_transaction_id(), None);
});
}
#[test]
fn test_current_transaction_id_is_stable_across_nested_transactions() {
with_sync_db(|| {
let (outer_id, inner_id, after_inner_id) = runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |txn| {
let txn = txn.clone();
async move {
let outer_id = crate::transactions::current_transaction_id()
.expect("outer transaction id should exist");
let inner_id = crate::transactions::transaction(&txn, |_| async move {
Ok::<String, RecordError>(
crate::transactions::current_transaction_id()
.expect("nested transaction id should exist"),
)
})
.await?;
let after_inner_id = crate::transactions::current_transaction_id()
.expect("outer transaction id should still exist");
Ok::<(String, String, String), RecordError>((
outer_id,
inner_id,
after_inner_id,
))
}
})
.await
.expect("transaction should commit")
});
assert_eq!(outer_id, inner_id);
assert_eq!(outer_id, after_inner_id);
});
}
#[test]
fn test_current_transaction_id_changes_between_outer_transactions() {
with_sync_db(|| {
let (first, second) = runtime::block_on(async {
let db = database::db();
let first = crate::transactions::transaction(&db, |_| async move {
Ok::<String, RecordError>(
crate::transactions::current_transaction_id()
.expect("transaction id should exist"),
)
})
.await
.expect("first transaction should commit");
let second = crate::transactions::transaction(&db, |_| async move {
Ok::<String, RecordError>(
crate::transactions::current_transaction_id()
.expect("transaction id should exist"),
)
})
.await
.expect("second transaction should commit");
(first, second)
});
assert_ne!(first, second);
});
}
#[test]
fn test_many_savepoints() {
test_double_nested_transaction_applies_parent_state_on_rollback();
}
ignored_rails_test!(
test_raising_exception_in_callback_rollbacks_in_save,
"Rails-specific: transactional save callbacks are not implemented"
);
ignored_rails_test!(
test_rolling_back_in_a_callback_rollbacks_before_save,
"Rails-specific: transactional save callbacks are not implemented"
);
ignored_rails_test!(
test_raising_exception_in_nested_transaction_restore_state_in_save,
"Rails-specific: transactional save callbacks are not implemented"
);