use std::collections::BTreeSet;
use cratestack_core::parse_composite_unique_attribute;
use cratestack_core::{Attribute, Model, parse_composite_id_attribute};
use crate::diagnostics::{SchemaError, span_error};
pub(super) fn validate_composite_id_attribute(
model: &Model,
attribute: &Attribute,
model_names: &BTreeSet<&str>,
) -> Result<(), SchemaError> {
let field_names = parse_composite_id_attribute(&attribute.raw)
.map_err(|message| span_error(message, attribute.span))?;
if let Some(single_id_field) = model
.fields
.iter()
.find(|field| field.attributes.iter().any(|a| a.raw.starts_with("@id")))
{
return Err(span_error(
format!(
"model `{}` declares both a field-level `@id` on `{}` and `@@id([...])`; use exactly one primary key declaration",
model.name, single_id_field.name,
),
attribute.span,
));
}
for field_name in &field_names {
let field = resolve_scalar_field(model, attribute, model_names, field_name, "@@id([...])")?;
if field
.attributes
.iter()
.any(|a| a.raw == "@readonly" || a.raw == "@server_only")
{
return Err(span_error(
format!(
"model `{}` `@@id([...])` field `{}` is part of the primary key and must not declare @readonly or @server_only",
model.name, field_name,
),
attribute.span,
));
}
if field.attributes.iter().any(|a| a.raw == "@version") {
return Err(span_error(
format!(
"model `{}` `@@id([...])` field `{}` must not also be the @version field",
model.name, field_name,
),
attribute.span,
));
}
}
Ok(())
}
pub(super) fn validate_composite_unique_attribute(
model: &Model,
attribute: &Attribute,
model_names: &BTreeSet<&str>,
seen: &mut Vec<Vec<String>>,
) -> Result<(), SchemaError> {
if !attribute.raw.starts_with("@@unique(") {
return Err(span_error(
format!(
"model `{}` `@@unique` requires a field list: `@@unique([field1, field2])`",
model.name,
),
attribute.span,
));
}
let field_names = parse_composite_unique_attribute(&attribute.raw)
.map_err(|message| span_error(message, attribute.span))?;
for field_name in &field_names {
resolve_scalar_field(model, attribute, model_names, field_name, "@@unique([...])")?;
}
if seen.contains(&field_names) {
return Err(span_error(
format!(
"model `{}` declares the same `@@unique([{}])` constraint more than once",
model.name,
field_names.join(", "),
),
attribute.span,
));
}
seen.push(field_names);
Ok(())
}
fn resolve_scalar_field<'model>(
model: &'model Model,
attribute: &Attribute,
model_names: &BTreeSet<&str>,
field_name: &str,
attribute_label: &str,
) -> Result<&'model cratestack_core::Field, SchemaError> {
let field = model
.fields
.iter()
.find(|candidate| candidate.name == field_name)
.ok_or_else(|| {
span_error(
format!(
"model `{}` `{attribute_label}` references unknown field `{field_name}`",
model.name,
),
attribute.span,
)
})?;
if model_names.contains(field.ty.name.as_str()) {
return Err(span_error(
format!(
"model `{}` `{attribute_label}` field `{field_name}` must be a scalar column, not a relation field",
model.name,
),
attribute.span,
));
}
Ok(field)
}