Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SQL Server: Detect referential action cycles #2113

Merged
merged 4 commits into from Aug 2, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/datamodel/connectors/datamodel-connector/src/lib.rs
Expand Up @@ -234,6 +234,7 @@ capabilities!(
RelationFieldsInArbitraryOrder,
ForeignKeys,
NamedPrimaryKeys,
ReferenceCycleDetection,
// Start of query-engine-only Capabilities
InsensitiveFilters,
CreateMany,
Expand Down
7 changes: 7 additions & 0 deletions libs/datamodel/connectors/dml/src/relation_info.rs
Expand Up @@ -96,3 +96,10 @@ impl fmt::Display for ReferentialAction {
}
}
}

impl ReferentialAction {
// True, if the action modifies the related items.
pub fn triggers_modification(self) -> bool {
!matches!(self, Self::NoAction | Self::Restrict)
}
}
Expand Up @@ -69,6 +69,7 @@ impl MsSqlDatamodelConnector {
ConnectorCapability::UpdateableId,
ConnectorCapability::AnyId,
ConnectorCapability::QueryRaw,
ConnectorCapability::ReferenceCycleDetection,
];

let constructors: Vec<NativeTypeConstructor> = vec![
Expand Down
3 changes: 2 additions & 1 deletion libs/datamodel/core/src/lib.rs
Expand Up @@ -139,10 +139,11 @@ fn parse_datamodel_internal(
let generators = GeneratorLoader::load_generators_from_ast(&ast, &mut diagnostics);
let preview_features = preview_features(&generators);
let datasources = load_sources(&ast, preview_features, &mut diagnostics);
let validator = ValidationPipeline::new(&datasources, preview_features);

diagnostics.to_result()?;

let validator = ValidationPipeline::new(&datasources, preview_features);

match validator.validate(&ast, transform) {
Ok(mut src) => {
src.warnings.append(diagnostics.warnings_mut());
Expand Down
219 changes: 161 additions & 58 deletions libs/datamodel/core/src/transform/ast_to_dml/validate.rs
@@ -1,12 +1,16 @@
#![allow(clippy::suspicious_operation_groupings)] // clippy is wrong there

use std::collections::HashSet;

use crate::{
ast,
ast::{self, Span},
common::preview_features::PreviewFeature,
configuration,
diagnostics::{DatamodelError, Diagnostics},
dml,
};
use ::dml::{datamodel::Datamodel, field::RelationField, model::Model, traits::WithName};
use datamodel_connector::ConnectorCapability;
use enumflags2::BitFlags;

/// Helper for validating a datamodel.
Expand Down Expand Up @@ -371,6 +375,97 @@ impl<'a> Validator<'a> {
errors.to_result()
}

// In certain databases, such as SQL Server, it is not allowd to create
// multiple reference paths between two models, if referential actions would
// cause modifications to the children objects.
//
// We detect this early before letting database to give us a much more
// cryptic error message.
fn detect_referential_action_cycles(
&self,
datamodel: &Datamodel,
parent_model: &Model,
parent_field: &RelationField,
span: Span,
errors: &mut Diagnostics,
) {
// we only do this if the referential actions preview feature is enabled
if !self
.source
.map(|source| &source.active_connector)
.map(|connector| connector.has_capability(ConnectorCapability::ReferenceCycleDetection))
.unwrap_or_default()
{
return;
}

// Keeps count on visited relations to iterate them only once.
let mut visited = HashSet::new();
// poor man's tail-recursion ;)
let mut next_relations = vec![(parent_model, parent_field)];

while let Some((model, field)) = next_relations.pop() {
// we expect to have both sides of the relation at this point...
let related_field = datamodel.find_related_field_bang(field).1;
let related_model = datamodel.find_model(&field.relation_info.to).unwrap();

// we do not visit the relation field on the other side
// after this run.
visited.insert((model.name(), field.name()));
visited.insert((related_model.name(), related_field.name()));

// skip many-to-many
if field.is_list() && related_field.is_list() {
return;
}

// we skipped many-to-many relations, so one of the sides either has
// referential actions set, or we can take the default actions
let on_update = field
.relation_info
.on_update
.or(related_field.relation_info.on_update)
.unwrap_or_else(|| field.default_on_update_action());

let on_delete = field
.relation_info
.on_update
.or(related_field.relation_info.on_delete)
.unwrap_or_else(|| field.default_on_delete_action());

// a cycle has a meaning only if every relation in it triggers
// modifications in the children
if on_delete.triggers_modification() || on_update.triggers_modification() {
if model.name() == related_model.name() {
errors.push_error(DatamodelError::new_attribute_validation_error(
"A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes.",
RELATION_ATTRIBUTE_NAME,
span,
));

return;
}

if related_model.name() == parent_model.name() {
errors.push_error(DatamodelError::new_attribute_validation_error(
"Reference causes a cycle or multiple cascade paths. One of the @relation attributes in this cycle must have `onDelete` and `onUpdate` referential actions set to `NoAction`.",
RELATION_ATTRIBUTE_NAME,
span,
));

return;
}

// bozo tail-recursion continues
for field in related_model.relation_fields() {
if !visited.contains(&(related_model.name(), field.name())) {
next_relations.push((related_model, field));
}
}
}
}
}

fn validate_relation_arguments_bla(
&self,
datamodel: &dml::Datamodel,
Expand All @@ -394,14 +489,14 @@ impl<'a> Validator<'a> {
let related_field_rel_info = &related_field.relation_info;

if related_model.is_ignored && !field.is_ignored && !model.is_ignored {
errors.push_error(DatamodelError::new_attribute_validation_error(
&format!(
let message = format!(
"The relation field `{}` on Model `{}` must specify the `@ignore` attribute, because the model {} it is pointing to is marked ignored.",
&field.name, &model.name, &related_model.name
),
"ignore",
field_span,
));
);

errors.push_error(DatamodelError::new_attribute_validation_error(
&message, "ignore", field_span,
));
}

// ONE TO MANY
Expand Down Expand Up @@ -629,6 +724,10 @@ impl<'a> Validator<'a> {
field_span,
));
}

if !field.is_list() && self.preview_features.contains(PreviewFeature::ReferentialActions) {
self.detect_referential_action_cycles(&datamodel, &model, &field, field_span, &mut errors);
}
} else {
let message = format!(
"The relation field `{}` on Model `{}` is missing an opposite relation field on the model `{}`. Either run `prisma format` or add it manually.",
Expand Down Expand Up @@ -666,36 +765,40 @@ impl<'a> Validator<'a> {
// and also no names set.
if rel_a.to == rel_b.to && rel_a.name == rel_b.name {
if rel_a.name.is_empty() {
let message = format!(
"Ambiguous relation detected. The fields `{}` and `{}` in model `{}` both refer to `{}`. Please provide different relation names for them by adding `@relation(<name>).",
&field_a.name,
&field_b.name,
&model.name,
&rel_a.to
);

// unnamed relation
return Err(DatamodelError::new_model_validation_error(
&format!(
"Ambiguous relation detected. The fields `{}` and `{}` in model `{}` both refer to `{}`. Please provide different relation names for them by adding `@relation(<name>).",
&field_a.name,
&field_b.name,
&model.name,
&rel_a.to
),
&model.name,
ast_schema
.find_field(&model.name, &field_a.name)
.expect(STATE_ERROR)
.span,
));
&message,
&model.name,
ast_schema
.find_field(&model.name, &field_a.name)
.expect(STATE_ERROR)
.span,
));
} else {
let message = format!(
"Wrongly named relation detected. The fields `{}` and `{}` in model `{}` both use the same relation name. Please provide different relation names for them through `@relation(<name>).",
&field_a.name,
&field_b.name,
&model.name,
);

// explicitly named relation
return Err(DatamodelError::new_model_validation_error(
&format!(
"Wrongly named relation detected. The fields `{}` and `{}` in model `{}` both use the same relation name. Please provide different relation names for them through `@relation(<name>).",
&field_a.name,
&field_b.name,
&model.name,
),
&model.name,
ast_schema
.find_field(&model.name, &field_a.name)
.expect(STATE_ERROR)
.span,
));
&message,
&model.name,
ast_schema
.find_field(&model.name, &field_a.name)
.expect(STATE_ERROR)
.span,
));
}
}
} else if rel_a.to == model.name && rel_b.to == model.name {
Expand Down Expand Up @@ -724,19 +827,19 @@ impl<'a> Validator<'a> {
));
} else {
return Err(DatamodelError::new_model_validation_error(
&format!(
"Wrongly named self relation detected. The fields `{}`, `{}` and `{}` in model `{}` have the same relation name. At most two relation fields can belong to the same relation and therefore have the same name. Please assign a different relation name to one of them.",
&field_a.name,
&field_b.name,
&field_c.name,
&model.name
),
&model.name,
ast_schema
.find_field(&model.name, &field_a.name)
.expect(STATE_ERROR)
.span,
));
&format!(
"Wrongly named self relation detected. The fields `{}`, `{}` and `{}` in model `{}` have the same relation name. At most two relation fields can belong to the same relation and therefore have the same name. Please assign a different relation name to one of them.",
&field_a.name,
&field_b.name,
&field_c.name,
&model.name
),
&model.name,
ast_schema
.find_field(&model.name, &field_a.name)
.expect(STATE_ERROR)
.span,
));
}
}
}
Expand All @@ -746,19 +849,19 @@ impl<'a> Validator<'a> {
if rel_a.name.is_empty() && rel_b.name.is_empty() {
// A self relation, but there are at least two fields without a name.
return Err(DatamodelError::new_model_validation_error(
&format!(
"Ambiguous self relation detected. The fields `{}` and `{}` in model `{}` both refer to `{}`. If they are part of the same relation add the same relation name for them with `@relation(<name>)`.",
&field_a.name,
&field_b.name,
&model.name,
&rel_a.to
),
&model.name,
ast_schema
.find_field(&model.name, &field_a.name)
.expect(STATE_ERROR)
.span,
));
&format!(
"Ambiguous self relation detected. The fields `{}` and `{}` in model `{}` both refer to `{}`. If they are part of the same relation add the same relation name for them with `@relation(<name>)`.",
&field_a.name,
&field_b.name,
&model.name,
&rel_a.to
),
&model.name,
ast_schema
.find_field(&model.name, &field_a.name)
.expect(STATE_ERROR)
.span,
));
}
}
}
Expand Down