From 9c92ac3af3543517a74dd0175dd8c6300b716b12 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Thu, 20 May 2021 16:55:13 +0200 Subject: [PATCH] Referential action support for IE, ME and PSL Supported actions: `Cascade`, `Restrict`, `NoAction`, `SetNull` and `SetDefault`. Per-database validation, e.g. `Restrict` doesn't validate on SQL Server. Defaults to: - `onUpdate`: `SetNull` on optional and `Cascade` on required relations. - `onDelete`: `SetNull` on optional and `Restrict` on required relations. Currently ALWAYS renders the actions on introspection, due to the database always has some action set. --- Cargo.lock | 4 + .../src/calculate_datamodel.rs | 17 +- .../src/introspection_helpers.rs | 21 +- .../src/test_api.rs | 4 + .../tests/commenting_out/mod.rs | 29 +- .../tests/re_introspection/mod.rs | 142 +++--- .../tests/relations/mod.rs | 423 +++++++++++------- .../tests/relations_with_compound_fk/mod.rs | 163 +++++-- .../tests/remapping_database_names/mod.rs | 81 ++-- .../connectors/datamodel-connector/Cargo.toml | 1 + .../connectors/datamodel-connector/src/lib.rs | 10 +- libs/datamodel/connectors/dml/Cargo.toml | 1 + .../connectors/dml/src/relation_info.rs | 54 ++- .../mongodb-datamodel-connector/Cargo.toml | 1 + .../mongodb-datamodel-connector/src/lib.rs | 8 +- .../sql-datamodel-connector/Cargo.toml | 1 + .../src/mssql_datamodel_connector.rs | 26 +- .../src/mysql_datamodel_connector.rs | 36 +- .../src/postgres_datamodel_connector.rs | 36 +- .../src/sqlite_datamodel_connector.rs | 20 +- libs/datamodel/core/src/json/dmmf/to_dmmf.rs | 2 +- .../ast_to_dml/standardise_formatting.rs | 10 +- .../core/src/transform/ast_to_dml/validate.rs | 33 ++ .../core/src/transform/attributes/relation.rs | 23 +- .../core/src/transform/dml_to_ast/lower.rs | 1 + .../src/transform/helpers/value_validator.rs | 20 + libs/datamodel/core/src/walkers.rs | 10 +- .../core/tests/attributes/relations/mod.rs | 1 + .../relations/referential_actions.rs | 160 +++++++ libs/datamodel/core/tests/common.rs | 12 +- libs/sql-ddl/src/mysql.rs | 4 +- libs/sql-ddl/src/postgres.rs | 4 +- libs/sql-schema-describer/src/sqlite.rs | 2 +- .../src/sql_renderer/common.rs | 14 +- .../src/sql_renderer/mssql_renderer.rs | 7 +- .../src/sql_renderer/mysql_renderer.rs | 4 +- .../src/sql_renderer/postgres_renderer.rs | 4 +- .../src/sql_renderer/sqlite_renderer.rs | 8 +- .../src/sql_schema_calculator.rs | 19 +- .../sql_schema_calculator_flavour.rs | 40 +- .../sql_schema_calculator_flavour/mssql.rs | 19 +- .../src/sql_schema_differ.rs | 5 + .../migration-engine-tests/src/assertions.rs | 21 +- .../create_migration_tests.rs | 2 +- .../tests/migration_tests.rs | 9 +- .../tests/migrations/dev_diagnostic_tests.rs | 2 +- .../diagnose_migration_history_tests.rs | 12 +- .../tests/migrations/foreign_keys.rs | 7 +- .../tests/migrations/postgres.rs | 2 +- .../tests/migrations/relations.rs | 231 +++++++++- 50 files changed, 1325 insertions(+), 441 deletions(-) create mode 100644 libs/datamodel/core/tests/attributes/relations/referential_actions.rs diff --git a/Cargo.lock b/Cargo.lock index 4dc6b77b1883..514d96d295f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1009,6 +1009,7 @@ name = "datamodel-connector" version = "0.1.0" dependencies = [ "dml", + "enumflags2 0.6.4", "itertools 0.8.2", "serde_json", "thiserror", @@ -1074,6 +1075,7 @@ version = "0.1.0" dependencies = [ "chrono", "cuid", + "enumflags2 0.6.4", "native-types", "prisma-value", "serde", @@ -2476,6 +2478,7 @@ version = "0.1.0" dependencies = [ "datamodel-connector", "dml", + "enumflags2 0.6.4", "lazy_static", "native-types", "once_cell", @@ -4283,6 +4286,7 @@ version = "0.1.0" dependencies = [ "datamodel-connector", "dml", + "enumflags2 0.6.4", "native-types", "once_cell", "regex", diff --git a/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs b/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs index 3c14603cde7a..b80b01e666ec 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs @@ -1,11 +1,10 @@ -use crate::commenting_out_guardrails::commenting_out_guardrails; -use crate::introspection::introspect; use crate::introspection_helpers::*; use crate::prisma_1_defaults::*; use crate::re_introspection::enrich; use crate::sanitize_datamodel_names::{sanitization_leads_to_duplicate_names, sanitize_datamodel_names}; use crate::version_checker::VersionChecker; use crate::SqlIntrospectionResult; +use crate::{commenting_out_guardrails::commenting_out_guardrails, introspection::introspect}; use datamodel::Datamodel; use introspection_connector::IntrospectionResult; use quaint::connector::SqlFamily; @@ -63,7 +62,7 @@ mod tests { use super::*; use datamodel::{ dml, Datamodel, DefaultValue as DMLDefault, Field, FieldArity, FieldType, Model, NativeTypeInstance, - OnDeleteStrategy, RelationField, RelationInfo, ScalarField, ScalarType, ValueGenerator, + RelationField, RelationInfo, ScalarField, ScalarType, ValueGenerator, }; use native_types::{NativeType, PostgresType}; use pretty_assertions::assert_eq; @@ -470,7 +469,8 @@ mod tests { fields: vec![], references: vec![], name: "CityToUser".to_string(), - on_delete: OnDeleteStrategy::None, + on_delete: None, + on_update: None, }, )), ], @@ -557,7 +557,8 @@ mod tests { to: "City".to_string(), fields: vec!["city_id".to_string(), "city_name".to_string()], references: vec!["id".to_string(), "name".to_string()], - on_delete: OnDeleteStrategy::None, + on_delete: None, + on_update: None, }, )), ], @@ -854,7 +855,8 @@ mod tests { fields: vec![], references: vec![], name: "CityToUser".to_string(), - on_delete: OnDeleteStrategy::None, + on_delete: None, + on_update: None, }, )), ], @@ -911,7 +913,8 @@ mod tests { to: "City".to_string(), fields: vec!["city_id".to_string()], references: vec!["id".to_string()], - on_delete: OnDeleteStrategy::None, + on_delete: None, + on_update: None, }, )), ], diff --git a/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs b/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs index e9aff7353c90..2f9af428a179 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs @@ -2,13 +2,13 @@ use crate::Dedup; use crate::SqlError; use datamodel::{ common::RelationNames, Datamodel, DefaultValue as DMLDef, FieldArity, FieldType, IndexDefinition, Model, - OnDeleteStrategy, RelationField, RelationInfo, ScalarField, ScalarType, ValueGenerator as VG, + ReferentialAction, RelationField, RelationInfo, ScalarField, ScalarType, ValueGenerator as VG, }; use datamodel_connector::Connector; use quaint::connector::SqlFamily; use sql_datamodel_connector::SqlDatamodelConnectors; -use sql_schema_describer::DefaultKind; use sql_schema_describer::{Column, ColumnArity, ColumnTypeFamily, ForeignKey, Index, IndexType, SqlSchema, Table}; +use sql_schema_describer::{DefaultKind, ForeignKeyAction}; use tracing::debug; //checks @@ -107,7 +107,8 @@ pub fn calculate_many_to_many_field( fields: vec![], to: opposite_foreign_key.referenced_table.clone(), references: opposite_foreign_key.referenced_columns.clone(), - on_delete: OnDeleteStrategy::None, + on_delete: None, + on_update: None, }; let basename = opposite_foreign_key.referenced_table.clone(); @@ -174,12 +175,21 @@ pub(crate) fn calculate_relation_field( ) -> Result { debug!("Handling foreign key {:?}", foreign_key); + let map_action = |action: ForeignKeyAction| match action { + ForeignKeyAction::NoAction => ReferentialAction::NoAction, + ForeignKeyAction::Restrict => ReferentialAction::Restrict, + ForeignKeyAction::Cascade => ReferentialAction::Cascade, + ForeignKeyAction::SetNull => ReferentialAction::SetNull, + ForeignKeyAction::SetDefault => ReferentialAction::SetDefault, + }; + let relation_info = RelationInfo { name: calculate_relation_name(schema, foreign_key, table)?, fields: foreign_key.columns.clone(), to: foreign_key.referenced_table.clone(), references: foreign_key.referenced_columns.clone(), - on_delete: OnDeleteStrategy::None, + on_delete: Some(map_action(foreign_key.on_delete_action)), + on_update: Some(map_action(foreign_key.on_update_action)), }; let columns: Vec<&Column> = foreign_key @@ -213,7 +223,8 @@ pub(crate) fn calculate_backrelation_field( to: model.name.clone(), fields: vec![], references: vec![], - on_delete: OnDeleteStrategy::None, + on_delete: None, + on_update: None, }; // unique or id diff --git a/introspection-engine/introspection-engine-tests/src/test_api.rs b/introspection-engine/introspection-engine-tests/src/test_api.rs index ba65c6a5e1e1..18384d579dcc 100644 --- a/introspection-engine/introspection-engine-tests/src/test_api.rs +++ b/introspection-engine/introspection-engine-tests/src/test_api.rs @@ -84,6 +84,10 @@ impl TestApi { self.tags().contains(Tags::Cockroach) } + pub fn is_mysql8(&self) -> bool { + self.tags().contains(Tags::Mysql8) + } + #[tracing::instrument(skip(self, data_model_string))] #[track_caller] pub async fn re_introspect(&self, data_model_string: &str) -> Result { diff --git a/introspection-engine/introspection-engine-tests/tests/commenting_out/mod.rs b/introspection-engine/introspection-engine-tests/tests/commenting_out/mod.rs index cae0b64cda54..fdc75af7fc92 100644 --- a/introspection-engine/introspection-engine-tests/tests/commenting_out/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/commenting_out/mod.rs @@ -19,13 +19,30 @@ async fn a_table_without_uniques_should_ignore(api: &TestApi) -> TestResult { }) .await?; - let dm = if api.sql_family().is_mysql() { + let dm = if api.sql_family().is_mysql() && !api.is_mysql8() { indoc! {r#" /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by the Prisma Client. model Post { id Int user_id Int - User User @relation(fields: [user_id], references: [id]) + User User @relation(fields: [user_id], references: [id], onDelete: Restrict, onUpdate: Restrict) + + @@index([user_id], name: "user_id") + @@ignore + } + + model User { + id Int @id @default(autoincrement()) + Post Post[] @ignore + } + "#} + } else if api.sql_family().is_mysql() { + indoc! {r#" + /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by the Prisma Client. + model Post { + id Int + user_id Int + User User @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction) @@index([user_id], name: "user_id") @@ignore @@ -42,7 +59,7 @@ async fn a_table_without_uniques_should_ignore(api: &TestApi) -> TestResult { model Post { id Int user_id Int - User User @relation(fields: [user_id], references: [id]) + User User @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction) @@ignore } @@ -79,7 +96,7 @@ async fn relations_between_ignored_models_should_not_have_field_level_ignores(ap model Post { id Unsupported("macaddr") @id user_id Unsupported("macaddr") - User User @relation(fields: [user_id], references: [id]) + User User @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction) @@ignore } @@ -274,7 +291,7 @@ async fn a_table_with_unsupported_types_in_a_relation(api: &TestApi) -> TestResu model Post { id Int @id @default(autoincrement()) user_ip Unsupported("cidr") - User User @relation(fields: [user_ip], references: [ip]) + User User @relation(fields: [user_ip], references: [ip], onDelete: NoAction, onUpdate: NoAction) } model User { @@ -395,7 +412,7 @@ async fn ignore_on_back_relation_field_if_pointing_to_ignored_model(api: &TestAp model Post { id Int user_ip Int - User User @relation(fields: [user_ip], references: [ip]) + User User @relation(fields: [user_ip], references: [ip], onDelete: NoAction, onUpdate: NoAction) @@ignore } 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 f8cc93a97b78..0f3640ab40fe 100644 --- a/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs @@ -1,8 +1,9 @@ use barrel::types; +use datamodel::ReferentialAction; use indoc::formatdoc; use indoc::indoc; use introspection_engine_tests::{assert_eq_json, test_api::*}; -use quaint::prelude::Queryable; +use quaint::prelude::{Queryable, SqlFamily}; use serde_json::json; use test_macros::test_connector; @@ -130,13 +131,18 @@ async fn mapped_model_and_field_name(api: &TestApi) -> TestResult { "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let input_dm = format!( r#" model Post {{ id Int @id @default(autoincrement()) c_user_id Int @map("user_id") - Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id]) - {} + Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id], onDelete: {action}, onUpdate: {action}) + {extra_index} }} model Custom_User {{ @@ -146,7 +152,8 @@ async fn mapped_model_and_field_name(api: &TestApi) -> TestResult { @@map(name: "User") }} "#, - extra_index + action = action, + extra_index = extra_index ); let final_dm = format!( @@ -154,8 +161,8 @@ async fn mapped_model_and_field_name(api: &TestApi) -> TestResult { model Post {{ id Int @id @default(autoincrement()) c_user_id Int @map("user_id") - Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id]) - {} + Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id], onDelete: {action}, onUpdate: {action}) + {extra_index} }} model Custom_User {{ @@ -169,7 +176,8 @@ async fn mapped_model_and_field_name(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) }} "#, - extra_index + action = action, + extra_index = extra_index ); api.assert_eq_datamodels(&final_dm, &api.re_introspect(&input_dm).await?); @@ -236,7 +244,7 @@ async fn manually_mapped_model_and_field_name(api: &TestApi) -> TestResult { model Post {{ id Int @id @default(autoincrement()) c_user_id Int @map("user_id") - Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id]) + Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id], onDelete: NoAction, onUpdate: NoAction) {} }} @@ -255,7 +263,7 @@ async fn manually_mapped_model_and_field_name(api: &TestApi) -> TestResult { model Post {{ id Int @id @default(autoincrement()) c_user_id Int @map("user_id") - Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id]) + Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id], onDelete: NoAction, onUpdate: NoAction) {} }} @@ -806,6 +814,11 @@ async fn multiple_changed_relation_names(api: &TestApi) -> TestResult { ("", "") }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let input_dm = format!( r#" model Employee {{ @@ -818,13 +831,15 @@ async fn multiple_changed_relation_names(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) morningEmployeeId Int eveningEmployeeId Int - Employee_EmployeeToSchedule_eveningEmployeeId Employee @relation("EmployeeToSchedule_eveningEmployeeId", fields: [eveningEmployeeId], references: [id]) - Employee_EmployeeToSchedule_morningEmployeeId Employee @relation("EmployeeToSchedule_morningEmployeeId", fields: [morningEmployeeId], references: [id]) - {} - {} + Employee_EmployeeToSchedule_eveningEmployeeId Employee @relation("EmployeeToSchedule_eveningEmployeeId", fields: [eveningEmployeeId], references: [id], onDelete: {action}, onUpdate: {action}) + Employee_EmployeeToSchedule_morningEmployeeId Employee @relation("EmployeeToSchedule_morningEmployeeId", fields: [morningEmployeeId], references: [id], onDelete: NoAction, onUpdate: {action}) + {idx1} + {idx2} }} "#, - idx1, idx2 + action = action, + idx1 = idx1, + idx2 = idx2, ); let final_dm = format!( @@ -839,17 +854,19 @@ async fn multiple_changed_relation_names(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) morningEmployeeId Int eveningEmployeeId Int - Employee_EmployeeToSchedule_eveningEmployeeId Employee @relation("EmployeeToSchedule_eveningEmployeeId", fields: [eveningEmployeeId], references: [id]) - Employee_EmployeeToSchedule_morningEmployeeId Employee @relation("EmployeeToSchedule_morningEmployeeId", fields: [morningEmployeeId], references: [id]) - {} - {} + Employee_EmployeeToSchedule_eveningEmployeeId Employee @relation("EmployeeToSchedule_eveningEmployeeId", fields: [eveningEmployeeId], references: [id], onDelete: {action}, onUpdate: {action}) + Employee_EmployeeToSchedule_morningEmployeeId Employee @relation("EmployeeToSchedule_morningEmployeeId", fields: [morningEmployeeId], references: [id], onDelete: {action}, onUpdate: {action}) + {idx1} + {idx2} }} model Unrelated {{ id Int @id @default(autoincrement()) }} "#, - idx1, idx2 + action = action, + idx1 = idx1, + idx2 = idx2, ); api.assert_eq_datamodels(&final_dm, &api.re_introspect(&input_dm).await?); @@ -877,38 +894,42 @@ async fn custom_virtual_relation_field_names(api: &TestApi) -> TestResult { }) .await?; - let input_dm = indoc! {r#" - model Post { + let action = match api.sql_family() { + SqlFamily::Mysql if !!api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let input_dm = formatdoc! {r#" + model Post {{ id Int @id @default(autoincrement()) user_id Int @unique - custom_User User @relation(fields: [user_id], references: [id]) - } + custom_User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) custom_Post Post? - } - "#}; + }} + "#, action = action}; - let final_dm = indoc! {r#" - model Post { + let final_dm = formatdoc! {r#" + model Post {{ id Int @id @default(autoincrement()) user_id Int @unique - custom_User User @relation(fields: [user_id], references: [id]) - } + custom_User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) custom_Post Post? - } + }} - model Unrelated { + model Unrelated {{ id Int @id @default(autoincrement()) - } - - "#}; + }} + "#, action = action}; - api.assert_eq_datamodels(final_dm, &api.re_introspect(input_dm).await?); + api.assert_eq_datamodels(&final_dm, &api.re_introspect(&input_dm).await?); Ok(()) } @@ -1117,47 +1138,52 @@ async fn multiple_changed_relation_names_due_to_mapped_models(api: &TestApi) -> }) .await?; - let input_dm = indoc! {r#" - model Post { + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let input_dm = formatdoc! {r#" + model Post {{ id Int @id @default(autoincrement()) user_id Int @unique user_id2 Int @unique - custom_User Custom_User @relation("CustomRelationName", fields: [user_id], references: [id]) - custom_User2 Custom_User @relation("AnotherCustomRelationName", fields: [user_id2], references: [id]) - } + custom_User Custom_User @relation("CustomRelationName", fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + custom_User2 Custom_User @relation("AnotherCustomRelationName", fields: [user_id2], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model Custom_User { + model Custom_User {{ id Int @id @default(autoincrement()) custom_Post Post? @relation("CustomRelationName") custom_Post2 Post? @relation("AnotherCustomRelationName") @@map("User") - } - "#}; + }} + "#, action = action}; - let final_dm = indoc! {r#" - model Post { + let final_dm = formatdoc! {r#" + model Post {{ id Int @id @default(autoincrement()) user_id Int @unique user_id2 Int @unique - custom_User Custom_User @relation("CustomRelationName", fields: [user_id], references: [id]) - custom_User2 Custom_User @relation("AnotherCustomRelationName", fields: [user_id2], references: [id]) - } + custom_User Custom_User @relation("CustomRelationName", fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + custom_User2 Custom_User @relation("AnotherCustomRelationName", fields: [user_id2], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model Custom_User { + model Custom_User {{ id Int @id @default(autoincrement()) custom_Post Post? @relation("CustomRelationName") custom_Post2 Post? @relation("AnotherCustomRelationName") @@map("User") - } + }} - model Unrelated { + model Unrelated {{ id Int @id @default(autoincrement()) - } - "#}; + }} + "#, action = action}; - api.assert_eq_datamodels(final_dm, &api.re_introspect(&input_dm).await?); + api.assert_eq_datamodels(&final_dm, &api.re_introspect(&input_dm).await?); Ok(()) } @@ -1584,7 +1610,7 @@ async fn custom_repro(api: &TestApi) -> TestResult { model Post{ id Int @id @default(autoincrement()) tag_id Int - tag Tag @relation("post_to_tag", fields:[tag_id], references: id) + tag Tag @relation("post_to_tag", fields:[tag_id], references: id, onDelete: NoAction, onUpdate: NoAction) } model Tag { @@ -1599,7 +1625,7 @@ async fn custom_repro(api: &TestApi) -> TestResult { model Post{ id Int @id @default(autoincrement()) tag_id Int - tag Tag @relation("post_to_tag", fields:[tag_id], references: id) + tag Tag @relation("post_to_tag", fields:[tag_id], references: id, onDelete: NoAction, onUpdate: NoAction) } model Tag { diff --git a/introspection-engine/introspection-engine-tests/tests/relations/mod.rs b/introspection-engine/introspection-engine-tests/tests/relations/mod.rs index 83494f153bd1..6d4f3f8f9fdc 100644 --- a/introspection-engine/introspection-engine-tests/tests/relations/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/relations/mod.rs @@ -1,4 +1,5 @@ use barrel::types; +use datamodel::ReferentialAction; use indoc::formatdoc; use indoc::indoc; use introspection_engine_tests::test_api::*; @@ -24,20 +25,25 @@ async fn one_to_one_req_relation(api: &TestApi) -> TestResult { ) .await?; - let dm = indoc! {r##" - model Post { + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let dm = formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int @unique - User User @relation(fields: [user_id], references: [id]) - } + User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post? - } - "##}; + }} + "##, action = action}; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -60,19 +66,24 @@ async fn one_to_one_relation_on_a_singular_primary_key(api: &TestApi) -> TestRes ) .await?; - let dm = indoc! {r##" - model Post { + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let dm = formatdoc! {r##" + model Post {{ id Int @unique - User User @relation(fields: [id], references: [id]) - } + User User @relation(fields: [id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post? - } - "##}; + }} + "##, action = action}; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -115,23 +126,28 @@ async fn two_one_to_one_relations_between_the_same_models(api: &TestApi) -> Test ) .await?; - let dm = indoc! {r##" - model Post { + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let dm = formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int @unique - User_Post_user_idToUser User @relation("Post_user_idToUser", fields: [user_id], references: [id]) + User_Post_user_idToUser User @relation("Post_user_idToUser", fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) User_PostToUser_post_id User? @relation("PostToUser_post_id") - } + }} - model User { + model User {{ id Int @id @default(autoincrement()) post_id Int @unique - Post_PostToUser_post_id Post @relation("PostToUser_post_id", fields: [post_id], references: [id]) + Post_PostToUser_post_id Post @relation("PostToUser_post_id", fields: [post_id], references: [id], onDelete: {action}, onUpdate: {action}) Post_Post_user_idToUser Post? @relation("Post_user_idToUser") - } - "##}; + }} + "##, action = action}; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -155,20 +171,25 @@ async fn a_one_to_one_relation(api: &TestApi) -> TestResult { ) .await?; - let dm = indoc! {r##" - model Post { + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let dm = formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int? @unique - User User? @relation(fields: [user_id], references: [id]) - } + User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post? - } - "##}; + }} + "##, action = action}; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -199,19 +220,24 @@ async fn a_one_to_one_relation_referencing_non_id(api: &TestApi) -> TestResult { "@db.VarChar(10)" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = formatdoc! {r##" model Post {{ id Int @id @default(autoincrement()) - user_email String? @unique {} - User User? @relation(fields: [user_email], references: [email]) + user_email String? @unique {native_type} + User User? @relation(fields: [user_email], references: [email], onDelete: {action}, onUpdate: {action}) }} model User {{ id Int @id @default(autoincrement()) - email String? @unique {} + email String? @unique {native_type} Post Post? }} - "##, native_type, native_type}; + "##, action = action, native_type = native_type}; api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -237,39 +263,44 @@ async fn a_one_to_many_relation(api: &TestApi) -> TestResult { ) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = match api.sql_family() { SqlFamily::Mysql => { - indoc! {r##" - model Post { + formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int? - User User? @relation(fields: [user_id], references: [id]) + User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) @@index([user_id], name: "user_id") - } + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post[] - } - "##} + }} + "##, action = action} } _ => { - indoc! {r##" - model Post { + formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int? - User User? @relation(fields: [user_id], references: [id]) - } + User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post[] - } - "##} + }} + "##, action = action} } }; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -293,39 +324,44 @@ async fn a_one_req_to_many_relation(api: &TestApi) -> TestResult { ) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = match api.sql_family() { SqlFamily::Mysql => { - indoc! {r##" - model Post { + formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int - User User @relation(fields: [user_id], references: [id]) + User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) @@index([user_id], name: "user_id") - } + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post[] - } - "##} + }} + "##, action = action} } _ => { - indoc! {r##" - model Post { + formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int - User User @relation(fields: [user_id], references: [id]) - } + User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post[] - } - "##} + }} + "##, action = action} } }; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -401,54 +437,59 @@ async fn a_many_to_many_relation_with_an_id(api: &TestApi) -> TestResult { ) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = match api.sql_family() { SqlFamily::Mysql => { - indoc! {r##" - model Post { + formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) PostsToUsers PostsToUsers[] - } + }} - model PostsToUsers { + model PostsToUsers {{ id Int @id @default(autoincrement()) user_id Int post_id Int - Post Post @relation(fields: [post_id], references: [id]) - User User @relation(fields: [user_id], references: [id]) + Post Post @relation(fields: [post_id], references: [id], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) @@index([post_id], name: "post_id") @@index([user_id], name: "user_id") - } + }} - model User { + model User {{ id Int @id @default(autoincrement()) PostsToUsers PostsToUsers[] - } - "##} + }} + "##, action = action} } _ => { - indoc! {r##" - model Post { + formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) PostsToUsers PostsToUsers[] - } + }} - model PostsToUsers { + model PostsToUsers {{ id Int @id @default(autoincrement()) user_id Int post_id Int - Post Post @relation(fields: [post_id], references: [id]) - User User @relation(fields: [user_id], references: [id]) - } + Post Post @relation(fields: [post_id], references: [id], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) PostsToUsers PostsToUsers[] - } - "##} + }} + "##, action = action} } }; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -471,38 +512,43 @@ async fn a_self_relation(api: &TestApi) -> TestResult { ) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = match api.sql_family() { SqlFamily::Mysql => { - indoc! {r##" - model User { + formatdoc! {r##" + model User {{ id Int @id @default(autoincrement()) recruited_by Int? direct_report Int? - User_UserToUser_direct_report User? @relation("UserToUser_direct_report", fields: [direct_report], references: [id]) - User_UserToUser_recruited_by User? @relation("UserToUser_recruited_by", fields: [recruited_by], references: [id]) + User_UserToUser_direct_report User? @relation("UserToUser_direct_report", fields: [direct_report], references: [id], onDelete: {action}, onUpdate: {action}) + User_UserToUser_recruited_by User? @relation("UserToUser_recruited_by", fields: [recruited_by], references: [id], onDelete: {action}, onUpdate: {action}) other_User_UserToUser_direct_report User[] @relation("UserToUser_direct_report") other_User_UserToUser_recruited_by User[] @relation("UserToUser_recruited_by") @@index([direct_report], name: "direct_report") @@index([recruited_by], name: "recruited_by") - } - "##} + }} + "##, action = action} } _ => { - indoc! {r##" - model User { + formatdoc! {r##" + model User {{ id Int @id @default(autoincrement()) recruited_by Int? direct_report Int? - User_UserToUser_direct_report User? @relation("UserToUser_direct_report", fields: [direct_report], references: [id]) - User_UserToUser_recruited_by User? @relation("UserToUser_recruited_by", fields: [recruited_by], references: [id]) + User_UserToUser_direct_report User? @relation("UserToUser_direct_report", fields: [direct_report], references: [id], onDelete: {action}, onUpdate: {action}) + User_UserToUser_recruited_by User? @relation("UserToUser_recruited_by", fields: [recruited_by], references: [id], onDelete: {action}, onUpdate: {action}) other_User_UserToUser_direct_report User[] @relation("UserToUser_direct_report") other_User_UserToUser_recruited_by User[] @relation("UserToUser_recruited_by") - } - "##} + }} + "##, action = action} } }; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -526,19 +572,24 @@ async fn id_fields_with_foreign_key(api: &TestApi) -> TestResult { ) .await?; - let dm = indoc! {r##" - model Post { + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let dm = formatdoc! {r##" + model Post {{ user_id Int @id - User User @relation(fields: [user_id], references: [id]) - } + User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post? - } - "##}; + }} + "##, action = action}; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -567,39 +618,44 @@ async fn duplicate_fks_should_ignore_one_of_them(api: &TestApi) -> TestResult { ) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = match api.sql_family() { SqlFamily::Mysql => { - indoc! {r##" - model Post { + formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int? - User User? @relation(fields: [user_id], references: [id]) + User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) @@index([user_id], name: "user_id") - } + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post[] - } - "##} + }} + "##, action = action} } _ => { - indoc! {r##" - model Post { + formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int? - User User? @relation(fields: [user_id], references: [id]) - } + User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post[] - } - "##} + }} + "##, action = action} } }; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -619,20 +675,25 @@ async fn default_values_on_relations(api: &TestApi) -> TestResult { }) .await?; - let dm = indoc! {r##" - model Post { + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let dm = formatdoc! {r##" + model Post {{ id Int @id @default(autoincrement()) user_id Int? @default(0) - User User? @relation(fields: [user_id], references: [id]) - } + User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post Post[] - } - "##}; + }} + "##, action = action}; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -703,56 +764,61 @@ async fn relations_should_avoid_name_clashes(api: &TestApi) -> TestResult { }) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = match api.sql_family() { SqlFamily::Sqlite => { - indoc! {r##" - model x { + formatdoc! {r##" + model x {{ id Int @id @default(autoincrement()) y Int - y_xToy y @relation(fields: [y], references: [id]) - } + y_xToy y @relation(fields: [y], references: [id], onDelete: NoAction, onUpdate: NoAction) + }} - model y { + model y {{ id Int @id @default(autoincrement()) x Int x_xToy x[] - } + }} "##} } SqlFamily::Mysql => { - indoc! {r##" - model x { + formatdoc! {r##" + model x {{ id Int @id y Int - y_xToy y @relation(fields: [y], references: [id]) + y_xToy y @relation(fields: [y], references: [id], onDelete: {action}, onUpdate: {action}) @@index([y], name: "y") - } + }} - model y { + model y {{ id Int @id x Int x_xToy x[] - } - "##} + }} + "##, action = action} } _ => { - indoc! {r##" - model x { + formatdoc! {r##" + model x {{ id Int @id y Int - y_xToy y @relation(fields: [y], references: [id]) - } + y_xToy y @relation(fields: [y], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model y { + model y {{ id Int @id x Int x_xToy x[] - } - "##} + }} + "##, action = action} } }; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -798,52 +864,57 @@ async fn relations_should_avoid_name_clashes_2(api: &TestApi) -> TestResult { }) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = match sql_family { SqlFamily::Mysql => { - indoc! { r##" - model x { + formatdoc! { r##" + model x {{ id Int @id @default(autoincrement()) y Int - y_x_yToy y @relation("x_yToy", fields: [y], references: [id]) + y_x_yToy y @relation("x_yToy", fields: [y], references: [id], onDelete: {action}, onUpdate: {action}) y_xToy_fk_x_1_fk_x_2 y[] @relation("xToy_fk_x_1_fk_x_2") @@unique([id, y], name: "unique_y_id") @@index([y], name: "y") - } + }} - model y { + model y {{ id Int @id @default(autoincrement()) x Int fk_x_1 Int fk_x_2 Int - x_xToy_fk_x_1_fk_x_2 x @relation("xToy_fk_x_1_fk_x_2", fields: [fk_x_1, fk_x_2], references: [id, y]) + x_xToy_fk_x_1_fk_x_2 x @relation("xToy_fk_x_1_fk_x_2", fields: [fk_x_1, fk_x_2], references: [id, y], onDelete: {action}, onUpdate: {action}) x_x_yToy x[] @relation("x_yToy") @@index([fk_x_1, fk_x_2], name: "fk_x_1") - } - "##} + }} + "##, action = action} } _ => { - indoc! { r##" - model x { + formatdoc! { r##" + model x {{ id Int @id @default(autoincrement()) y Int - y_x_yToy y @relation("x_yToy", fields: [y], references: [id]) + y_x_yToy y @relation("x_yToy", fields: [y], references: [id], onDelete: NoAction, onUpdate: NoAction) y_xToy_fk_x_1_fk_x_2 y[] @relation("xToy_fk_x_1_fk_x_2") @@unique([id, y], name: "unique_y_id") - } + }} - model y { + model y {{ id Int @id @default(autoincrement()) x Int fk_x_1 Int fk_x_2 Int - x_xToy_fk_x_1_fk_x_2 x @relation("xToy_fk_x_1_fk_x_2", fields: [fk_x_1, fk_x_2], references: [id, y]) + x_xToy_fk_x_1_fk_x_2 x @relation("xToy_fk_x_1_fk_x_2", fields: [fk_x_1, fk_x_2], references: [id, y], onDelete: {action}, onUpdate: {action}) x_x_yToy x[] @relation("x_yToy") - } - "##} + }} + "##, action = action} } }; - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -890,14 +961,19 @@ async fn one_to_many_relation_field_names_do_not_conflict_with_many_to_many_rela "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let expected_dm = format!( r#" model Event {{ id Int @id @default(autoincrement()) host_id Int - User_EventToUser User @relation(fields: [host_id], references: [id]) + User_EventToUser User @relation(fields: [host_id], references: [id], onDelete: {action}, onUpdate: {action}) User_EventToUserManyToMany User[] @relation("EventToUserManyToMany") - {} + {extra_index} }} model User {{ @@ -906,7 +982,8 @@ async fn one_to_many_relation_field_names_do_not_conflict_with_many_to_many_rela Event_EventToUserManyToMany Event[] @relation("EventToUserManyToMany") }} "#, - extra_index + action = action, + extra_index = extra_index, ); api.assert_eq_datamodels(&expected_dm, &api.introspect().await?); diff --git a/introspection-engine/introspection-engine-tests/tests/relations_with_compound_fk/mod.rs b/introspection-engine/introspection-engine-tests/tests/relations_with_compound_fk/mod.rs index e7e3568152fb..f100b5852181 100644 --- a/introspection-engine/introspection-engine-tests/tests/relations_with_compound_fk/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/relations_with_compound_fk/mod.rs @@ -1,6 +1,8 @@ use barrel::types; +use datamodel::ReferentialAction; use indoc::indoc; use introspection_engine_tests::test_api::*; +use quaint::prelude::SqlFamily; use test_macros::test_connector; #[test_connector] @@ -32,15 +34,20 @@ async fn compound_foreign_keys_for_one_to_one_relations(api: &TestApi) -> TestRe }) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Post {{ id Int @id @default(autoincrement()) user_id Int? user_age Int? - User User? @relation(fields: [user_id, user_age], references: [id, age]) + User User? @relation(fields: [user_id, user_age], references: [id, age], onDelete: {action}, onUpdate: {action}) - @@unique([user_id, user_age], name: "{}") + @@unique([user_id, user_age], name: "{constraint_name}") }} model User {{ @@ -51,7 +58,8 @@ async fn compound_foreign_keys_for_one_to_one_relations(api: &TestApi) -> TestRe @@unique([id, age], name: "user_unique") }} "#, - constraint_name + constraint_name = constraint_name, + action = action, ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -88,15 +96,20 @@ async fn compound_foreign_keys_for_required_one_to_one_relations(api: &TestApi) }) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Post {{ id Int @id @default(autoincrement()) user_id Int user_age Int - User User @relation(fields: [user_id, user_age], references: [id, age]) + User User @relation(fields: [user_id, user_age], references: [id, age], onDelete: {action}, onUpdate: {action}) - @@unique([user_id, user_age], name: "{}") + @@unique([user_id, user_age], name: "{constraint_name}") }} model User {{ @@ -107,7 +120,8 @@ async fn compound_foreign_keys_for_required_one_to_one_relations(api: &TestApi) @@unique([id, age], name: "user_unique") }} "#, - constraint_name + constraint_name = constraint_name, + action = action ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -142,14 +156,19 @@ async fn compound_foreign_keys_for_one_to_many_relations(api: &TestApi) -> TestR "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Post {{ id Int @id @default(autoincrement()) user_id Int? user_age Int? - User User? @relation(fields: [user_id, user_age], references: [id, age]) - {} + User User? @relation(fields: [user_id, user_age], references: [id, age], onDelete: {action}, onUpdate: {action}) + {extra_index} }} model User {{ @@ -160,7 +179,8 @@ async fn compound_foreign_keys_for_one_to_many_relations(api: &TestApi) -> TestR @@unique([id, age], name: "user_unique") }} "#, - extra_index + action = action, + extra_index = extra_index, ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -195,14 +215,19 @@ async fn compound_foreign_keys_for_one_to_many_relations_with_mixed_requiredness "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Post {{ id Int @id @default(autoincrement()) user_id Int user_age Int? - User User? @relation(fields: [user_id, user_age], references: [id, age]) - {} + User User? @relation(fields: [user_id, user_age], references: [id, age], onDelete: {action}, onUpdate: {action}) + {extra_index} }} model User {{ @@ -213,7 +238,8 @@ async fn compound_foreign_keys_for_one_to_many_relations_with_mixed_requiredness @@unique([id, age], name: "user_unique") }} "#, - extra_index + action = action, + extra_index = extra_index, ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -248,14 +274,19 @@ async fn compound_foreign_keys_for_required_one_to_many_relations(api: &TestApi) "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Post {{ id Int @id @default(autoincrement()) user_id Int user_age Int - User User @relation(fields: [user_id, user_age], references: [id, age]) - {} + User User @relation(fields: [user_id, user_age], references: [id, age], onDelete: {action}, onUpdate: {action}) + {extra_index} }} model User {{ @@ -266,7 +297,8 @@ async fn compound_foreign_keys_for_required_one_to_many_relations(api: &TestApi) @@unique([id, age], name: "user_unique") }} "#, - extra_index + action = action, + extra_index = extra_index ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -302,6 +334,11 @@ async fn compound_foreign_keys_for_required_self_relations(api: &TestApi) -> Tes "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Person {{ @@ -309,14 +346,16 @@ async fn compound_foreign_keys_for_required_self_relations(api: &TestApi) -> Tes age Int partner_id Int partner_age Int - Person Person @relation("PersonToPerson_partner_id_partner_age", fields: [partner_id, partner_age], references: [id, age]) + Person Person @relation("PersonToPerson_partner_id_partner_age", fields: [partner_id, partner_age], references: [id, age], onDelete: {action}, onUpdate: {action}) other_Person Person[] @relation("PersonToPerson_partner_id_partner_age") - @@unique([id, age], name: "{}") - {} + @@unique([id, age], name: "{constraint_name}") + {extra_index} }} "#, - constraint_name, extra_index, + action = action, + constraint_name = constraint_name, + extra_index = extra_index, ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -352,6 +391,11 @@ async fn compound_foreign_keys_for_self_relations(api: &TestApi) -> TestResult { "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Person {{ @@ -359,14 +403,16 @@ async fn compound_foreign_keys_for_self_relations(api: &TestApi) -> TestResult { age Int partner_id Int? partner_age Int? - Person Person? @relation("PersonToPerson_partner_id_partner_age", fields: [partner_id, partner_age], references: [id, age]) + Person Person? @relation("PersonToPerson_partner_id_partner_age", fields: [partner_id, partner_age], references: [id, age], onDelete: {action}, onUpdate: {action}) other_Person Person[] @relation("PersonToPerson_partner_id_partner_age") - @@unique([id, age], name: "{}") - {} + @@unique([id, age], name: "{constraint_name}") + {extra_index} }} "#, - constraint_name, extra_index + action = action, + constraint_name = constraint_name, + extra_index = extra_index ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -402,6 +448,11 @@ async fn compound_foreign_keys_with_defaults(api: &TestApi) -> TestResult { "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Person {{ @@ -409,14 +460,16 @@ async fn compound_foreign_keys_with_defaults(api: &TestApi) -> TestResult { age Int partner_id Int @default(0) partner_age Int @default(0) - Person Person @relation("PersonToPerson_partner_id_partner_age", fields: [partner_id, partner_age], references: [id, age]) + Person Person @relation("PersonToPerson_partner_id_partner_age", fields: [partner_id, partner_age], references: [id, age], onDelete: {action}, onUpdate: {action}) other_Person Person[] @relation("PersonToPerson_partner_id_partner_age") - @@unique([id, age], name: "{}") - {} + @@unique([id, age], name: "{constraint_name}") + {extra_index} }} "#, - constraint_name, extra_index + action = action, + constraint_name = constraint_name, + extra_index = extra_index ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -457,14 +510,19 @@ async fn compound_foreign_keys_for_one_to_many_relations_with_non_unique_index(a "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Post {{ id Int @id @default(autoincrement()) user_id Int user_age Int - User User @relation(fields: [user_id, user_age], references: [id, age]) - {} + User User @relation(fields: [user_id, user_age], references: [id, age], onDelete: {action}, onUpdate: {action}) + {extra_index} }} model User {{ @@ -472,10 +530,12 @@ async fn compound_foreign_keys_for_one_to_many_relations_with_non_unique_index(a age Int Post Post[] - @@unique([id, age], name: "{}") + @@unique([id, age], name: "{constraint_name}") }} "#, - extra_index, constraint_name + action = action, + extra_index = extra_index, + constraint_name = constraint_name ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -508,6 +568,11 @@ async fn repro_matt_references_on_wrong_side(api: &TestApi) -> TestResult { "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model a {{ @@ -523,11 +588,12 @@ async fn repro_matt_references_on_wrong_side(api: &TestApi) -> TestResult { one Int two Int - a a @relation(fields: [one, two], references: [one, two]) - {} + a a @relation(fields: [one, two], references: [one, two], onDelete: {action}, onUpdate: {action}) + {extra_index} }} "#, - extra_index + action = action, + extra_index = extra_index, ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -562,6 +628,11 @@ async fn a_compound_fk_pk_with_overlapping_primary_key(api: &TestApi) -> TestRes "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model a {{ @@ -576,13 +647,14 @@ async fn a_compound_fk_pk_with_overlapping_primary_key(api: &TestApi) -> TestRes dummy Int one Int two Int - a a @relation(fields: [one, two], references: [one, two]) + a a @relation(fields: [one, two], references: [one, two], onDelete: {action}, onUpdate: {action}) @@id([dummy, one, two]) - {} + {extra_index} }} "#, - extra_index + action = action, + extra_index = extra_index, ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -629,6 +701,11 @@ async fn compound_foreign_keys_for_duplicate_one_to_many_relations(api: &TestApi "" }; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Post {{ @@ -637,9 +714,9 @@ async fn compound_foreign_keys_for_duplicate_one_to_many_relations(api: &TestApi user_age Int? other_user_id Int? other_user_age Int? - User_Post_other_user_id_other_user_ageToUser User? @relation("Post_other_user_id_other_user_ageToUser", fields: [other_user_id, other_user_age], references: [id, age]) - User_Post_user_id_user_ageToUser User? @relation("Post_user_id_user_ageToUser", fields: [user_id, user_age], references: [id, age]) - {} + User_Post_other_user_id_other_user_ageToUser User? @relation("Post_other_user_id_other_user_ageToUser", fields: [other_user_id, other_user_age], references: [id, age], onDelete: {action}, onUpdate: {action}) + User_Post_user_id_user_ageToUser User? @relation("Post_user_id_user_ageToUser", fields: [user_id, user_age], references: [id, age], onDelete: {action}, onUpdate: {action}) + {extra_index} }} model User {{ @@ -648,10 +725,12 @@ async fn compound_foreign_keys_for_duplicate_one_to_many_relations(api: &TestApi Post_Post_other_user_id_other_user_ageToUser Post[] @relation("Post_other_user_id_other_user_ageToUser") Post_Post_user_id_user_ageToUser Post[] @relation("Post_user_id_user_ageToUser") - @@unique([id, age], name: "{}") + @@unique([id, age], name: "{constraint_name}") }} "#, - extra_index, constraint_name + action = action, + extra_index = extra_index, + constraint_name = constraint_name ); api.assert_eq_datamodels(&dm, &api.introspect().await?); diff --git a/introspection-engine/introspection-engine-tests/tests/remapping_database_names/mod.rs b/introspection-engine/introspection-engine-tests/tests/remapping_database_names/mod.rs index 07d73ed66b0e..b85d1db5445b 100644 --- a/introspection-engine/introspection-engine-tests/tests/remapping_database_names/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/remapping_database_names/mod.rs @@ -1,8 +1,9 @@ use barrel::types; +use datamodel::ReferentialAction; use indoc::formatdoc; use indoc::indoc; use introspection_engine_tests::test_api::*; -use quaint::prelude::Queryable; +use quaint::prelude::{Queryable, SqlFamily}; use test_macros::test_connector; #[test_connector(tags(Postgres))] @@ -127,24 +128,30 @@ async fn remapping_models_in_relations(api: &TestApi) -> TestResult { }) .await?; - let dm = { + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let dm = formatdoc!( r#" - model Post { + model Post {{ id Int @id @default(autoincrement()) user_id Int @unique - User_with_Space User_with_Space @relation(fields: [user_id], references: [id]) - } + User_with_Space User_with_Space @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + }} - model User_with_Space { + model User_with_Space {{ id Int @id @default(autoincrement()) Post Post? @@map("User with Space") - } - "# - }; + }} + "#, + action = action + ); - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -167,22 +174,30 @@ async fn remapping_models_in_relations_should_not_map_virtual_fields(api: &TestA }) .await?; - let dm = indoc! {r#" - model Post_With_Space { + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + + let dm = formatdoc!( + r#" + model Post_With_Space {{ id Int @id @default(autoincrement()) user_id Int @unique - User User @relation(fields: [user_id], references: [id]) + User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) @@map("Post With Space") - } + }} - model User { + model User {{ id Int @id @default(autoincrement()) Post_With_Space Post_With_Space? - } - "#}; + }} + "#, + action = action + ); - api.assert_eq_datamodels(dm, &api.introspect().await?); + api.assert_eq_datamodels(&dm, &api.introspect().await?); Ok(()) } @@ -225,15 +240,20 @@ async fn remapping_models_in_compound_relations(api: &TestApi) -> TestResult { }) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Post {{ id Int @id @default(autoincrement()) user_id Int user_age Int - User_with_Space User_with_Space @relation(fields: [user_id, user_age], references: [id, age]) + User_with_Space User_with_Space @relation(fields: [user_id, user_age], references: [id, age], onDelete: {action}, onUpdate: {action}) - @@unique([user_id, user_age], name: "{}") + @@unique([user_id, user_age], name: "{post_constraint}") }} model User_with_Space {{ @@ -242,10 +262,12 @@ async fn remapping_models_in_compound_relations(api: &TestApi) -> TestResult { Post Post? @@map("User with Space") - @@unique([id, age], name: "{}") + @@unique([id, age], name: "{user_constraint}") }} "#, - post_constraint, user_constraint + post_constraint = post_constraint, + user_constraint = user_constraint, + action = action ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -294,15 +316,20 @@ async fn remapping_fields_in_compound_relations(api: &TestApi) -> TestResult { }) .await?; + let action = match api.sql_family() { + SqlFamily::Mysql if !api.is_mysql8() => ReferentialAction::Restrict, + _ => ReferentialAction::NoAction, + }; + let dm = format!( r#" model Post {{ id Int @id @default(autoincrement()) user_id Int user_age Int - User User @relation(fields: [user_id, user_age], references: [id, age_that_is_invalid]) + User User @relation(fields: [user_id, user_age], references: [id, age_that_is_invalid], onDelete: {action}, onUpdate: {action}) - @@unique([user_id, user_age], name: "{}") + @@unique([user_id, user_age], name: "{user_post_constraint}") }} model User {{ @@ -310,10 +337,12 @@ async fn remapping_fields_in_compound_relations(api: &TestApi) -> TestResult { age_that_is_invalid Int @map("age-that-is-invalid") Post Post? - @@unique([id, age_that_is_invalid], name: "{}") + @@unique([id, age_that_is_invalid], name: "{user_constraint}") }} "#, - user_post_constraint, user_constraint + user_post_constraint = user_post_constraint, + user_constraint = user_constraint, + action = action ); api.assert_eq_datamodels(&dm, &api.introspect().await?); diff --git a/libs/datamodel/connectors/datamodel-connector/Cargo.toml b/libs/datamodel/connectors/datamodel-connector/Cargo.toml index 8a2d1046880e..d41fb0f55a31 100644 --- a/libs/datamodel/connectors/datamodel-connector/Cargo.toml +++ b/libs/datamodel/connectors/datamodel-connector/Cargo.toml @@ -12,3 +12,4 @@ dml = { path = "../dml" } thiserror = "1.0" itertools = "0.8" url = "2.2.1" +enumflags2 = "0.6" diff --git a/libs/datamodel/connectors/datamodel-connector/src/lib.rs b/libs/datamodel/connectors/datamodel-connector/src/lib.rs index 2d09bb913620..a3e03d848a1f 100644 --- a/libs/datamodel/connectors/datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/datamodel-connector/src/lib.rs @@ -4,8 +4,9 @@ pub mod helper; use crate::connector_error::{ConnectorError, ConnectorErrorFactory, ErrorKind}; use dml::{ field::Field, model::Model, native_type_constructor::NativeTypeConstructor, - native_type_instance::NativeTypeInstance, scalars::ScalarType, + native_type_instance::NativeTypeInstance, relation_info::ReferentialAction, scalars::ScalarType, }; +use enumflags2::BitFlags; use std::{borrow::Cow, collections::BTreeMap}; pub trait Connector: Send + Sync { @@ -17,6 +18,12 @@ pub trait Connector: Send + Sync { self.capabilities().contains(&capability) } + fn referential_actions(&self) -> BitFlags; + + fn supports_referential_action(&self, action: ReferentialAction) -> bool { + self.referential_actions().contains(action) + } + fn validate_field(&self, field: &Field) -> Result<(), ConnectorError>; fn validate_model(&self, model: &Model) -> Result<(), ConnectorError>; @@ -168,6 +175,7 @@ pub enum ConnectorCapability { AutoIncrementMultipleAllowed, AutoIncrementNonIndexedAllowed, RelationFieldsInArbitraryOrder, + ReferentialActions, // start of Query Engine Capabilities InsensitiveFilters, diff --git a/libs/datamodel/connectors/dml/Cargo.toml b/libs/datamodel/connectors/dml/Cargo.toml index 8125a3e51bba..4d5ea81c0f26 100644 --- a/libs/datamodel/connectors/dml/Cargo.toml +++ b/libs/datamodel/connectors/dml/Cargo.toml @@ -14,3 +14,4 @@ chrono = { version = "0.4.6", features = ["serde"] } serde = { version = "1.0.90", features = ["derive"] } serde_json = { version = "1.0", features = ["float_roundtrip"] } native-types = { path = "../../../native-types" } +enumflags2 = "0.6" diff --git a/libs/datamodel/connectors/dml/src/relation_info.rs b/libs/datamodel/connectors/dml/src/relation_info.rs index 24a50a005c3f..dd99df9855c9 100644 --- a/libs/datamodel/connectors/dml/src/relation_info.rs +++ b/libs/datamodel/connectors/dml/src/relation_info.rs @@ -1,3 +1,6 @@ +use enumflags2::BitFlags; +use std::fmt; + /// Holds information about a relation field. #[derive(Debug, Clone)] pub struct RelationInfo { @@ -11,7 +14,10 @@ pub struct RelationInfo { pub name: String, /// A strategy indicating what happens when /// a related node is deleted. - pub on_delete: OnDeleteStrategy, + pub on_delete: Option, + /// A strategy indicating what happens when + /// a related node is deleted. + pub on_update: Option, } impl PartialEq for RelationInfo { @@ -21,6 +27,7 @@ impl PartialEq for RelationInfo { && self.fields == other.fields && self.references == other.references && self.on_delete == other.on_delete + && self.on_update == other.on_update } } @@ -33,23 +40,50 @@ impl RelationInfo { fields: Vec::new(), references: Vec::new(), name: String::new(), - on_delete: OnDeleteStrategy::None, + on_delete: None, + on_update: None, } } } /// Describes what happens when related nodes are deleted. -#[derive(Debug, Copy, PartialEq, Clone)] -pub enum OnDeleteStrategy { - Cascade, - None, +#[repr(u8)] +#[derive(Debug, Copy, PartialEq, Clone, BitFlags)] +pub enum ReferentialAction { + /// Deletes record if dependent record is deleted. Updates relation scalar + /// fields if referenced scalar fields of the dependent record are updated. + /// Prevents operation (both updates and deletes) from succeeding if any + /// records are connected. This behavior will always result in a runtime + /// error for required relations. + Cascade = 1 << 0, + /// Prevents operation (both updates and deletes) from succeeding if any + /// records are connected. This behavior will always result in a runtime + /// error for required relations. + Restrict = 1 << 1, + /// Behavior is database specific. Either defers throwing an integrity check + /// error until the end of the transaction or errors immediately. If + /// deferred, this makes it possible to temporarily violate integrity in a + /// transaction while making sure that subsequent operations in the + /// transaction restore integrity. + NoAction = 1 << 2, + /// Sets relation scalar fields to null if the relation is deleted or + /// updated. This will always result in a runtime error if one or more of the + /// relation scalar fields are required. + SetNull = 1 << 3, + /// Sets relation scalar fields to their default values on update or delete + /// of relation. Will always result in a runtime error if no defaults are + /// provided for any relation scalar fields. + SetDefault = 1 << 4, } -impl ToString for OnDeleteStrategy { - fn to_string(&self) -> String { +impl fmt::Display for ReferentialAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - OnDeleteStrategy::Cascade => String::from("CASCADE"), - OnDeleteStrategy::None => String::from("NONE"), + ReferentialAction::Cascade => write!(f, "Cascade"), + ReferentialAction::Restrict => write!(f, "Restrict"), + ReferentialAction::NoAction => write!(f, "NoAction"), + ReferentialAction::SetNull => write!(f, "SetNull"), + ReferentialAction::SetDefault => write!(f, "SetDefault"), } } } diff --git a/libs/datamodel/connectors/mongodb-datamodel-connector/Cargo.toml b/libs/datamodel/connectors/mongodb-datamodel-connector/Cargo.toml index ef85adb882d3..c94767b10bf9 100644 --- a/libs/datamodel/connectors/mongodb-datamodel-connector/Cargo.toml +++ b/libs/datamodel/connectors/mongodb-datamodel-connector/Cargo.toml @@ -13,3 +13,4 @@ lazy_static = "1.4" native-types = { path = "../../../native-types" } once_cell = "1.3" serde_json = { version = "1.0", features = ["float_roundtrip"] } +enumflags2 = "0.6" diff --git a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs index e16ae73c1d2f..7ff926f9562c 100644 --- a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs @@ -5,8 +5,10 @@ use datamodel_connector::{ Connector, ConnectorCapability, }; use dml::{ - native_type_constructor::NativeTypeConstructor, native_type_instance::NativeTypeInstance, traits::WithDatabaseName, + native_type_constructor::NativeTypeConstructor, native_type_instance::NativeTypeInstance, + relation_info::ReferentialAction, traits::WithDatabaseName, }; +use enumflags2::BitFlags; use mongodb_types::*; use native_types::MongoDbType; use std::result::Result as StdResult; @@ -56,6 +58,10 @@ impl Connector for MongoDbDatamodelConnector { &self.capabilities } + fn referential_actions(&self) -> BitFlags { + BitFlags::empty() + } + fn validate_field(&self, field: &dml::field::Field) -> Result<()> { // WIP, I don't really know what I'm doing with the dml. diff --git a/libs/datamodel/connectors/sql-datamodel-connector/Cargo.toml b/libs/datamodel/connectors/sql-datamodel-connector/Cargo.toml index 1bfe9fe53572..907031c8cc6a 100644 --- a/libs/datamodel/connectors/sql-datamodel-connector/Cargo.toml +++ b/libs/datamodel/connectors/sql-datamodel-connector/Cargo.toml @@ -13,3 +13,4 @@ native-types = { path = "../../../native-types" } serde_json = { version = "1.0", features = ["float_roundtrip"] } once_cell = "1.3" regex = "1" +enumflags2 = "0.6" 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 50a47b9c68eb..4ef1afaa74a6 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 @@ -1,14 +1,19 @@ use datamodel_connector::connector_error::ConnectorError; use datamodel_connector::helper::{arg_vec_from_opt, args_vec_from_opt, parse_one_opt_u32, parse_two_opt_u32}; use datamodel_connector::{Connector, ConnectorCapability}; -use dml::field::{Field, FieldType}; -use dml::model::{IndexType, Model}; -use dml::native_type_constructor::NativeTypeConstructor; -use dml::native_type_instance::NativeTypeInstance; -use dml::scalars::ScalarType; +use dml::{ + field::{Field, FieldType}, + model::{IndexType, Model}, + native_type_constructor::NativeTypeConstructor, + native_type_instance::NativeTypeInstance, + relation_info::ReferentialAction, + scalars::ScalarType, +}; +use enumflags2::BitFlags; use native_types::{MsSqlType, MsSqlTypeParameter}; use once_cell::sync::Lazy; use std::borrow::Cow; + use MsSqlType::*; use MsSqlTypeParameter::*; @@ -44,10 +49,13 @@ const UNIQUE_IDENTIFIER_TYPE_NAME: &str = "UniqueIdentifier"; pub struct MsSqlDatamodelConnector { capabilities: Vec, constructors: Vec, + referential_actions: BitFlags, } impl MsSqlDatamodelConnector { pub fn new() -> MsSqlDatamodelConnector { + use ReferentialAction::*; + let capabilities = vec![ ConnectorCapability::AutoIncrementAllowedOnNonId, ConnectorCapability::AutoIncrementMultipleAllowed, @@ -56,6 +64,7 @@ impl MsSqlDatamodelConnector { ConnectorCapability::UpdateableId, ConnectorCapability::MultipleIndexesWithSameName, ConnectorCapability::AutoIncrement, + ConnectorCapability::ReferentialActions, ]; let constructors: Vec = vec![ @@ -89,9 +98,12 @@ impl MsSqlDatamodelConnector { NativeTypeConstructor::without_args(UNIQUE_IDENTIFIER_TYPE_NAME, vec![ScalarType::String]), ]; + let referential_actions = NoAction | Cascade | SetNull | SetDefault; + MsSqlDatamodelConnector { capabilities, constructors, + referential_actions, } } @@ -141,6 +153,10 @@ impl Connector for MsSqlDatamodelConnector { &self.capabilities } + fn referential_actions(&self) -> BitFlags { + self.referential_actions + } + fn scalar_type_for_native_type(&self, native_type: serde_json::Value) -> ScalarType { let native_type: MsSqlType = serde_json::from_value(native_type).unwrap(); diff --git a/libs/datamodel/connectors/sql-datamodel-connector/src/mysql_datamodel_connector.rs b/libs/datamodel/connectors/sql-datamodel-connector/src/mysql_datamodel_connector.rs index 88b0a8a70080..137e2030689a 100644 --- a/libs/datamodel/connectors/sql-datamodel-connector/src/mysql_datamodel_connector.rs +++ b/libs/datamodel/connectors/sql-datamodel-connector/src/mysql_datamodel_connector.rs @@ -1,13 +1,18 @@ -use datamodel_connector::connector_error::ConnectorError; -use datamodel_connector::helper::{args_vec_from_opt, parse_one_opt_u32, parse_one_u32, parse_two_opt_u32}; -use datamodel_connector::{Connector, ConnectorCapability}; -use dml::field::{Field, FieldType}; -use dml::model::{IndexType, Model}; -use dml::native_type_constructor::NativeTypeConstructor; -use dml::native_type_instance::NativeTypeInstance; -use dml::scalars::ScalarType; -use native_types::MySqlType; -use native_types::MySqlType::*; +use datamodel_connector::{ + connector_error::ConnectorError, + helper::{args_vec_from_opt, parse_one_opt_u32, parse_one_u32, parse_two_opt_u32}, + Connector, ConnectorCapability, +}; +use dml::{ + field::{Field, FieldType}, + model::{IndexType, Model}, + native_type_constructor::NativeTypeConstructor, + native_type_instance::NativeTypeInstance, + relation_info::ReferentialAction, + scalars::ScalarType, +}; +use enumflags2::BitFlags; +use native_types::MySqlType::{self, *}; const INT_TYPE_NAME: &str = "Int"; const UNSIGNED_INT_TYPE_NAME: &str = "UnsignedInt"; @@ -56,10 +61,13 @@ const NATIVE_TYPES_THAT_CAN_NOT_BE_USED_IN_KEY_SPECIFICATION: &[&str] = &[ pub struct MySqlDatamodelConnector { capabilities: Vec, constructors: Vec, + referential_actions: BitFlags, } impl MySqlDatamodelConnector { pub fn new() -> MySqlDatamodelConnector { + use ReferentialAction::*; + let capabilities = vec![ ConnectorCapability::RelationsOverNonUniqueCriteria, ConnectorCapability::Enums, @@ -74,6 +82,7 @@ impl MySqlDatamodelConnector { ConnectorCapability::JsonFilteringJsonPath, ConnectorCapability::CreateManyWriteableAutoIncId, ConnectorCapability::AutoIncrement, + ConnectorCapability::ReferentialActions, ]; let int = NativeTypeConstructor::without_args(INT_TYPE_NAME, vec![ScalarType::Int]); @@ -148,9 +157,12 @@ impl MySqlDatamodelConnector { json, ]; + let referential_actions = Restrict | Cascade | SetNull | NoAction | SetDefault; + MySqlDatamodelConnector { capabilities, constructors, + referential_actions, } } } @@ -176,6 +188,10 @@ impl Connector for MySqlDatamodelConnector { &self.capabilities } + fn referential_actions(&self) -> BitFlags { + self.referential_actions + } + fn scalar_type_for_native_type(&self, native_type: serde_json::Value) -> ScalarType { let native_type: MySqlType = serde_json::from_value(native_type).unwrap(); diff --git a/libs/datamodel/connectors/sql-datamodel-connector/src/postgres_datamodel_connector.rs b/libs/datamodel/connectors/sql-datamodel-connector/src/postgres_datamodel_connector.rs index 95715547ccc9..ad97a13cce24 100644 --- a/libs/datamodel/connectors/sql-datamodel-connector/src/postgres_datamodel_connector.rs +++ b/libs/datamodel/connectors/sql-datamodel-connector/src/postgres_datamodel_connector.rs @@ -1,13 +1,18 @@ -use datamodel_connector::connector_error::ConnectorError; -use datamodel_connector::helper::{arg_vec_from_opt, args_vec_from_opt, parse_one_opt_u32, parse_two_opt_u32}; -use datamodel_connector::{Connector, ConnectorCapability}; -use dml::field::{Field, FieldType}; -use dml::model::Model; -use dml::native_type_constructor::NativeTypeConstructor; -use dml::native_type_instance::NativeTypeInstance; -use dml::scalars::ScalarType; -use native_types::PostgresType; -use native_types::PostgresType::*; +use datamodel_connector::{ + connector_error::ConnectorError, + helper::{arg_vec_from_opt, args_vec_from_opt, parse_one_opt_u32, parse_two_opt_u32}, + Connector, ConnectorCapability, +}; +use dml::{ + field::{Field, FieldType}, + model::Model, + native_type_constructor::NativeTypeConstructor, + native_type_instance::NativeTypeInstance, + relation_info::ReferentialAction, + scalars::ScalarType, +}; +use enumflags2::BitFlags; +use native_types::PostgresType::{self, *}; const SMALL_INT_TYPE_NAME: &str = "SmallInt"; const INTEGER_TYPE_NAME: &str = "Integer"; @@ -39,11 +44,14 @@ const JSON_B_TYPE_NAME: &str = "JsonB"; pub struct PostgresDatamodelConnector { capabilities: Vec, constructors: Vec, + referential_actions: BitFlags, } //todo should this also contain the pretty printed output for SQL rendering? impl PostgresDatamodelConnector { pub fn new() -> PostgresDatamodelConnector { + use ReferentialAction::*; + let capabilities = vec![ ConnectorCapability::ScalarLists, ConnectorCapability::Enums, @@ -60,6 +68,7 @@ impl PostgresDatamodelConnector { ConnectorCapability::JsonFilteringArrayPath, ConnectorCapability::CreateManyWriteableAutoIncId, ConnectorCapability::AutoIncrement, + ConnectorCapability::ReferentialActions, ]; let small_int = NativeTypeConstructor::without_args(SMALL_INT_TYPE_NAME, vec![ScalarType::Int]); @@ -119,9 +128,12 @@ impl PostgresDatamodelConnector { json_b, ]; + let referential_actions = NoAction | Restrict | Cascade | SetNull | SetDefault; + PostgresDatamodelConnector { capabilities, constructors, + referential_actions, } } } @@ -147,6 +159,10 @@ impl Connector for PostgresDatamodelConnector { &self.capabilities } + fn referential_actions(&self) -> BitFlags { + self.referential_actions + } + fn scalar_type_for_native_type(&self, native_type: serde_json::Value) -> ScalarType { let native_type: PostgresType = serde_json::from_value(native_type).unwrap(); diff --git a/libs/datamodel/connectors/sql-datamodel-connector/src/sqlite_datamodel_connector.rs b/libs/datamodel/connectors/sql-datamodel-connector/src/sqlite_datamodel_connector.rs index 6d4ab1dd72b0..a88e84eb4541 100644 --- a/libs/datamodel/connectors/sql-datamodel-connector/src/sqlite_datamodel_connector.rs +++ b/libs/datamodel/connectors/sql-datamodel-connector/src/sqlite_datamodel_connector.rs @@ -1,28 +1,35 @@ use datamodel_connector::{connector_error::ConnectorError, Connector, ConnectorCapability}; -use dml::model::Model; -use dml::native_type_constructor::NativeTypeConstructor; -use dml::native_type_instance::NativeTypeInstance; -use dml::{field::Field, scalars::ScalarType}; +use dml::{ + field::Field, model::Model, native_type_constructor::NativeTypeConstructor, + native_type_instance::NativeTypeInstance, relation_info::ReferentialAction, scalars::ScalarType, +}; +use enumflags2::BitFlags; use std::borrow::Cow; pub struct SqliteDatamodelConnector { capabilities: Vec, constructors: Vec, + referential_actions: BitFlags, } impl SqliteDatamodelConnector { pub fn new() -> SqliteDatamodelConnector { + use ReferentialAction::*; + let capabilities = vec![ ConnectorCapability::RelationFieldsInArbitraryOrder, ConnectorCapability::UpdateableId, ConnectorCapability::AutoIncrement, + ConnectorCapability::ReferentialActions, ]; let constructors: Vec = vec![]; + let referential_actions = SetNull | SetDefault | Cascade | Restrict | NoAction; SqliteDatamodelConnector { capabilities, constructors, + referential_actions, } } } @@ -31,10 +38,15 @@ impl Connector for SqliteDatamodelConnector { fn name(&self) -> String { "sqlite".to_string() } + fn capabilities(&self) -> &Vec { &self.capabilities } + fn referential_actions(&self) -> BitFlags { + self.referential_actions + } + fn scalar_type_for_native_type(&self, _native_type: serde_json::Value) -> ScalarType { unreachable!("No native types on Sqlite"); } diff --git a/libs/datamodel/core/src/json/dmmf/to_dmmf.rs b/libs/datamodel/core/src/json/dmmf/to_dmmf.rs index cf1f58a55866..c42e12df07aa 100644 --- a/libs/datamodel/core/src/json/dmmf/to_dmmf.rs +++ b/libs/datamodel/core/src/json/dmmf/to_dmmf.rs @@ -204,7 +204,7 @@ fn get_relation_to_fields(field: &dml::Field) -> Option> { fn get_relation_delete_strategy(field: &dml::Field) -> Option { match &field { - dml::Field::RelationField(rf) => Some(rf.relation_info.on_delete.to_string()), + dml::Field::RelationField(rf) => rf.relation_info.on_delete.map(|ri| ri.to_string()), _ => None, } } diff --git a/libs/datamodel/core/src/transform/ast_to_dml/standardise_formatting.rs b/libs/datamodel/core/src/transform/ast_to_dml/standardise_formatting.rs index 346ccdd38831..ea2f6929f155 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/standardise_formatting.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/standardise_formatting.rs @@ -1,8 +1,6 @@ use super::common::*; use crate::diagnostics::DatamodelError; -use crate::{ - ast, common::NameNormalizer, diagnostics::Diagnostics, dml, Field, OnDeleteStrategy, ScalarField, UniqueCriteria, -}; +use crate::{ast, common::NameNormalizer, diagnostics::Diagnostics, dml, Field, ScalarField, UniqueCriteria}; use itertools::Itertools; use std::collections::HashMap; @@ -212,7 +210,8 @@ impl StandardiserForFormatting { fields: vec![], references: vec![], name: rel_info.name.clone(), - on_delete: OnDeleteStrategy::None, + on_delete: None, + on_update: None, }; let mut opposite_relation_field = dml::RelationField::new_generated(&model.name, relation_info, false); @@ -289,7 +288,8 @@ impl StandardiserForFormatting { fields: underlying_field_names, references: unique_criteria_field_names, name: rel_info.name.clone(), - on_delete: OnDeleteStrategy::None, + on_delete: None, + on_update: None, }; let is_required = all_existing_underlying_fields_on_opposite_model_are_required 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 4a11150b41dc..b1194637c9fa 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/validate.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/validate.rs @@ -6,6 +6,7 @@ use crate::{ DefaultValue, FieldType, }; use crate::{ast::WithAttributes, walkers::walk_models}; +use datamodel_connector::ConnectorCapability; use itertools::Itertools; use prisma_value::PrismaValue; use std::collections::{HashMap, HashSet}; @@ -505,6 +506,38 @@ impl<'a> Validator<'a> { ast_model.find_field(&field.name()).span, )); } + + if let dml::Field::RelationField(ref rf) = field { + let actions = &[rf.relation_info.on_delete, rf.relation_info.on_update]; + + actions.iter().flatten().for_each(|action| { + if !connector.has_capability(ConnectorCapability::ReferentialActions) { + diagnostics.push_error(DatamodelError::new_attribute_validation_error( + "Referential actions are not supported for current connector.", + "relation", + ast_model.find_field(field.name()).span, + )); + } else if !connector.supports_referential_action(*action) { + let allowed_values: Vec<_> = connector + .referential_actions() + .iter() + .map(|f| format!("`{}`", f)) + .collect(); + + let message = format!( + "Invalid referential action: `{}`. Allowed values: ({})", + action, + allowed_values.join(", "), + ); + + diagnostics.push_error(DatamodelError::new_attribute_validation_error( + &message, + "relation", + ast_model.find_field(field.name()).span, + )); + } + }); + } } } diff --git a/libs/datamodel/core/src/transform/attributes/relation.rs b/libs/datamodel/core/src/transform/attributes/relation.rs index 7d0cbb56e0f0..ffcdc333e910 100644 --- a/libs/datamodel/core/src/transform/attributes/relation.rs +++ b/libs/datamodel/core/src/transform/attributes/relation.rs @@ -32,6 +32,14 @@ impl AttributeValidator for RelationAttributeValidator { rf.relation_info.fields = base_fields.as_array().to_literal_vec()?; } + if let Ok(on_delete) = args.arg("onDelete") { + rf.relation_info.on_delete = Some(on_delete.as_referential_action()?); + } + + if let Ok(on_update) = args.arg("onUpdate") { + rf.relation_info.on_update = Some(on_update.as_referential_action()?); + } + Ok(()) } else { self.new_attribute_validation_error("Invalid field type, not a relation.", args.span()) @@ -95,11 +103,16 @@ impl AttributeValidator for RelationAttributeValidator { } } - if relation_info.on_delete != dml::OnDeleteStrategy::None { - args.push(ast::Argument::new_constant( - "onDelete", - &relation_info.on_delete.to_string(), - )); + 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)); + } + + 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 !args.is_empty() { diff --git a/libs/datamodel/core/src/transform/dml_to_ast/lower.rs b/libs/datamodel/core/src/transform/dml_to_ast/lower.rs index cf57a19ce16c..cf3b0b87bd4e 100644 --- a/libs/datamodel/core/src/transform/dml_to_ast/lower.rs +++ b/libs/datamodel/core/src/transform/dml_to_ast/lower.rs @@ -72,6 +72,7 @@ impl<'a> LowerDmlToAst<'a> { pub fn lower_field(&self, field: &dml::Field, datamodel: &dml::Datamodel) -> ast::Field { let mut attributes = self.attributes.field.serialize(field, datamodel); + if let (Some((scalar_type, native_type)), Some(datasource)) = ( field.as_scalar_field().and_then(|sf| sf.field_type.as_native_type()), self.datasource, diff --git a/libs/datamodel/core/src/transform/helpers/value_validator.rs b/libs/datamodel/core/src/transform/helpers/value_validator.rs index 2ceee380e33d..e0509e222c92 100644 --- a/libs/datamodel/core/src/transform/helpers/value_validator.rs +++ b/libs/datamodel/core/src/transform/helpers/value_validator.rs @@ -8,6 +8,7 @@ use crate::{ }; use bigdecimal::BigDecimal; use chrono::{DateTime, FixedOffset}; +use dml::relation_info::ReferentialAction; use dml::scalars::ScalarType; use prisma_value::PrismaValue; use std::error; @@ -171,6 +172,25 @@ impl ValueValidator { } } + pub fn as_referential_action(&self) -> Result { + match self.as_constant_literal()?.as_str() { + "Cascade" => Ok(ReferentialAction::Cascade), + "Restrict" => Ok(ReferentialAction::Restrict), + "NoAction" => Ok(ReferentialAction::NoAction), + "SetNull" => Ok(ReferentialAction::SetNull), + "SetDefault" => Ok(ReferentialAction::SetDefault), + s => { + let message = format!("Invalid referential action: `{}`", s); + + Err(DatamodelError::AttributeValidationError { + message, + attribute_name: String::from("relation"), + span: self.span(), + }) + } + } + } + /// Unwraps the wrapped value as a constant literal.. pub fn as_array(&self) -> Vec { match &self.value { diff --git a/libs/datamodel/core/src/walkers.rs b/libs/datamodel/core/src/walkers.rs index de0bf4c9fac2..9c9a3e190466 100644 --- a/libs/datamodel/core/src/walkers.rs +++ b/libs/datamodel/core/src/walkers.rs @@ -7,7 +7,7 @@ use crate::{ }, NativeTypeInstance, RelationField, }; -use dml::scalars::ScalarType; +use dml::{relation_info::ReferentialAction, scalars::ScalarType}; use itertools::Itertools; /// Iterator over all the models in the schema. @@ -341,6 +341,14 @@ impl<'a> RelationFieldWalker<'a> { }), } } + + pub fn on_update_action(&self) -> Option { + self.get().relation_info.on_update + } + + pub fn on_delete_action(&self) -> Option { + self.get().relation_info.on_delete + } } #[derive(Debug, Clone, Copy)] diff --git a/libs/datamodel/core/tests/attributes/relations/mod.rs b/libs/datamodel/core/tests/attributes/relations/mod.rs index 0a6e722b5c97..ed67e30dcf26 100644 --- a/libs/datamodel/core/tests/attributes/relations/mod.rs +++ b/libs/datamodel/core/tests/attributes/relations/mod.rs @@ -1,3 +1,4 @@ +pub mod referential_actions; pub mod relations_negative; pub mod relations_new; pub mod relations_positive; diff --git a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs new file mode 100644 index 000000000000..9c12b5705e76 --- /dev/null +++ b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs @@ -0,0 +1,160 @@ +use crate::common::*; +use datamodel::{ast::Span, diagnostics::DatamodelError, ReferentialAction::*}; +use indoc::{formatdoc, indoc}; + +#[test] +fn on_delete_actions() { + let actions = &[Cascade, Restrict, NoAction, SetNull, SetDefault]; + + for action in actions { + let dml = formatdoc!( + r#" + model A {{ + id Int @id + bs B[] + }} + + model B {{ + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onDelete: {}) + }} + "#, + action + ); + + parse(&dml) + .assert_has_model("B") + .assert_has_relation_field("a") + .assert_relation_delete_strategy(*action); + } +} + +#[test] +fn on_update_actions() { + let actions = &[Cascade, Restrict, NoAction, SetNull, SetDefault]; + + for action in actions { + let dml = formatdoc!( + r#" + model A {{ + id Int @id + bs B[] + }} + + model B {{ + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onUpdate: {}) + }} + "#, + action + ); + + parse(&dml) + .assert_has_model("B") + .assert_has_relation_field("a") + .assert_relation_update_strategy(*action); + } +} + +#[test] +fn invalid_on_delete_action() { + let dml = indoc! { r#" + model A { + id Int @id + bs B[] + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onDelete: MeowMeow) + } + "#}; + + parse_error(dml).assert_is(DatamodelError::new_attribute_validation_error( + "Invalid referential action: `MeowMeow`", + "relation", + Span::new(137, 145), + )); +} + +#[test] +fn invalid_on_update_action() { + let dml = indoc! { r#" + model A { + id Int @id + bs B[] + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onUpdate: MeowMeow) + } + "#}; + + parse_error(dml).assert_is(DatamodelError::new_attribute_validation_error( + "Invalid referential action: `MeowMeow`", + "relation", + Span::new(137, 145), + )); +} + +#[test] +fn restrict_should_not_work_on_sql_server() { + let dml = indoc! { r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + model A { + id Int @id + bs B[] + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onUpdate: Restrict, onDelete: Restrict) + } + "#}; + + let message = + "Invalid referential action: `Restrict`. Allowed values: (`Cascade`, `NoAction`, `SetNull`, `SetDefault`)"; + + parse_error(dml).assert_are(&[ + DatamodelError::new_attribute_validation_error(&message, "relation", Span::new(151, 238)), + DatamodelError::new_attribute_validation_error(&message, "relation", Span::new(151, 238)), + ]); +} + +#[test] +fn nothing_should_work_on_mongo() { + let dml = indoc! {r#" + datasource db { + provider = "mongodb" + url = "mongodb://" + } + + model A { + id Int @id @map("_id") + bs B[] + } + + model B { + id Int @id @map("_id") + aId Int + a A @relation(fields: [aId], references: [id], onUpdate: Cascade, onDelete: Cascade) + } + "#}; + + let message = "Referential actions are not supported for current connector."; + + parse_error(&dml).assert_are(&[ + DatamodelError::new_attribute_validation_error(&message, "relation", Span::new(171, 256)), + DatamodelError::new_attribute_validation_error(&message, "relation", Span::new(171, 256)), + ]); +} diff --git a/libs/datamodel/core/tests/common.rs b/libs/datamodel/core/tests/common.rs index 06bf1f1d8695..54f5a42e128b 100644 --- a/libs/datamodel/core/tests/common.rs +++ b/libs/datamodel/core/tests/common.rs @@ -32,7 +32,8 @@ pub trait ScalarFieldAsserts { pub trait RelationFieldAsserts { fn assert_relation_name(&self, t: &str) -> &Self; fn assert_relation_to(&self, t: &str) -> &Self; - fn assert_relation_delete_strategy(&self, t: dml::OnDeleteStrategy) -> &Self; + fn assert_relation_delete_strategy(&self, t: dml::ReferentialAction) -> &Self; + fn assert_relation_update_strategy(&self, t: dml::ReferentialAction) -> &Self; fn assert_relation_referenced_fields(&self, t: &[&str]) -> &Self; fn assert_relation_base_fields(&self, t: &[&str]) -> &Self; fn assert_ignored(&self, state: bool) -> &Self; @@ -202,8 +203,13 @@ impl RelationFieldAsserts for dml::RelationField { self } - fn assert_relation_delete_strategy(&self, t: dml::OnDeleteStrategy) -> &Self { - assert_eq!(self.relation_info.on_delete, t); + fn assert_relation_delete_strategy(&self, t: dml::ReferentialAction) -> &Self { + assert_eq!(self.relation_info.on_delete, Some(t)); + self + } + + fn assert_relation_update_strategy(&self, t: dml::ReferentialAction) -> &Self { + assert_eq!(self.relation_info.on_update, Some(t)); self } diff --git a/libs/sql-ddl/src/mysql.rs b/libs/sql-ddl/src/mysql.rs index 07aff6069f87..91db45066ba8 100644 --- a/libs/sql-ddl/src/mysql.rs +++ b/libs/sql-ddl/src/mysql.rs @@ -111,7 +111,7 @@ impl<'a> Display for ForeignKey<'a> { #[derive(Debug)] pub enum ForeignKeyAction { Cascade, - DoNothing, + NoAction, Restrict, SetDefault, SetNull, @@ -122,7 +122,7 @@ impl Display for ForeignKeyAction { let s = match self { ForeignKeyAction::Cascade => "CASCADE", ForeignKeyAction::Restrict => "RESTRICT", - ForeignKeyAction::DoNothing => "DO NOTHING", + ForeignKeyAction::NoAction => "NO ACTION", ForeignKeyAction::SetNull => "SET NULL", ForeignKeyAction::SetDefault => "SET DEFAULT", }; diff --git a/libs/sql-ddl/src/postgres.rs b/libs/sql-ddl/src/postgres.rs index c2cf73c26822..78ddfbfc406b 100644 --- a/libs/sql-ddl/src/postgres.rs +++ b/libs/sql-ddl/src/postgres.rs @@ -222,7 +222,7 @@ impl Display for ForeignKey<'_> { #[derive(Debug)] pub enum ForeignKeyAction { Cascade, - DoNothing, + NoAction, Restrict, SetDefault, SetNull, @@ -233,7 +233,7 @@ impl Display for ForeignKeyAction { let s = match self { ForeignKeyAction::Cascade => "CASCADE", ForeignKeyAction::Restrict => "RESTRICT", - ForeignKeyAction::DoNothing => "DO NOTHING", + ForeignKeyAction::NoAction => "NO ACTION", ForeignKeyAction::SetNull => "SET NULL", ForeignKeyAction::SetDefault => "SET DEFAULT", }; diff --git a/libs/sql-schema-describer/src/sqlite.rs b/libs/sql-schema-describer/src/sqlite.rs index f31da0661a9b..c1755e8c5fb2 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 row + let on_delete_action = match dbg!(&row) .get("on_delete") .and_then(|x| x.to_string()) .expect("on_delete") diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/common.rs b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/common.rs index 3288438e5220..55b7fdc9a9f3 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/common.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/common.rs @@ -67,13 +67,13 @@ pub(crate) fn render_nullability(column: &ColumnWalker<'_>) -> &'static str { } } -pub(crate) fn render_on_delete(on_delete: &ForeignKeyAction) -> &'static str { - match on_delete { - ForeignKeyAction::NoAction => "", - ForeignKeyAction::SetNull => "ON DELETE SET NULL", - ForeignKeyAction::Cascade => "ON DELETE CASCADE", - ForeignKeyAction::SetDefault => "ON DELETE SET DEFAULT", - ForeignKeyAction::Restrict => "ON DELETE RESTRICT", +pub(crate) fn render_referential_action(action: &ForeignKeyAction) -> &'static str { + match action { + ForeignKeyAction::NoAction => "NO ACTION", + ForeignKeyAction::Restrict => "RESTRICT", + ForeignKeyAction::Cascade => "CASCADE", + ForeignKeyAction::SetNull => "SET NULL", + ForeignKeyAction::SetDefault => "SET DEFAULT", } } diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/mssql_renderer.rs b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/mssql_renderer.rs index f2ff252db8b6..cc702c4bdd0f 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/mssql_renderer.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/mssql_renderer.rs @@ -1,7 +1,7 @@ mod alter_table; use super::{ - common::{self, render_on_delete}, + common::{self, render_referential_action}, IteratorJoin, Quoted, SqlRenderer, }; use crate::{ @@ -77,10 +77,11 @@ impl MssqlFlavour { .join(","); format!( - " REFERENCES {}({}) {} ON UPDATE CASCADE", + " REFERENCES {}({}) ON DELETE {} ON UPDATE {}", self.quote_with_schema(&foreign_key.referenced_table().name()), cols, - render_on_delete(&foreign_key.on_delete_action()), + render_referential_action(&foreign_key.on_delete_action()), + render_referential_action(&foreign_key.on_update_action()), ) } } diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/mysql_renderer.rs b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/mysql_renderer.rs index 3941ce028064..68e38b8c3faa 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/mysql_renderer.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/mysql_renderer.rs @@ -66,14 +66,14 @@ impl SqlRenderer for MysqlFlavour { .collect(), on_delete: Some(match foreign_key.on_delete_action() { ForeignKeyAction::Cascade => ddl::ForeignKeyAction::Cascade, - ForeignKeyAction::NoAction => ddl::ForeignKeyAction::DoNothing, + ForeignKeyAction::NoAction => ddl::ForeignKeyAction::NoAction, ForeignKeyAction::Restrict => ddl::ForeignKeyAction::Restrict, ForeignKeyAction::SetDefault => ddl::ForeignKeyAction::SetDefault, ForeignKeyAction::SetNull => ddl::ForeignKeyAction::SetNull, }), on_update: Some(match foreign_key.on_update_action() { ForeignKeyAction::Cascade => ddl::ForeignKeyAction::Cascade, - ForeignKeyAction::NoAction => ddl::ForeignKeyAction::DoNothing, + ForeignKeyAction::NoAction => ddl::ForeignKeyAction::NoAction, ForeignKeyAction::Restrict => ddl::ForeignKeyAction::Restrict, ForeignKeyAction::SetDefault => ddl::ForeignKeyAction::SetDefault, ForeignKeyAction::SetNull => ddl::ForeignKeyAction::SetNull, diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/postgres_renderer.rs b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/postgres_renderer.rs index 7eeb06a66090..9f2943834a7b 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/postgres_renderer.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/postgres_renderer.rs @@ -52,14 +52,14 @@ impl SqlRenderer for PostgresFlavour { referenced_table: foreign_key.referenced_table().name().into(), on_delete: Some(match foreign_key.on_delete_action() { ForeignKeyAction::Cascade => ddl::ForeignKeyAction::Cascade, - ForeignKeyAction::NoAction => ddl::ForeignKeyAction::DoNothing, + ForeignKeyAction::NoAction => ddl::ForeignKeyAction::NoAction, ForeignKeyAction::Restrict => ddl::ForeignKeyAction::Restrict, ForeignKeyAction::SetDefault => ddl::ForeignKeyAction::SetDefault, ForeignKeyAction::SetNull => ddl::ForeignKeyAction::SetNull, }), on_update: Some(match foreign_key.on_update_action() { ForeignKeyAction::Cascade => ddl::ForeignKeyAction::Cascade, - ForeignKeyAction::NoAction => ddl::ForeignKeyAction::DoNothing, + ForeignKeyAction::NoAction => ddl::ForeignKeyAction::NoAction, ForeignKeyAction::Restrict => ddl::ForeignKeyAction::Restrict, ForeignKeyAction::SetDefault => ddl::ForeignKeyAction::SetDefault, ForeignKeyAction::SetNull => ddl::ForeignKeyAction::SetNull, diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/sqlite_renderer.rs b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/sqlite_renderer.rs index af1d0059a59b..4293916695e5 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_renderer/sqlite_renderer.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_renderer/sqlite_renderer.rs @@ -100,7 +100,13 @@ impl SqlRenderer for SqliteFlavour { ForeignKeyAction::SetNull => sql_ddl::sqlite::ForeignKeyAction::SetNull, ForeignKeyAction::SetDefault => sql_ddl::sqlite::ForeignKeyAction::SetDefault, }), - on_update: Some(sql_ddl::sqlite::ForeignKeyAction::Cascade), + on_update: Some(match fk.on_update_action() { + ForeignKeyAction::NoAction => sql_ddl::sqlite::ForeignKeyAction::NoAction, + ForeignKeyAction::Restrict => sql_ddl::sqlite::ForeignKeyAction::Restrict, + ForeignKeyAction::Cascade => sql_ddl::sqlite::ForeignKeyAction::Cascade, + ForeignKeyAction::SetNull => sql_ddl::sqlite::ForeignKeyAction::SetNull, + ForeignKeyAction::SetDefault => sql_ddl::sqlite::ForeignKeyAction::SetDefault, + }), }) .collect(), }; diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator.rs b/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator.rs index 755c49959757..10cc8d0852a1 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator.rs @@ -3,13 +3,12 @@ mod sql_schema_calculator_flavour; pub(super) use sql_schema_calculator_flavour::SqlSchemaCalculatorFlavour; use crate::{flavour::SqlFlavour, sql_renderer::IteratorJoin}; -use datamodel::walkers::RelationFieldWalker; use datamodel::{ walkers::{walk_models, walk_relations, ModelWalker, ScalarFieldWalker, TypeWalker}, Datamodel, DefaultValue, FieldArity, IndexDefinition, IndexType, ScalarType, }; use prisma_value::PrismaValue; -use sql_schema_describer::{self as sql, walkers::SqlSchemaExt, ColumnType, ForeignKeyAction}; +use sql_schema_describer::{self as sql, walkers::SqlSchemaExt, ColumnType}; pub(crate) fn calculate_sql_schema(datamodel: &Datamodel, flavour: &dyn SqlFlavour) -> sql::SqlSchema { let mut schema = sql::SqlSchema::empty(); @@ -93,13 +92,13 @@ fn calculate_model_tables<'a>( foreign_keys: Vec::new(), }; - push_inline_relations(model, &mut table); + push_inline_relations(model, &mut table, flavour); table }) } -fn push_inline_relations(model: ModelWalker<'_>, table: &mut sql::Table) { +fn push_inline_relations(model: ModelWalker<'_>, table: &mut sql::Table, flavour: &dyn SqlFlavour) { let relation_fields = model .relation_fields() .filter(|relation_field| !relation_field.is_virtual()); @@ -119,8 +118,8 @@ fn push_inline_relations(model: ModelWalker<'_>, table: &mut sql::Table) { columns: fk_columns, referenced_table: relation_field.referenced_model().database_name().to_owned(), referenced_columns: relation_field.referenced_columns().map(String::from).collect(), - on_update_action: sql::ForeignKeyAction::Cascade, - on_delete_action: calculate_on_delete_action(relation_field), + on_update_action: flavour.on_update_action(&relation_field), + on_delete_action: flavour.on_delete_action(&relation_field), }; table.foreign_keys.push(fk); @@ -128,14 +127,6 @@ fn push_inline_relations(model: ModelWalker<'_>, table: &mut sql::Table) { } } -fn calculate_on_delete_action(relation_field: RelationFieldWalker<'_>) -> ForeignKeyAction { - if relation_field.scalar_arities().any(|ar| ar.is_required()) { - sql::ForeignKeyAction::Cascade - } else { - sql::ForeignKeyAction::SetNull - } -} - fn push_one_to_one_relation_unique_index(column_names: &[String], table: &mut sql::Table) { // Don't add a duplicate index. if table diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour.rs b/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour.rs index 17534065b26e..6c02eecb697b 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour.rs @@ -3,7 +3,11 @@ mod mysql; mod postgres; mod sqlite; -use datamodel::{walkers::ModelWalker, walkers::ScalarFieldWalker, Datamodel, FieldArity, ScalarType}; +use datamodel::{ + walkers::ModelWalker, + walkers::{RelationFieldWalker, ScalarFieldWalker}, + Datamodel, FieldArity, ReferentialAction, ScalarType, +}; use sql_schema_describer::{self as sql, ColumnArity, ColumnType, ColumnTypeFamily}; pub(crate) trait SqlSchemaCalculatorFlavour { @@ -34,6 +38,30 @@ pub(crate) trait SqlSchemaCalculatorFlavour { false } + fn on_update_action(&self, rf: &RelationFieldWalker<'_>) -> sql::ForeignKeyAction { + let default = || match rf.arity() { + FieldArity::Required => sql::ForeignKeyAction::Cascade, + FieldArity::Optional => sql::ForeignKeyAction::SetNull, + FieldArity::List => unreachable!(), + }; + + rf.on_update_action() + .map(convert_referential_action) + .unwrap_or_else(default) + } + + fn on_delete_action(&self, rf: &RelationFieldWalker<'_>) -> sql::ForeignKeyAction { + let default = || match rf.arity() { + FieldArity::Required => sql::ForeignKeyAction::Restrict, + FieldArity::Optional => sql::ForeignKeyAction::SetNull, + FieldArity::List => unreachable!(), + }; + + rf.on_delete_action() + .map(convert_referential_action) + .unwrap_or_else(default) + } + fn m2m_foreign_key_action(&self, _model_a: &ModelWalker<'_>, _model_b: &ModelWalker<'_>) -> sql::ForeignKeyAction { sql::ForeignKeyAction::Cascade } @@ -43,3 +71,13 @@ pub(crate) trait SqlSchemaCalculatorFlavour { format!("{}.{}_unique", model_name, field_name) } } + +fn convert_referential_action(action: ReferentialAction) -> sql::ForeignKeyAction { + match action { + ReferentialAction::Cascade => sql::ForeignKeyAction::Cascade, + ReferentialAction::Restrict => sql::ForeignKeyAction::Restrict, + ReferentialAction::NoAction => sql::ForeignKeyAction::NoAction, + ReferentialAction::SetNull => sql::ForeignKeyAction::SetNull, + ReferentialAction::SetDefault => sql::ForeignKeyAction::SetDefault, + } +} diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour/mssql.rs b/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour/mssql.rs index 7c465b54aab5..0c4fec66ceae 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour/mssql.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour/mssql.rs @@ -1,8 +1,11 @@ use super::SqlSchemaCalculatorFlavour; use crate::flavour::MssqlFlavour; -use datamodel::{walkers::ModelWalker, ScalarType}; +use datamodel::{ + walkers::{ModelWalker, RelationFieldWalker}, + FieldArity, ScalarType, +}; use datamodel_connector::Connector; -use sql_schema_describer::ForeignKeyAction; +use sql_schema_describer::{self as sql, ForeignKeyAction}; impl SqlSchemaCalculatorFlavour for MssqlFlavour { fn default_native_type_for_scalar_type(&self, scalar_type: &ScalarType) -> serde_json::Value { @@ -18,6 +21,18 @@ impl SqlSchemaCalculatorFlavour for MssqlFlavour { } } + fn on_delete_action(&self, rf: &RelationFieldWalker<'_>) -> sql::ForeignKeyAction { + let default = || match rf.arity() { + FieldArity::Required => sql::ForeignKeyAction::NoAction, + FieldArity::Optional => sql::ForeignKeyAction::SetNull, + FieldArity::List => unreachable!(), + }; + + rf.on_delete_action() + .map(super::convert_referential_action) + .unwrap_or_else(default) + } + fn single_field_index_name(&self, model_name: &str, field_name: &str) -> String { format!("{}_{}_unique", model_name, field_name) } diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ.rs b/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ.rs index 25b6c71723d6..2c44ac252713 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ.rs @@ -584,11 +584,16 @@ fn foreign_keys_match(fks: Pair<&ForeignKeyWalker<'_>>, flavour: &dyn SqlFlavour .interleave(|fk| fk.referenced_column_names()) .all(|pair| pair.previous() == pair.next()); + let same_on_delete_action = fks.previous().on_delete_action() == fks.next().on_delete_action(); + let same_on_update_action = fks.previous().on_update_action() == fks.next().on_update_action(); + references_same_table && references_same_column_count && constrains_same_column_count && constrains_same_columns && references_same_columns + && same_on_delete_action + && same_on_update_action } fn enums_match(previous: &EnumWalker<'_>, next: &EnumWalker<'_>) -> bool { diff --git a/migration-engine/migration-engine-tests/src/assertions.rs b/migration-engine/migration-engine-tests/src/assertions.rs index 172c7373efbd..64718914036a 100644 --- a/migration-engine/migration-engine-tests/src/assertions.rs +++ b/migration-engine/migration-engine-tests/src/assertions.rs @@ -630,10 +630,25 @@ impl<'a> ForeignKeyAssertion<'a> { Ok(self) } - pub fn assert_cascades_on_delete(self) -> AssertionResult { + pub fn assert_referential_action_on_delete(self, action: ForeignKeyAction) -> AssertionResult { anyhow::ensure!( - self.fk.on_delete_action == ForeignKeyAction::Cascade, - "Assertion failed: expected foreign key to cascade on delete." + self.fk.on_delete_action == action, + format!( + "Assertion failed: expected foreign key to {:?} on delete, but got {:?}.", + action, self.fk.on_delete_action + ) + ); + + Ok(self) + } + + pub fn assert_referential_action_on_update(self, action: ForeignKeyAction) -> AssertionResult { + anyhow::ensure!( + self.fk.on_update_action == action, + format!( + "Assertion failed: expected foreign key to {:?} on update, but got {:?}.", + action, self.fk.on_update_action + ) ); Ok(self) diff --git a/migration-engine/migration-engine-tests/tests/create_migration/create_migration_tests.rs b/migration-engine/migration-engine-tests/tests/create_migration/create_migration_tests.rs index d77ac1c07d0f..9bb0bc538ff6 100644 --- a/migration-engine/migration-engine-tests/tests/create_migration/create_migration_tests.rs +++ b/migration-engine/migration-engine-tests/tests/create_migration/create_migration_tests.rs @@ -457,7 +457,7 @@ fn no_additional_unique_created(api: TestApi) { ); -- AddForeignKey - ALTER TABLE "Collar" ADD FOREIGN KEY ("id") REFERENCES "Cat"("id") ON DELETE CASCADE ON UPDATE CASCADE; + ALTER TABLE "Collar" ADD FOREIGN KEY ("id") REFERENCES "Cat"("id") ON DELETE RESTRICT ON UPDATE CASCADE; "# } } diff --git a/migration-engine/migration-engine-tests/tests/migration_tests.rs b/migration-engine/migration-engine-tests/tests/migration_tests.rs index 96529b217477..40257ccee442 100644 --- a/migration-engine/migration-engine-tests/tests/migration_tests.rs +++ b/migration-engine/migration-engine-tests/tests/migration_tests.rs @@ -1233,8 +1233,8 @@ async fn join_tables_between_models_with_compound_primary_keys_must_work(api: &T human_lastName String cat_id String - cat Cat @relation(fields: [cat_id], references: [id]) - human Human @relation(fields: [human_firstName, human_lastName], references: [firstName, lastName]) + cat Cat @relation(fields: [cat_id], references: [id], onDelete: Cascade) + human Human @relation(fields: [human_firstName, human_lastName], references: [firstName, lastName], onDelete: Cascade) @@unique([cat_id, human_firstName, human_lastName], name: "joinTableUnique") @@index([human_firstName, human_lastName], name: "joinTableIndex") @@ -1255,10 +1255,11 @@ async fn join_tables_between_models_with_compound_primary_keys_must_work(api: &T .assert_has_column("cat_id")? .assert_fk_on_columns(&["human_firstName", "human_lastName"], |fk| { fk.assert_references("Human", &["firstName", "lastName"])? - .assert_cascades_on_delete() + .assert_referential_action_on_delete(ForeignKeyAction::Cascade) })? .assert_fk_on_columns(&["cat_id"], |fk| { - fk.assert_references("Cat", &["id"])?.assert_cascades_on_delete() + fk.assert_references("Cat", &["id"])? + .assert_referential_action_on_delete(ForeignKeyAction::Cascade) })? .assert_indexes_count(2)? .assert_index_on_columns(&["cat_id", "human_firstName", "human_lastName"], |idx| { diff --git a/migration-engine/migration-engine-tests/tests/migrations/dev_diagnostic_tests.rs b/migration-engine/migration-engine-tests/tests/migrations/dev_diagnostic_tests.rs index b4076e20e3fe..c44071e851e3 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/dev_diagnostic_tests.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/dev_diagnostic_tests.rs @@ -462,7 +462,7 @@ fn with_an_invalid_unapplied_migration_should_report_it(api: TestApi) { } #[test_connector(tags(Postgres))] -fn drift_can_be_detected_without_migrations_table(api: TestApi) { +fn drift_can_be_detected_without_migrations_table2(api: TestApi) { let directory = api.create_migrations_directory(); api.raw_cmd("CREATE TABLE \"cat\" (\nid SERIAL PRIMARY KEY\n);"); diff --git a/migration-engine/migration-engine-tests/tests/migrations/diagnose_migration_history_tests.rs b/migration-engine/migration-engine-tests/tests/migrations/diagnose_migration_history_tests.rs index fb3f94e5498d..b0b643be2825 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/diagnose_migration_history_tests.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/diagnose_migration_history_tests.rs @@ -856,12 +856,12 @@ fn shadow_database_creation_error_is_special_cased_mssql(api: TestApi) { api.raw_cmd( " - CREATE LOGIN prismashadowdbtestuser - WITH PASSWORD = '1234batmanZ'; - - CREATE USER prismashadowdbuser FOR LOGIN prismashadowdbtestuser; - - GRANT SELECT TO prismashadowdbuser; + BEGIN TRY + CREATE LOGIN prismashadowdbtestuser WITH PASSWORD = '1234batmanZ'; + GRANT SELECT TO prismashadowdbuser; + END TRY + BEGIN CATCH + END CATCH; ", ); diff --git a/migration-engine/migration-engine-tests/tests/migrations/foreign_keys.rs b/migration-engine/migration-engine-tests/tests/migrations/foreign_keys.rs index 6b951639c734..3f1e670deb15 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/foreign_keys.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/foreign_keys.rs @@ -1,4 +1,5 @@ use migration_engine_tests::sync_test_api::*; +use sql_schema_describer::ForeignKeyAction; #[test_connector] fn foreign_keys_of_inline_one_to_one_relations_have_a_unique_constraint(api: TestApi) { @@ -213,8 +214,9 @@ fn changing_a_relation_field_to_a_scalar_field_must_work(api: TestApi) { model A { id Int @id b Int - b_rel B @relation(fields: [b], references: [id]) + b_rel B @relation(fields: [b], references: [id], onDelete: Cascade) } + model B { id Int @id a A? @@ -231,7 +233,8 @@ fn changing_a_relation_field_to_a_scalar_field_must_work(api: TestApi) { .assert_foreign_keys_count(1) .unwrap() .assert_fk_on_columns(&["b"], |fk| { - fk.assert_references("B", &["id"])?.assert_cascades_on_delete() + fk.assert_references("B", &["id"])? + .assert_referential_action_on_delete(ForeignKeyAction::Cascade) }) }) .unwrap(); diff --git a/migration-engine/migration-engine-tests/tests/migrations/postgres.rs b/migration-engine/migration-engine-tests/tests/migrations/postgres.rs index bef61f19afcb..48558fb09aee 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/postgres.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/postgres.rs @@ -171,7 +171,7 @@ async fn uuids_do_not_generate_drift_issue_5282(api: &TestApi) -> TestResult { r#" CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE a (id uuid DEFAULT uuid_generate_v4() primary key); - CREATE TABLE b (id uuid DEFAULT uuid_generate_v4() primary key, a_id uuid, CONSTRAINT aaa FOREIGN KEY (a_id) REFERENCES a(id)); + CREATE TABLE b (id uuid DEFAULT uuid_generate_v4() primary key, a_id uuid, CONSTRAINT aaa FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE SET NULL ON UPDATE SET NULL); "# ).await?; diff --git a/migration-engine/migration-engine-tests/tests/migrations/relations.rs b/migration-engine/migration-engine-tests/tests/migrations/relations.rs index 108ed44d36bc..0ad05d1e348c 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/relations.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/relations.rs @@ -1,4 +1,6 @@ +use datamodel::ReferentialAction; use migration_engine_tests::sync_test_api::*; +use sql_schema_describer::ForeignKeyAction; #[test_connector] fn adding_a_many_to_many_relation_must_result_in_a_prisma_style_relation_table(api: TestApi) { @@ -22,10 +24,14 @@ fn adding_a_many_to_many_relation_must_result_in_a_prisma_style_relation_table(a .assert_column("A", |col| col.assert_type_is_int())? .assert_column("B", |col| col.assert_type_is_string())? .assert_fk_on_columns(&["A"], |fk| { - fk.assert_references("A", &["id"])?.assert_cascades_on_delete() + fk.assert_references("A", &["id"])? + .assert_referential_action_on_update(ForeignKeyAction::Cascade)? + .assert_referential_action_on_delete(ForeignKeyAction::Cascade) })? .assert_fk_on_columns(&["B"], |fk| { - fk.assert_references("B", &["id"])?.assert_cascades_on_delete() + fk.assert_references("B", &["id"])? + .assert_referential_action_on_update(ForeignKeyAction::Cascade)? + .assert_referential_action_on_delete(ForeignKeyAction::Cascade) }) }); } @@ -63,8 +69,8 @@ fn adding_an_inline_relation_must_result_in_a_foreign_key_in_the_model_table(api id Int @id bid Int cid Int? - b B @relation(fields: [bid], references: [id]) - c C? @relation(fields: [cid], references: [id]) + b B @relation(fields: [bid], references: [id], onDelete: NoAction) + c C? @relation(fields: [cid], references: [id], onDelete: NoAction) } model B { @@ -84,7 +90,9 @@ fn adding_an_inline_relation_must_result_in_a_foreign_key_in_the_model_table(api .assert_column("cid", |c| c.assert_type_is_int()?.assert_is_nullable())? .assert_foreign_keys_count(2)? .assert_fk_on_columns(&["bid"], |fk| { - fk.assert_references("B", &["id"])?.assert_cascades_on_delete() + fk.assert_references("B", &["id"])? + .assert_referential_action_on_update(ForeignKeyAction::Cascade)? + .assert_referential_action_on_delete(ForeignKeyAction::NoAction) })? .assert_fk_on_columns(&["cid"], |fk| fk.assert_references("C", &["id"])) }); @@ -119,7 +127,7 @@ fn adding_an_inline_relation_to_a_model_with_an_exotic_id_type(api: TestApi) { model A { id Int @id b_id String - b B @relation(fields: [b_id], references: [id]) + b B @relation(fields: [b_id], references: [id], onDelete: NoAction) } model B { @@ -133,7 +141,9 @@ fn adding_an_inline_relation_to_a_model_with_an_exotic_id_type(api: TestApi) { t.assert_column("b_id", |c| c.assert_type_is_string())? .assert_foreign_keys_count(1)? .assert_fk_on_columns(&["b_id"], |fk| { - fk.assert_references("B", &["id"])?.assert_cascades_on_delete() + fk.assert_references("B", &["id"])? + .assert_referential_action_on_update(ForeignKeyAction::Cascade)? + .assert_referential_action_on_delete(ForeignKeyAction::NoAction) }) }); } @@ -186,7 +196,7 @@ fn compound_foreign_keys_should_work_in_correct_order(api: TestApi) { b Int a Int d Int - bb B @relation(fields: [a, b, d], references: [a_id, b_id, d_id]) + bb B @relation(fields: [a, b, d], references: [a_id, b_id, d_id], onDelete: NoAction) } model B { @@ -203,7 +213,8 @@ fn compound_foreign_keys_should_work_in_correct_order(api: TestApi) { api.assert_schema().assert_table_bang("A", |t| { t.assert_foreign_keys_count(1)? .assert_fk_on_columns(&["a", "b", "d"], |fk| { - fk.assert_cascades_on_delete()? + fk.assert_referential_action_on_delete(ForeignKeyAction::NoAction)? + .assert_referential_action_on_update(ForeignKeyAction::Cascade)? .assert_references("B", &["a_id", "b_id", "d_id"]) }) }); @@ -215,7 +226,7 @@ fn moving_an_inline_relation_to_the_other_side_must_work(api: TestApi) { model A { id Int @id b_id Int - b B @relation(fields: [b_id], references: [id]) + b B @relation(fields: [b_id], references: [id], onDelete: NoAction) } model B { @@ -227,7 +238,9 @@ fn moving_an_inline_relation_to_the_other_side_must_work(api: TestApi) { api.schema_push(dm1).send_sync().assert_green_bang(); api.assert_schema().assert_table_bang("A", |t| { t.assert_foreign_keys_count(1)?.assert_fk_on_columns(&["b_id"], |fk| { - fk.assert_cascades_on_delete()?.assert_references("B", &["id"]) + fk.assert_referential_action_on_delete(ForeignKeyAction::NoAction)? + .assert_referential_action_on_update(ForeignKeyAction::Cascade)? + .assert_references("B", &["id"]) }) }); @@ -240,7 +253,7 @@ fn moving_an_inline_relation_to_the_other_side_must_work(api: TestApi) { model B { id Int @id a_id Int - a A @relation(fields: [a_id], references: [id]) + a A @relation(fields: [a_id], references: [id], onDelete: NoAction) } "#; @@ -250,7 +263,9 @@ fn moving_an_inline_relation_to_the_other_side_must_work(api: TestApi) { table .assert_foreign_keys_count(1)? .assert_fk_on_columns(&["a_id"], |fk| { - fk.assert_references("A", &["id"])?.assert_cascades_on_delete() + fk.assert_references("A", &["id"])? + .assert_referential_action_on_delete(ForeignKeyAction::NoAction)? + .assert_referential_action_on_update(ForeignKeyAction::Cascade) }) }) .assert_table_bang("A", |table| table.assert_foreign_keys_count(0)?.assert_indexes_count(0)); @@ -433,3 +448,193 @@ fn relations_with_mappings_on_referencing_side_can_reference_multiple_fields(api }) }); } + +#[test_connector] +fn on_delete_referential_actions_should_work(api: TestApi) { + let actions = &[ + (ReferentialAction::SetNull, ForeignKeyAction::SetNull), + (ReferentialAction::Cascade, ForeignKeyAction::Cascade), + (ReferentialAction::NoAction, ForeignKeyAction::NoAction), + ]; + + for (ra, fka) in actions { + let dm = format!( + r#" + model A {{ + id Int @id @default(autoincrement()) + b B[] + }} + + model B {{ + id Int @id + aId Int? + a A? @relation(fields: [aId], references: [id], onDelete: {}) + }} + "#, + ra + ); + + api.schema_push(&dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table_bang("B", |table| { + table + .assert_foreign_keys_count(1)? + .assert_fk_on_columns(&["aId"], |fk| { + fk.assert_references("A", &["id"])? + .assert_referential_action_on_delete(*fka) + }) + }); + + api.schema_push("").send_sync().assert_green_bang(); + } +} + +// 5.6 and 5.7 doesn't let you `SET DEFAULT` without setting the default value +// (even if nullable). Maria will silently just use `RESTRICT` instead. +#[test_connector(exclude(Mysql56, Mysql57, Mariadb, Mssql))] +fn on_delete_set_default_should_work(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], onDelete: SetDefault) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table_bang("B", |table| { + table + .assert_foreign_keys_count(1)? + .assert_fk_on_columns(&["aId"], |fk| { + fk.assert_references("A", &["id"])? + .assert_referential_action_on_delete(ForeignKeyAction::SetDefault) + }) + }); +} + +#[test_connector(exclude(Mssql))] +fn on_delete_restrict_should_work(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], onDelete: Restrict) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table_bang("B", |table| { + table + .assert_foreign_keys_count(1)? + .assert_fk_on_columns(&["aId"], |fk| { + fk.assert_references("A", &["id"])? + .assert_referential_action_on_delete(ForeignKeyAction::Restrict) + }) + }); +} + +#[test_connector] +fn on_update_referential_actions_should_work(api: TestApi) { + let actions = &[ + (ReferentialAction::Cascade, ForeignKeyAction::Cascade), + (ReferentialAction::NoAction, ForeignKeyAction::NoAction), + (ReferentialAction::SetNull, ForeignKeyAction::SetNull), + ]; + + for (ra, fka) in actions { + let dm = format!( + r#" + model A {{ + id Int @id @default(autoincrement()) + b B[] + }} + + model B {{ + id Int @id + aId Int? + a A? @relation(fields: [aId], references: [id], onUpdate: {}) + }} + "#, + ra + ); + + api.schema_push(&dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table_bang("B", |table| { + table + .assert_foreign_keys_count(1)? + .assert_fk_on_columns(&["aId"], |fk| { + fk.assert_references("A", &["id"])? + .assert_referential_action_on_update(*fka) + }) + }); + } +} + +// 5.6 and 5.7 doesn't let you `SET DEFAULT` without setting the default value +// (even if nullable). Maria will silently just use `RESTRICT` instead. +#[test_connector(exclude(Mysql56, Mysql57, Mariadb, Mssql))] +fn on_update_set_default_should_work(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: SetDefault) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table_bang("B", |table| { + table + .assert_foreign_keys_count(1)? + .assert_fk_on_columns(&["aId"], |fk| { + fk.assert_references("A", &["id"])? + .assert_referential_action_on_update(ForeignKeyAction::SetDefault) + }) + }); +} + +#[test_connector(exclude(Mssql))] +fn on_update_restrict_should_work(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) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table_bang("B", |table| { + table + .assert_foreign_keys_count(1)? + .assert_fk_on_columns(&["aId"], |fk| { + fk.assert_references("A", &["id"])? + .assert_referential_action_on_update(ForeignKeyAction::Restrict) + }) + }); +}