use async_trait::async_trait;
use sqlx::Pool;
use sqlx::Postgres;
use std::marker::PhantomData;
use super::metadata::{RelationshipMetadata, RelationshipType};
use crate::error::ModelResult;
use crate::model::Model;
use crate::query::QueryBuilder;
#[derive(Debug, Clone)]
pub struct Relationship<T> {
metadata: RelationshipMetadata,
loaded: bool,
data: Option<T>,
_phantom: PhantomData<T>,
}
impl<T> Relationship<T> {
pub fn new(metadata: RelationshipMetadata) -> Self {
Self {
metadata,
loaded: false,
data: None,
_phantom: PhantomData,
}
}
pub fn metadata(&self) -> &RelationshipMetadata {
&self.metadata
}
pub fn is_loaded(&self) -> bool {
self.loaded
}
pub fn set_loaded(&mut self, data: T) {
self.data = Some(data);
self.loaded = true;
}
pub fn unload(&mut self) {
self.data = None;
self.loaded = false;
}
pub fn get(&self) -> Option<&T> {
self.data.as_ref()
}
pub fn get_mut(&mut self) -> Option<&mut T> {
self.data.as_mut()
}
pub fn take(&mut self) -> Option<T> {
self.loaded = false;
self.data.take()
}
pub fn relationship_type(&self) -> RelationshipType {
self.metadata.relationship_type
}
pub fn name(&self) -> &str {
&self.metadata.name
}
pub fn is_collection(&self) -> bool {
self.metadata.relationship_type.is_collection()
}
pub fn is_polymorphic(&self) -> bool {
self.metadata.relationship_type.is_polymorphic()
}
}
pub type HasOneRelationship<Related> = Relationship<Option<Related>>;
pub type HasManyRelationship<Related> = Relationship<Vec<Related>>;
pub type BelongsToRelationship<Related> = Relationship<Option<Related>>;
pub type ManyToManyRelationship<Related> = Relationship<Vec<Related>>;
pub type MorphOneRelationship<Related> = Relationship<Option<Related>>;
pub type MorphManyRelationship<Related> = Relationship<Vec<Related>>;
pub type MorphToRelationship<Related> = Relationship<Option<Related>>;
#[async_trait]
pub trait RelationshipLoader<Parent, Related>
where
Parent: Model + Send + Sync,
Related: Model + Send + Sync,
{
async fn load_for_instance(&self, parent: &Parent, pool: &Pool<Postgres>) -> ModelResult<()>;
async fn load_for_instances(
&self,
parents: &mut [Parent],
pool: &Pool<Postgres>,
) -> ModelResult<()>;
fn build_query(&self, parent: &Parent) -> QueryBuilder<Related>;
fn get_constraints(&self) -> &[super::metadata::RelationshipConstraint];
}
#[derive(Debug, Clone)]
pub struct RelationshipQueryBuilder<T> {
query: QueryBuilder<T>,
metadata: RelationshipMetadata,
constraints: Vec<super::metadata::RelationshipConstraint>,
}
impl<T> RelationshipQueryBuilder<T>
where
T: Model + Send + Sync,
{
pub fn new(metadata: RelationshipMetadata) -> Self {
let mut query = QueryBuilder::<T>::new();
for constraint in &metadata.constraints {
query = query.where_raw(&format!(
"{} {} '{}'",
constraint.column,
constraint.operator.to_sql(),
constraint.value
));
}
Self {
query,
constraints: metadata.constraints.clone(),
metadata,
}
}
pub fn where_constraint(
mut self,
column: &str,
operator: super::metadata::ConstraintOperator,
value: String,
) -> Self {
self.constraints
.push(super::metadata::RelationshipConstraint {
column: column.to_string(),
operator: operator.clone(),
value: value.clone(),
});
self.query = self
.query
.where_raw(&format!("{} {} '{}'", column, operator.to_sql(), value));
self
}
pub fn query(&self) -> &QueryBuilder<T> {
&self.query
}
pub fn query_mut(&mut self) -> &mut QueryBuilder<T> {
&mut self.query
}
pub fn metadata(&self) -> &RelationshipMetadata {
&self.metadata
}
pub async fn execute(&self, pool: &Pool<Postgres>) -> ModelResult<Vec<T>> {
self.query.clone().get(pool).await
}
pub async fn first(&self, pool: &Pool<Postgres>) -> ModelResult<Option<T>> {
self.query.clone().first(pool).await
}
}
pub trait WithRelationships {
fn relationship_metadata(name: &str) -> Option<&'static RelationshipMetadata>;
fn all_relationship_metadata() -> &'static [RelationshipMetadata];
fn has_relationship(name: &str) -> bool {
Self::relationship_metadata(name).is_some()
}
fn relationship_names() -> Vec<&'static str> {
Self::all_relationship_metadata()
.iter()
.map(|meta| meta.name.as_str())
.collect()
}
fn has_eager_relationships() -> bool {
Self::all_relationship_metadata()
.iter()
.any(|meta| meta.eager_load)
}
fn eager_relationships() -> Vec<&'static RelationshipMetadata> {
Self::all_relationship_metadata()
.iter()
.filter(|meta| meta.eager_load)
.collect()
}
}
pub mod utils {
use super::*;
pub fn inverse_relationship_type(
relationship_type: RelationshipType,
) -> Option<RelationshipType> {
match relationship_type {
RelationshipType::HasOne => Some(RelationshipType::BelongsTo),
RelationshipType::HasMany => Some(RelationshipType::BelongsTo),
RelationshipType::BelongsTo => Some(RelationshipType::HasOne), RelationshipType::ManyToMany => Some(RelationshipType::ManyToMany),
RelationshipType::MorphOne => None,
RelationshipType::MorphMany => None,
RelationshipType::MorphTo => None,
}
}
pub fn default_foreign_key(model_name: &str) -> String {
format!("{}_id", model_name.to_lowercase())
}
pub fn default_pivot_table_name(local_table: &str, foreign_table: &str) -> String {
let mut tables = [local_table, foreign_table];
tables.sort();
tables.join("_")
}
pub fn validate_relationship_configuration(
relationship_type: RelationshipType,
metadata: &RelationshipMetadata,
) -> ModelResult<()> {
match relationship_type {
RelationshipType::ManyToMany => {
if metadata.pivot_config.is_none() {
return Err(crate::error::ModelError::Configuration(
"ManyToMany relationships require pivot configuration".to_string(),
));
}
}
RelationshipType::MorphOne
| RelationshipType::MorphMany
| RelationshipType::MorphTo => {
if metadata.polymorphic_config.is_none() {
return Err(crate::error::ModelError::Configuration(
"Polymorphic relationships require polymorphic configuration".to_string(),
));
}
}
_ => {}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::metadata::*;
use super::*;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct MockModel {
id: Option<i64>,
name: String,
}
impl Model for MockModel {
type PrimaryKey = i64;
fn table_name() -> &'static str {
"mock_models"
}
fn primary_key(&self) -> Option<Self::PrimaryKey> {
self.id
}
fn set_primary_key(&mut self, key: Self::PrimaryKey) {
self.id = Some(key);
}
fn to_fields(&self) -> std::collections::HashMap<String, serde_json::Value> {
let mut fields = std::collections::HashMap::new();
fields.insert("id".to_string(), serde_json::json!(self.id));
fields.insert(
"name".to_string(),
serde_json::Value::String(self.name.clone()),
);
fields
}
fn from_row(row: &sqlx::postgres::PgRow) -> crate::error::ModelResult<Self> {
use sqlx::Row;
Ok(Self {
id: row.try_get("id").ok(),
name: row.try_get("name").unwrap_or_default(),
})
}
}
#[test]
fn test_relationship_creation() {
let metadata = RelationshipMetadata::new(
RelationshipType::HasMany,
"posts".to_string(),
"posts".to_string(),
"Post".to_string(),
ForeignKeyConfig::simple("user_id".to_string(), "posts".to_string()),
);
let relationship: HasManyRelationship<MockModel> = Relationship::new(metadata);
assert!(!relationship.is_loaded());
assert_eq!(relationship.relationship_type(), RelationshipType::HasMany);
assert_eq!(relationship.name(), "posts");
assert!(relationship.is_collection());
assert!(!relationship.is_polymorphic());
}
#[test]
fn test_relationship_loading_state() {
let metadata = RelationshipMetadata::new(
RelationshipType::HasOne,
"profile".to_string(),
"profiles".to_string(),
"Profile".to_string(),
ForeignKeyConfig::simple("user_id".to_string(), "profiles".to_string()),
);
let mut relationship: HasOneRelationship<MockModel> = Relationship::new(metadata);
assert!(!relationship.is_loaded());
assert!(relationship.get().is_none());
let mock_profile = MockModel {
id: Some(1),
name: "Profile".to_string(),
};
relationship.set_loaded(Some(mock_profile));
assert!(relationship.is_loaded());
assert!(relationship.get().is_some());
relationship.unload();
assert!(!relationship.is_loaded());
assert!(relationship.get().is_none());
}
#[test]
fn test_relationship_query_builder() {
let metadata = RelationshipMetadata::new(
RelationshipType::HasMany,
"posts".to_string(),
"posts".to_string(),
"Post".to_string(),
ForeignKeyConfig::simple("user_id".to_string(), "posts".to_string()),
);
let query_builder = RelationshipQueryBuilder::<MockModel>::new(metadata.clone());
assert_eq!(query_builder.metadata().name, "posts");
assert_eq!(
query_builder.metadata().relationship_type,
RelationshipType::HasMany
);
}
#[test]
fn test_relationship_utils() {
use super::utils::*;
assert_eq!(
inverse_relationship_type(RelationshipType::HasOne),
Some(RelationshipType::BelongsTo)
);
assert_eq!(
inverse_relationship_type(RelationshipType::HasMany),
Some(RelationshipType::BelongsTo)
);
assert_eq!(
inverse_relationship_type(RelationshipType::ManyToMany),
Some(RelationshipType::ManyToMany)
);
assert_eq!(default_foreign_key("User"), "user_id");
assert_eq!(default_pivot_table_name("users", "roles"), "roles_users");
assert_eq!(default_pivot_table_name("roles", "users"), "roles_users"); }
#[test]
fn test_relationship_type_properties() {
assert!(RelationshipType::HasMany.is_collection());
assert!(RelationshipType::ManyToMany.is_collection());
assert!(!RelationshipType::HasOne.is_collection());
assert!(!RelationshipType::BelongsTo.is_collection());
assert!(RelationshipType::MorphOne.is_polymorphic());
assert!(RelationshipType::MorphMany.is_polymorphic());
assert!(RelationshipType::MorphTo.is_polymorphic());
assert!(!RelationshipType::HasOne.is_polymorphic());
assert!(RelationshipType::ManyToMany.requires_pivot());
assert!(!RelationshipType::HasMany.requires_pivot());
}
}