use super::*;
impl SchemaValidator {
pub(super) fn validate_relations(&mut self) {
let models: Vec<_> = self.schema.models().cloned().collect();
for model in &models {
self.validate_model_relations(model);
}
}
pub(super) fn validate_model_relations(&mut self, model: &ModelDecl) {
let mut relations_to_models: HashMap<String, Vec<Option<String>>> = HashMap::new();
for field in &model.fields {
for attr in &field.attributes {
let FieldAttribute::Relation {
name,
fields,
references,
on_delete,
on_update,
..
} = attr
else {
continue;
};
let target_model = match &field.field_type {
FieldType::UserType(name) => name,
_ => {
self.errors.push_back(SchemaError::Validation(
format!(
"Relation field '{}' must have a model type, not a scalar or enum",
field.name.value
),
field.span,
));
continue;
}
};
if !self.models.contains_key(target_model) {
self.errors.push_back(SchemaError::Validation(
format!(
"Relation field '{}' references unknown model '{}'",
field.name.value, target_model
),
field.span,
));
continue;
}
relations_to_models
.entry(target_model.clone())
.or_default()
.push(name.clone());
if field.modifier == FieldModifier::Array {
if fields.is_some() || references.is_some() {
self.errors.push_back(SchemaError::Validation(
format!(
"Relation field '{}' is an array (back-reference) and must not specify 'fields' or 'references'. Move them to the opposite field on model '{}'",
field.name.value, target_model
),
field.span,
));
}
if on_delete.is_some() || on_update.is_some() {
self.errors.push_back(SchemaError::Validation(
format!(
"Relation field '{}' is an array (back-reference) and must not specify 'onDelete' or 'onUpdate'. Move them to the opposite field on model '{}'",
field.name.value, target_model
),
field.span,
));
}
continue;
}
let (Some(fk_fields), Some(ref_fields)) = (fields, references) else {
continue;
};
if fk_fields.len() != ref_fields.len() {
self.errors.push_back(SchemaError::Validation(
format!(
"Relation '{}' has {} fields but {} references (must be equal)",
field.name.value,
fk_fields.len(),
ref_fields.len()
),
field.span,
));
continue;
}
for fk_field in fk_fields {
if model.find_field(&fk_field.value).is_some() {
continue;
}
self.errors.push_back(SchemaError::Validation(
format!(
"Relation field '{}' references non-existent field '{}' in model '{}'",
field.name.value, fk_field.value, model.name.value
),
fk_field.span,
));
}
let Some(target_model_decl) = self
.schema
.models()
.find(|candidate| candidate.name.value == *target_model)
.cloned()
else {
continue;
};
let mut all_reference_fields_exist = true;
for ref_field in ref_fields {
if target_model_decl.find_field(&ref_field.value).is_some() {
continue;
}
all_reference_fields_exist = false;
self.errors.push_back(SchemaError::Validation(
format!(
"Relation references non-existent field '{}' in model '{}'",
ref_field.value, target_model
),
ref_field.span,
));
}
if !all_reference_fields_exist {
continue;
}
self.validate_referenced_fields_are_unique(
&target_model_decl,
ref_fields,
&field.name.value,
field.span,
);
}
}
for (target_model, relation_names) in relations_to_models {
if relation_names.len() > 1 {
let named_count = relation_names.iter().filter(|n| n.is_some()).count();
if named_count < relation_names.len() {
self.errors.push_back(SchemaError::Validation(
format!(
"Model '{}' has multiple relations to '{}' but not all have unique 'name' parameters",
model.name.value, target_model
),
model.span,
));
} else {
let mut seen_names = HashSet::new();
for n in relation_names.into_iter().flatten() {
if !seen_names.insert(n.clone()) {
self.errors.push_back(SchemaError::Validation(
format!(
"Duplicate relation name '{}' in model '{}' to model '{}'",
n, model.name.value, target_model
),
model.span,
));
}
}
}
}
}
}
pub(super) fn validate_back_relations(&mut self) {
let models: Vec<_> = self.schema.models().cloned().collect();
for model in &models {
let mut used_back_fields: HashMap<(String, String), String> = HashMap::new();
for field in &model.fields {
if field.modifier == FieldModifier::Array {
continue;
}
for attr in &field.attributes {
let FieldAttribute::Relation {
name,
fields,
references,
..
} = attr
else {
continue;
};
let (Some(_), Some(_)) = (fields, references) else {
continue;
};
let FieldType::UserType(target_model_name) = &field.field_type else {
continue;
};
let Some(target_model_decl) = self
.schema
.models()
.find(|candidate| candidate.name.value == *target_model_name)
.cloned()
else {
continue;
};
let back_relation_candidates: Vec<&FieldDecl> = target_model_decl
.fields
.iter()
.filter(|candidate| {
Self::is_back_relation_candidate(candidate, &model.name.value)
})
.collect();
let matching_back_relations =
Self::matching_back_relations(&back_relation_candidates, name.as_deref());
if matching_back_relations.is_empty() {
let message = if back_relation_candidates.is_empty() {
Self::missing_back_relation_message(
field,
&model.name.value,
target_model_name,
name.as_deref(),
)
} else if let Some(relation_name) = name.as_deref() {
format!(
"Relation field '{}' on model '{}' expects an opposite relation field on model '{}' with @relation(name: \"{}\").",
field.name.value, model.name.value, target_model_name, relation_name
)
} else {
let candidate_names = back_relation_candidates
.iter()
.map(|candidate| format!("'{}'", candidate.name.value))
.collect::<Vec<_>>()
.join(", ");
format!(
"Relation field '{}' on model '{}' has multiple possible opposite relation fields on model '{}': {}. Use matching @relation(name: ...) to disambiguate.",
field.name.value, model.name.value, target_model_name, candidate_names
)
};
self.errors
.push_back(SchemaError::Validation(message, field.span));
continue;
}
if matching_back_relations.len() > 1 {
let candidate_names = matching_back_relations
.iter()
.map(|candidate| format!("'{}'", candidate.name.value))
.collect::<Vec<_>>()
.join(", ");
self.errors.push_back(SchemaError::Validation(
format!(
"Relation field '{}' on model '{}' has multiple possible opposite relation fields on model '{}': {}. Use matching @relation(name: ...) to disambiguate.",
field.name.value, model.name.value, target_model_name, candidate_names
),
field.span,
));
continue;
}
let back_field = matching_back_relations[0];
let back_field_key = (target_model_name.clone(), back_field.name.value.clone());
if let Some(existing_field) =
used_back_fields.insert(back_field_key, field.name.value.clone())
{
self.errors.push_back(SchemaError::Validation(
format!(
"Opposite relation field '{}' on model '{}' is already paired with relation field '{}' on model '{}', so it cannot also satisfy relation field '{}'. Add a distinct opposite relation field.",
back_field.name.value,
target_model_name,
existing_field,
model.name.value,
field.name.value,
),
field.span,
));
}
}
}
}
}
pub(super) fn is_back_relation_candidate(field: &FieldDecl, current_model_name: &str) -> bool {
matches!(&field.field_type, FieldType::UserType(name) if name == current_model_name)
&& !Self::relation_is_owning_side(field)
}
pub(super) fn relation_is_owning_side(field: &FieldDecl) -> bool {
field.attributes.iter().any(|attr| {
matches!(
attr,
FieldAttribute::Relation {
fields,
references,
..
} if fields.is_some() || references.is_some()
)
})
}
pub(super) fn relation_name(field: &FieldDecl) -> Option<&str> {
field.attributes.iter().find_map(|attr| match attr {
FieldAttribute::Relation { name, .. } => name.as_deref(),
_ => None,
})
}
pub(super) fn matching_back_relations<'a>(
candidates: &[&'a FieldDecl],
relation_name: Option<&str>,
) -> Vec<&'a FieldDecl> {
if let Some(relation_name) = relation_name {
let named_matches: Vec<&FieldDecl> = candidates
.iter()
.copied()
.filter(|candidate| Self::relation_name(candidate) == Some(relation_name))
.collect();
if !named_matches.is_empty() {
return named_matches;
}
if candidates.len() == 1 && Self::relation_name(candidates[0]).is_none() {
return vec![candidates[0]];
}
return Vec::new();
}
let unnamed_matches: Vec<&FieldDecl> = candidates
.iter()
.copied()
.filter(|candidate| Self::relation_name(candidate).is_none())
.collect();
if !unnamed_matches.is_empty() {
return unnamed_matches;
}
if candidates.len() == 1 {
return vec![candidates[0]];
}
Vec::new()
}
pub(super) fn missing_back_relation_message(
field: &FieldDecl,
model_name: &str,
target_model_name: &str,
relation_name: Option<&str>,
) -> String {
if let Some(relation_name) = relation_name {
format!(
"Relation field '{}' on model '{}' is missing an opposite relation field on model '{}'. Add a '{}?' (one-to-one) or '{}[]' (one-to-many) field to model '{}' with @relation(name: \"{}\").",
field.name.value,
model_name,
target_model_name,
model_name,
model_name,
target_model_name,
relation_name,
)
} else {
format!(
"Relation field '{}' on model '{}' is missing an opposite relation field on model '{}'. Add a '{}?' (one-to-one) or '{}[]' (one-to-many) field to model '{}'.",
field.name.value,
model_name,
target_model_name,
model_name,
model_name,
target_model_name,
)
}
}
pub(super) fn validate_referenced_field_is_unique(
&mut self,
target_model: &ModelDecl,
field_name: &str,
span: Span,
relation_name: &str,
) {
let is_pk = target_model.find_field(field_name).is_some_and(|field| {
field
.attributes
.iter()
.any(|attr| matches!(attr, FieldAttribute::Id))
}) || target_model.attributes.iter().any(|attr| {
matches!(attr, ModelAttribute::Id(fields) if fields.len() == 1 && fields[0].value == field_name)
});
if is_pk {
return;
}
let is_unique = target_model.find_field(field_name).is_some_and(|field| {
field
.attributes
.iter()
.any(|attr| matches!(attr, FieldAttribute::Unique))
}) || target_model.attributes.iter().any(|attr| {
matches!(attr, ModelAttribute::Unique(fields) if fields.len() == 1 && fields[0].value == field_name)
});
if is_unique {
return;
}
self.errors.push_back(SchemaError::Validation(
format!(
"Relation '{}' references field '{}' in model '{}', but it is not a primary key or unique field",
relation_name, field_name, target_model.name.value
),
span,
));
}
pub(super) fn validate_referenced_fields_are_unique(
&mut self,
target_model: &ModelDecl,
ref_fields: &[Ident],
relation_name: &str,
relation_span: Span,
) {
if ref_fields.len() == 1 {
let ref_field = &ref_fields[0];
self.validate_referenced_field_is_unique(
target_model,
&ref_field.value,
ref_field.span,
relation_name,
);
return;
}
let ref_names: Vec<&str> = ref_fields
.iter()
.map(|field| field.value.as_str())
.collect();
let matches_composite_pk = target_model.attributes.iter().any(|attr| match attr {
ModelAttribute::Id(fields) => {
fields.len() == ref_names.len()
&& fields
.iter()
.map(|field| field.value.as_str())
.eq(ref_names.iter().copied())
}
_ => false,
});
let matches_composite_unique = target_model.attributes.iter().any(|attr| match attr {
ModelAttribute::Unique(fields) => {
fields.len() == ref_names.len()
&& fields
.iter()
.map(|field| field.value.as_str())
.eq(ref_names.iter().copied())
}
_ => false,
});
if !matches_composite_pk && !matches_composite_unique {
self.errors.push_back(SchemaError::Validation(
format!(
"Relation '{}' references fields [{}] in model '{}', but they do not match a composite primary key or unique constraint",
relation_name,
ref_names.join(", "),
target_model.name.value
),
relation_span,
));
}
}
}