use std::collections::HashMap;
use sea_orm::{ActiveModelBehavior, ColumnTrait, EntityTrait, IntoActiveModel, Iterable};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use crate::{RecordError, persistence::AsyncPersistence};
use rustrails_support::{database, runtime};
#[allow(private_bounds)]
pub trait Persistence: AsyncPersistence {
fn save_sync(&mut self) -> Result<(), RecordError>
where
Self: Serialize,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
<Self::Entity as EntityTrait>::Model:
IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
{
database::with_db(|db| runtime::block_on(self.save(db)))
}
fn save_bang_sync(&mut self) -> Result<(), RecordError>
where
Self: Serialize,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
<Self::Entity as EntityTrait>::Model:
IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
{
database::with_db(|db| runtime::block_on(self.save_bang(db)))
}
fn create_sync(attrs: HashMap<String, Value>) -> Result<Self, RecordError>
where
Self: Default + Serialize + DeserializeOwned,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
<Self::Entity as EntityTrait>::Model:
IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
{
database::with_db(|db| runtime::block_on(Self::create(attrs, db)))
}
fn insert_all_sync(records: Vec<HashMap<String, Value>>) -> Result<Vec<Self>, RecordError>
where
Self: Default + Serialize + DeserializeOwned,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
<Self::Entity as EntityTrait>::Model:
IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
{
database::with_db(|db| runtime::block_on(Self::insert_all(records, db)))
}
fn upsert_sync(attrs: HashMap<String, Value>, unique_by: &[&str]) -> Result<Self, RecordError>
where
Self: Default + Serialize + DeserializeOwned,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
<Self::Entity as EntityTrait>::Model:
IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
{
database::with_db(|db| runtime::block_on(Self::upsert(attrs, unique_by, db)))
}
fn upsert_all_sync(
records: Vec<HashMap<String, Value>>,
unique_by: &[&str],
) -> Result<Vec<Self>, RecordError>
where
Self: Default + Serialize + DeserializeOwned,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
<Self::Entity as EntityTrait>::Model:
IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
{
database::with_db(|db| runtime::block_on(Self::upsert_all(records, unique_by, db)))
}
fn update_attributes_sync(&mut self, attrs: HashMap<String, Value>) -> Result<(), RecordError>
where
Self: Serialize + DeserializeOwned,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
<Self::Entity as EntityTrait>::Model:
IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
{
database::with_db(|db| runtime::block_on(self.update_attributes(attrs, db)))
}
fn destroy_sync(&mut self) -> Result<(), RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(self.destroy(db)))
}
fn reload_sync(&mut self) -> Result<(), RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(self.reload(db)))
}
fn delete_sync(id: i64) -> Result<(), RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::delete(id, db)))
}
fn update_all_sync(
conditions: HashMap<String, Value>,
updates: HashMap<String, Value>,
) -> Result<u64, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::update_all(conditions, updates, db)))
}
fn destroy_all_sync(conditions: HashMap<String, Value>) -> Result<u64, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::destroy_all(conditions, db)))
}
}
impl<T: AsyncPersistence> Persistence for T {}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use sea_orm::{ConnectionTrait, Schema};
use serde_json::{Value, json};
use super::Persistence;
use crate::{
Querying, Record, RecordError, RecordState,
base::test_support::{TestUser, test_user},
};
use rustrails_support::{database, runtime};
fn setup_sync_db() -> tokio::runtime::Runtime {
let runtime_handle = runtime::init_runtime();
database::establish("sqlite::memory:").expect("sqlite in-memory connection should succeed");
runtime::block_on(async {
let db = database::db();
let schema = Schema::new(db.get_database_backend());
db.execute(&schema.create_table_from_entity(test_user::Entity))
.await
.expect("test_users table should be created");
});
runtime_handle
}
fn user_attrs(name: &str, email: &str) -> HashMap<String, Value> {
HashMap::from([
("name".to_owned(), json!(name)),
("email".to_owned(), json!(email)),
])
}
fn run_sync_test(test: impl FnOnce() + Send + 'static) {
let handle = std::thread::spawn(test);
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
fn with_sync_db(test: impl FnOnce(&tokio::runtime::Runtime) + Send + 'static) {
run_sync_test(move || {
let runtime_handle = setup_sync_db();
test(&runtime_handle);
});
}
#[test]
fn create_sync_inserts_a_row_and_returns_it() {
with_sync_db(|_| {
let user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should succeed");
assert!(user.id().is_some());
assert!(user.persisted());
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
1
);
});
}
#[test]
fn create_sync_preserves_supplied_attributes() {
with_sync_db(|_| {
let user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should succeed");
assert_eq!(user.name, "Dana");
assert_eq!(user.email, "dana@example.com");
});
}
#[test]
fn create_sync_allows_explicit_primary_key() {
with_sync_db(|_| {
let user = TestUser::create_sync(HashMap::from([
("id".to_owned(), json!(42)),
("name".to_owned(), json!("Dana")),
("email".to_owned(), json!("dana@example.com")),
]))
.expect("create_sync should succeed");
assert_eq!(user.id(), Some(42));
assert!(user.persisted());
});
}
#[test]
fn create_sync_with_empty_attributes_uses_defaults_and_persists() {
with_sync_db(|_| {
let user =
TestUser::create_sync(HashMap::new()).expect("empty create_sync should succeed");
assert!(user.id().is_some());
assert_eq!(user.name, "");
assert_eq!(user.email, "");
assert!(user.persisted());
});
}
#[test]
fn create_sync_rejects_unknown_attributes() {
with_sync_db(|_| {
let error = TestUser::create_sync(HashMap::from([
("name".to_owned(), json!("Dana")),
("email".to_owned(), json!("dana@example.com")),
("role".to_owned(), json!("admin")),
]))
.expect_err("unknown attributes should fail");
assert!(matches!(error, RecordError::Invalid(_)));
});
}
#[test]
fn create_sync_with_invalid_attributes_fails() {
with_sync_db(|_| {
let error = TestUser::create_sync(HashMap::from([
("name".to_owned(), json!(123)),
("email".to_owned(), json!("alice@example.com")),
]))
.expect_err("invalid attributes should fail");
assert!(matches!(error, RecordError::Invalid(_)));
});
}
#[test]
fn create_sync_with_duplicate_primary_key_returns_database_error() {
with_sync_db(|_| {
let duplicate_id = 70_001;
let _existing = TestUser::create_sync(HashMap::from([
("id".to_owned(), json!(duplicate_id)),
("name".to_owned(), json!("Alice")),
("email".to_owned(), json!("alice@example.com")),
]))
.expect("initial create_sync should succeed");
let error = TestUser::create_sync(HashMap::from([
("id".to_owned(), json!(duplicate_id)),
("name".to_owned(), json!("Dana")),
("email".to_owned(), json!("dana@example.com")),
]))
.expect_err("duplicate ids should fail");
assert!(matches!(error, RecordError::Database(_)));
});
}
#[test]
fn save_sync_inserts_new_records() {
with_sync_db(|_| {
let mut user = TestUser {
name: "Alice".to_owned(),
email: "alice@example.com".to_owned(),
..Default::default()
};
user.save_sync().expect("save_sync should insert");
assert!(user.id().is_some());
assert!(user.persisted());
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
1
);
});
}
#[test]
fn save_sync_updates_existing_records() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should succeed");
user.name = "Alicia".to_owned();
user.save_sync().expect("save_sync should update");
let reloaded = TestUser::find_sync(user.id().expect("saved user should have id"))
.expect("find_sync should succeed");
assert_eq!(reloaded.name, "Alicia");
});
}
#[test]
fn save_sync_preserves_existing_primary_key_when_updating() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should succeed");
let id = user.id().expect("created user should have id");
user.email = "updated@example.com".to_owned();
user.save_sync().expect("save_sync should update");
assert_eq!(user.id(), Some(id));
});
}
#[test]
fn save_sync_on_destroyed_state_returns_not_saved() {
with_sync_db(|_| {
let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
user.set_record_state(RecordState::Destroyed);
let error = user
.save_sync()
.expect_err("destroyed records cannot be saved");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn save_sync_with_duplicate_primary_key_returns_database_error() {
with_sync_db(|_| {
let duplicate_id = 70_002;
let _existing = TestUser::create_sync(HashMap::from([
("id".to_owned(), json!(duplicate_id)),
("name".to_owned(), json!("Alice")),
("email".to_owned(), json!("alice@example.com")),
]))
.expect("initial create_sync should succeed");
let mut user = TestUser {
id: Some(duplicate_id),
name: "Dana".to_owned(),
email: "dana@example.com".to_owned(),
..Default::default()
};
let error = user.save_sync().expect_err("duplicate ids should fail");
assert!(matches!(error, RecordError::Database(_)));
});
}
#[test]
fn save_bang_sync_behaves_like_save_sync() {
with_sync_db(|_| {
let mut user = TestUser {
name: "Alice".to_owned(),
email: "alice@example.com".to_owned(),
..Default::default()
};
user.save_bang_sync()
.expect("save_bang_sync should succeed");
assert!(user.persisted());
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
1
);
});
}
#[test]
fn save_bang_sync_on_destroyed_state_returns_not_saved() {
with_sync_db(|_| {
let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
user.set_record_state(RecordState::Destroyed);
let error = user
.save_bang_sync()
.expect_err("destroyed records cannot be saved");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn save_bang_sync_propagates_duplicate_primary_key_errors() {
with_sync_db(|_| {
let duplicate_id = 70_003;
let _existing = TestUser::create_sync(HashMap::from([
("id".to_owned(), json!(duplicate_id)),
("name".to_owned(), json!("Alice")),
("email".to_owned(), json!("alice@example.com")),
]))
.expect("initial create_sync should succeed");
let mut user = TestUser {
id: Some(duplicate_id),
name: "Dana".to_owned(),
email: "dana@example.com".to_owned(),
..Default::default()
};
let error = user
.save_bang_sync()
.expect_err("duplicate ids should fail");
assert!(matches!(error, RecordError::Database(_)));
});
}
#[test]
fn update_attributes_sync_merges_and_persists_changes() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should succeed");
user.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Alicia"))]))
.expect("update_attributes_sync should succeed");
assert_eq!(user.name, "Alicia");
let from_db = TestUser::find_sync(user.id().expect("user should have id"))
.expect("find_sync should succeed");
assert_eq!(from_db.name, "Alicia");
});
}
#[test]
fn update_attributes_sync_preserves_unspecified_fields() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should succeed");
user.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Dani"))]))
.expect("update_attributes_sync should succeed");
assert_eq!(user.email, "dana@example.com");
});
}
#[test]
fn update_attributes_sync_keeps_record_persisted() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should succeed");
user.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Dani"))]))
.expect("update_attributes_sync should succeed");
assert_eq!(user.record_state(), RecordState::Persisted);
});
}
#[test]
fn update_attributes_sync_on_new_record_inserts_and_persists() {
with_sync_db(|_| {
let mut user = TestUser::default();
user.update_attributes_sync(user_attrs("Dana", "dana@example.com"))
.expect("update_attributes_sync should insert new records");
assert!(user.id().is_some());
assert!(user.persisted());
});
}
#[test]
fn update_attributes_sync_rejects_unknown_fields() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should succeed");
let error = user
.update_attributes_sync(HashMap::from([("missing".to_owned(), json!(true))]))
.expect_err("unknown field should fail");
assert!(matches!(error, RecordError::Invalid(_)));
});
}
#[test]
fn update_attributes_sync_on_destroyed_record_returns_not_saved() {
with_sync_db(|_| {
let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
user.set_record_state(RecordState::Destroyed);
let error = user
.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Dani"))]))
.expect_err("destroyed records cannot be updated");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn update_attributes_sync_with_type_mismatch_returns_invalid() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should succeed");
let error = user
.update_attributes_sync(HashMap::from([("id".to_owned(), json!("oops"))]))
.expect_err("type mismatches should fail");
assert!(matches!(error, RecordError::Invalid(_)));
});
}
#[test]
fn destroy_sync_deletes_rows_and_marks_records_destroyed() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should succeed");
let id = user.id().expect("created user should have id");
user.destroy_sync().expect("destroy_sync should succeed");
assert_eq!(user.record_state(), RecordState::Destroyed);
assert!(matches!(
TestUser::find_sync(id),
Err(RecordError::NotFound)
));
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
0
);
});
}
#[test]
fn destroyed_record_cannot_be_saved_again() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should succeed");
user.destroy_sync().expect("destroy_sync should succeed");
let error = user
.save_sync()
.expect_err("saving destroyed record should fail");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn destroy_sync_without_id_returns_not_saved() {
with_sync_db(|_| {
let mut user = TestUser::default();
let error = user
.destroy_sync()
.expect_err("unsaved records cannot be destroyed");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn destroy_sync_after_row_is_missing_returns_not_found() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should succeed");
let id = user.id().expect("created user should have id");
TestUser::delete_sync(id).expect("delete_sync should remove row");
let error = user
.destroy_sync()
.expect_err("missing rows should fail destroy_sync");
assert!(matches!(error, RecordError::NotFound));
assert_eq!(user.record_state(), RecordState::Persisted);
});
}
#[test]
fn reload_sync_refreshes_from_the_database() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should succeed");
let id = user.id().expect("created user should have id");
TestUser::update_all_sync(
HashMap::from([("id".to_owned(), json!(id))]),
HashMap::from([("name".to_owned(), json!("Alicia"))]),
)
.expect("update_all_sync should succeed");
user.reload_sync().expect("reload_sync should succeed");
assert_eq!(user.name, "Alicia");
});
}
#[test]
fn reload_sync_without_id_returns_not_saved() {
with_sync_db(|_| {
let mut user = TestUser::default();
let error = user
.reload_sync()
.expect_err("unsaved records cannot reload");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn reload_sync_after_row_is_deleted_returns_not_found() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should succeed");
let id = user.id().expect("created user should have id");
TestUser::delete_sync(id).expect("delete_sync should remove row");
let error = user
.reload_sync()
.expect_err("missing rows should fail reload_sync");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn reload_sync_restores_persisted_state_from_database() {
with_sync_db(|_| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should succeed");
user.set_record_state(RecordState::Destroyed);
user.reload_sync().expect("reload_sync should succeed");
assert_eq!(user.record_state(), RecordState::Persisted);
});
}
#[test]
fn delete_sync_removes_rows_by_id_without_loading_record() {
with_sync_db(|_| {
let id = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should succeed")
.id()
.expect("created user should have id");
TestUser::delete_sync(id).expect("delete_sync should succeed");
assert!(matches!(
TestUser::find_sync(id),
Err(RecordError::NotFound)
));
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
0
);
});
}
#[test]
fn delete_sync_missing_id_returns_not_found() {
with_sync_db(|_| {
let error = TestUser::delete_sync(999).expect_err("missing delete_sync should fail");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn sync_persistence_helpers_can_run_inside_the_same_async_runtime() {
with_sync_db(|runtime_handle| {
let user = runtime_handle.block_on(async {
TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should work inside async context")
});
assert_eq!(user.name, "Dana");
});
}
#[test]
fn persistence_trait_is_the_sync_api() {
run_sync_test(|| {
fn assert_sync_api<T: Persistence>() {}
assert_sync_api::<crate::base::test_support::TestUser>();
});
}
#[test]
fn upsert_sync_creates_a_new_record_when_none_exists() {
with_sync_db(|_| {
let user = TestUser::upsert_sync(user_attrs("Alice", "alice@example.com"), &["email"])
.expect("upsert_sync should create a missing row");
assert!(user.id().is_some());
assert!(user.persisted());
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
1
);
});
}
#[test]
fn upsert_sync_updates_an_existing_record_when_match_exists() {
with_sync_db(|_| {
let existing = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("seed create_sync should succeed");
let user =
TestUser::upsert_sync(user_attrs("Updated Alice", "alice@example.com"), &["email"])
.expect("upsert_sync should update the matching row");
assert_eq!(user.id(), existing.id());
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
1
);
let reloaded =
TestUser::find_sync(existing.id().expect("existing record should have id"))
.expect("find_sync should reload the updated row");
assert_eq!(reloaded.name, "Updated Alice");
assert_eq!(reloaded.email, "alice@example.com");
});
}
#[test]
fn upsert_all_sync_handles_mixed_new_and_existing_records() {
with_sync_db(|_| {
let existing = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("seed create_sync should succeed");
let users = TestUser::upsert_all_sync(
vec![
user_attrs("Updated Alice", "alice@example.com"),
user_attrs("Bob", "bob@example.com"),
],
&["email"],
)
.expect("upsert_all_sync should handle mixed rows");
assert_eq!(users.len(), 2);
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
2
);
let updated =
TestUser::find_sync(existing.id().expect("existing record should have id"))
.expect("find_sync should reload the updated row");
assert_eq!(updated.name, "Updated Alice");
let inserted = TestUser::find_by_sync(HashMap::from([(
"email".to_owned(),
json!("bob@example.com"),
)]))
.expect("find_by_sync should succeed")
.expect("new row should exist");
assert_eq!(inserted.name, "Bob");
});
}
}