diff --git a/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs b/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs index b30452a910b6..0c45b4d6e529 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs @@ -41,7 +41,10 @@ pub fn introspect( for foreign_key in &foreign_keys_copy { version_check.has_inline_relations(table); version_check.uses_on_delete(foreign_key, table); - let relation_field = calculate_relation_field(schema, table, foreign_key)?; + + let mut relation_field = calculate_relation_field(schema, table, foreign_key)?; + relation_field.supports_restrict_action(!sql_family.is_mssql()); + model.add_field(Field::RelationField(relation_field)); } diff --git a/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs b/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs index 0f3640ab40fe..e47e13b0d0c2 100644 --- a/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs @@ -1752,3 +1752,144 @@ async fn do_not_try_to_keep_custom_many_to_many_self_relation_names(api: &TestAp Ok(()) } + +#[test_connector(tags(Postgres, Mysql, Sqlite))] +async fn default_required_actions_with_restrict(api: &TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("a", |t| { + t.add_column("id", types::primary()); + }); + + migration.create_table("b", |t| { + t.add_column("id", types::primary()); + t.add_column("a_id", types::integer().nullable(false)); + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE RESTRICT ON UPDATE CASCADE", + ); + }); + }) + .await?; + + let extra_index = if api.sql_family().is_mysql() { + r#"@@index([a_id], name: "asdf")"# + } else { + "" + }; + + let input_dm = formatdoc! {r#" + model a {{ + id Int @id @default(autoincrement()) + bs b[] + }} + + model b {{ + id Int @id @default(autoincrement()) + a_id Int + a a @relation(fields: [a_id], references: [id]) + {} + }} + "#, extra_index}; + + api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?); + + Ok(()) +} + +#[test_connector(tags(Mssql))] +async fn default_required_actions_without_restrict(api: &TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("a", |t| { + t.add_column("id", types::primary()); + }); + + migration.create_table("b", |t| { + t.add_column("id", types::primary()); + t.add_column("a_id", types::integer().nullable(false)); + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_required_actions_without_restrict.a(id) ON DELETE NO ACTION ON UPDATE CASCADE", + ); + }); + }) + .await?; + + let extra_index = if api.sql_family().is_mysql() { + r#"@@index([a_id], name: "asdf")"# + } else { + "" + }; + + let input_dm = formatdoc! {r#" + model a {{ + id Int @id @default(autoincrement()) + bs b[] + }} + + model b {{ + id Int @id @default(autoincrement()) + a_id Int + a a @relation(fields: [a_id], references: [id]) + {} + }} + "#, extra_index}; + + api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?); + + Ok(()) +} + +#[test_connector] +async fn default_optional_actions(api: &TestApi) -> TestResult { + let family = api.sql_family(); + + api.barrel() + .execute(move |migration| { + migration.create_table("a", |t| { + t.add_column("id", types::primary()); + }); + + migration.create_table("b", move |t| { + t.add_column("id", types::primary()); + t.add_column("a_id", types::integer().nullable(true)); + + match family { + SqlFamily::Mssql => { + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_optional_actions.a(id) ON DELETE SET NULL ON UPDATE SET NULL", + ); + } + _ => { + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE SET NULL ON UPDATE SET NULL", + ); + } + } + }); + }) + .await?; + + let extra_index = if api.sql_family().is_mysql() { + r#"@@index([a_id], name: "asdf")"# + } else { + "" + }; + + let input_dm = formatdoc! {r#" + model a {{ + id Int @id @default(autoincrement()) + bs b[] + }} + + model b {{ + id Int @id @default(autoincrement()) + a_id Int? + a a? @relation(fields: [a_id], references: [id]) + {} + }} + "#, extra_index}; + + api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?); + + Ok(()) +} diff --git a/libs/datamodel/connectors/dml/src/datamodel.rs b/libs/datamodel/connectors/dml/src/datamodel.rs index 8188ae4bc008..440635f636db 100644 --- a/libs/datamodel/connectors/dml/src/datamodel.rs +++ b/libs/datamodel/connectors/dml/src/datamodel.rs @@ -1,4 +1,4 @@ -use crate::field::{Field, FieldType, RelationField, ScalarField}; +use crate::field::{Field, RelationField, ScalarField}; use crate::model::Model; use crate::r#enum::Enum; use crate::relation_info::RelationInfo; @@ -117,7 +117,7 @@ impl Datamodel { let mut fields = vec![]; for model in self.models() { for field in model.scalar_fields() { - if FieldType::Enum(enum_name.to_owned()) == field.field_type { + if field.field_type.is_enum(enum_name) { fields.push((model.name.clone(), field.name.clone())) } } diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index 8296db532d69..8d333feda761 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -1,8 +1,11 @@ use super::*; -use crate::default_value::{DefaultValue, ValueGenerator}; use crate::native_type_instance::NativeTypeInstance; use crate::scalars::ScalarType; use crate::traits::{Ignorable, WithDatabaseName, WithName}; +use crate::{ + default_value::{DefaultValue, ValueGenerator}, + relation_info::ReferentialAction, +}; use std::hash::Hash; /// Arity of a Field in a Model. @@ -74,6 +77,10 @@ impl FieldType { self.scalar_type().map(|st| st.is_string()).unwrap_or(false) } + pub fn is_enum(&self, name: &str) -> bool { + matches!(self, Self::Enum(this) if this == name) + } + pub fn scalar_type(&self) -> Option { match self { FieldType::NativeType(st, _) => Some(*st), @@ -230,7 +237,7 @@ impl WithDatabaseName for Field { } /// Represents a relation field in a model. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Clone)] pub struct RelationField { /// Name of the field. pub name: String, @@ -252,6 +259,45 @@ pub struct RelationField { /// Indicates if this field has to be ignored by the Client. pub is_ignored: bool, + + /// Is `ON DELETE/UPDATE RESTRICT` allowed. + pub supports_restrict_action: Option, +} + +impl PartialEq for RelationField { + //ignores the relation name for reintrospection + fn eq(&self, other: &Self) -> bool { + let this_matches = self.name == other.name + && self.arity == other.arity + && self.documentation == other.documentation + && self.is_generated == other.is_generated + && self.is_commented_out == other.is_commented_out + && self.is_ignored == other.is_ignored + && self.relation_info == other.relation_info; + + let this_on_delete = self.relation_info.on_delete.or_else(|| self.default_on_delete_action()); + + let other_on_delete = other + .relation_info + .on_delete + .or_else(|| other.default_on_delete_action()); + + let on_delete_matches = this_on_delete == other_on_delete; + + let this_on_update = self + .relation_info + .on_update + .unwrap_or_else(|| self.default_on_update_action()); + + let other_on_update = other + .relation_info + .on_update + .unwrap_or_else(|| other.default_on_update_action()); + + let on_update_matches = this_on_update == other_on_update; + + this_matches && on_delete_matches && on_update_matches + } } impl RelationField { @@ -265,8 +311,15 @@ impl RelationField { is_generated: false, is_commented_out: false, is_ignored: false, + supports_restrict_action: None, } } + + /// The default `onDelete` can be `Restrict`. + pub fn supports_restrict_action(&mut self, value: bool) { + self.supports_restrict_action = Some(value); + } + /// Creates a new field with the given name and type, marked as generated and optional. pub fn new_generated(name: &str, info: RelationInfo, required: bool) -> Self { let arity = if required { @@ -300,6 +353,21 @@ impl RelationField { pub fn is_optional(&self) -> bool { self.arity.is_optional() } + + pub fn default_on_delete_action(&self) -> Option { + self.supports_restrict_action.map(|restrict_ok| match self.arity { + FieldArity::Required if restrict_ok => ReferentialAction::Restrict, + FieldArity::Required => ReferentialAction::NoAction, + _ => ReferentialAction::SetNull, + }) + } + + pub fn default_on_update_action(&self) -> ReferentialAction { + match self.arity { + FieldArity::Required => ReferentialAction::Cascade, + _ => ReferentialAction::SetNull, + } + } } /// Represents a scalar field in a model. diff --git a/libs/datamodel/connectors/dml/src/relation_info.rs b/libs/datamodel/connectors/dml/src/relation_info.rs index dd99df9855c9..ae39ff4c0ac4 100644 --- a/libs/datamodel/connectors/dml/src/relation_info.rs +++ b/libs/datamodel/connectors/dml/src/relation_info.rs @@ -21,13 +21,9 @@ pub struct RelationInfo { } impl PartialEq for RelationInfo { - //ignores the relation name for reintrospection + //ignores the relation name for reintrospection, ignores referential actions that are compared in the relation field. fn eq(&self, other: &Self) -> bool { - self.to == other.to - && self.fields == other.fields - && self.references == other.references - && self.on_delete == other.on_delete - && self.on_update == other.on_update + self.to == other.to && self.fields == other.fields && self.references == other.references } } diff --git a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs index 926e07c0cdd9..fc02371450aa 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs @@ -8,6 +8,7 @@ use crate::{ diagnostics::{DatamodelError, Diagnostics}, }; use crate::{dml::ScalarType, Datasource}; +use ::dml::relation_info::ReferentialAction; use datamodel_connector::connector_error::{ConnectorError, ErrorKind}; use itertools::Itertools; use once_cell::sync::Lazy; @@ -157,6 +158,15 @@ impl<'a> LiftAstToDml<'a> { FieldType::Relation(info) => { let arity = self.lift_field_arity(&ast_field.arity); let mut field = dml::RelationField::new(&ast_field.name.name, arity, info); + + if let Some(ref source) = self.source { + field.supports_restrict_action( + source + .active_connector + .supports_referential_action(ReferentialAction::Restrict), + ); + } + field.documentation = ast_field.documentation.clone().map(|comment| comment.text); Field::RelationField(field) } diff --git a/libs/datamodel/core/src/transform/attributes/relation.rs b/libs/datamodel/core/src/transform/attributes/relation.rs index ffcdc333e910..25041ef511d1 100644 --- a/libs/datamodel/core/src/transform/attributes/relation.rs +++ b/libs/datamodel/core/src/transform/attributes/relation.rs @@ -49,9 +49,7 @@ impl AttributeValidator for RelationAttributeValidator { fn serialize(&self, field: &dml::Field, datamodel: &dml::Datamodel) -> Vec { if let dml::Field::RelationField(rf) = field { let mut args = Vec::new(); - let relation_info = &rf.relation_info; - let parent_model = datamodel.find_model_by_relation_field_ref(rf).unwrap(); let related_model = datamodel @@ -104,15 +102,22 @@ impl AttributeValidator for RelationAttributeValidator { } if let Some(ref_action) = relation_info.on_delete { - let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); - - args.push(ast::Argument::new("onDelete", expression)); + let is_default = rf + .default_on_delete_action() + .map(|default| default == ref_action) + .unwrap_or(false); + + if !is_default { + let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); + args.push(ast::Argument::new("onDelete", expression)); + } } if let Some(ref_action) = relation_info.on_update { - let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); - - args.push(ast::Argument::new("onUpdate", expression)); + if rf.default_on_update_action() != ref_action { + let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); + args.push(ast::Argument::new("onUpdate", expression)); + } } if !args.is_empty() { diff --git a/libs/sql-schema-describer/src/sqlite.rs b/libs/sql-schema-describer/src/sqlite.rs index c1755e8c5fb2..f31da0661a9b 100644 --- a/libs/sql-schema-describer/src/sqlite.rs +++ b/libs/sql-schema-describer/src/sqlite.rs @@ -338,7 +338,7 @@ impl SqlSchemaDescriber { if let Some(column) = referenced_column { referenced_columns.insert(seq, column); }; - let on_delete_action = match dbg!(&row) + let on_delete_action = match row .get("on_delete") .and_then(|x| x.to_string()) .expect("on_delete") diff --git a/migration-engine/migration-engine-tests/tests/migrations/relations.rs b/migration-engine/migration-engine-tests/tests/migrations/relations.rs index 0ad05d1e348c..500cdc997f4d 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/relations.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/relations.rs @@ -638,3 +638,19 @@ fn on_update_restrict_should_work(api: TestApi) { }) }); } + +#[test_connector] +fn on_delete_default_values(api: TestApi) { + let dm = r#" + model A { + id Int @id + b B[] + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onUpdate: Restrict) + } + "#; +}