use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RelationType {
OneToOne,
OneToMany,
ManyToOne,
ManyToMany,
}
impl RelationType {
pub fn is_many(&self) -> bool {
matches!(self, Self::OneToMany | Self::ManyToMany)
}
pub fn is_one(&self) -> bool {
matches!(self, Self::OneToOne | Self::ManyToOne)
}
}
#[derive(Debug, Clone)]
pub struct RelationSpec {
pub name: String,
pub relation_type: RelationType,
pub related_model: String,
pub related_table: String,
pub fields: Vec<String>,
pub references: Vec<String>,
pub join_table: Option<JoinTableSpec>,
pub on_delete: Option<ReferentialAction>,
pub on_update: Option<ReferentialAction>,
pub map: Option<String>,
}
impl RelationSpec {
pub fn one_to_one(
name: impl Into<String>,
related_model: impl Into<String>,
related_table: impl Into<String>,
) -> Self {
Self {
name: name.into(),
relation_type: RelationType::OneToOne,
related_model: related_model.into(),
related_table: related_table.into(),
fields: Vec::new(),
references: Vec::new(),
join_table: None,
on_delete: None,
on_update: None,
map: None,
}
}
pub fn one_to_many(
name: impl Into<String>,
related_model: impl Into<String>,
related_table: impl Into<String>,
) -> Self {
Self {
name: name.into(),
relation_type: RelationType::OneToMany,
related_model: related_model.into(),
related_table: related_table.into(),
fields: Vec::new(),
references: Vec::new(),
join_table: None,
on_delete: None,
on_update: None,
map: None,
}
}
pub fn many_to_one(
name: impl Into<String>,
related_model: impl Into<String>,
related_table: impl Into<String>,
) -> Self {
Self {
name: name.into(),
relation_type: RelationType::ManyToOne,
related_model: related_model.into(),
related_table: related_table.into(),
fields: Vec::new(),
references: Vec::new(),
join_table: None,
on_delete: None,
on_update: None,
map: None,
}
}
pub fn many_to_many(
name: impl Into<String>,
related_model: impl Into<String>,
related_table: impl Into<String>,
join_table: JoinTableSpec,
) -> Self {
Self {
name: name.into(),
relation_type: RelationType::ManyToMany,
related_model: related_model.into(),
related_table: related_table.into(),
fields: Vec::new(),
references: Vec::new(),
join_table: Some(join_table),
on_delete: None,
on_update: None,
map: None,
}
}
pub fn fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.fields = fields.into_iter().map(Into::into).collect();
self
}
pub fn references(mut self, refs: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.references = refs.into_iter().map(Into::into).collect();
self
}
pub fn on_delete(mut self, action: ReferentialAction) -> Self {
self.on_delete = Some(action);
self
}
pub fn on_update(mut self, action: ReferentialAction) -> Self {
self.on_update = Some(action);
self
}
pub fn map(mut self, name: impl Into<String>) -> Self {
self.map = Some(name.into());
self
}
pub fn to_join_clause(&self, parent_alias: &str, child_alias: &str) -> String {
if let Some(ref jt) = self.join_table {
format!(
"JOIN {} ON {}.{} = {}.{} JOIN {} AS {} ON {}.{} = {}.{}",
jt.table_name,
parent_alias,
self.fields.first().unwrap_or(&"id".to_string()),
jt.table_name,
jt.source_column,
self.related_table,
child_alias,
jt.table_name,
jt.target_column,
child_alias,
self.references.first().unwrap_or(&"id".to_string()),
)
} else {
let join_conditions: Vec<_> = self
.fields
.iter()
.zip(self.references.iter())
.map(|(f, r)| format!("{}.{} = {}.{}", parent_alias, f, child_alias, r))
.collect();
format!(
"JOIN {} AS {} ON {}",
self.related_table,
child_alias,
join_conditions.join(" AND ")
)
}
}
}
#[derive(Debug, Clone)]
pub struct JoinTableSpec {
pub table_name: String,
pub source_column: String,
pub target_column: String,
}
impl JoinTableSpec {
pub fn new(
table_name: impl Into<String>,
source_column: impl Into<String>,
target_column: impl Into<String>,
) -> Self {
Self {
table_name: table_name.into(),
source_column: source_column.into(),
target_column: target_column.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReferentialAction {
Cascade,
SetNull,
SetDefault,
Restrict,
NoAction,
}
impl ReferentialAction {
pub fn as_sql(&self) -> &'static str {
match self {
Self::Cascade => "CASCADE",
Self::SetNull => "SET NULL",
Self::SetDefault => "SET DEFAULT",
Self::Restrict => "RESTRICT",
Self::NoAction => "NO ACTION",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RelationRegistry {
relations: HashMap<String, RelationSpec>,
}
impl RelationRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, spec: RelationSpec) {
self.relations.insert(spec.name.clone(), spec);
}
pub fn get(&self, name: &str) -> Option<&RelationSpec> {
self.relations.get(name)
}
pub fn all(&self) -> impl Iterator<Item = &RelationSpec> {
self.relations.values()
}
pub fn one_to_many(&self) -> impl Iterator<Item = &RelationSpec> {
self.relations
.values()
.filter(|r| r.relation_type == RelationType::OneToMany)
}
pub fn many_to_one(&self) -> impl Iterator<Item = &RelationSpec> {
self.relations
.values()
.filter(|r| r.relation_type == RelationType::ManyToOne)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relation_type() {
assert!(RelationType::OneToMany.is_many());
assert!(RelationType::ManyToMany.is_many());
assert!(!RelationType::OneToOne.is_many());
assert!(RelationType::OneToOne.is_one());
}
#[test]
fn test_relation_spec() {
let spec = RelationSpec::one_to_many("posts", "Post", "posts")
.fields(["id"])
.references(["author_id"]);
assert_eq!(spec.name, "posts");
assert_eq!(spec.relation_type, RelationType::OneToMany);
assert_eq!(spec.fields, vec!["id"]);
assert_eq!(spec.references, vec!["author_id"]);
}
#[test]
fn test_join_table_spec() {
let jt = JoinTableSpec::new("_post_tags", "post_id", "tag_id");
assert_eq!(jt.table_name, "_post_tags");
assert_eq!(jt.source_column, "post_id");
assert_eq!(jt.target_column, "tag_id");
}
#[test]
fn test_referential_action() {
assert_eq!(ReferentialAction::Cascade.as_sql(), "CASCADE");
assert_eq!(ReferentialAction::SetNull.as_sql(), "SET NULL");
}
#[test]
fn test_relation_registry() {
let mut registry = RelationRegistry::new();
registry.register(RelationSpec::one_to_many("posts", "Post", "posts"));
registry.register(RelationSpec::many_to_one("author", "User", "users"));
assert!(registry.get("posts").is_some());
assert!(registry.get("author").is_some());
assert!(registry.get("nonexistent").is_none());
}
}