use heck::ToSnakeCase;
use crate::parser::naming::rust_identifier;
use crate::schema::{Column, Relationship, RelationshipKind, Table};
use super::find_column;
use super::many_to_many::many_to_many;
use crate::generator::keys;
struct Edge {
child: usize,
parent: usize,
column: String,
parent_column: String,
name: String,
unique: bool,
}
pub(super) fn record(tables: &mut [Table]) {
let edges = edges(tables);
let mut inferred: Vec<Vec<Relationship>> = vec![Vec::new(); tables.len()];
for edge in &edges {
inferred[edge.child].push(Relationship {
name: edge.name.clone(),
kind: RelationshipKind::BelongsTo,
target: tables[edge.parent].module_name.clone(),
column: edge.column.clone(),
target_column: edge.parent_column.clone(),
through: None,
});
}
for edge in &edges {
let child = &tables[edge.child];
let kind = if edge.unique {
RelationshipKind::HasOne
} else {
RelationshipKind::HasMany
};
inferred[edge.parent].push(Relationship {
name: inverse_name(edge, &edges, child, kind),
kind,
target: child.module_name.clone(),
column: edge.parent_column.clone(),
target_column: edge.column.clone(),
through: None,
});
}
for (table, relationships) in tables.iter_mut().zip(inferred) {
table.relationships = relationships;
}
let many_to_many = many_to_many(tables);
for (table, relationships) in tables.iter_mut().zip(many_to_many) {
table.relationships.extend(relationships);
}
}
fn edges(tables: &[Table]) -> Vec<Edge> {
let mut edges = Vec::new();
for (child_index, child) in tables.iter().enumerate() {
for foreign_key in &child.foreign_keys {
if child.tenant_column.as_deref() == Some(foreign_key.column.as_str()) {
continue;
}
let Some(parent) = keys::referenced_table(tables, foreign_key) else {
continue;
};
let parent_index = tables
.iter()
.position(|table| std::ptr::eq(table, parent))
.expect("the referenced table is one of `tables`");
let (Some(column), Some(parent_column)) = (
find_column(child, &foreign_key.column),
find_column(parent, &foreign_key.referenced_column),
) else {
continue;
};
if column.sqlx_type != parent_column.sqlx_type || !column.supports_typed_id() {
continue;
}
edges.push(Edge {
child: child_index,
parent: parent_index,
column: column.sql_name.clone(),
parent_column: parent_column.sql_name.clone(),
name: belongs_to_name(column, parent_column, parent),
unique: is_unique(child, column),
});
}
}
edges
}
fn belongs_to_name(column: &Column, parent_column: &Column, parent: &Table) -> String {
let name = column.sql_name.to_snake_case();
let referenced = parent_column.sql_name.to_snake_case();
if name == referenced {
return parent.struct_name.to_snake_case();
}
let stripped = name
.strip_suffix("_id")
.or_else(|| name.strip_suffix(&format!("_{referenced}")))
.filter(|stripped| !stripped.is_empty());
match stripped {
Some(stripped) => rust_identifier(stripped).unwrap_or_else(|_| stripped.to_owned()),
None => column.rust_name.clone(),
}
}
fn is_unique(table: &Table, column: &Column) -> bool {
table
.unique_key_columns(table.soft_delete_column.as_deref())
.iter()
.any(|key| {
let rest: Vec<&String> = key
.iter()
.filter(|name| table.tenant_column.as_deref() != Some(name.as_str()))
.collect();
rest == [&column.sql_name]
})
}
fn inverse_name(edge: &Edge, edges: &[Edge], child: &Table, kind: RelationshipKind) -> String {
let self_reference = edge.child == edge.parent;
if self_reference && edge.column == "parent_id" {
return if kind.is_to_many() {
"children"
} else {
"child"
}
.to_owned();
}
let base = if kind.is_to_many() {
child.module_name.clone()
} else {
child.struct_name.to_snake_case()
};
let siblings = edges
.iter()
.filter(|other| other.child == edge.child && other.parent == edge.parent)
.count();
if self_reference || siblings > 1 {
format!("{base}_by_{}", edge.name)
} else {
base
}
}