use std::collections::HashMap;
use sea_orm::{ColumnTrait, EntityTrait, FromQueryResult, Iterable};
use serde_json::Value;
use crate::{RecordError, querying::AsyncQuerying};
use rustrails_support::{database, runtime};
#[allow(private_bounds)]
pub trait Querying: AsyncQuerying {
fn find_sync(id: i64) -> Result<Self, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::find(id, db)))
}
fn find_by_id_sync(id: i64) -> Result<Option<Self>, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::find_by_id(id, db)))
}
fn find_by_sync(conditions: HashMap<String, Value>) -> Result<Option<Self>, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::find_by(conditions, db)))
}
fn find_by_bang_sync(conditions: HashMap<String, Value>) -> Result<Self, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::find_by_bang(conditions, db)))
}
fn take_sync() -> Result<Option<Self>, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::take(db)))
}
fn take_bang_sync() -> Result<Self, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::take_bang(db)))
}
fn sole_sync() -> Result<Self, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::sole(db)))
}
fn find_sole_by_sync(conditions: HashMap<String, Value>) -> Result<Self, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::find_sole_by(conditions, db)))
}
fn pluck_sync(column: &str) -> Result<Vec<Value>, RecordError>
where
Self: serde::Serialize,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::pluck(column, db)))
}
fn pick_sync(column: &str) -> Result<Option<Value>, RecordError>
where
Self: serde::Serialize,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::pick(column, db)))
}
fn ids_sync() -> Result<Vec<i64>, RecordError>
where
Self: serde::Serialize,
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::ids(db)))
}
fn all_sync() -> Result<Vec<Self>, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::all(db)))
}
fn first_sync() -> Result<Option<Self>, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::first(db)))
}
fn last_sync() -> Result<Option<Self>, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::last(db)))
}
fn count_sync() -> Result<u64, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
<Self::Entity as EntityTrait>::Model: FromQueryResult + Send + Sync,
{
database::with_db(|db| runtime::block_on(Self::count(db)))
}
fn exists_with_conditions_sync(conditions: HashMap<String, Value>) -> Result<bool, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
database::with_db(|db| runtime::block_on(Self::exists_with_conditions(conditions, db)))
}
fn exists_sync(conditions: HashMap<String, Value>) -> Result<bool, RecordError>
where
<Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
{
Self::exists_with_conditions_sync(conditions)
}
}
impl<T: AsyncQuerying> Querying for T {}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use sea_orm::{ActiveModelTrait, ActiveValue::Set, ConnectionTrait, Schema};
use serde_json::{Value, json};
use super::Querying;
use crate::{
OrderDirection, RecordError, RecordState,
base::test_support::{TestUser, seed_users, test_user},
querying::AsyncQuerying,
};
use rustrails_support::{database, runtime};
fn run_sync_test(seed: bool, test: impl FnOnce() + Send + 'static) {
std::thread::spawn(move || {
let _rt = 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");
if seed {
seed_users(&db).await;
}
});
test();
})
.join()
.unwrap();
}
fn run_seeded_sync_test(test: impl FnOnce() + Send + 'static) {
run_sync_test(true, test);
}
fn run_empty_sync_test(test: impl FnOnce() + Send + 'static) {
run_sync_test(false, test);
}
#[test]
fn find_sync_returns_first_matching_record() {
run_seeded_sync_test(|| {
let user = TestUser::find_sync(1).expect("find_sync should return a record");
assert_eq!(user.name, "Alice");
assert_eq!(user.email, "alice@example.com");
});
}
#[test]
fn find_sync_returns_middle_matching_record() {
run_seeded_sync_test(|| {
let user = TestUser::find_sync(2).expect("find_sync should return a record");
assert_eq!(user.name, "Bob");
assert_eq!(user.email, "bob@example.com");
});
}
#[test]
fn find_sync_returns_last_matching_record() {
run_seeded_sync_test(|| {
let user = TestUser::find_sync(3).expect("find_sync should return a record");
assert_eq!(user.name, "Carol");
assert_eq!(user.email, "carol@example.com");
});
}
#[test]
fn find_sync_missing_returns_not_found_when_seeded() {
run_seeded_sync_test(|| {
let error = TestUser::find_sync(404).expect_err("missing row should return an error");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn find_sync_missing_returns_not_found_when_table_empty() {
run_empty_sync_test(|| {
let error = TestUser::find_sync(404).expect_err("missing row should return an error");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn find_by_id_sync_returns_matching_record_when_present() {
run_seeded_sync_test(|| {
let user = TestUser::find_by_id_sync(3)
.expect("query should succeed")
.expect("row should exist");
assert_eq!(user.name, "Carol");
assert_eq!(user.state, RecordState::Persisted);
});
}
#[test]
fn find_by_id_sync_returns_none_for_missing_rows() {
run_seeded_sync_test(|| {
let user = TestUser::find_by_id_sync(404).expect("query should succeed");
assert!(user.is_none());
});
}
#[test]
fn find_by_id_sync_returns_none_for_empty_table() {
run_empty_sync_test(|| {
let user = TestUser::find_by_id_sync(404).expect("query should succeed");
assert!(user.is_none());
});
}
#[test]
fn find_by_sync_filters_by_email() {
run_seeded_sync_test(|| {
let user = TestUser::find_by_sync(HashMap::from([(
"email".to_owned(),
json!("carol@example.com"),
)]))
.expect("query should succeed")
.expect("row should exist");
assert_eq!(user.name, "Carol");
});
}
#[test]
fn find_by_sync_filters_by_name() {
run_seeded_sync_test(|| {
let user = TestUser::find_by_sync(HashMap::from([("name".to_owned(), json!("Bob"))]))
.expect("query should succeed")
.expect("row should exist");
assert_eq!(user.email, "bob@example.com");
});
}
#[test]
fn find_by_sync_returns_none_when_no_match_exists() {
run_seeded_sync_test(|| {
let user = TestUser::find_by_sync(HashMap::from([(
"email".to_owned(),
json!("missing@example.com"),
)]))
.expect("query should succeed");
assert!(user.is_none());
});
}
#[test]
fn find_by_sync_with_multiple_conditions_requires_all_matches() {
run_seeded_sync_test(|| {
let user = TestUser::find_by_sync(HashMap::from([
("name".to_owned(), json!("Bob")),
("email".to_owned(), json!("bob@example.com")),
]))
.expect("query should succeed")
.expect("row should exist");
assert_eq!(user.id, Some(2));
});
}
#[test]
fn find_by_sync_with_multiple_conditions_returns_none_for_partial_match() {
run_seeded_sync_test(|| {
let user = TestUser::find_by_sync(HashMap::from([
("name".to_owned(), json!("Bob")),
("email".to_owned(), json!("alice@example.com")),
]))
.expect("query should succeed");
assert!(user.is_none());
});
}
#[test]
fn find_by_sync_with_unknown_column_returns_invalid_error() {
run_seeded_sync_test(|| {
let error = TestUser::find_by_sync(HashMap::from([("missing".to_owned(), json!(1))]))
.expect_err("unknown columns should fail");
assert!(matches!(error, RecordError::Invalid(_)));
});
}
#[test]
fn find_by_sync_with_null_condition_returns_none_for_non_null_column() {
run_seeded_sync_test(|| {
let user = TestUser::find_by_sync(HashMap::from([("email".to_owned(), Value::Null)]))
.expect("query should succeed");
assert!(user.is_none());
});
}
#[test]
fn all_sync_returns_every_row() {
run_seeded_sync_test(|| {
let users = TestUser::all_sync().expect("all_sync should succeed");
assert_eq!(users.len(), 3);
});
}
#[test]
fn all_sync_returns_empty_vec_when_table_empty() {
run_empty_sync_test(|| {
let users = TestUser::all_sync().expect("all_sync should succeed");
assert!(users.is_empty());
});
}
#[test]
fn all_sync_marks_loaded_rows_persisted() {
run_seeded_sync_test(|| {
let users = TestUser::all_sync().expect("all_sync should succeed");
assert!(
users
.iter()
.all(|user| user.state == RecordState::Persisted)
);
});
}
#[test]
fn first_sync_returns_lowest_primary_key() {
run_seeded_sync_test(|| {
let user = TestUser::first_sync()
.expect("query should succeed")
.expect("row should exist");
assert_eq!(user.name, "Alice");
});
}
#[test]
fn first_sync_returns_none_for_empty_tables() {
run_empty_sync_test(|| {
let user = TestUser::first_sync().expect("query should succeed");
assert!(user.is_none());
});
}
#[test]
fn last_sync_returns_highest_primary_key() {
run_seeded_sync_test(|| {
let user = TestUser::last_sync()
.expect("query should succeed")
.expect("row should exist");
assert_eq!(user.name, "Carol");
});
}
#[test]
fn last_sync_returns_none_for_empty_tables() {
run_empty_sync_test(|| {
let user = TestUser::last_sync().expect("query should succeed");
assert!(user.is_none());
});
}
#[test]
fn count_sync_returns_row_total() {
run_seeded_sync_test(|| {
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
3
);
});
}
#[test]
fn count_sync_returns_zero_when_table_is_empty() {
run_empty_sync_test(|| {
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
0
);
});
}
#[test]
fn exists_sync_returns_true_for_matching_conditions() {
run_seeded_sync_test(|| {
assert!(
TestUser::exists_sync(HashMap::from([("name".to_owned(), json!("Bob"))]))
.expect("exists_sync should succeed")
);
});
}
#[test]
fn exists_sync_returns_false_for_missing_conditions() {
run_seeded_sync_test(|| {
assert!(
!TestUser::exists_sync(HashMap::from([("name".to_owned(), json!("Nobody"))]))
.expect("exists_sync should succeed")
);
});
}
#[test]
fn exists_sync_with_empty_conditions_is_true_when_rows_exist() {
run_seeded_sync_test(|| {
assert!(TestUser::exists_sync(HashMap::new()).expect("exists_sync should succeed"));
});
}
#[test]
fn exists_sync_with_empty_conditions_is_false_without_rows() {
run_empty_sync_test(|| {
assert!(!TestUser::exists_sync(HashMap::new()).expect("exists_sync should succeed"));
});
}
#[test]
fn exists_sync_with_multiple_conditions_requires_all_conditions() {
run_seeded_sync_test(|| {
assert!(
!TestUser::exists_sync(HashMap::from([
("name".to_owned(), json!("Bob")),
("email".to_owned(), json!("alice@example.com")),
]))
.expect("exists_sync should succeed")
);
});
}
#[test]
fn exists_sync_with_null_condition_returns_false_for_non_null_column() {
run_seeded_sync_test(|| {
assert!(
!TestUser::exists_sync(HashMap::from([("email".to_owned(), Value::Null)]))
.expect("exists_sync should succeed")
);
});
}
#[test]
fn where_helper_loads_matching_rows() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(
TestUser::r#where(HashMap::from([("name".to_owned(), json!("Bob"))])).load(db),
)
})
.expect("where relation should load");
assert_eq!(users.len(), 1);
assert_eq!(users[0].email, "bob@example.com");
});
}
#[test]
fn where_helper_with_multiple_conditions_loads_match() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(
TestUser::r#where(HashMap::from([
("name".to_owned(), json!("Carol")),
("email".to_owned(), json!("carol@example.com")),
]))
.load(db),
)
})
.expect("where relation should load");
assert_eq!(users.len(), 1);
assert_eq!(users[0].id, Some(3));
});
}
#[test]
fn where_helper_with_empty_conditions_loads_all_rows() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(TestUser::r#where(HashMap::new()).load(db))
})
.expect("where relation should load");
assert_eq!(users.len(), 3);
});
}
#[test]
fn where_helper_with_null_condition_returns_no_rows() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(
TestUser::r#where(HashMap::from([("email".to_owned(), Value::Null)])).load(db),
)
})
.expect("where relation should load");
assert!(users.is_empty());
});
}
#[test]
fn where_helper_with_negated_null_condition_returns_all_rows() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(
TestUser::r#where(HashMap::new())
.not(HashMap::from([("email".to_owned(), Value::Null)]))
.load(db),
)
})
.expect("where relation should load");
assert_eq!(users.len(), 3);
});
}
#[test]
fn where_helper_with_unknown_column_returns_invalid_error() {
run_seeded_sync_test(|| {
let error = database::with_db(|db| {
runtime::block_on(
TestUser::r#where(HashMap::from([("missing".to_owned(), json!(1))])).load(db),
)
})
.expect_err("unknown columns should fail");
assert!(matches!(error, RecordError::Invalid(_)));
});
}
#[test]
fn order_helper_descending_returns_expected_sequence() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(TestUser::order("id", OrderDirection::Desc).load(db))
})
.expect("ordered relation should load");
let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
assert_eq!(names, vec!["Carol", "Bob", "Alice"]);
});
}
#[test]
fn order_helper_ascending_returns_expected_sequence() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(TestUser::order("id", OrderDirection::Asc).load(db))
})
.expect("ordered relation should load");
let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
assert_eq!(names, vec!["Alice", "Bob", "Carol"]);
});
}
#[test]
fn limit_helper_zero_returns_no_rows() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| runtime::block_on(TestUser::limit(0).load(db)))
.expect("limit should load");
assert!(users.is_empty());
});
}
#[test]
fn limit_helper_can_chain_with_order() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(
TestUser::limit(2)
.order("id", OrderDirection::Desc)
.load(db),
)
})
.expect("relation should load");
let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
assert_eq!(names, vec!["Carol", "Bob"]);
});
}
#[test]
fn offset_helper_beyond_row_count_returns_empty() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(
TestUser::offset(10)
.order("id", OrderDirection::Asc)
.load(db),
)
})
.expect("relation should load");
assert!(users.is_empty());
});
}
#[test]
fn offset_helper_can_chain_with_limit_and_order() {
run_seeded_sync_test(|| {
let users = database::with_db(|db| {
runtime::block_on(
TestUser::offset(1)
.order("id", OrderDirection::Asc)
.limit(1)
.load(db),
)
})
.expect("relation should load");
assert_eq!(users.len(), 1);
assert_eq!(users[0].name, "Bob");
});
}
#[test]
fn sync_query_helpers_can_run_inside_the_same_async_runtime() {
run_seeded_sync_test(|| {
let user = runtime::block_on(async {
TestUser::find_sync(1).expect("find_sync should work inside async context")
});
assert_eq!(user.name, "Alice");
});
}
#[test]
fn querying_trait_is_the_sync_api() {
fn assert_sync_api<T: Querying>() {}
assert_sync_api::<crate::base::test_support::TestUser>();
}
#[test]
fn find_by_bang_sync_returns_matching_record() {
run_seeded_sync_test(|| {
let user = TestUser::find_by_bang_sync(HashMap::from([(
"email".to_owned(),
json!("bob@example.com"),
)]))
.expect("row should exist");
assert_eq!(user.name, "Bob");
});
}
#[test]
fn find_by_bang_sync_returns_not_found_when_missing() {
run_empty_sync_test(|| {
let error = TestUser::find_by_bang_sync(HashMap::from([(
"email".to_owned(),
json!("missing@example.com"),
)]))
.expect_err("missing rows should fail");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn take_sync_returns_first_row_without_order() {
run_seeded_sync_test(|| {
let user = TestUser::take_sync()
.expect("take_sync should succeed")
.expect("row should exist");
assert_eq!(user.id, Some(1));
});
}
#[test]
fn take_bang_sync_returns_not_found_when_empty() {
run_empty_sync_test(|| {
let error = TestUser::take_bang_sync().expect_err("empty tables should fail");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn sole_sync_returns_only_row_when_one_exists() {
run_empty_sync_test(|| {
database::with_db(|db| {
runtime::block_on(async {
test_user::ActiveModel {
name: Set("Solo".to_owned()),
email: Set("solo@example.com".to_owned()),
..Default::default()
}
.insert(db)
.await
.expect("fixture insert should succeed");
})
});
let user = TestUser::sole_sync().expect("sole_sync should succeed");
assert_eq!(user.name, "Solo");
});
}
#[test]
fn sole_sync_returns_exceeded_when_multiple_rows_exist() {
run_seeded_sync_test(|| {
let error = TestUser::sole_sync().expect_err("multiple rows should fail");
assert!(matches!(error, RecordError::SoleRecordExceeded));
});
}
#[test]
fn find_sole_by_sync_returns_matching_row() {
run_seeded_sync_test(|| {
let user =
TestUser::find_sole_by_sync(HashMap::from([("name".to_owned(), json!("Carol"))]))
.expect("single matching row should exist");
assert_eq!(user.email, "carol@example.com");
});
}
#[test]
fn pluck_sync_returns_requested_values() {
run_seeded_sync_test(|| {
let values = TestUser::pluck_sync("name").expect("pluck_sync should succeed");
assert_eq!(values, vec![json!("Alice"), json!("Bob"), json!("Carol")]);
});
}
#[test]
fn pick_sync_returns_first_value() {
run_seeded_sync_test(|| {
let value = TestUser::pick_sync("email").expect("pick_sync should succeed");
assert_eq!(value, Some(json!("alice@example.com")));
});
}
#[test]
fn ids_sync_returns_primary_keys() {
run_seeded_sync_test(|| {
let ids = TestUser::ids_sync().expect("ids_sync should succeed");
assert_eq!(ids, vec![1, 2, 3]);
});
}
#[test]
fn exists_with_conditions_sync_matches_rows() {
run_seeded_sync_test(|| {
assert!(
TestUser::exists_with_conditions_sync(HashMap::from([(
"name".to_owned(),
json!("Alice"),
)]))
.expect("exists_with_conditions_sync should succeed")
);
});
}
#[test]
fn exists_with_conditions_sync_returns_false_when_missing() {
run_empty_sync_test(|| {
assert!(
!TestUser::exists_with_conditions_sync(HashMap::from([(
"name".to_owned(),
json!("Nobody"),
)]))
.expect("exists_with_conditions_sync should succeed")
);
});
}
#[test]
#[ignore = "Rails-specific: Active Record find coerces string ids from params; Rust sync API only accepts i64"]
fn rails_find_with_string_id_is_not_applicable() {}
#[test]
#[ignore = "Rails-specific: Active Record find accepts variadic ids and preserves caller order; Rust sync API only loads one id per call"]
fn rails_find_with_multiple_ids_is_not_applicable() {}
#[test]
#[ignore = "Rails-specific: Active Record exists? overloads ids, nil, and false; Rust sync API only accepts condition maps"]
fn rails_exists_overloads_are_not_applicable() {}
}