use std::collections::HashMap;
use std::future::Future;
use reinhardt_query::{InsertStatement, SelectStatement};
use super::annotation::Annotation;
use super::composite_pk::PkValue;
use super::connection::{DatabaseBackend, DatabaseConnection};
use super::cte::CTE;
use super::manager::Manager;
use super::model::Model;
use super::query::{FilterCondition, QuerySet};
pub trait CustomManager: Sized + Send + Sync {
type Model: Model;
fn new() -> Self;
fn all(&self) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().all()
}
fn filter(&self, filter: impl Into<FilterCondition>) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().filter(filter)
}
fn get(&self, pk: <Self::Model as Model>::PrimaryKey) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().get(pk)
}
fn limit(&self, limit: usize) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().limit(limit)
}
fn order_by(&self, fields: &[&str]) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().order_by(fields)
}
fn annotate(&self, annotation: Annotation) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().annotate(annotation)
}
fn defer(&self, fields: &[&str]) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().defer(fields)
}
fn only(&self, fields: &[&str]) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().only(fields)
}
fn values(&self, fields: &[&str]) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().values(fields)
}
fn select_related(&self, fields: &[&str]) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().select_related(fields)
}
fn offset(&self, offset: usize) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().offset(offset)
}
fn paginate(&self, page: usize, page_size: usize) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().paginate(page, page_size)
}
fn prefetch_related(&self, fields: &[&str]) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().prefetch_related(fields)
}
fn values_list(&self, fields: &[&str]) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().values_list(fields)
}
fn filter_array_overlap(&self, field: &str, values: &[&str]) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().filter_array_overlap(field, values)
}
fn filter_array_contains(&self, field: &str, values: &[&str]) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().filter_array_contains(field, values)
}
fn filter_jsonb_contains(&self, field: &str, json: &str) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().filter_jsonb_contains(field, json)
}
fn filter_jsonb_key_exists(&self, field: &str, key: &str) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().filter_jsonb_key_exists(field, key)
}
fn filter_range_contains(&self, field: &str, value: &str) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().filter_range_contains(field, value)
}
fn filter_in_subquery<R: Model, F>(&self, field: &str, subquery_fn: F) -> QuerySet<Self::Model>
where
F: FnOnce(QuerySet<R>) -> QuerySet<R>,
{
Manager::<Self::Model>::new().filter_in_subquery(field, subquery_fn)
}
fn filter_not_in_subquery<R: Model, F>(
&self,
field: &str,
subquery_fn: F,
) -> QuerySet<Self::Model>
where
F: FnOnce(QuerySet<R>) -> QuerySet<R>,
{
Manager::<Self::Model>::new().filter_not_in_subquery(field, subquery_fn)
}
fn filter_exists<R: Model, F>(&self, subquery_fn: F) -> QuerySet<Self::Model>
where
F: FnOnce(QuerySet<R>) -> QuerySet<R>,
{
Manager::<Self::Model>::new().filter_exists(subquery_fn)
}
fn filter_not_exists<R: Model, F>(&self, subquery_fn: F) -> QuerySet<Self::Model>
where
F: FnOnce(QuerySet<R>) -> QuerySet<R>,
{
Manager::<Self::Model>::new().filter_not_exists(subquery_fn)
}
fn with_cte(&self, cte: CTE) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().with_cte(cte)
}
fn full_text_search(&self, field: &str, query: &str) -> QuerySet<Self::Model> {
Manager::<Self::Model>::new().full_text_search(field, query)
}
fn annotate_subquery<R, F>(&self, name: &str, builder: F) -> QuerySet<Self::Model>
where
R: Model + 'static,
F: FnOnce(QuerySet<R>) -> QuerySet<R>,
{
Manager::<Self::Model>::new().annotate_subquery(name, builder)
}
fn get_composite<'a>(
&'a self,
pk_values: &'a HashMap<String, PkValue>,
) -> impl Future<Output = reinhardt_core::exception::Result<Self::Model>> + Send + 'a
where
Self::Model: Clone + serde::de::DeserializeOwned,
{
async move { Manager::<Self::Model>::new().get_composite(pk_values).await }
}
fn create<'a>(
&'a self,
model: &'a Self::Model,
) -> impl Future<Output = reinhardt_core::exception::Result<Self::Model>> + Send + 'a {
async move {
let mut model = model.clone();
self.before_save(&mut model)?;
Manager::<Self::Model>::new().create(&model).await
}
}
fn create_with_conn<'a>(
&'a self,
conn: &'a DatabaseConnection,
model: &'a Self::Model,
) -> impl Future<Output = reinhardt_core::exception::Result<Self::Model>> + Send + 'a {
async move {
let mut model = model.clone();
self.before_save(&mut model)?;
Manager::<Self::Model>::new()
.create_with_conn(conn, &model)
.await
}
}
fn update<'a>(
&'a self,
model: &'a Self::Model,
) -> impl Future<Output = reinhardt_core::exception::Result<Self::Model>> + Send + 'a {
async move {
let mut model = model.clone();
self.before_save(&mut model)?;
Manager::<Self::Model>::new().update(&model).await
}
}
fn update_with_conn<'a>(
&'a self,
conn: &'a DatabaseConnection,
model: &'a Self::Model,
) -> impl Future<Output = reinhardt_core::exception::Result<Self::Model>> + Send + 'a {
async move {
let mut model = model.clone();
self.before_save(&mut model)?;
Manager::<Self::Model>::new()
.update_with_conn(conn, &model)
.await
}
}
fn delete<'a>(
&'a self,
pk: <Self::Model as Model>::PrimaryKey,
) -> impl Future<Output = reinhardt_core::exception::Result<()>> + Send + 'a {
async move {
let conn = super::manager::get_connection().await?;
self.delete_with_conn(&conn, pk).await
}
}
fn delete_with_conn<'a>(
&'a self,
conn: &'a DatabaseConnection,
pk: <Self::Model as Model>::PrimaryKey,
) -> impl Future<Output = reinhardt_core::exception::Result<()>> + Send + 'a {
async move {
let manager = Manager::<Self::Model>::new();
if let Some(model) = manager.get(pk.clone()).first_with_db(conn).await? {
self.before_delete(&model)?;
}
manager.delete_with_conn(conn, pk).await
}
}
fn count<'a>(
&'a self,
) -> impl Future<Output = reinhardt_core::exception::Result<i64>> + Send + 'a {
async move { Manager::<Self::Model>::new().count().await }
}
fn count_with_conn<'a>(
&'a self,
conn: &'a DatabaseConnection,
) -> impl Future<Output = reinhardt_core::exception::Result<i64>> + Send + 'a {
async move { Manager::<Self::Model>::new().count_with_conn(conn).await }
}
fn get_or_create<'a>(
&'a self,
lookup_fields: HashMap<String, String>,
defaults: Option<HashMap<String, String>>,
) -> impl Future<Output = reinhardt_core::exception::Result<(Self::Model, bool)>> + Send + 'a {
async move {
Manager::<Self::Model>::new()
.get_or_create(lookup_fields, defaults)
.await
}
}
fn bulk_create<'a>(
&'a self,
models: Vec<Self::Model>,
batch_size: Option<usize>,
ignore_conflicts: bool,
update_conflicts: bool,
) -> impl Future<Output = reinhardt_core::exception::Result<Vec<Self::Model>>> + Send + 'a
where
Self::Model: 'a,
{
async move {
Manager::<Self::Model>::new()
.bulk_create(models, batch_size, ignore_conflicts, update_conflicts)
.await
}
}
fn bulk_update<'a>(
&'a self,
models: Vec<Self::Model>,
fields: Vec<String>,
batch_size: Option<usize>,
) -> impl Future<Output = reinhardt_core::exception::Result<usize>> + Send + 'a
where
Self::Model: 'a,
{
async move {
if models.is_empty() || fields.is_empty() {
return Ok(0);
}
let mut models = models;
self.before_bulk_update(&mut models)?;
Manager::<Self::Model>::new()
.bulk_update(models, fields, batch_size)
.await
}
}
fn bulk_create_query(&self, models: &[Self::Model]) -> Option<InsertStatement> {
Manager::<Self::Model>::new().bulk_create_query(models)
}
fn bulk_create_sql(&self, models: &[Self::Model], backend: DatabaseBackend) -> String {
Manager::<Self::Model>::new().bulk_create_sql(models, backend)
}
fn update_queryset(
&self,
queryset: &QuerySet<Self::Model>,
updates: &[(&str, &str)],
) -> (String, Vec<String>) {
Manager::<Self::Model>::new().update_queryset(queryset, updates)
}
fn delete_queryset(&self, queryset: &QuerySet<Self::Model>) -> (String, Vec<String>) {
Manager::<Self::Model>::new().delete_queryset(queryset)
}
fn get_or_create_queries(
&self,
lookup_fields: &HashMap<String, String>,
defaults: &HashMap<String, String>,
) -> (SelectStatement, InsertStatement) {
Manager::<Self::Model>::new().get_or_create_queries(lookup_fields, defaults)
}
fn get_or_create_sql(
&self,
lookup_fields: &HashMap<String, String>,
defaults: &HashMap<String, String>,
backend: DatabaseBackend,
) -> (String, String) {
Manager::<Self::Model>::new().get_or_create_sql(lookup_fields, defaults, backend)
}
fn bulk_create_sql_detailed(
&self,
field_names: &[String],
value_rows: &[Vec<serde_json::Value>],
ignore_conflicts: bool,
) -> String {
Manager::<Self::Model>::new().bulk_create_sql_detailed(
field_names,
value_rows,
ignore_conflicts,
)
}
#[allow(clippy::type_complexity)]
fn bulk_update_sql_detailed(
&self,
updates: &[(
<Self::Model as Model>::PrimaryKey,
HashMap<String, serde_json::Value>,
)],
fields: &[String],
backend: DatabaseBackend,
) -> String
where
<Self::Model as Model>::PrimaryKey: std::fmt::Display + Clone,
{
Manager::<Self::Model>::new().bulk_update_sql_detailed(updates, fields, backend)
}
fn before_save(&self, _model: &mut Self::Model) -> reinhardt_core::exception::Result<()> {
Ok(())
}
fn before_delete(&self, _model: &Self::Model) -> reinhardt_core::exception::Result<()> {
Ok(())
}
fn before_bulk_update(
&self,
_models: &mut [Self::Model],
) -> reinhardt_core::exception::Result<()> {
Ok(())
}
}
impl<M: Model> CustomManager for Manager<M> {
type Model = M;
fn new() -> Self {
Manager::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::orm::fields::{CharField, Field};
use crate::orm::inspection::FieldInfo;
use crate::orm::model::FieldSelector;
use crate::orm::query::{Filter, FilterOperator, FilterValue};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
struct Article {
id: Option<i64>,
title: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ArticleFields;
impl FieldSelector for ArticleFields {
fn with_alias(self, _alias: &str) -> Self {
self
}
}
impl Model for Article {
type PrimaryKey = i64;
type Fields = ArticleFields;
type Objects = ArticleManager;
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);
}
fn field_metadata() -> Vec<FieldInfo> {
let mut id = CharField::new(20);
id.set_attributes_from_name("id");
let mut title = CharField::new(255);
title.set_attributes_from_name("title");
vec![FieldInfo::from_field(&id), FieldInfo::from_field(&title)]
}
}
#[derive(Default)]
struct ArticleManager;
impl CustomManager for ArticleManager {
type Model = Article;
fn new() -> Self {
Self
}
}
#[derive(Default)]
struct VetoArticleManager;
impl CustomManager for VetoArticleManager {
type Model = Article;
fn new() -> Self {
Self
}
fn before_save(&self, _model: &mut Article) -> reinhardt_core::exception::Result<()> {
Err(reinhardt_core::exception::Error::Database(
"save vetoed".to_string(),
))
}
fn before_delete(&self, _model: &Article) -> reinhardt_core::exception::Result<()> {
Err(reinhardt_core::exception::Error::Database(
"delete vetoed".to_string(),
))
}
fn before_bulk_update(
&self,
_models: &mut [Article],
) -> reinhardt_core::exception::Result<()> {
Err(reinhardt_core::exception::Error::Database(
"bulk update vetoed".to_string(),
))
}
}
#[test]
fn custom_manager_get_preserves_the_primary_key_filter() {
let query = ArticleManager::new().get(42);
assert_eq!(query.filters().len(), 1);
assert!(matches!(query.filters()[0].value, FilterValue::Integer(42)));
}
#[test]
fn custom_manager_builder_delegation_preserves_query_output() {
let manager = ArticleManager::new();
let filter = manager.filter(Filter::new(
"title",
FilterOperator::Eq,
FilterValue::String("Rust".to_string()),
));
assert_eq!(manager.all().to_sql(), "SELECT * FROM \"articles\"");
assert_eq!(filter.filters().len(), 1);
assert_eq!(
filter.to_sql(),
"SELECT * FROM \"articles\" WHERE \"title\" = 'Rust'"
);
assert_eq!(
manager.limit(3).to_sql(),
"SELECT * FROM \"articles\" LIMIT 3"
);
assert_eq!(
manager.offset(2).to_sql(),
"SELECT * FROM \"articles\" OFFSET 2"
);
assert_eq!(
manager.paginate(2, 5).to_sql(),
"SELECT * FROM \"articles\" LIMIT 5 OFFSET 5"
);
assert_eq!(
manager.order_by(&["-title", "id"]).to_sql(),
"SELECT * FROM \"articles\" ORDER BY \"title\" DESC, \"id\" ASC"
);
assert_eq!(
manager.defer(&["title"]).to_sql(),
"SELECT \"id\" FROM \"articles\""
);
assert_eq!(
manager.only(&["id", "title"]).to_sql(),
"SELECT \"id\", \"title\" FROM \"articles\""
);
assert_eq!(
manager.values(&["title"]).to_sql(),
"SELECT \"title\" FROM \"articles\""
);
assert_eq!(
manager.values_list(&["id", "title"]).to_sql(),
"SELECT \"id\", \"title\" FROM \"articles\""
);
assert_eq!(
manager
.filter_array_overlap("tags", &["rust", "orm"])
.to_sql(),
"SELECT * FROM \"articles\" WHERE \"tags\" && ARRAY['rust', 'orm']"
);
assert_eq!(
manager
.filter_array_contains("tags", &["rust", "orm"])
.to_sql(),
"SELECT * FROM \"articles\" WHERE \"tags\" @> ARRAY['rust', 'orm']"
);
assert_eq!(
manager
.filter_jsonb_contains("metadata", r#"{"published":true}"#)
.to_sql(),
"SELECT * FROM \"articles\" WHERE \"metadata\" @> '{\"published\":true}'::jsonb"
);
assert_eq!(
manager
.filter_jsonb_key_exists("metadata", "published")
.to_sql(),
"SELECT * FROM \"articles\" WHERE \"metadata\" ? 'published'"
);
assert_eq!(
manager
.filter_range_contains("published_range", "2026-08-06")
.to_sql(),
"SELECT * FROM \"articles\" WHERE \"published_range\" @> '2026-08-06'"
);
assert_eq!(
manager
.filter_in_subquery::<Article, _>("id", |query| query.only(&["id"]))
.to_sql(),
"SELECT * FROM \"articles\" WHERE \"id\" IN (SELECT \"id\" FROM \"articles\")"
);
assert_eq!(
manager
.filter_not_in_subquery::<Article, _>("id", |query| query.only(&["id"]))
.to_sql(),
"SELECT * FROM \"articles\" WHERE \"id\" NOT IN (SELECT \"id\" FROM \"articles\")"
);
assert_eq!(
manager
.filter_exists::<Article, _>(|query| query.filter(Filter::new(
"title",
FilterOperator::Eq,
FilterValue::String("Rust".to_string()),
)))
.to_sql(),
"SELECT * FROM \"articles\" WHERE EXISTS (SELECT * FROM \"articles\" WHERE \"title\" = 'Rust')"
);
assert_eq!(
manager
.filter_not_exists::<Article, _>(|query| query.filter(Filter::new(
"title",
FilterOperator::Eq,
FilterValue::String("Rust".to_string()),
)))
.to_sql(),
"SELECT * FROM \"articles\" WHERE NOT EXISTS (SELECT * FROM \"articles\" WHERE \"title\" = 'Rust')"
);
assert_eq!(
manager
.with_cte(CTE::new("published_articles", "SELECT id FROM articles"))
.to_sql(),
"WITH published_articles AS (SELECT id FROM articles) SELECT * FROM \"articles\""
);
assert_eq!(
manager.full_text_search("title", "rust orm").to_sql(),
"SELECT * FROM \"articles\" WHERE \"title\" @@ plainto_tsquery('english', 'rust orm')"
);
}
#[test]
fn custom_manager_sql_utilities_preserve_complete_statements() {
let manager = ArticleManager::new();
let queryset = manager.filter(Filter::new(
"title",
FilterOperator::Eq,
FilterValue::String("Rust".to_string()),
));
let mut lookup_fields = HashMap::new();
lookup_fields.insert("title".to_string(), "Rust".to_string());
let mut defaults = HashMap::new();
defaults.insert("title".to_string(), "Rust ORM".to_string());
assert_eq!(
manager.update_queryset(&queryset, &[("title", "Rust ORM")]),
(
"UPDATE \"articles\" SET \"title\" = $1 WHERE \"title\" = $2".to_string(),
vec!["Rust ORM".to_string(), "Rust".to_string()],
)
);
assert_eq!(
manager.delete_queryset(&queryset),
(
"DELETE FROM \"articles\" WHERE \"title\" = $1".to_string(),
vec!["Rust".to_string()],
)
);
assert_eq!(
manager.get_or_create_sql(&lookup_fields, &defaults, DatabaseBackend::Sqlite),
(
"SELECT * FROM \"articles\" WHERE \"title\" = ?".to_string(),
"INSERT INTO \"articles\" (\"title\") VALUES (?)".to_string(),
)
);
}
#[test]
fn custom_manager_default_hooks_are_noops() {
let manager = ArticleManager::new();
let mut article = Article {
id: Some(1),
title: "unchanged".to_string(),
};
assert_eq!(Article::new_fields().with_alias("articles"), ArticleFields);
assert_eq!(article.primary_key(), Some(1));
article.set_primary_key(2);
assert_eq!(article.primary_key(), Some(2));
assert!(manager.before_save(&mut article).is_ok());
assert!(manager.before_delete(&article).is_ok());
assert!(
manager
.before_bulk_update(std::slice::from_mut(&mut article))
.is_ok()
);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn custom_manager_vetoes_create_before_writing_to_sqlite() {
let connection = DatabaseConnection::connect_sqlite("sqlite::memory:")
.await
.expect("in-memory SQLite connection should be available");
connection
.execute(
"CREATE TABLE articles (id INTEGER PRIMARY KEY, title TEXT NOT NULL)",
Vec::new(),
)
.await
.expect("articles table should be created");
let article = Article {
id: None,
title: "blocked".to_string(),
};
let result = VetoArticleManager::new()
.create_with_conn(&connection, &article)
.await;
assert!(matches!(
result,
Err(reinhardt_core::exception::Error::Database(message)) if message == "save vetoed"
));
let row = connection
.query_one("SELECT COUNT(*) AS count FROM articles", Vec::new())
.await
.expect("article count should be queryable");
assert_eq!(row.get::<i64>("count"), Some(0));
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn custom_manager_vetoes_delete_without_removing_the_sqlite_row() {
let connection = DatabaseConnection::connect_sqlite("sqlite::memory:")
.await
.expect("in-memory SQLite connection should be available");
connection
.execute(
"CREATE TABLE articles (id INTEGER PRIMARY KEY, title TEXT NOT NULL)",
Vec::new(),
)
.await
.expect("articles table should be created");
connection
.execute(
"INSERT INTO articles (id, title) VALUES (1, 'retained')",
Vec::new(),
)
.await
.expect("article should be inserted");
let result = VetoArticleManager::new()
.delete_with_conn(&connection, 1)
.await;
assert!(matches!(
result,
Err(reinhardt_core::exception::Error::Database(message)) if message == "delete vetoed"
));
let row = connection
.query_one("SELECT COUNT(*) AS count FROM articles", Vec::new())
.await
.expect("article count should be queryable");
assert_eq!(row.get::<i64>("count"), Some(1));
}
#[tokio::test]
async fn custom_manager_vetoes_bulk_update_before_global_database_lookup() {
let articles = vec![Article {
id: Some(1),
title: "blocked".to_string(),
}];
let result = VetoArticleManager::new()
.bulk_update(articles, vec!["title".to_string()], None)
.await;
assert!(matches!(
result,
Err(reinhardt_core::exception::Error::Database(message)) if message == "bulk update vetoed"
));
}
}