use std::collections::HashMap;
use rstest::rstest;
use serde::{Deserialize, Serialize};
use reinhardt_db::orm::connection::DatabaseBackend;
use reinhardt_db::orm::custom_manager::{CustomManager, HasCustomManager};
use reinhardt_db::orm::manager::Manager;
use reinhardt_db::orm::model::{FieldSelector, Model};
use reinhardt_db::orm::query::{Filter, FilterOperator, FilterValue, QuerySet};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct Article {
id: Option<i64>,
title: String,
is_archived: bool,
}
#[derive(Clone)]
struct ArticleFields;
impl FieldSelector for ArticleFields {
fn with_alias(self, _alias: &str) -> Self {
self
}
}
impl Model for Article {
type PrimaryKey = i64;
type Fields = ArticleFields;
fn table_name() -> &'static str {
"articles"
}
fn new_fields() -> Self::Fields {
ArticleFields
}
fn primary_key(&self) -> Option<Self::PrimaryKey> {
self.id
}
fn set_primary_key(&mut self, value: Self::PrimaryKey) {
self.id = Some(value);
}
}
#[derive(Default)]
struct ActiveArticleManager;
impl CustomManager for ActiveArticleManager {
type Model = Article;
fn new() -> Self {
Self
}
fn all(&self) -> QuerySet<Article> {
Manager::<Article>::new().all().filter(Filter::new(
"is_archived".to_string(),
FilterOperator::Eq,
FilterValue::Boolean(false),
))
}
}
impl HasCustomManager for Article {
type Manager = ActiveArticleManager;
}
#[derive(Default)]
struct GuardedArticleManager;
impl CustomManager for GuardedArticleManager {
type Model = Article;
fn new() -> Self {
Self
}
fn before_save(&self, model: &mut Article) -> reinhardt_core::exception::Result<()> {
if model.title.trim().is_empty() {
return Err(reinhardt_core::exception::Error::Database(
"title must not be empty".into(),
));
}
Ok(())
}
}
fn count_via<M: CustomManager<Model = Article>>(m: &M) -> usize {
m.all().filters().len()
}
#[rstest]
fn blanket_impl_lets_existing_manager_satisfy_custom_manager() {
let manager = Manager::<Article>::new();
let filter_count = count_via(&manager);
assert_eq!(filter_count, 0);
}
#[rstest]
fn user_defined_manager_can_be_passed_through_generic_api() {
let manager = ActiveArticleManager;
let filter_count = count_via(&manager);
assert_eq!(filter_count, 1);
}
#[rstest]
fn blanket_impl_delegates_filter_to_manager_inherent() {
let manager = Manager::<Article>::new();
let qs = CustomManager::filter(
&manager,
"title",
FilterOperator::Eq,
FilterValue::String("rust".into()),
);
assert_eq!(qs.filters().len(), 1);
assert_eq!(qs.filters()[0].field, "title");
}
#[rstest]
fn blanket_impl_get_returns_pk_filtered_queryset() {
let manager = Manager::<Article>::new();
let qs = CustomManager::get(&manager, 7_i64);
assert_eq!(qs.filters().len(), 1);
assert_eq!(qs.filters()[0].field, "id");
}
#[rstest]
fn has_custom_manager_returns_user_supplied_type() {
let manager: ActiveArticleManager = <Article as HasCustomManager>::custom_manager();
assert_eq!(manager.all().filters().len(), 1);
}
#[rstest]
fn has_custom_manager_default_filter_is_applied() {
let manager = Article::custom_manager();
let qs = manager.all();
let filters = qs.filters();
assert_eq!(filters.len(), 1);
assert_eq!(filters[0].field, "is_archived");
let dbg = format!("{:?}", filters[0]);
assert!(dbg.contains("Eq"), "expected Eq operator, got: {dbg}");
assert!(
dbg.contains("Boolean(false)"),
"expected Boolean(false) value, got: {dbg}"
);
}
#[rstest]
fn before_save_default_is_a_noop() {
let manager = Manager::<Article>::new();
let mut article = Article {
id: None,
title: String::new(),
is_archived: false,
};
let result = CustomManager::before_save(&manager, &mut article);
assert!(result.is_ok());
}
#[rstest]
fn custom_before_save_can_veto_with_error() {
let manager = GuardedArticleManager;
let mut article = Article {
id: None,
title: " ".into(),
is_archived: false,
};
let result = manager.before_save(&mut article);
assert!(result.is_err());
}
#[rstest]
fn custom_before_save_passes_for_valid_input() {
let manager = GuardedArticleManager;
let mut article = Article {
id: None,
title: "Custom Managers in Reinhardt".into(),
is_archived: false,
};
let result = manager.before_save(&mut article);
assert!(result.is_ok());
}
#[rstest]
fn before_delete_default_is_a_noop() {
let manager = Manager::<Article>::new();
let article = Article {
id: Some(1),
title: "to delete".into(),
is_archived: false,
};
let result = CustomManager::before_delete(&manager, &article);
assert!(result.is_ok());
}
#[rstest]
fn before_bulk_update_default_is_a_noop_and_keeps_models_unchanged() {
let manager = Manager::<Article>::new();
let mut models = vec![
Article {
id: Some(1),
title: "first".into(),
is_archived: false,
},
Article {
id: Some(2),
title: "second".into(),
is_archived: false,
},
];
let snapshot = models.clone();
let result = CustomManager::before_bulk_update(&manager, &mut models);
assert!(result.is_ok());
assert_eq!(models, snapshot);
}
#[rstest]
fn bulk_create_sql_via_trait_matches_inherent_method() {
let manager = Manager::<Article>::new();
let models = vec![
Article {
id: None,
title: "alpha".into(),
is_archived: false,
},
Article {
id: None,
title: "beta".into(),
is_archived: true,
},
];
let inherent_sql = manager.bulk_create_sql(&models, DatabaseBackend::Postgres);
let trait_sql = CustomManager::bulk_create_sql(&manager, &models, DatabaseBackend::Postgres);
assert_eq!(inherent_sql, trait_sql);
assert!(inherent_sql.contains("INSERT INTO"));
assert!(inherent_sql.contains("articles"));
}
#[rstest]
fn get_or_create_sql_via_trait_matches_inherent_method() {
let manager = Manager::<Article>::new();
let mut lookup = HashMap::new();
lookup.insert("title".into(), "Reinhardt Custom Managers".into());
let defaults = HashMap::new();
let (inherent_select, inherent_insert) =
manager.get_or_create_sql(&lookup, &defaults, DatabaseBackend::Postgres);
let (trait_select, trait_insert) =
CustomManager::get_or_create_sql(&manager, &lookup, &defaults, DatabaseBackend::Postgres);
assert_eq!(inherent_select, trait_select);
assert_eq!(inherent_insert, trait_insert);
}
#[rstest]
#[case::postgres(DatabaseBackend::Postgres)]
#[case::mysql(DatabaseBackend::MySql)]
#[case::sqlite(DatabaseBackend::Sqlite)]
fn bulk_create_sql_parity_across_backends(#[case] backend: DatabaseBackend) {
let manager = Manager::<Article>::new();
let models = vec![Article {
id: None,
title: "parity".into(),
is_archived: false,
}];
let inherent_sql = manager.bulk_create_sql(&models, backend);
let trait_sql = CustomManager::bulk_create_sql(&manager, &models, backend);
assert_eq!(inherent_sql, trait_sql);
}
#[rstest]
fn get_or_create_sql_parity_with_defaults() {
let manager = Manager::<Article>::new();
let mut lookup = HashMap::new();
lookup.insert("title".into(), "Reinhardt Custom Managers".into());
let mut defaults = HashMap::new();
defaults.insert("is_archived".into(), "false".into());
let (inherent_select, inherent_insert) =
manager.get_or_create_sql(&lookup, &defaults, DatabaseBackend::Postgres);
let (trait_select, trait_insert) =
CustomManager::get_or_create_sql(&manager, &lookup, &defaults, DatabaseBackend::Postgres);
assert_eq!(inherent_select, trait_select);
assert_eq!(inherent_insert, trait_insert);
}
#[rstest]
fn delete_queryset_sql_via_trait_matches_inherent_method() {
let manager = Manager::<Article>::new();
let qs = manager.filter(
"is_archived",
FilterOperator::Eq,
FilterValue::Boolean(true),
);
let (inherent_sql, inherent_params) = manager.delete_queryset(&qs);
let (trait_sql, trait_params) = CustomManager::delete_queryset(&manager, &qs);
assert_eq!(inherent_sql, trait_sql);
assert_eq!(inherent_params, trait_params);
}
#[rstest]
fn update_queryset_sql_via_trait_matches_inherent_method() {
let manager = Manager::<Article>::new();
let qs = manager.filter(
"is_archived",
FilterOperator::Eq,
FilterValue::Boolean(false),
);
let updates: &[(&str, &str)] = &[("title", "renamed")];
let (inherent_sql, inherent_params) = manager.update_queryset(&qs, updates);
let (trait_sql, trait_params) = CustomManager::update_queryset(&manager, &qs, updates);
assert_eq!(inherent_sql, trait_sql);
assert_eq!(inherent_params, trait_params);
}
#[rstest]
fn user_defined_manager_inherits_sql_builders_via_default_impl() {
let active = ActiveArticleManager;
let manager = Manager::<Article>::new();
let models = vec![Article {
id: None,
title: "via custom".into(),
is_archived: false,
}];
let custom_sql = active.bulk_create_sql(&models, DatabaseBackend::Postgres);
let canonical_sql = manager.bulk_create_sql(&models, DatabaseBackend::Postgres);
assert_eq!(custom_sql, canonical_sql);
}