use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use crate::{
base::test_support::{TestUser, seed_users, with_sync_test_user_db as with_sync_db},
locking::{LockOption, pessimistic::PessimisticLocking},
persistence::AsyncPersistence,
querying::AsyncQuerying,
};
use rustrails_support::{database, ignored_rails_test, runtime};
fn seed_sync_users() {
runtime::block_on(async {
let db = database::db();
seed_users(&db).await;
});
}
ignored_rails_test!(
test_quote_value_passed_lock_col,
"Rails-specific: TestUser does not expose an optimistic lock column"
);
ignored_rails_test!(
test_non_integer_lock_existing,
"Rails-specific: TestUser does not expose an optimistic lock column"
);
ignored_rails_test!(
test_non_integer_lock_destroy,
"Rails-specific: TestUser does not expose an optimistic lock column"
);
#[test]
fn test_lock_existing() {
with_sync_db(|| {
seed_sync_users();
let user = runtime::block_on(async {
let db = database::db();
TestUser::lock(1, &db).await
})
.expect("locking should load the seeded row");
assert_eq!(user.name, "Alice");
assert_eq!(user.email, "alice@example.com");
});
}
ignored_rails_test!(
test_lock_destroy,
"Rails-specific: TestUser does not expose optimistic stale-destroy semantics"
);
#[test]
fn test_lock_repeating() {
with_sync_db(|| {
seed_sync_users();
let first = runtime::block_on(async {
let db = database::db();
TestUser::lock(1, &db).await
})
.expect("first lock should succeed");
let second = runtime::block_on(async {
let db = database::db();
TestUser::lock(1, &db).await
})
.expect("second lock should succeed");
assert_eq!(first, second);
});
}
ignored_rails_test!(
test_lock_new,
"Rails-specific: rustrails-record only exposes locking by persisted primary key"
);
ignored_rails_test!(
test_lock_exception_record,
"Rails-specific: TestUser does not expose an optimistic lock column"
);
ignored_rails_test!(
test_lock_new_when_explicitly_passing_nil,
"Rails-specific: rustrails-record only exposes locking by persisted primary key"
);
ignored_rails_test!(
test_lock_new_when_explicitly_passing_value,
"Rails-specific: rustrails-record only exposes locking by persisted primary key"
);
ignored_rails_test!(
test_touch_existing_lock,
"Rails-specific: TestUser does not expose optimistic lock-version touch semantics"
);
ignored_rails_test!(
test_touch_stale_object,
"Rails-specific: TestUser does not expose optimistic stale-object touch semantics"
);
ignored_rails_test!(
test_update_with_dirty_primary_key,
"Rails-specific: TestUser does not expose optimistic lock-version updates"
);
ignored_rails_test!(
test_delete_with_dirty_primary_key,
"Rails-specific: TestUser does not expose optimistic lock-version deletes"
);
ignored_rails_test!(
test_destroy_with_dirty_primary_key,
"Rails-specific: TestUser does not expose optimistic lock-version destroys"
);
ignored_rails_test!(
test_explicit_update_lock_column_raise_error,
"Rails-specific: TestUser does not expose an optimistic lock column"
);
ignored_rails_test!(
test_lock_column_name_existing,
"Rails-specific: TestUser does not expose a configurable optimistic lock column"
);
ignored_rails_test!(
test_lock_column_is_mass_assignable,
"Rails-specific: TestUser does not expose a mass-assignable optimistic lock column"
);
ignored_rails_test!(
test_lock_without_default_sets_version_to_zero,
"Rails-specific: TestUser does not expose optimistic lock-version defaults"
);
ignored_rails_test!(
test_touch_existing_lock_without_default_should_work_with_null_in_the_database,
"Rails-specific: TestUser does not expose optimistic lock-version touch semantics"
);
ignored_rails_test!(
test_update_lock_version_to_nil_without_validation_or_constraint_raises_error,
"Rails-specific: TestUser does not expose optimistic lock-version validation"
);
ignored_rails_test!(
test_update_lock_version_to_nil_without_validation_raises,
"Rails-specific: TestUser does not expose optimistic lock-version validation"
);
ignored_rails_test!(
test_update_lock_version_to_nil_with_validation_does_not_raise_runtime_lock_version_error,
"Rails-specific: TestUser does not expose optimistic lock-version validation"
);
ignored_rails_test!(
test_update_bang_lock_version_to_nil_with_validation_does_not_raise_runtime_lock_version_error,
"Rails-specific: TestUser does not expose optimistic lock-version validation"
);
ignored_rails_test!(
test_touch_stale_object_with_lock_without_default,
"Rails-specific: TestUser does not expose optimistic stale-object touch semantics"
);
ignored_rails_test!(
test_lock_without_default_should_work_with_null_in_the_database,
"Rails-specific: TestUser does not expose optimistic lock-version defaults"
);
ignored_rails_test!(
test_update_with_lock_version_without_default_should_work_on_dirty_value_before_type_cast,
"Rails-specific: TestUser does not expose optimistic lock-version updates"
);
ignored_rails_test!(
test_destroy_with_lock_version_without_default_should_work_on_dirty_value_before_type_cast,
"Rails-specific: TestUser does not expose optimistic lock-version destroys"
);
ignored_rails_test!(
test_lock_without_default_queries_count,
"Rails-specific: TestUser does not expose optimistic lock-version query behavior"
);
ignored_rails_test!(
test_lock_with_custom_column_without_default_sets_version_to_zero,
"Rails-specific: TestUser does not expose a configurable optimistic lock column"
);
ignored_rails_test!(
test_lock_with_custom_column_without_default_should_work_with_null_in_the_database,
"Rails-specific: TestUser does not expose a configurable optimistic lock column"
);
ignored_rails_test!(
test_lock_with_custom_column_without_default_queries_count,
"Rails-specific: TestUser does not expose a configurable optimistic lock column"
);
ignored_rails_test!(
test_readonly_attributes,
"Rails-specific: readonly lock-version attributes are not modeled on TestUser"
);
ignored_rails_test!(
test_quote_table_name_reserved_word_references,
"Rails-specific: adapter-specific reserved-word locking fixtures are not available for TestUser"
);
ignored_rails_test!(
test_update_without_attributes_does_not_only_update_lock_version,
"Rails-specific: TestUser does not expose optimistic lock-version updates"
);
ignored_rails_test!(
test_counter_cache_with_touch_and_lock_version,
"Rails-specific: TestUser has no counter-cache lock-version integration"
);
ignored_rails_test!(
test_polymorphic_destroy_with_dependencies_and_lock_version,
"Rails-specific: TestUser has no polymorphic dependency graph or lock-version integration"
);
ignored_rails_test!(
test_removing_has_and_belongs_to_many_associations_upon_destroy,
"Rails-specific: TestUser has no HABTM association cleanup with lock-version semantics"
);
ignored_rails_test!(
test_yaml_dumping_with_lock_column,
"Rails-specific: YAML serialization of optimistic lock columns is not modeled on TestUser"
);
ignored_rails_test!(
test_destroy_dependents,
"Rails-specific: TestUser has no dependent-destroy locking fixtures"
);
ignored_rails_test!(
test_destroy_existing_object_with_locking_column_value_null_in_the_database,
"Rails-specific: TestUser does not expose optimistic lock-version destroy semantics"
);
ignored_rails_test!(
test_destroy_stale_object,
"Rails-specific: TestUser does not expose optimistic stale-object destroy semantics"
);
#[test]
fn test_typical_find_with_lock() {
with_sync_db(|| {
seed_sync_users();
let locked = runtime::block_on(async {
let db = database::db();
TestUser::lock(2, &db).await
})
.expect("locking should load the row");
let found = runtime::block_on(async {
let db = database::db();
TestUser::find(2, &db).await
})
.expect("plain find should load the same row");
assert_eq!(locked, found);
});
}
ignored_rails_test!(
test_eager_find_with_lock,
"Rails-specific: TestUser has no eager-load association graph to combine with locking"
);
#[test]
fn test_lock_does_not_raise_when_the_object_is_not_dirty() {
with_sync_db(|| {
seed_sync_users();
let user = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::find(1, &db).await.expect("row should exist");
user.lock_bang(&db)
.await
.expect("clean rows should reload without error");
user
});
assert_eq!(user.name, "Alice");
assert_eq!(user.email, "alice@example.com");
});
}
ignored_rails_test!(
test_lock_raises_when_the_record_is_dirty,
"rustrails-record reloads dirty records on lock_bang instead of raising a dirty-state error"
);
ignored_rails_test!(
test_locking_in_after_save_callback,
"Rails-specific: ActiveRecord after_save callback locking is not modeled by rustrails-record"
);
#[test]
fn test_with_lock_commits_transaction() {
with_sync_db(|| {
seed_sync_users();
let updated_name = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::find(1, &db).await.expect("row should exist");
user.with_lock(&db, LockOption::ForUpdate, |locked, txn| {
Box::pin(async move {
locked.name = "Locked Alice".to_owned();
locked.save(txn).await?;
Ok::<String, crate::RecordError>(locked.name.clone())
})
})
.await
.expect("with_lock should commit successful changes")
});
let reloaded = runtime::block_on(async {
let db = database::db();
TestUser::find(1, &db)
.await
.expect("row should still exist")
});
assert_eq!(updated_name, "Locked Alice");
assert_eq!(reloaded.name, "Locked Alice");
});
}
#[test]
fn test_with_lock_rolls_back_transaction() {
with_sync_db(|| {
seed_sync_users();
let error = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::find(1, &db).await.expect("row should exist");
user.with_lock(&db, LockOption::ForUpdate, |locked, txn| {
Box::pin(async move {
locked.name = "Should Roll Back".to_owned();
locked.save(txn).await?;
Err::<(), crate::RecordError>(crate::RecordError::Invalid(
"force rollback".to_owned(),
))
})
})
.await
})
.expect_err("errors should trigger with_lock rollbacks");
assert!(
matches!(error, crate::RecordError::Invalid(message) if message == "force rollback")
);
let reloaded = runtime::block_on(async {
let db = database::db();
TestUser::find(1, &db)
.await
.expect("row should still exist")
});
assert_eq!(reloaded.name, "Alice");
});
}
ignored_rails_test!(
test_with_lock_configures_transaction,
"Rails-specific: rustrails-record does not expose joinable or requires_new transaction options"
);
ignored_rails_test!(
test_lock_sending_custom_lock_statement,
"Rails-specific: rustrails-record does not expose custom SQL lock clauses"
);
ignored_rails_test!(
test_with_lock_sets_isolation,
"Rails-specific: rustrails-record does not expose with_lock isolation options"
);
ignored_rails_test!(
test_with_lock_locks_with_no_args,
"rustrails-record requires an explicit LockOption when calling with_lock"
);
#[test]
fn test_with_lock_yields_transaction() {
with_sync_db(|| {
seed_sync_users();
let count = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::find(1, &db).await.expect("row should exist");
user.with_lock(&db, LockOption::ForUpdate, |locked, txn| {
Box::pin(async move {
assert_eq!(locked.id, Some(1));
TestUser::count(txn).await
})
})
.await
})
.expect("with_lock should yield the active transaction connection");
assert_eq!(count, 3);
});
}
ignored_rails_test!(
test_no_locks_no_wait,
"Rails-specific: this Rails check depends on concurrent blocking-lock timing, but rustrails-record's SQLite API only offers explicit LockOption values and rejects LockOption::Nowait up front"
);
#[test]
fn test_lock_when_not_preventing_writes() {
with_sync_db(|| {
seed_sync_users();
let user = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::find(3, &db).await.expect("row should exist");
user.lock_bang(&db)
.await
.expect("ordinary lock_bang should succeed when writes are allowed");
user
});
assert_eq!(user.id, Some(3));
assert_eq!(user.name, "Carol");
assert_eq!(user.email, "carol@example.com");
});
}
ignored_rails_test!(
test_lock_when_preventing_writes,
"Rails-specific: rustrails-record has no write-prevention mode that rejects lock_bang"
);
#[test]
fn test_lock_when_not_preventing_writes_nested() {
with_sync_db(|| {
seed_sync_users();
let locked_user = runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |txn| {
let txn = txn.clone();
Box::pin(async move {
let outer_id = crate::transactions::current_transaction_id()
.expect("outer transaction should expose an id");
assert_eq!(crate::transactions::open_transactions(), 1);
let mut user = TestUser::find(3, &txn).await?;
user.lock_bang(&txn).await?;
assert_eq!(
crate::transactions::current_transaction_id(),
Some(outer_id)
);
assert_eq!(crate::transactions::open_transactions(), 1);
Ok::<(Option<i64>, String, String), crate::RecordError>((
user.id,
user.name.clone(),
user.email.clone(),
))
})
})
.await
.expect("nested lock_bang should reuse the active transaction")
});
assert_eq!(locked_user.0, Some(3));
assert_eq!(locked_user.1, "Carol");
assert_eq!(locked_user.2, "carol@example.com");
assert_eq!(crate::transactions::current_transaction_id(), None);
assert_eq!(crate::transactions::open_transactions(), 0);
});
}
ignored_rails_test!(
test_custom_lock_when_preventing_writes,
"Rails-specific: write-prevention modes and custom lock clauses are not implemented"
);
#[test]
fn test_with_lock_when_not_preventing_writes() {
with_sync_db(|| {
seed_sync_users();
let result = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::find(3, &db).await.expect("row should exist");
user.with_lock(&db, LockOption::ForUpdate, |locked, _| {
Box::pin(async move { Ok::<Option<i64>, crate::RecordError>(locked.id) })
})
.await
.expect("ordinary with_lock should succeed when writes are allowed")
});
assert_eq!(result, Some(3));
});
}
ignored_rails_test!(
test_with_lock_when_preventing_writes,
"Rails-specific: rustrails-record has no write-prevention mode that rejects with_lock"
);
ignored_rails_test!(
test_custom_with_lock_when_preventing_writes,
"Rails-specific: write-prevention modes and custom with_lock clauses are not implemented"
);
ignored_rails_test!(
test_relation_lock_when_not_preventing_writes,
"Rails-specific: relation-level lock clauses are not implemented"
);
ignored_rails_test!(
test_relation_lock_when_preventing_writes,
"Rails-specific: rustrails-record has no write-prevention mode for relation-level locks"
);
ignored_rails_test!(
test_relation_lock_when_not_preventing_writes_nested,
"Rails-specific: rustrails-record has no nested write-prevention override API"
);
ignored_rails_test!(
test_custom_relation_lock_when_preventing_writes,
"Rails-specific: relation-level custom lock clauses are not implemented"
);
#[test]
fn test_lock_returns_not_found_for_missing_row() {
with_sync_db(|| {
let error = runtime::block_on(async {
let db = database::db();
TestUser::lock(404, &db).await
})
.expect_err("missing rows should fail lock lookups");
assert!(matches!(error, crate::RecordError::NotFound));
});
}
#[test]
fn test_lock_marks_record_as_persisted() {
with_sync_db(|| {
seed_sync_users();
let user = runtime::block_on(async {
let db = database::db();
TestUser::lock(1, &db).await
})
.expect("lock should load the seeded row");
assert_eq!(user.state, crate::RecordState::Persisted);
assert_eq!(user.id, Some(1));
});
}
#[test]
fn test_lock_preserves_identifier_and_email() {
with_sync_db(|| {
seed_sync_users();
let user = runtime::block_on(async {
let db = database::db();
TestUser::lock(2, &db).await
})
.expect("lock should load the seeded row");
assert_eq!(user.id, Some(2));
assert_eq!(user.email, "bob@example.com");
});
}
#[test]
fn test_lock_does_not_change_row_count() {
with_sync_db(|| {
seed_sync_users();
let count = runtime::block_on(async {
let db = database::db();
let _ = TestUser::lock(3, &db)
.await
.expect("lock should load the row");
TestUser::count(&db).await
})
.expect("count should succeed after lock");
assert_eq!(count, 3);
});
}
#[test]
fn test_lock_matches_plain_find_for_same_row() {
with_sync_db(|| {
seed_sync_users();
let (locked, found) = runtime::block_on(async {
let db = database::db();
let locked = TestUser::lock(3, &db)
.await
.expect("lock should load the row");
let found = TestUser::find(3, &db)
.await
.expect("find should load the same row");
(locked, found)
});
assert_eq!(locked, found);
});
}
#[test]
fn test_lock_with_option_skip_locked_degrades_on_sqlite() {
with_sync_db(|| {
seed_sync_users();
let user = runtime::block_on(async {
let db = database::db();
TestUser::lock_with_option(1, LockOption::SkipLocked, &db).await
})
.expect("skip-locked should degrade to a plain lookup on sqlite");
assert_eq!(user.name, "Alice");
});
}
#[test]
fn test_lock_with_option_nowait_returns_informative_error_on_sqlite() {
with_sync_db(|| {
seed_sync_users();
let error = runtime::block_on(async {
let db = database::db();
TestUser::lock_with_option(1, LockOption::Nowait, &db).await
})
.expect_err("sqlite should reject NOWAIT row locks");
assert!(
matches!(error, crate::RecordError::Invalid(message) if message.contains("NOWAIT"))
);
});
}
#[test]
fn test_lock_with_option_returns_not_found_for_missing_row() {
with_sync_db(|| {
let error = runtime::block_on(async {
let db = database::db();
TestUser::lock_with_option(99, LockOption::SkipLocked, &db).await
})
.expect_err("missing rows should remain missing with lock options");
assert!(matches!(error, crate::RecordError::NotFound));
});
}
#[test]
fn test_lock_bang_reloads_latest_persisted_state() {
with_sync_db(|| {
seed_sync_users();
let user = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::find(1, &db).await.expect("row should exist");
let mut other = TestUser::find(1, &db).await.expect("row should exist");
other.name = "Alicia".to_owned();
other
.save(&db)
.await
.expect("save should persist the change");
user.lock_bang(&db)
.await
.expect("lock_bang should reload latest state");
user
});
assert_eq!(user.name, "Alicia");
assert_eq!(user.email, "alice@example.com");
});
}
#[test]
fn test_lock_bang_noops_for_new_records() {
with_sync_db(|| {
let user = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::default();
user.lock_bang(&db).await.expect("new records should no-op");
user
});
assert_eq!(user.state, crate::RecordState::New);
assert_eq!(user.id, None);
});
}
#[test]
fn test_with_lock_when_not_preventing_writes_nested() {
with_sync_db(|| {
seed_sync_users();
let locked_id = runtime::block_on(async {
let db = database::db();
crate::transactions::transaction(&db, |txn| {
let txn = txn.clone();
Box::pin(async move {
let outer_id = crate::transactions::current_transaction_id()
.expect("outer transaction should expose an id");
assert_eq!(crate::transactions::open_transactions(), 1);
let mut user = TestUser::find(2, &txn).await?;
let expected_outer_id = outer_id.clone();
let locked_id = user
.with_lock(&txn, LockOption::ForUpdate, move |locked, inner| {
let expected_outer_id = expected_outer_id.clone();
Box::pin(async move {
assert_eq!(
crate::transactions::current_transaction_id(),
Some(expected_outer_id)
);
assert_eq!(crate::transactions::open_transactions(), 1);
locked.name = "Nested Bob".to_owned();
locked.save(inner).await?;
Ok::<Option<i64>, crate::RecordError>(locked.id)
})
})
.await?;
assert_eq!(
crate::transactions::current_transaction_id(),
Some(outer_id)
);
assert_eq!(crate::transactions::open_transactions(), 1);
Ok::<Option<i64>, crate::RecordError>(locked_id)
})
})
.await
.expect("nested with_lock should reuse the active transaction")
});
assert_eq!(locked_id, Some(2));
assert_eq!(
runtime::block_on(async {
let db = database::db();
TestUser::find(2, &db)
.await
.expect("row should still exist")
.name
}),
"Nested Bob"
);
assert_eq!(crate::transactions::current_transaction_id(), None);
assert_eq!(crate::transactions::open_transactions(), 0);
});
}
#[test]
fn test_with_lock_skip_locked_still_executes_closure_on_sqlite() {
with_sync_db(|| {
seed_sync_users();
let (result, name) = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::find(3, &db).await.expect("row should exist");
let result = user
.with_lock(&db, LockOption::SkipLocked, |locked, _| {
Box::pin(async move {
locked.name.push_str("-seen");
Ok::<String, crate::RecordError>(locked.name.clone())
})
})
.await
.expect("skip-locked should still yield the record on sqlite");
(result, user.name)
});
assert_eq!(result, "Carol-seen");
assert_eq!(name, "Carol-seen");
});
}
#[test]
fn test_with_lock_nowait_returns_error_before_running_closure() {
with_sync_db(|| {
seed_sync_users();
let ran = Arc::new(AtomicBool::new(false));
let error = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::find(1, &db).await.expect("row should exist");
user.with_lock(&db, LockOption::Nowait, {
let ran = Arc::clone(&ran);
move |_locked, _| {
ran.store(true, Ordering::SeqCst);
Box::pin(async { Ok::<(), crate::RecordError>(()) })
}
})
.await
})
.expect_err("sqlite NOWAIT should fail before entering the closure");
assert!(
matches!(error, crate::RecordError::Invalid(message) if message.contains("NOWAIT"))
);
assert!(!ran.load(Ordering::SeqCst));
});
}
#[test]
fn test_lock_reads_latest_persisted_values_after_update() {
with_sync_db(|| {
seed_sync_users();
let refreshed = runtime::block_on(async {
let db = database::db();
let mut user = TestUser::lock(2, &db).await.expect("row should lock");
user.name = "Bobby".to_owned();
user.save(&db)
.await
.expect("save should persist the change");
TestUser::lock(2, &db)
.await
.expect("updated row should lock")
});
assert_eq!(refreshed.name, "Bobby");
assert_eq!(refreshed.email, "bob@example.com");
assert_eq!(refreshed.state, crate::RecordState::Persisted);
});
}