From ba4f48dd8d7a541a57b86473e83527838939b9b0 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Fri, 30 Jul 2021 16:19:56 +0200 Subject: [PATCH] SQL Server: Detect referential action cycles --- .../connectors/datamodel-connector/src/lib.rs | 1 + .../connectors/dml/src/relation_info.rs | 7 + .../src/mssql_datamodel_connector.rs | 1 + libs/datamodel/core/src/lib.rs | 3 +- .../core/src/transform/ast_to_dml/validate.rs | 219 ++++++--- .../relations/referential_actions.rs | 427 ++++++++++++++++++ 6 files changed, 599 insertions(+), 59 deletions(-) diff --git a/libs/datamodel/connectors/datamodel-connector/src/lib.rs b/libs/datamodel/connectors/datamodel-connector/src/lib.rs index aa4da2426ea1..046dfe48854d 100644 --- a/libs/datamodel/connectors/datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/datamodel-connector/src/lib.rs @@ -234,6 +234,7 @@ capabilities!( RelationFieldsInArbitraryOrder, ForeignKeys, NamedPrimaryKeys, + ReferenceCycleDetection, // Start of query-engine-only Capabilities InsensitiveFilters, CreateMany, diff --git a/libs/datamodel/connectors/dml/src/relation_info.rs b/libs/datamodel/connectors/dml/src/relation_info.rs index cdd8a3efafeb..fc0c1a3af028 100644 --- a/libs/datamodel/connectors/dml/src/relation_info.rs +++ b/libs/datamodel/connectors/dml/src/relation_info.rs @@ -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) + } +} diff --git a/libs/datamodel/connectors/sql-datamodel-connector/src/mssql_datamodel_connector.rs b/libs/datamodel/connectors/sql-datamodel-connector/src/mssql_datamodel_connector.rs index 0d2c46a7f674..0213f8c00637 100644 --- a/libs/datamodel/connectors/sql-datamodel-connector/src/mssql_datamodel_connector.rs +++ b/libs/datamodel/connectors/sql-datamodel-connector/src/mssql_datamodel_connector.rs @@ -69,6 +69,7 @@ impl MsSqlDatamodelConnector { ConnectorCapability::UpdateableId, ConnectorCapability::AnyId, ConnectorCapability::QueryRaw, + ConnectorCapability::ReferenceCycleDetection, ]; let constructors: Vec = vec![ diff --git a/libs/datamodel/core/src/lib.rs b/libs/datamodel/core/src/lib.rs index 7717732fa585..057332906d1e 100644 --- a/libs/datamodel/core/src/lib.rs +++ b/libs/datamodel/core/src/lib.rs @@ -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()); diff --git a/libs/datamodel/core/src/transform/ast_to_dml/validate.rs b/libs/datamodel/core/src/transform/ast_to_dml/validate.rs index 244b99c5a244..e6506d092eea 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/validate.rs +++ b/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. @@ -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, @@ -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 @@ -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.", @@ -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().", + &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().", - &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().", + &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().", - &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 { @@ -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, + )); } } } @@ -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()`.", - &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()`.", + &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, + )); } } } diff --git a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs index 56a0fbf9ec4a..2e954cac076d 100644 --- a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs +++ b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs @@ -434,3 +434,430 @@ fn on_update_without_preview_feature_should_error() { Span::new(127, 145), )]); } + +#[test] +fn sql_server_cascading_on_delete_self_relations() { + let dml = indoc! { + r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions", "microsoftSqlServer"] + } + + model A { + id Int @id @default(autoincrement()) + child A? @relation(name: "a_self_relation") + parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onDelete: Cascade) + aId Int? + } + "#}; + + let expect = expect![[r#" + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onDelete: Cascade) +  |  + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:14 +  |  + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onDelete: Cascade) + 15 |  aId Int? +  |  + "#]]; + + expect.assert_eq(&datamodel::parse_schema(dml).map(drop).unwrap_err()); +} + +#[test] +fn sql_server_cascading_on_update_self_relations() { + let dml = indoc! { + r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions", "microsoftSqlServer"] + } + + model A { + id Int @id @default(autoincrement()) + child A? @relation(name: "a_self_relation") + parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onUpdate: Cascade) + aId Int? + } + "#}; + + let expect = expect![[r#" + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onUpdate: Cascade) +  |  + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:14 +  |  + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onUpdate: Cascade) + 15 |  aId Int? +  |  + "#]]; + + expect.assert_eq(&datamodel::parse_schema(dml).map(drop).unwrap_err()); +} + +#[test] +fn sql_server_null_setting_on_delete_self_relations() { + let dml = indoc! { + r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions", "microsoftSqlServer"] + } + + model A { + id Int @id @default(autoincrement()) + child A? @relation(name: "a_self_relation") + parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onDelete: SetNull) + aId Int? + } + "#}; + + let expect = expect![[r#" + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onDelete: SetNull) +  |  + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:14 +  |  + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onDelete: SetNull) + 15 |  aId Int? +  |  + "#]]; + + expect.assert_eq(&datamodel::parse_schema(dml).map(drop).unwrap_err()); +} + +#[test] +fn sql_server_null_setting_on_update_self_relations() { + let dml = indoc! { + r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions", "microsoftSqlServer"] + } + + model A { + id Int @id @default(autoincrement()) + child A? @relation(name: "a_self_relation") + parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onUpdate: SetNull) + aId Int? + } + "#}; + + let expect = expect![[r#" + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onUpdate: SetNull) +  |  + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:14 +  |  + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onUpdate: SetNull) + 15 |  aId Int? +  |  + "#]]; + + expect.assert_eq(&datamodel::parse_schema(dml).map(drop).unwrap_err()); +} + +#[test] +fn sql_server_default_setting_on_delete_self_relations() { + let dml = indoc! { + r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions", "microsoftSqlServer"] + } + + model A { + id Int @id @default(autoincrement()) + child A? @relation(name: "a_self_relation") + parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onDelete: SetDefault) + aId Int? + } + "#}; + + let expect = expect![[r#" + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onDelete: SetDefault) +  |  + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:14 +  |  + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onDelete: SetDefault) + 15 |  aId Int? +  |  + "#]]; + + expect.assert_eq(&datamodel::parse_schema(dml).map(drop).unwrap_err()); +} + +#[test] +fn sql_server_default_setting_on_update_self_relations() { + let dml = indoc! { + r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions", "microsoftSqlServer"] + } + + model A { + id Int @id @default(autoincrement()) + child A? @relation(name: "a_self_relation") + parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onUpdate: SetDefault) + aId Int? + } + "#}; + + let expect = expect![[r#" + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onUpdate: SetDefault) +  |  + error: Error parsing attribute "@relation": A self-relation must have `onDelete` and `onUpdate` referential actions set to `NoAction` in one of the @relation attributes. + --> schema.prisma:14 +  |  + 13 |  child A? @relation(name: "a_self_relation") + 14 |  parent A? @relation(name: "a_self_relation", fields: [aId], references: [id], onUpdate: SetDefault) + 15 |  aId Int? +  |  + "#]]; + + expect.assert_eq(&datamodel::parse_schema(dml).map(drop).unwrap_err()); +} + +#[test] +fn sql_server_cascading_cyclic_one_hop_relations() { + let dml = indoc! { + r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions", "microsoftSqlServer"] + } + + model A { + id Int @id @default(autoincrement()) + b B @relation(name: "foo", fields: [bId], references: [id], onDelete: Cascade) + bId Int + bs B[] @relation(name: "bar") + } + + model B { + id Int @id @default(autoincrement()) + a A @relation(name: "bar", fields: [aId], references: [id], onUpdate: Cascade) + as A[] @relation(name: "foo") + aId Int + } + "#}; + + let expect = expect![[r#" + error: Error parsing attribute "@relation": 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`. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  b B @relation(name: "foo", fields: [bId], references: [id], onDelete: Cascade) + 14 |  bId Int +  |  + error: Error parsing attribute "@relation": 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`. + --> schema.prisma:20 +  |  + 19 |  id Int @id @default(autoincrement()) + 20 |  a A @relation(name: "bar", fields: [aId], references: [id], onUpdate: Cascade) + 21 |  as A[] @relation(name: "foo") +  |  + "#]]; + + expect.assert_eq(&datamodel::parse_schema(dml).map(drop).unwrap_err()); +} + +#[test] +fn sql_server_cascading_cyclic_hop_over_table_relations() { + let dml = indoc! { + r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions", "microsoftSqlServer"] + } + + model A { + id Int @id @default(autoincrement()) + bId Int + b B @relation(fields: [bId], references: [id]) + cs C[] + } + + model B { + id Int @id @default(autoincrement()) + as A[] + cId Int + c C @relation(fields: [cId], references: [id]) + } + + model C { + id Int @id @default(autoincrement()) + bs B[] + aId Int + a A @relation(fields: [aId], references: [id]) + } + "#}; + + let expect = expect![[r#" + error: Error parsing attribute "@relation": 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`. + --> schema.prisma:14 +  |  + 13 |  bId Int + 14 |  b B @relation(fields: [bId], references: [id]) + 15 |  cs C[] +  |  + error: Error parsing attribute "@relation": 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`. + --> schema.prisma:22 +  |  + 21 |  cId Int + 22 |  c C @relation(fields: [cId], references: [id]) + 23 | } +  |  + error: Error parsing attribute "@relation": 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`. + --> schema.prisma:29 +  |  + 28 |  aId Int + 29 |  a A @relation(fields: [aId], references: [id]) + 30 | } +  |  + "#]]; + + expect.assert_eq(&datamodel::parse_schema(dml).map(drop).unwrap_err()); +} + +#[test] +fn sql_server_cascading_cyclic_crossing_path_relations() { + let dml = indoc! { + r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions", "microsoftSqlServer"] + } + + model A { + id Int @id @default(autoincrement()) + bId Int + b B @relation(fields: [bId], references: [id]) + cs C[] + } + + model B { + id Int @id @default(autoincrement()) + as A[] + cs C[] + } + + model C { + id Int @id @default(autoincrement()) + aId Int + bId Int + a A @relation(fields: [aId], references: [id]) + b B @relation(fields: [bId], references: [id]) + } + "#}; + + let expect = expect![[r#" + error: Error parsing attribute "@relation": 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`. + --> schema.prisma:14 +  |  + 13 |  bId Int + 14 |  b B @relation(fields: [bId], references: [id]) + 15 |  cs C[] +  |  + error: Error parsing attribute "@relation": 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`. + --> schema.prisma:28 +  |  + 27 |  bId Int + 28 |  a A @relation(fields: [aId], references: [id]) + 29 |  b B @relation(fields: [bId], references: [id]) +  |  + error: Error parsing attribute "@relation": 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`. + --> schema.prisma:29 +  |  + 28 |  a A @relation(fields: [aId], references: [id]) + 29 |  b B @relation(fields: [bId], references: [id]) + 30 | } +  |  + "#]]; + + expect.assert_eq(&datamodel::parse_schema(dml).map(drop).unwrap_err()); +}