From 619733a0c910581a8e3d554f26cedf114e805f19 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Thu, 20 May 2021 16:55:13 +0200 Subject: [PATCH 01/47] 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 | 50 ++- .../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 | 23 +- 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 | 8 +- 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 | 21 +- .../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, 1363 insertions(+), 458 deletions(-) create mode 100644 libs/datamodel/core/tests/attributes/relations/referential_actions.rs diff --git a/Cargo.lock b/Cargo.lock index 71d9c68c63b6..f8fc35ef1fcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1004,6 +1004,7 @@ name = "datamodel-connector" version = "0.1.0" dependencies = [ "dml", + "enumflags2 0.6.4", "itertools 0.8.2", "serde_json", "thiserror", @@ -1069,6 +1070,7 @@ version = "0.1.0" dependencies = [ "chrono", "cuid", + "enumflags2 0.6.4", "native-types", "prisma-value", "serde", @@ -1191,13 +1193,33 @@ dependencies = [ "syn", ] +[[package]] +name = "enumflags2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" +dependencies = [ + "enumflags2_derive 0.6.4", +] + [[package]] name = "enumflags2" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8672257d642ffdd235f6e9c723c2326ac1253c8f3c022e7cfd2e57da55b1131" dependencies = [ - "enumflags2_derive", + "enumflags2_derive 0.7.0", +] + +[[package]] +name = "enumflags2_derive" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1892,7 +1914,7 @@ dependencies = [ "barrel", "datamodel", "datamodel-connector", - "enumflags2", + "enumflags2 0.7.1", "indoc", "introspection-connector", "introspection-core", @@ -2273,7 +2295,7 @@ dependencies = [ "async-trait", "chrono", "datamodel", - "enumflags2", + "enumflags2 0.7.1", "jsonrpc-core", "migration-connector", "mongodb-migration-connector", @@ -2292,7 +2314,7 @@ name = "migration-engine-cli" version = "0.1.0" dependencies = [ "base64 0.13.0", - "enumflags2", + "enumflags2 0.7.1", "futures 0.3.13", "json-rpc-stdio", "migration-connector", @@ -2321,7 +2343,7 @@ dependencies = [ "connection-string", "datamodel", "datamodel-connector", - "enumflags2", + "enumflags2 0.7.1", "indoc", "migration-connector", "migration-core", @@ -2447,6 +2469,7 @@ version = "0.1.0" dependencies = [ "datamodel-connector", "dml", + "enumflags2 0.6.4", "lazy_static", "native-types", "once_cell", @@ -2463,7 +2486,7 @@ dependencies = [ "connection-string", "datamodel", "datamodel-connector", - "enumflags2", + "enumflags2 0.7.1", "indoc", "migration-connector", "mongodb", @@ -3567,7 +3590,7 @@ dependencies = [ "datamodel", "datamodel-connector", "enum_dispatch", - "enumflags2", + "enumflags2 0.7.1", "indoc", "itertools 0.10.0", "lazy_static", @@ -4086,7 +4109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" dependencies = [ "lazy_static", - "parking_lot 0.10.2", + "parking_lot 0.11.1", "serial_test_derive", ] @@ -4247,6 +4270,7 @@ version = "0.1.0" dependencies = [ "datamodel-connector", "dml", + "enumflags2 0.6.4", "native-types", "once_cell", "regex", @@ -4296,7 +4320,7 @@ dependencies = [ "connection-string", "datamodel", "datamodel-connector", - "enumflags2", + "enumflags2 0.7.1", "indoc", "migration-connector", "native-types", @@ -4350,7 +4374,7 @@ dependencies = [ "async-trait", "barrel", "bigdecimal", - "enumflags2", + "enumflags2 0.7.1", "indoc", "native-types", "once_cell", @@ -4561,7 +4585,7 @@ version = "0.1.0" dependencies = [ "anyhow", "colored 1.9.3", - "enumflags2", + "enumflags2 0.7.1", "introspection-core", "migration-core", "serde_json", @@ -4586,7 +4610,7 @@ name = "test-setup" version = "0.1.0" dependencies = [ "connection-string", - "enumflags2", + "enumflags2 0.7.1", "once_cell", "quaint", "tokio", @@ -4650,7 +4674,7 @@ dependencies = [ "chrono", "connection-string", "encoding", - "enumflags2", + "enumflags2 0.7.1", "futures 0.3.13", "futures-sink", "futures-util", 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 13c7e782b7a9..ef537b588e93 100644 --- a/introspection-engine/introspection-engine-tests/src/test_api.rs +++ b/introspection-engine/introspection-engine-tests/src/test_api.rs @@ -85,6 +85,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 582cfd448ccf..c842898f0a17 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>; @@ -172,6 +179,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 dd140329299c..c615a25a799b 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, @@ -57,6 +65,7 @@ impl MsSqlDatamodelConnector { ConnectorCapability::MultipleIndexesWithSameName, ConnectorCapability::AutoIncrement, ConnectorCapability::CompoundIds, + ConnectorCapability::ReferentialActions, ]; let constructors: Vec = vec![ @@ -90,9 +99,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, } } @@ -142,6 +154,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 2fc3f8e666a6..4f4ff78978de 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, @@ -75,6 +83,7 @@ impl MySqlDatamodelConnector { ConnectorCapability::CreateManyWriteableAutoIncId, ConnectorCapability::AutoIncrement, ConnectorCapability::CompoundIds, + ConnectorCapability::ReferentialActions, ]; let int = NativeTypeConstructor::without_args(INT_TYPE_NAME, vec![ScalarType::Int]); @@ -149,9 +158,12 @@ impl MySqlDatamodelConnector { json, ]; + let referential_actions = Restrict | Cascade | SetNull | NoAction | SetDefault; + MySqlDatamodelConnector { capabilities, constructors, + referential_actions, } } } @@ -177,6 +189,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 43b0ea514e85..71df566c18ad 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, @@ -61,6 +69,7 @@ impl PostgresDatamodelConnector { ConnectorCapability::CreateManyWriteableAutoIncId, ConnectorCapability::AutoIncrement, ConnectorCapability::CompoundIds, + ConnectorCapability::ReferentialActions, ]; let small_int = NativeTypeConstructor::without_args(SMALL_INT_TYPE_NAME, vec![ScalarType::Int]); @@ -120,9 +129,12 @@ impl PostgresDatamodelConnector { json_b, ]; + let referential_actions = NoAction | Restrict | Cascade | SetNull | SetDefault; + PostgresDatamodelConnector { capabilities, constructors, + referential_actions, } } } @@ -148,6 +160,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 33f7a9b71a67..aaadebbd39d8 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,29 +1,36 @@ 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::CompoundIds, + ConnectorCapability::ReferentialActions, ]; let constructors: Vec = vec![]; + let referential_actions = SetNull | SetDefault | Cascade | Restrict | NoAction; SqliteDatamodelConnector { capabilities, constructors, + referential_actions, } } } @@ -32,10 +39,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 1175469652e0..1272c35ad832 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}; @@ -511,6 +512,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 dd78b88769e2..0df1f4d7ba70 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; @@ -164,7 +165,27 @@ impl ValueValidator { } } - /// Unwraps the wrapped value as an array literal. + /// Unwraps the wrapped value as a referential action. + 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 { ast::Expression::Array(values, _) => { 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..c513a3d5cca8 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", }; @@ -310,14 +310,14 @@ mod tests { changes: vec![AlterTableClause::AddForeignKey(ForeignKey { constrained_columns: vec!["bestFriendId".into()], constraint_name: Some("myfk".into()), - on_delete: Some(ForeignKeyAction::DoNothing), + on_delete: Some(ForeignKeyAction::NoAction), on_update: Some(ForeignKeyAction::SetNull), referenced_columns: vec!["id".into()], referenced_table: "Dog".into(), })], }; - let expected = "ALTER TABLE `Cat` ADD CONSTRAINT `myfk` FOREIGN KEY (`bestFriendId`) REFERENCES `Dog`(`id`) ON DELETE DO NOTHING ON UPDATE SET NULL"; + let expected = "ALTER TABLE `Cat` ADD CONSTRAINT `myfk` FOREIGN KEY (`bestFriendId`) REFERENCES `Dog`(`id`) ON DELETE NO ACTION ON UPDATE SET NULL"; assert_eq!(alter_table.to_string(), expected); } 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 c460fb985135..ed31b98c6cae 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, Configuration}; use datamodel::{ walkers::{walk_models, walk_relations, ModelWalker, ScalarFieldWalker, TypeWalker}, - Datamodel, DefaultValue, FieldArity, IndexDefinition, IndexType, ScalarType, + Configuration, 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( (configuration, datamodel): (&Configuration, &Datamodel), @@ -102,13 +101,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()); @@ -128,8 +127,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); @@ -137,14 +136,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 06161a824b38..5a2f33186331 100644 --- a/migration-engine/migration-engine-tests/src/assertions.rs +++ b/migration-engine/migration-engine-tests/src/assertions.rs @@ -631,10 +631,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 15ca6bd342c0..f351eb9df4f5 100644 --- a/migration-engine/migration-engine-tests/tests/migration_tests.rs +++ b/migration-engine/migration-engine-tests/tests/migration_tests.rs @@ -1108,8 +1108,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") @@ -1130,10 +1130,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 b83cd25c1955..6a5fa5709b85 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 23b8f10c47d8..9821be75f4a5 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/postgres.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/postgres.rs @@ -167,7 +167,7 @@ fn uuids_do_not_generate_drift_issue_5282(api: TestApi) { 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); "# ); 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) + }) + }); +} From 18ac766928efa74b83f87c7cc1a55428d9ee77b3 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Wed, 2 Jun 2021 17:46:08 +0200 Subject: [PATCH 02/47] Do not render default referential actions --- .../src/introspection.rs | 5 +- .../tests/re_introspection/mod.rs | 141 ++++++++++++++++++ .../datamodel/connectors/dml/src/datamodel.rs | 4 +- libs/datamodel/connectors/dml/src/field.rs | 72 ++++++++- .../connectors/dml/src/relation_info.rs | 8 +- .../core/src/transform/ast_to_dml/lift.rs | 10 ++ .../core/src/transform/attributes/relation.rs | 21 ++- libs/sql-schema-describer/src/sqlite.rs | 2 +- 8 files changed, 243 insertions(+), 20 deletions(-) diff --git a/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs b/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs index b30452a910b6..0c45b4d6e529 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs @@ -41,7 +41,10 @@ pub fn introspect( for foreign_key in &foreign_keys_copy { version_check.has_inline_relations(table); version_check.uses_on_delete(foreign_key, table); - let relation_field = calculate_relation_field(schema, table, foreign_key)?; + + let mut relation_field = calculate_relation_field(schema, table, foreign_key)?; + relation_field.supports_restrict_action(!sql_family.is_mssql()); + model.add_field(Field::RelationField(relation_field)); } diff --git a/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs b/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs index 0f3640ab40fe..e47e13b0d0c2 100644 --- a/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs @@ -1752,3 +1752,144 @@ async fn do_not_try_to_keep_custom_many_to_many_self_relation_names(api: &TestAp Ok(()) } + +#[test_connector(tags(Postgres, Mysql, Sqlite))] +async fn default_required_actions_with_restrict(api: &TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("a", |t| { + t.add_column("id", types::primary()); + }); + + migration.create_table("b", |t| { + t.add_column("id", types::primary()); + t.add_column("a_id", types::integer().nullable(false)); + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE RESTRICT ON UPDATE CASCADE", + ); + }); + }) + .await?; + + let extra_index = if api.sql_family().is_mysql() { + r#"@@index([a_id], name: "asdf")"# + } else { + "" + }; + + let input_dm = formatdoc! {r#" + model a {{ + id Int @id @default(autoincrement()) + bs b[] + }} + + model b {{ + id Int @id @default(autoincrement()) + a_id Int + a a @relation(fields: [a_id], references: [id]) + {} + }} + "#, extra_index}; + + api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?); + + Ok(()) +} + +#[test_connector(tags(Mssql))] +async fn default_required_actions_without_restrict(api: &TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("a", |t| { + t.add_column("id", types::primary()); + }); + + migration.create_table("b", |t| { + t.add_column("id", types::primary()); + t.add_column("a_id", types::integer().nullable(false)); + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_required_actions_without_restrict.a(id) ON DELETE NO ACTION ON UPDATE CASCADE", + ); + }); + }) + .await?; + + let extra_index = if api.sql_family().is_mysql() { + r#"@@index([a_id], name: "asdf")"# + } else { + "" + }; + + let input_dm = formatdoc! {r#" + model a {{ + id Int @id @default(autoincrement()) + bs b[] + }} + + model b {{ + id Int @id @default(autoincrement()) + a_id Int + a a @relation(fields: [a_id], references: [id]) + {} + }} + "#, extra_index}; + + api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?); + + Ok(()) +} + +#[test_connector] +async fn default_optional_actions(api: &TestApi) -> TestResult { + let family = api.sql_family(); + + api.barrel() + .execute(move |migration| { + migration.create_table("a", |t| { + t.add_column("id", types::primary()); + }); + + migration.create_table("b", move |t| { + t.add_column("id", types::primary()); + t.add_column("a_id", types::integer().nullable(true)); + + match family { + SqlFamily::Mssql => { + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_optional_actions.a(id) ON DELETE SET NULL ON UPDATE SET NULL", + ); + } + _ => { + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE SET NULL ON UPDATE SET NULL", + ); + } + } + }); + }) + .await?; + + let extra_index = if api.sql_family().is_mysql() { + r#"@@index([a_id], name: "asdf")"# + } else { + "" + }; + + let input_dm = formatdoc! {r#" + model a {{ + id Int @id @default(autoincrement()) + bs b[] + }} + + model b {{ + id Int @id @default(autoincrement()) + a_id Int? + a a? @relation(fields: [a_id], references: [id]) + {} + }} + "#, extra_index}; + + api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?); + + Ok(()) +} diff --git a/libs/datamodel/connectors/dml/src/datamodel.rs b/libs/datamodel/connectors/dml/src/datamodel.rs index 8188ae4bc008..440635f636db 100644 --- a/libs/datamodel/connectors/dml/src/datamodel.rs +++ b/libs/datamodel/connectors/dml/src/datamodel.rs @@ -1,4 +1,4 @@ -use crate::field::{Field, FieldType, RelationField, ScalarField}; +use crate::field::{Field, RelationField, ScalarField}; use crate::model::Model; use crate::r#enum::Enum; use crate::relation_info::RelationInfo; @@ -117,7 +117,7 @@ impl Datamodel { let mut fields = vec![]; for model in self.models() { for field in model.scalar_fields() { - if FieldType::Enum(enum_name.to_owned()) == field.field_type { + if field.field_type.is_enum(enum_name) { fields.push((model.name.clone(), field.name.clone())) } } diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index 8296db532d69..8d333feda761 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -1,8 +1,11 @@ use super::*; -use crate::default_value::{DefaultValue, ValueGenerator}; use crate::native_type_instance::NativeTypeInstance; use crate::scalars::ScalarType; use crate::traits::{Ignorable, WithDatabaseName, WithName}; +use crate::{ + default_value::{DefaultValue, ValueGenerator}, + relation_info::ReferentialAction, +}; use std::hash::Hash; /// Arity of a Field in a Model. @@ -74,6 +77,10 @@ impl FieldType { self.scalar_type().map(|st| st.is_string()).unwrap_or(false) } + pub fn is_enum(&self, name: &str) -> bool { + matches!(self, Self::Enum(this) if this == name) + } + pub fn scalar_type(&self) -> Option { match self { FieldType::NativeType(st, _) => Some(*st), @@ -230,7 +237,7 @@ impl WithDatabaseName for Field { } /// Represents a relation field in a model. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Clone)] pub struct RelationField { /// Name of the field. pub name: String, @@ -252,6 +259,45 @@ pub struct RelationField { /// Indicates if this field has to be ignored by the Client. pub is_ignored: bool, + + /// Is `ON DELETE/UPDATE RESTRICT` allowed. + pub supports_restrict_action: Option, +} + +impl PartialEq for RelationField { + //ignores the relation name for reintrospection + fn eq(&self, other: &Self) -> bool { + let this_matches = self.name == other.name + && self.arity == other.arity + && self.documentation == other.documentation + && self.is_generated == other.is_generated + && self.is_commented_out == other.is_commented_out + && self.is_ignored == other.is_ignored + && self.relation_info == other.relation_info; + + let this_on_delete = self.relation_info.on_delete.or_else(|| self.default_on_delete_action()); + + let other_on_delete = other + .relation_info + .on_delete + .or_else(|| other.default_on_delete_action()); + + let on_delete_matches = this_on_delete == other_on_delete; + + let this_on_update = self + .relation_info + .on_update + .unwrap_or_else(|| self.default_on_update_action()); + + let other_on_update = other + .relation_info + .on_update + .unwrap_or_else(|| other.default_on_update_action()); + + let on_update_matches = this_on_update == other_on_update; + + this_matches && on_delete_matches && on_update_matches + } } impl RelationField { @@ -265,8 +311,15 @@ impl RelationField { is_generated: false, is_commented_out: false, is_ignored: false, + supports_restrict_action: None, } } + + /// The default `onDelete` can be `Restrict`. + pub fn supports_restrict_action(&mut self, value: bool) { + self.supports_restrict_action = Some(value); + } + /// Creates a new field with the given name and type, marked as generated and optional. pub fn new_generated(name: &str, info: RelationInfo, required: bool) -> Self { let arity = if required { @@ -300,6 +353,21 @@ impl RelationField { pub fn is_optional(&self) -> bool { self.arity.is_optional() } + + pub fn default_on_delete_action(&self) -> Option { + self.supports_restrict_action.map(|restrict_ok| match self.arity { + FieldArity::Required if restrict_ok => ReferentialAction::Restrict, + FieldArity::Required => ReferentialAction::NoAction, + _ => ReferentialAction::SetNull, + }) + } + + pub fn default_on_update_action(&self) -> ReferentialAction { + match self.arity { + FieldArity::Required => ReferentialAction::Cascade, + _ => ReferentialAction::SetNull, + } + } } /// Represents a scalar field in a model. diff --git a/libs/datamodel/connectors/dml/src/relation_info.rs b/libs/datamodel/connectors/dml/src/relation_info.rs index dd99df9855c9..ae39ff4c0ac4 100644 --- a/libs/datamodel/connectors/dml/src/relation_info.rs +++ b/libs/datamodel/connectors/dml/src/relation_info.rs @@ -21,13 +21,9 @@ pub struct RelationInfo { } impl PartialEq for RelationInfo { - //ignores the relation name for reintrospection + //ignores the relation name for reintrospection, ignores referential actions that are compared in the relation field. fn eq(&self, other: &Self) -> bool { - self.to == other.to - && self.fields == other.fields - && self.references == other.references - && self.on_delete == other.on_delete - && self.on_update == other.on_update + self.to == other.to && self.fields == other.fields && self.references == other.references } } diff --git a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs index 926e07c0cdd9..fc02371450aa 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs @@ -8,6 +8,7 @@ use crate::{ diagnostics::{DatamodelError, Diagnostics}, }; use crate::{dml::ScalarType, Datasource}; +use ::dml::relation_info::ReferentialAction; use datamodel_connector::connector_error::{ConnectorError, ErrorKind}; use itertools::Itertools; use once_cell::sync::Lazy; @@ -157,6 +158,15 @@ impl<'a> LiftAstToDml<'a> { FieldType::Relation(info) => { let arity = self.lift_field_arity(&ast_field.arity); let mut field = dml::RelationField::new(&ast_field.name.name, arity, info); + + if let Some(ref source) = self.source { + field.supports_restrict_action( + source + .active_connector + .supports_referential_action(ReferentialAction::Restrict), + ); + } + field.documentation = ast_field.documentation.clone().map(|comment| comment.text); Field::RelationField(field) } diff --git a/libs/datamodel/core/src/transform/attributes/relation.rs b/libs/datamodel/core/src/transform/attributes/relation.rs index ffcdc333e910..25041ef511d1 100644 --- a/libs/datamodel/core/src/transform/attributes/relation.rs +++ b/libs/datamodel/core/src/transform/attributes/relation.rs @@ -49,9 +49,7 @@ impl AttributeValidator for RelationAttributeValidator { fn serialize(&self, field: &dml::Field, datamodel: &dml::Datamodel) -> Vec { if let dml::Field::RelationField(rf) = field { let mut args = Vec::new(); - let relation_info = &rf.relation_info; - let parent_model = datamodel.find_model_by_relation_field_ref(rf).unwrap(); let related_model = datamodel @@ -104,15 +102,22 @@ impl AttributeValidator for RelationAttributeValidator { } if let Some(ref_action) = relation_info.on_delete { - let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); - - args.push(ast::Argument::new("onDelete", expression)); + let is_default = rf + .default_on_delete_action() + .map(|default| default == ref_action) + .unwrap_or(false); + + if !is_default { + let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); + args.push(ast::Argument::new("onDelete", expression)); + } } if let Some(ref_action) = relation_info.on_update { - let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); - - args.push(ast::Argument::new("onUpdate", expression)); + if rf.default_on_update_action() != ref_action { + let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); + args.push(ast::Argument::new("onUpdate", expression)); + } } if !args.is_empty() { diff --git a/libs/sql-schema-describer/src/sqlite.rs b/libs/sql-schema-describer/src/sqlite.rs index c1755e8c5fb2..f31da0661a9b 100644 --- a/libs/sql-schema-describer/src/sqlite.rs +++ b/libs/sql-schema-describer/src/sqlite.rs @@ -338,7 +338,7 @@ impl SqlSchemaDescriber { if let Some(column) = referenced_column { referenced_columns.insert(seq, column); }; - let on_delete_action = match dbg!(&row) + let on_delete_action = match row .get("on_delete") .and_then(|x| x.to_string()) .expect("on_delete") From feef5f31c22437dcc13ad8aede1ee11f4e7a9b3f Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Fri, 4 Jun 2021 17:56:04 +0200 Subject: [PATCH 03/47] Remove legacy cascades. Mark some places that require work. --- libs/prisma-models/src/datamodel_converter.rs | 2 -- libs/prisma-models/src/fields.rs | 15 --------- libs/prisma-models/src/relation.rs | 31 ------------------- .../src/query_graph_builder/write/delete.rs | 3 ++ .../write/nested/delete_nested.rs | 3 ++ 5 files changed, 6 insertions(+), 48 deletions(-) diff --git a/libs/prisma-models/src/datamodel_converter.rs b/libs/prisma-models/src/datamodel_converter.rs index bef8b1368331..826a9fd5fa00 100644 --- a/libs/prisma-models/src/datamodel_converter.rs +++ b/libs/prisma-models/src/datamodel_converter.rs @@ -132,8 +132,6 @@ impl<'a> DatamodelConverter<'a> { .filter(|r| r.model_a.is_relation_supported(&r.field_a) && r.model_b.is_relation_supported(&r.field_b)) .map(|r| RelationTemplate { name: r.name(), - model_a_on_delete: OnDelete::SetNull, - model_b_on_delete: OnDelete::SetNull, manifestation: r.manifestation(), model_a_name: r.model_a.name.clone(), model_b_name: r.model_b.name.clone(), diff --git a/libs/prisma-models/src/fields.rs b/libs/prisma-models/src/fields.rs index 663b914344f6..09f91b5fcef1 100644 --- a/libs/prisma-models/src/fields.rs +++ b/libs/prisma-models/src/fields.rs @@ -113,21 +113,6 @@ impl Fields { self.relation_weak().iter().map(|f| f.upgrade().unwrap()).collect() } - pub fn cascading_relation(&self) -> Vec> { - self.relation_weak() - .iter() - .map(|f| f.upgrade().unwrap()) - .fold(Vec::new(), |mut acc, rf| { - match rf.relation_side { - RelationSide::A if rf.relation().model_a_on_delete.is_cascade() => acc.push(rf), - RelationSide::B if rf.relation().model_b_on_delete.is_cascade() => acc.push(rf), - _ => (), - } - - acc - }) - } - fn relation_weak(&self) -> &[Weak] { self.relation .get_or_init(|| self.all.iter().fold(Vec::new(), Self::relation_filter)) diff --git a/libs/prisma-models/src/relation.rs b/libs/prisma-models/src/relation.rs index e02a11d31587..0e5aea5c72e9 100644 --- a/libs/prisma-models/src/relation.rs +++ b/libs/prisma-models/src/relation.rs @@ -11,8 +11,6 @@ pub type RelationWeakRef = Weak; #[derive(Debug)] pub struct RelationTemplate { pub name: String, - pub model_a_on_delete: OnDelete, - pub model_b_on_delete: OnDelete, pub manifestation: RelationLinkManifestation, pub model_a_name: String, pub model_b_name: String, @@ -26,9 +24,6 @@ pub struct Relation { model_a_name: String, model_b_name: String, - pub model_a_on_delete: OnDelete, - pub model_b_on_delete: OnDelete, - model_a: OnceCell, model_b: OnceCell, @@ -46,8 +41,6 @@ impl Debug for Relation { .field("name", &self.name) .field("model_a_name", &self.model_a_name) .field("model_b_name", &self.model_b_name) - .field("model_a_on_delete", &self.model_a_on_delete) - .field("model_b_on_delete", &self.model_b_on_delete) .field("model_a", &self.model_a) .field("model_b", &self.model_b) .field("field_a", &self.field_a) @@ -76,28 +69,6 @@ pub struct RelationTable { pub model_b_column: String, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OnDelete { - SetNull, - Cascade, -} - -impl OnDelete { - pub fn is_cascade(self) -> bool { - match self { - OnDelete::Cascade => true, - OnDelete::SetNull => false, - } - } - - pub fn is_set_null(self) -> bool { - match self { - OnDelete::Cascade => false, - OnDelete::SetNull => true, - } - } -} - impl RelationTemplate { pub fn build(self, internal_data_model: InternalDataModelWeakRef) -> RelationRef { let relation = Relation { @@ -105,8 +76,6 @@ impl RelationTemplate { manifestation: self.manifestation, model_a_name: self.model_a_name, model_b_name: self.model_b_name, - model_a_on_delete: self.model_a_on_delete, - model_b_on_delete: self.model_b_on_delete, model_a: OnceCell::new(), model_b: OnceCell::new(), field_a: OnceCell::new(), diff --git a/query-engine/core/src/query_graph_builder/write/delete.rs b/query-engine/core/src/query_graph_builder/write/delete.rs index a15f5efd135a..75c8fe828631 100644 --- a/query-engine/core/src/query_graph_builder/write/delete.rs +++ b/query-engine/core/src/query_graph_builder/write/delete.rs @@ -28,6 +28,8 @@ pub fn delete_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedF })); let delete_node = graph.create_node(delete_query); + + // Todo [RA] utils::insert_deletion_checks(graph, &model, &read_node, &delete_node)?; graph.create_edge( @@ -76,6 +78,7 @@ pub fn delete_many_records( let read_query_node = graph.create_node(read_query); let delete_many_node = graph.create_node(Query::Write(delete_many)); + // Todo [RA] utils::insert_deletion_checks(graph, &model, &read_query_node, &delete_many_node)?; graph.create_edge( &read_query_node, diff --git a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs index 56787825907a..a9d56f0cbc6b 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs @@ -50,6 +50,7 @@ pub fn nested_delete( let find_child_records_node = utils::insert_find_children_by_parent_node(graph, parent_node, parent_relation_field, or_filter)?; + // Todo [RA] utils::insert_deletion_checks(graph, child_model, &find_child_records_node, &delete_many_node)?; let relation_name = parent_relation_field.relation().name.clone(); @@ -147,6 +148,8 @@ pub fn nested_delete_many( }); let delete_many_node = graph.create_node(Query::Write(delete_many)); + + // Todo [RA] utils::insert_deletion_checks(graph, child_model, &find_child_records_node, &delete_many_node)?; graph.create_edge( From d4b9fc075524b01a38f0656b86f41c04242e096e Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 7 Jun 2021 11:26:47 +0200 Subject: [PATCH 04/47] Emulated actions --- Cargo.lock | 52 +++++------------ .../src/introspection_helpers.rs | 2 +- .../connectors/datamodel-connector/Cargo.toml | 2 +- libs/datamodel/connectors/dml/Cargo.toml | 2 +- .../connectors/dml/src/relation_info.rs | 21 ++++--- .../mongodb-datamodel-connector/Cargo.toml | 2 +- .../mongodb-datamodel-connector/src/lib.rs | 7 ++- .../sql-datamodel-connector/Cargo.toml | 2 +- .../sql-datamodel-connector/src/lib.rs | 4 +- .../src/mysql_datamodel_connector.rs | 10 +++- .../builtin_datasource_providers.rs | 10 ++-- .../transform/ast_to_dml/datasource_loader.rs | 58 ++++++++----------- .../mysql.rs | 2 +- .../sql_schema_calculator_flavour.rs | 3 + .../sql_schema_calculator_flavour/mysql.rs | 2 +- .../tests/native_types/mysql.rs | 6 +- .../src/connector_tag/mysql.rs | 2 +- .../src/connector_tag/vitess.rs | 2 +- 18 files changed, 90 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8fc35ef1fcf..8ec3322168be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1004,7 +1004,7 @@ name = "datamodel-connector" version = "0.1.0" dependencies = [ "dml", - "enumflags2 0.6.4", + "enumflags2", "itertools 0.8.2", "serde_json", "thiserror", @@ -1070,7 +1070,7 @@ version = "0.1.0" dependencies = [ "chrono", "cuid", - "enumflags2 0.6.4", + "enumflags2", "native-types", "prisma-value", "serde", @@ -1193,33 +1193,13 @@ dependencies = [ "syn", ] -[[package]] -name = "enumflags2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" -dependencies = [ - "enumflags2_derive 0.6.4", -] - [[package]] name = "enumflags2" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8672257d642ffdd235f6e9c723c2326ac1253c8f3c022e7cfd2e57da55b1131" dependencies = [ - "enumflags2_derive 0.7.0", -] - -[[package]] -name = "enumflags2_derive" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "enumflags2_derive", ] [[package]] @@ -1914,7 +1894,7 @@ dependencies = [ "barrel", "datamodel", "datamodel-connector", - "enumflags2 0.7.1", + "enumflags2", "indoc", "introspection-connector", "introspection-core", @@ -2295,7 +2275,7 @@ dependencies = [ "async-trait", "chrono", "datamodel", - "enumflags2 0.7.1", + "enumflags2", "jsonrpc-core", "migration-connector", "mongodb-migration-connector", @@ -2314,7 +2294,7 @@ name = "migration-engine-cli" version = "0.1.0" dependencies = [ "base64 0.13.0", - "enumflags2 0.7.1", + "enumflags2", "futures 0.3.13", "json-rpc-stdio", "migration-connector", @@ -2343,7 +2323,7 @@ dependencies = [ "connection-string", "datamodel", "datamodel-connector", - "enumflags2 0.7.1", + "enumflags2", "indoc", "migration-connector", "migration-core", @@ -2469,7 +2449,7 @@ version = "0.1.0" dependencies = [ "datamodel-connector", "dml", - "enumflags2 0.6.4", + "enumflags2", "lazy_static", "native-types", "once_cell", @@ -2486,7 +2466,7 @@ dependencies = [ "connection-string", "datamodel", "datamodel-connector", - "enumflags2 0.7.1", + "enumflags2", "indoc", "migration-connector", "mongodb", @@ -3590,7 +3570,7 @@ dependencies = [ "datamodel", "datamodel-connector", "enum_dispatch", - "enumflags2 0.7.1", + "enumflags2", "indoc", "itertools 0.10.0", "lazy_static", @@ -4270,7 +4250,7 @@ version = "0.1.0" dependencies = [ "datamodel-connector", "dml", - "enumflags2 0.6.4", + "enumflags2", "native-types", "once_cell", "regex", @@ -4320,7 +4300,7 @@ dependencies = [ "connection-string", "datamodel", "datamodel-connector", - "enumflags2 0.7.1", + "enumflags2", "indoc", "migration-connector", "native-types", @@ -4374,7 +4354,7 @@ dependencies = [ "async-trait", "barrel", "bigdecimal", - "enumflags2 0.7.1", + "enumflags2", "indoc", "native-types", "once_cell", @@ -4585,7 +4565,7 @@ version = "0.1.0" dependencies = [ "anyhow", "colored 1.9.3", - "enumflags2 0.7.1", + "enumflags2", "introspection-core", "migration-core", "serde_json", @@ -4610,7 +4590,7 @@ name = "test-setup" version = "0.1.0" dependencies = [ "connection-string", - "enumflags2 0.7.1", + "enumflags2", "once_cell", "quaint", "tokio", @@ -4674,7 +4654,7 @@ dependencies = [ "chrono", "connection-string", "encoding", - "enumflags2 0.7.1", + "enumflags2", "futures 0.3.13", "futures-sink", "futures-util", 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 2f9af428a179..4d00bf241870 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs @@ -350,7 +350,7 @@ pub(crate) fn calculate_scalar_field_type_with_native_types(column: &Column, fam //fixme move this out of function let connector: Box = match family { - SqlFamily::Mysql => Box::new(SqlDatamodelConnectors::mysql()), + SqlFamily::Mysql => Box::new(SqlDatamodelConnectors::mysql(false)), SqlFamily::Postgres => Box::new(SqlDatamodelConnectors::postgres()), SqlFamily::Sqlite => Box::new(SqlDatamodelConnectors::sqlite()), SqlFamily::Mssql => Box::new(SqlDatamodelConnectors::mssql()), diff --git a/libs/datamodel/connectors/datamodel-connector/Cargo.toml b/libs/datamodel/connectors/datamodel-connector/Cargo.toml index d41fb0f55a31..d84150d20567 100644 --- a/libs/datamodel/connectors/datamodel-connector/Cargo.toml +++ b/libs/datamodel/connectors/datamodel-connector/Cargo.toml @@ -12,4 +12,4 @@ dml = { path = "../dml" } thiserror = "1.0" itertools = "0.8" url = "2.2.1" -enumflags2 = "0.6" +enumflags2 = "0.7" diff --git a/libs/datamodel/connectors/dml/Cargo.toml b/libs/datamodel/connectors/dml/Cargo.toml index 4d5ea81c0f26..b06d1f5fad94 100644 --- a/libs/datamodel/connectors/dml/Cargo.toml +++ b/libs/datamodel/connectors/dml/Cargo.toml @@ -14,4 +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" +enumflags2 = "0.7" diff --git a/libs/datamodel/connectors/dml/src/relation_info.rs b/libs/datamodel/connectors/dml/src/relation_info.rs index ae39ff4c0ac4..1c1f6cfd373d 100644 --- a/libs/datamodel/connectors/dml/src/relation_info.rs +++ b/libs/datamodel/connectors/dml/src/relation_info.rs @@ -1,4 +1,4 @@ -use enumflags2::BitFlags; +use enumflags2::bitflags; use std::fmt; /// Holds information about a relation field. @@ -44,32 +44,37 @@ impl RelationInfo { /// Describes what happens when related nodes are deleted. #[repr(u8)] -#[derive(Debug, Copy, PartialEq, Clone, BitFlags)] +#[bitflags] +#[derive(Debug, Copy, PartialEq, Clone)] 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, + Cascade, /// 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, + Restrict, /// 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, + NoAction, /// 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, + SetNull, /// 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, + SetDefault, + /// An emulated version of `SetNull` for databases without foreign keys. + EmulateSetNull, + /// An emulated version of `Restrict` for databases without foreign keys. + EmulateRestrict, } impl fmt::Display for ReferentialAction { @@ -80,6 +85,8 @@ impl fmt::Display for ReferentialAction { ReferentialAction::NoAction => write!(f, "NoAction"), ReferentialAction::SetNull => write!(f, "SetNull"), ReferentialAction::SetDefault => write!(f, "SetDefault"), + ReferentialAction::EmulateSetNull => write!(f, "EmulateSetNull"), + ReferentialAction::EmulateRestrict => write!(f, "EmulateRestrict"), } } } diff --git a/libs/datamodel/connectors/mongodb-datamodel-connector/Cargo.toml b/libs/datamodel/connectors/mongodb-datamodel-connector/Cargo.toml index c94767b10bf9..b34be96ae258 100644 --- a/libs/datamodel/connectors/mongodb-datamodel-connector/Cargo.toml +++ b/libs/datamodel/connectors/mongodb-datamodel-connector/Cargo.toml @@ -13,4 +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" +enumflags2 = "0.7" diff --git a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs index 7ff926f9562c..0b48243082f0 100644 --- a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs @@ -18,10 +18,13 @@ type Result = std::result::Result; pub struct MongoDbDatamodelConnector { capabilities: Vec, native_types: Vec, + referential_actions: BitFlags, } impl MongoDbDatamodelConnector { pub fn new() -> Self { + use ReferentialAction::*; + let capabilities = vec![ ConnectorCapability::RelationsOverNonUniqueCriteria, ConnectorCapability::Json, @@ -35,10 +38,12 @@ impl MongoDbDatamodelConnector { ]; let native_types = mongodb_types::available_types(); + let referential_actions = EmulateRestrict | EmulateSetNull; Self { capabilities, native_types, + referential_actions, } } } @@ -59,7 +64,7 @@ impl Connector for MongoDbDatamodelConnector { } fn referential_actions(&self) -> BitFlags { - BitFlags::empty() + self.referential_actions } fn validate_field(&self, field: &dml::field::Field) -> Result<()> { diff --git a/libs/datamodel/connectors/sql-datamodel-connector/Cargo.toml b/libs/datamodel/connectors/sql-datamodel-connector/Cargo.toml index 907031c8cc6a..5e59c2a7ab10 100644 --- a/libs/datamodel/connectors/sql-datamodel-connector/Cargo.toml +++ b/libs/datamodel/connectors/sql-datamodel-connector/Cargo.toml @@ -13,4 +13,4 @@ native-types = { path = "../../../native-types" } serde_json = { version = "1.0", features = ["float_roundtrip"] } once_cell = "1.3" regex = "1" -enumflags2 = "0.6" +enumflags2 = "0.7" diff --git a/libs/datamodel/connectors/sql-datamodel-connector/src/lib.rs b/libs/datamodel/connectors/sql-datamodel-connector/src/lib.rs index 474940276bee..e90ac0243fb2 100644 --- a/libs/datamodel/connectors/sql-datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/sql-datamodel-connector/src/lib.rs @@ -15,8 +15,8 @@ impl SqlDatamodelConnectors { PostgresDatamodelConnector::new() } - pub fn mysql() -> MySqlDatamodelConnector { - MySqlDatamodelConnector::new() + pub fn mysql(is_planetscale: bool) -> MySqlDatamodelConnector { + MySqlDatamodelConnector::new(is_planetscale) } pub fn sqlite() -> SqliteDatamodelConnector { 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 4f4ff78978de..b20a84968133 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 @@ -65,7 +65,7 @@ pub struct MySqlDatamodelConnector { } impl MySqlDatamodelConnector { - pub fn new() -> MySqlDatamodelConnector { + pub fn new(is_planetscale: bool) -> MySqlDatamodelConnector { use ReferentialAction::*; let capabilities = vec![ @@ -158,7 +158,11 @@ impl MySqlDatamodelConnector { json, ]; - let referential_actions = Restrict | Cascade | SetNull | NoAction | SetDefault; + let referential_actions = if is_planetscale { + EmulateRestrict | EmulateSetNull + } else { + Restrict | Cascade | SetNull | NoAction | SetDefault + }; MySqlDatamodelConnector { capabilities, @@ -443,6 +447,6 @@ impl Connector for MySqlDatamodelConnector { impl Default for MySqlDatamodelConnector { fn default() -> Self { - Self::new() + Self::new(false) } } diff --git a/libs/datamodel/core/src/transform/ast_to_dml/builtin_datasource_providers.rs b/libs/datamodel/core/src/transform/ast_to_dml/builtin_datasource_providers.rs index 4e8a901e68d3..9563d7898891 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/builtin_datasource_providers.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/builtin_datasource_providers.rs @@ -48,11 +48,13 @@ impl DatasourceProvider for PostgresDatasourceProvider { } } -pub struct MySqlDatasourceProvider {} +pub struct MySqlDatasourceProvider { + is_planetscale: bool, +} impl MySqlDatasourceProvider { - pub fn new() -> Self { - Self {} + pub fn new(is_planetscale: bool) -> Self { + Self { is_planetscale } } } @@ -66,7 +68,7 @@ impl DatasourceProvider for MySqlDatasourceProvider { } fn connector(&self) -> Box { - Box::new(SqlDatamodelConnectors::mysql()) + Box::new(SqlDatamodelConnectors::mysql(self.is_planetscale)) } } diff --git a/libs/datamodel/core/src/transform/ast_to_dml/datasource_loader.rs b/libs/datamodel/core/src/transform/ast_to_dml/datasource_loader.rs index 984a067bb98f..ae0f5514aa10 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/datasource_loader.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/datasource_loader.rs @@ -10,9 +10,10 @@ use crate::{ ast::SourceConfig, diagnostics::{DatamodelError, Diagnostics, ValidatedDatasource, ValidatedDatasources}, }; -use crate::{ast::Span, common::preview_features::PreviewFeature, configuration::StringFromEnvVar}; use crate::{ - ast::{self}, + ast::{self, Span}, + common::{preview_features::PreviewFeature, provider_names::*}, + configuration::StringFromEnvVar, Datasource, }; use std::collections::{HashMap, HashSet}; @@ -22,16 +23,12 @@ const SHADOW_DATABASE_URL_KEY: &str = "shadowDatabaseUrl"; const URL_KEY: &str = "url"; /// Is responsible for loading and validating Datasources defined in an AST. -pub struct DatasourceLoader { - source_definitions: Vec>, -} +pub struct DatasourceLoader {} impl DatasourceLoader { #[allow(clippy::new_without_default)] pub fn new() -> Self { - Self { - source_definitions: get_builtin_datasource_providers(), - } + Self {} } /// Loads all datasources from the provided schema AST. @@ -168,15 +165,25 @@ impl DatasourceLoader { preview_features_guardrail(&args)?; let documentation = ast_source.documentation.as_ref().map(|comment| comment.text.clone()); + let planet_scale_mode = get_planet_scale_mode_arg(&args, preview_features, ast_source)?; - let datasource_provider = self.get_datasource_provider(&provider).ok_or_else(|| { - diagnostics - .clone() - .merge_error(DatamodelError::new_datasource_provider_not_known_error( - provider, - provider_arg.span(), - )) - })?; + let datasource_provider: Box = match provider { + p if p == MYSQL_SOURCE_NAME => Box::new(MySqlDatasourceProvider::new(planet_scale_mode)), + p if p == POSTGRES_SOURCE_NAME || p == POSTGRES_SOURCE_NAME_HEROKU => { + Box::new(PostgresDatasourceProvider::new()) + } + p if p == SQLITE_SOURCE_NAME => Box::new(SqliteDatasourceProvider::new()), + p if p == MSSQL_SOURCE_NAME => Box::new(MsSqlDatasourceProvider::new()), + p if p == MONGODB_SOURCE_NAME => Box::new(MongoDbDatasourceProvider::new()), + _ => { + return Err( + diagnostics.merge_error(DatamodelError::new_datasource_provider_not_known_error( + provider, + provider_arg.span(), + )), + ) + } + }; Ok(ValidatedDatasource { subject: Datasource { @@ -188,28 +195,11 @@ impl DatasourceLoader { documentation, active_connector: datasource_provider.connector(), shadow_database_url, - planet_scale_mode: get_planet_scale_mode_arg(&args, preview_features, ast_source)?, + planet_scale_mode, }, warnings: diagnostics.warnings, }) } - - fn get_datasource_provider(&self, provider: &str) -> Option<&dyn DatasourceProvider> { - self.source_definitions - .iter() - .find(|sd| sd.is_provider(provider)) - .map(|b| b.as_ref()) - } -} - -fn get_builtin_datasource_providers() -> Vec> { - vec![ - Box::new(MySqlDatasourceProvider::new()), - Box::new(PostgresDatasourceProvider::new()), - Box::new(SqliteDatasourceProvider::new()), - Box::new(MsSqlDatasourceProvider::new()), - Box::new(MongoDbDatasourceProvider::new()), - ] } const PLANET_SCALE_PREVIEW_FEATURE_ERR: &str = r#" diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_destructive_change_checker/destructive_change_checker_flavour/mysql.rs b/migration-engine/connectors/sql-migration-connector/src/sql_destructive_change_checker/destructive_change_checker_flavour/mysql.rs index 7f5c5d9b16bf..37f0ecb7279c 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_destructive_change_checker/destructive_change_checker_flavour/mysql.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_destructive_change_checker/destructive_change_checker_flavour/mysql.rs @@ -51,7 +51,7 @@ impl DestructiveChangeCheckerFlavour for MysqlFlavour { return; } - let datamodel_connector = SqlDatamodelConnectors::mysql(); + let datamodel_connector = SqlDatamodelConnectors::mysql(false); let previous_type = match &columns.previous().column_type().native_type { Some(tpe) => datamodel_connector.render_native_type(tpe.clone()), _ => format!("{:?}", columns.previous().column_type_family()), 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 6c02eecb697b..d453b11aa772 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 @@ -79,5 +79,8 @@ fn convert_referential_action(action: ReferentialAction) -> sql::ForeignKeyActio ReferentialAction::NoAction => sql::ForeignKeyAction::NoAction, ReferentialAction::SetNull => sql::ForeignKeyAction::SetNull, ReferentialAction::SetDefault => sql::ForeignKeyAction::SetDefault, + // These will be only used for databases with no foreign keys. + ReferentialAction::EmulateSetNull => unreachable!("EmulateSetNull conversion"), + ReferentialAction::EmulateRestrict => unreachable!("EmulateRestrict conversion"), } } diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour/mysql.rs b/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour/mysql.rs index 495257cc91e3..04e2b8e65fdb 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour/mysql.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_schema_calculator/sql_schema_calculator_flavour/mysql.rs @@ -33,7 +33,7 @@ impl SqlSchemaCalculatorFlavour for MysqlFlavour { } fn default_native_type_for_scalar_type(&self, scalar_type: &ScalarType) -> serde_json::Value { - sql_datamodel_connector::SqlDatamodelConnectors::mysql().default_native_type_for_scalar_type(scalar_type) + sql_datamodel_connector::SqlDatamodelConnectors::mysql(false).default_native_type_for_scalar_type(scalar_type) } fn enum_column_type(&self, field: &ScalarFieldWalker<'_>, _db_name: &str) -> sql::ColumnType { diff --git a/migration-engine/migration-engine-tests/tests/native_types/mysql.rs b/migration-engine/migration-engine-tests/tests/native_types/mysql.rs index 90050b01d95d..46d204a3c011 100644 --- a/migration-engine/migration-engine-tests/tests/native_types/mysql.rs +++ b/migration-engine/migration-engine-tests/tests/native_types/mysql.rs @@ -750,7 +750,7 @@ fn filter_to_types(api: &TestApi, to_types: &'static [&'static str]) -> Cow<'sta #[test_connector(tags(Mysql))] fn safe_casts_with_existing_data_should_work(api: TestApi) { - let connector = sql_datamodel_connector::MySqlDatamodelConnector::new(); + let connector = sql_datamodel_connector::MySqlDatamodelConnector::new(false); let mut dm1 = String::with_capacity(256); let mut dm2 = String::with_capacity(256); let colnames = colnames_for_cases(SAFE_CASTS); @@ -798,7 +798,7 @@ fn safe_casts_with_existing_data_should_work(api: TestApi) { #[test_connector(tags(Mysql))] fn risky_casts_with_existing_data_should_warn(api: TestApi) { - let connector = sql_datamodel_connector::MySqlDatamodelConnector::new(); + let connector = sql_datamodel_connector::MySqlDatamodelConnector::new(false); let mut dm1 = String::with_capacity(256); let mut dm2 = String::with_capacity(256); let colnames = colnames_for_cases(RISKY_CASTS); @@ -863,7 +863,7 @@ fn risky_casts_with_existing_data_should_warn(api: TestApi) { #[test_connector(tags(Mysql))] fn impossible_casts_with_existing_data_should_warn(api: TestApi) { - let connector = sql_datamodel_connector::MySqlDatamodelConnector::new(); + let connector = sql_datamodel_connector::MySqlDatamodelConnector::new(false); let mut dm1 = String::with_capacity(256); let mut dm2 = String::with_capacity(256); let colnames = colnames_for_cases(IMPOSSIBLE_CASTS); diff --git a/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/mysql.rs b/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/mysql.rs index 7951b816447e..5a766953163b 100644 --- a/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/mysql.rs +++ b/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/mysql.rs @@ -146,6 +146,6 @@ impl ToString for MySqlVersion { } fn mysql_capabilities() -> Vec { - let dm_connector = MySqlDatamodelConnector::new(); + let dm_connector = MySqlDatamodelConnector::new(false); dm_connector.capabilities().clone() } diff --git a/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/vitess.rs b/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/vitess.rs index e3f0aa58b40a..7dabd6de8a01 100644 --- a/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/vitess.rs +++ b/query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/vitess.rs @@ -112,6 +112,6 @@ impl Display for VitessVersion { } fn vitess_capabilities() -> Vec { - let dm_connector = MySqlDatamodelConnector::new(); + let dm_connector = MySqlDatamodelConnector::new(true); dm_connector.capabilities().clone() } From efa606ed8e46cf10e7eec233c37af3aea49431bc Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 7 Jun 2021 12:46:38 +0200 Subject: [PATCH 05/47] First pass over the graph --- .../connectors/datamodel-connector/src/lib.rs | 2 +- .../core/src/common/preview_features.rs | 2 + .../core/src/query_graph_builder/builder.rs | 18 ++--- .../src/query_graph_builder/write/create.rs | 15 +++-- .../src/query_graph_builder/write/delete.rs | 17 ++++- .../write/nested/connect_or_create_nested.rs | 65 ++++++++++++++++--- .../write/nested/create_nested.rs | 3 +- .../write/nested/delete_nested.rs | 2 + .../query_graph_builder/write/nested/mod.rs | 18 ++--- .../write/nested/update_nested.rs | 10 ++- .../write/nested/upsert_nested.rs | 5 +- .../src/query_graph_builder/write/update.rs | 15 +++-- .../src/query_graph_builder/write/upsert.rs | 25 +++++-- query-engine/core/src/schema/query_schema.rs | 38 ++++++++++- query-engine/core/src/schema_builder/mod.rs | 4 +- 15 files changed, 187 insertions(+), 52 deletions(-) diff --git a/libs/datamodel/connectors/datamodel-connector/src/lib.rs b/libs/datamodel/connectors/datamodel-connector/src/lib.rs index c842898f0a17..1747e98c2dea 100644 --- a/libs/datamodel/connectors/datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/datamodel-connector/src/lib.rs @@ -197,7 +197,7 @@ pub enum ConnectorCapability { /// Contains all capabilities that the connector is able to serve. #[derive(Debug)] pub struct ConnectorCapabilities { - capabilities: Vec, + pub capabilities: Vec, } impl ConnectorCapabilities { diff --git a/libs/datamodel/core/src/common/preview_features.rs b/libs/datamodel/core/src/common/preview_features.rs index f793763585c5..901fbfa14dc4 100644 --- a/libs/datamodel/core/src/common/preview_features.rs +++ b/libs/datamodel/core/src/common/preview_features.rs @@ -53,6 +53,7 @@ features!( OrderByAggregateGroup, FilterJson, PlanetScaleMode, + ReferentialActions, ); // Mapping of which active, deprecated and hidden @@ -69,6 +70,7 @@ pub static GENERATOR: Lazy = Lazy::new(|| { OrderByAggregateGroup, FilterJson, PlanetScaleMode, + ReferentialActions, ]) .with_hidden(vec![MongoDb]) .with_deprecated(vec![ diff --git a/query-engine/core/src/query_graph_builder/builder.rs b/query-engine/core/src/query_graph_builder/builder.rs index 0cb4bd61502f..25442d847d22 100644 --- a/query-engine/core/src/query_graph_builder/builder.rs +++ b/query-engine/core/src/query_graph_builder/builder.rs @@ -4,8 +4,6 @@ use super::*; use crate::{constants::args, query_document::*, query_graph::*, schema::*, IrSerializer}; use prisma_value::PrismaValue; -// TODO: Think about if this is really necessary here, or if the whole code should move into -// the query_document module, possibly already as part of the parser. pub struct QueryGraphBuilder { pub query_schema: QuerySchemaRef, } @@ -85,9 +83,11 @@ impl QueryGraphBuilder { } #[tracing::instrument(skip(self, field_pair))] + #[rustfmt::skip] fn dispatch_build(&self, field_pair: FieldPair) -> QueryGraphBuilderResult { let query_info = field_pair.schema_field.query_info.as_ref().unwrap(); let parsed_field = field_pair.parsed_field; + let connector_ctx = self.query_schema.context(); let mut graph = match (&query_info.tag, query_info.model.clone()) { (QueryTag::FindUnique, Some(m)) => read::find_unique(parsed_field, m).map(Into::into), @@ -95,13 +95,13 @@ impl QueryGraphBuilder { (QueryTag::FindMany, Some(m)) => read::find_many(parsed_field, m).map(Into::into), (QueryTag::Aggregate, Some(m)) => read::aggregate(parsed_field, m).map(Into::into), (QueryTag::GroupBy, Some(m)) => read::group_by(parsed_field, m).map(Into::into), - (QueryTag::CreateOne, Some(m)) => QueryGraph::root(|g| write::create_record(g, m, parsed_field)), - (QueryTag::CreateMany, Some(m)) => QueryGraph::root(|g| write::create_many_records(g, m, parsed_field)), - (QueryTag::UpdateOne, Some(m)) => QueryGraph::root(|g| write::update_record(g, m, parsed_field)), - (QueryTag::UpdateMany, Some(m)) => QueryGraph::root(|g| write::update_many_records(g, m, parsed_field)), - (QueryTag::UpsertOne, Some(m)) => QueryGraph::root(|g| write::upsert_record(g, m, parsed_field)), - (QueryTag::DeleteOne, Some(m)) => QueryGraph::root(|g| write::delete_record(g, m, parsed_field)), - (QueryTag::DeleteMany, Some(m)) => QueryGraph::root(|g| write::delete_many_records(g, m, parsed_field)), + (QueryTag::CreateOne, Some(m)) => QueryGraph::root(|g| write::create_record(g, connector_ctx, m, parsed_field)), + (QueryTag::CreateMany, Some(m)) => QueryGraph::root(|g| write::create_many_records(g, connector_ctx,m, parsed_field)), + (QueryTag::UpdateOne, Some(m)) => QueryGraph::root(|g| write::update_record(g, connector_ctx, m, parsed_field)), + (QueryTag::UpdateMany, Some(m)) => QueryGraph::root(|g| write::update_many_records(g, connector_ctx, m, parsed_field)), + (QueryTag::UpsertOne, Some(m)) => QueryGraph::root(|g| write::upsert_record(g, connector_ctx, m, parsed_field)), + (QueryTag::DeleteOne, Some(m)) => QueryGraph::root(|g| write::delete_record(g, connector_ctx, m, parsed_field)), + (QueryTag::DeleteMany, Some(m)) => QueryGraph::root(|g| write::delete_many_records(g, connector_ctx, m, parsed_field)), (QueryTag::ExecuteRaw, _) => QueryGraph::root(|g| write::execute_raw(g, parsed_field)), (QueryTag::QueryRaw, _) => QueryGraph::root(|g| write::query_raw(g, parsed_field)), _ => unreachable!("Query builder dispatching failed."), diff --git a/query-engine/core/src/query_graph_builder/write/create.rs b/query-engine/core/src/query_graph_builder/write/create.rs index 2e051cddc590..1faeabd8258e 100644 --- a/query-engine/core/src/query_graph_builder/write/create.rs +++ b/query-engine/core/src/query_graph_builder/write/create.rs @@ -3,7 +3,7 @@ use crate::{ constants::args, query_ast::*, query_graph::{Node, NodeRef, QueryGraph, QueryGraphDependency}, - ArgumentListLookup, ParsedField, ParsedInputList, ParsedInputMap, + ArgumentListLookup, ConnectorContext, ParsedField, ParsedInputList, ParsedInputMap, }; use connector::IdFilter; use prisma_models::ModelRef; @@ -12,7 +12,12 @@ use write_args_parser::*; /// Creates a create record query and adds it to the query graph, together with it's nested queries and companion read query. #[tracing::instrument(skip(graph, model, field))] -pub fn create_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedField) -> QueryGraphBuilderResult<()> { +pub fn create_record( + graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, + model: ModelRef, + mut field: ParsedField, +) -> QueryGraphBuilderResult<()> { graph.flag_transactional(); let data_map = match field.arguments.lookup(args::DATA) { @@ -20,7 +25,7 @@ pub fn create_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedF None => ParsedInputMap::new(), }; - let create_node = create::create_record_node(graph, Arc::clone(&model), data_map)?; + let create_node = create::create_record_node(graph, connector_ctx, Arc::clone(&model), data_map)?; // Follow-up read query on the write let read_query = read::find_unique(field, model.clone())?; @@ -56,6 +61,7 @@ pub fn create_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedF #[tracing::instrument(skip(graph, model, field))] pub fn create_many_records( graph: &mut QueryGraph, + _connector_ctx: &ConnectorContext, model: ModelRef, mut field: ParsedField, ) -> QueryGraphBuilderResult<()> { @@ -95,6 +101,7 @@ pub fn create_many_records( #[tracing::instrument(skip(graph, model, data_map))] pub fn create_record_node( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, model: ModelRef, data_map: ParsedInputMap, ) -> QueryGraphBuilderResult { @@ -107,7 +114,7 @@ pub fn create_record_node( let create_node = graph.create_node(Query::Write(WriteQuery::CreateRecord(cr))); for (relation_field, data_map) in create_args.nested { - nested::connect_nested_query(graph, create_node, relation_field, data_map)?; + nested::connect_nested_query(graph, connector_ctx, create_node, relation_field, data_map)?; } Ok(create_node) diff --git a/query-engine/core/src/query_graph_builder/write/delete.rs b/query-engine/core/src/query_graph_builder/write/delete.rs index 75c8fe828631..57d0275c3cd1 100644 --- a/query-engine/core/src/query_graph_builder/write/delete.rs +++ b/query-engine/core/src/query_graph_builder/write/delete.rs @@ -3,15 +3,21 @@ use crate::{ constants::args, query_ast::*, query_graph::{QueryGraph, QueryGraphDependency}, - ArgumentListLookup, FilteredQuery, ParsedField, + ArgumentListLookup, ConnectorContext, FilteredQuery, ParsedField, }; use connector::filter::Filter; +use datamodel::common::preview_features::PreviewFeature; use prisma_models::ModelRef; use std::{convert::TryInto, sync::Arc}; /// Creates a top level delete record query and adds it to the query graph. #[tracing::instrument(skip(graph, model, field))] -pub fn delete_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedField) -> QueryGraphBuilderResult<()> { +pub fn delete_record( + graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, + model: ModelRef, + mut field: ParsedField, +) -> QueryGraphBuilderResult<()> { graph.flag_transactional(); let where_arg = field.arguments.lookup(args::WHERE).unwrap(); @@ -30,7 +36,11 @@ pub fn delete_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedF let delete_node = graph.create_node(delete_query); // Todo [RA] - utils::insert_deletion_checks(graph, &model, &read_node, &delete_node)?; + if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) { + todo!() + } else { + utils::insert_deletion_checks(graph, &model, &read_node, &delete_node)?; + } graph.create_edge( &read_node, @@ -57,6 +67,7 @@ pub fn delete_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedF #[tracing::instrument(skip(graph, model, field))] pub fn delete_many_records( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, model: ModelRef, mut field: ParsedField, ) -> QueryGraphBuilderResult<()> { diff --git a/query-engine/core/src/query_graph_builder/write/nested/connect_or_create_nested.rs b/query-engine/core/src/query_graph_builder/write/nested/connect_or_create_nested.rs index 669ad84fe514..1a832fb4ab56 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/connect_or_create_nested.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/connect_or_create_nested.rs @@ -16,6 +16,7 @@ use std::{convert::TryInto, sync::Arc}; #[tracing::instrument(skip(graph, parent_node, parent_relation_field, value, child_model))] pub fn nested_connect_or_create( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, value: ParsedInputValue, @@ -25,11 +26,32 @@ pub fn nested_connect_or_create( let values = utils::coerce_vec(value); if relation.is_many_to_many() { - handle_many_to_many(graph, parent_node, parent_relation_field, values, child_model) + handle_many_to_many( + graph, + connector_ctx, + parent_node, + parent_relation_field, + values, + child_model, + ) } else if relation.is_one_to_many() { - handle_one_to_many(graph, parent_node, parent_relation_field, values, child_model) + handle_one_to_many( + graph, + connector_ctx, + parent_node, + parent_relation_field, + values, + child_model, + ) } else { - handle_one_to_one(graph, parent_node, parent_relation_field, values, child_model) + handle_one_to_one( + graph, + connector_ctx, + parent_node, + parent_relation_field, + values, + child_model, + ) } } @@ -70,6 +92,7 @@ pub fn nested_connect_or_create( #[tracing::instrument(skip(graph, parent_node, parent_relation_field, values, child_model))] fn handle_many_to_many( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, values: Vec, @@ -91,7 +114,7 @@ fn handle_many_to_many( filter, )); - let create_node = create::create_record_node(graph, Arc::clone(child_model), create_map)?; + let create_node = create::create_record_node(graph, connector_ctx, Arc::clone(child_model), create_map)?; let if_node = graph.create_node(Flow::default_if()); let connect_exists_node = @@ -127,15 +150,30 @@ fn handle_many_to_many( #[tracing::instrument(skip(graph, parent_node, parent_relation_field, values, child_model))] fn handle_one_to_many( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, values: Vec, child_model: &ModelRef, ) -> QueryGraphBuilderResult<()> { if parent_relation_field.is_inlined_on_enclosing_model() { - one_to_many_inlined_parent(graph, parent_node, parent_relation_field, values, child_model) + one_to_many_inlined_parent( + graph, + connector_ctx, + parent_node, + parent_relation_field, + values, + child_model, + ) } else { - one_to_many_inlined_child(graph, parent_node, parent_relation_field, values, child_model) + one_to_many_inlined_child( + graph, + connector_ctx, + parent_node, + parent_relation_field, + values, + child_model, + ) } } @@ -143,6 +181,7 @@ fn handle_one_to_many( #[tracing::instrument(skip(graph, parent_node, parent_relation_field, values, child_model))] fn handle_one_to_one( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, mut values: Vec, @@ -162,6 +201,7 @@ fn handle_one_to_one( if parent_relation_field.is_inlined_on_enclosing_model() { one_to_one_inlined_parent( graph, + connector_ctx, parent_node, parent_relation_field, filter, @@ -171,6 +211,7 @@ fn handle_one_to_one( } else { one_to_one_inlined_child( graph, + connector_ctx, parent_node, parent_relation_field, filter, @@ -212,6 +253,7 @@ fn handle_one_to_one( #[tracing::instrument(skip(graph, parent_node, parent_relation_field, values, child_model))] fn one_to_many_inlined_child( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, values: Vec, @@ -238,7 +280,7 @@ fn one_to_many_inlined_child( let if_node = graph.create_node(Flow::default_if()); let update_child_node = utils::update_records_node_placeholder(graph, filter, Arc::clone(child_model)); - let create_node = create::create_record_node(graph, Arc::clone(child_model), create_map)?; + let create_node = create::create_record_node(graph, connector_ctx, Arc::clone(child_model), create_map)?; graph.create_edge(&parent_node, &read_node, QueryGraphDependency::ExecutionOrder)?; graph.create_edge(&if_node, &update_child_node, QueryGraphDependency::Then)?; @@ -353,6 +395,7 @@ fn one_to_many_inlined_child( #[tracing::instrument(skip(graph, parent_node, parent_relation_field, values, child_model))] fn one_to_many_inlined_parent( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, mut values: Vec, @@ -381,7 +424,7 @@ fn one_to_many_inlined_parent( graph.create_edge(&parent_node, &read_node, QueryGraphDependency::ExecutionOrder)?; let if_node = graph.create_node(Flow::default_if()); - let create_node = create::create_record_node(graph, Arc::clone(child_model), create_map)?; + let create_node = create::create_record_node(graph, connector_ctx, Arc::clone(child_model), create_map)?; let return_existing = graph.create_node(Flow::Return(None)); let return_create = graph.create_node(Flow::Return(None)); @@ -524,6 +567,7 @@ fn one_to_many_inlined_parent( #[tracing::instrument(skip(graph, parent_node, parent_relation_field, filter, create_data, child_model))] fn one_to_one_inlined_parent( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, filter: Filter, @@ -543,7 +587,7 @@ fn one_to_one_inlined_parent( graph.create_edge(&parent_node, &read_node, QueryGraphDependency::ExecutionOrder)?; let if_node = graph.create_node(Flow::default_if()); - let create_node = create::create_record_node(graph, Arc::clone(child_model), create_data)?; + let create_node = create::create_record_node(graph, connector_ctx, Arc::clone(child_model), create_data)?; let return_existing = graph.create_node(Flow::Return(None)); let return_create = graph.create_node(Flow::Return(None)); @@ -748,6 +792,7 @@ fn one_to_one_inlined_parent( #[tracing::instrument(skip(graph, parent_node, parent_relation_field, filter, create_data, child_model))] fn one_to_one_inlined_child( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, filter: Filter, @@ -771,7 +816,7 @@ fn one_to_one_inlined_child( graph.create_edge(&parent_node, &read_node, QueryGraphDependency::ExecutionOrder)?; let if_node = graph.create_node(Flow::default_if()); - let create_node = create::create_record_node(graph, Arc::clone(child_model), create_data)?; + let create_node = create::create_record_node(graph, connector_ctx, Arc::clone(child_model), create_data)?; graph.create_edge( &read_node, diff --git a/query-engine/core/src/query_graph_builder/write/nested/create_nested.rs b/query-engine/core/src/query_graph_builder/write/nested/create_nested.rs index a94373738f64..f2d1ef475fbe 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/create_nested.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/create_nested.rs @@ -16,6 +16,7 @@ use std::{convert::TryInto, sync::Arc}; #[tracing::instrument(skip(graph, parent_node, parent_relation_field, value, child_model))] pub fn nested_create( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, value: ParsedInputValue, @@ -26,7 +27,7 @@ pub fn nested_create( // Build all create nodes upfront. let creates: Vec = utils::coerce_vec(value) .into_iter() - .map(|value| create::create_record_node(graph, Arc::clone(child_model), value.try_into()?)) + .map(|value| create::create_record_node(graph, connector_ctx, Arc::clone(child_model), value.try_into()?)) .collect::>>()?; if relation.is_many_to_many() { diff --git a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs index a9d56f0cbc6b..712b2273f36a 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs @@ -23,6 +23,7 @@ use std::{convert::TryInto, sync::Arc}; #[tracing::instrument(skip(graph, parent_node, parent_relation_field, value, child_model))] pub fn nested_delete( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: &NodeRef, parent_relation_field: &RelationFieldRef, value: ParsedInputValue, @@ -128,6 +129,7 @@ pub fn nested_delete( #[tracing::instrument(skip(graph, parent, parent_relation_field, value, child_model))] pub fn nested_delete_many( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent: &NodeRef, parent_relation_field: &RelationFieldRef, value: ParsedInputValue, diff --git a/query-engine/core/src/query_graph_builder/write/nested/mod.rs b/query-engine/core/src/query_graph_builder/write/nested/mod.rs index a078709bd935..828c0a246c34 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/mod.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/mod.rs @@ -11,7 +11,7 @@ use super::*; use crate::{ constants::operations, query_graph::{NodeRef, QueryGraph}, - ParsedInputMap, + ConnectorContext, ParsedInputMap, }; use connect_nested::*; use connect_or_create_nested::*; @@ -24,8 +24,10 @@ use update_nested::*; use upsert_nested::*; #[tracing::instrument(skip(graph, parent, parent_relation_field, data_map))] +#[rustfmt::skip] pub fn connect_nested_query( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent: NodeRef, parent_relation_field: RelationFieldRef, data_map: ParsedInputMap, @@ -34,19 +36,17 @@ pub fn connect_nested_query( for (field_name, value) in data_map { match field_name.as_str() { - operations::CREATE => nested_create(graph, parent, &parent_relation_field, value, &child_model)?, + operations::CREATE => nested_create(graph, connector_ctx,parent, &parent_relation_field, value, &child_model)?, operations::CREATE_MANY => nested_create_many(graph, parent, &parent_relation_field, value, &child_model)?, - operations::UPDATE => nested_update(graph, &parent, &parent_relation_field, value, &child_model)?, - operations::UPSERT => nested_upsert(graph, parent, &parent_relation_field, value)?, - operations::DELETE => nested_delete(graph, &parent, &parent_relation_field, value, &child_model)?, + operations::UPDATE => nested_update(graph, connector_ctx, &parent, &parent_relation_field, value, &child_model)?, + operations::UPSERT => nested_upsert(graph, connector_ctx, parent, &parent_relation_field, value)?, + operations::DELETE => nested_delete(graph, connector_ctx, &parent, &parent_relation_field, value, &child_model)?, operations::CONNECT => nested_connect(graph, parent, &parent_relation_field, value, &child_model)?, operations::DISCONNECT => nested_disconnect(graph, parent, &parent_relation_field, value, &child_model)?, operations::SET => nested_set(graph, &parent, &parent_relation_field, value, &child_model)?, operations::UPDATE_MANY => nested_update_many(graph, &parent, &parent_relation_field, value, &child_model)?, - operations::DELETE_MANY => nested_delete_many(graph, &parent, &parent_relation_field, value, &child_model)?, - operations::CONNECT_OR_CREATE => { - nested_connect_or_create(graph, parent, &parent_relation_field, value, &child_model)? - } + operations::DELETE_MANY => nested_delete_many(graph, connector_ctx, &parent, &parent_relation_field, value, &child_model)?, + operations::CONNECT_OR_CREATE => nested_connect_or_create(graph, connector_ctx, parent, &parent_relation_field, value, &child_model)?, _ => panic!("Unhandled nested operation: {}", field_name), }; } diff --git a/query-engine/core/src/query_graph_builder/write/nested/update_nested.rs b/query-engine/core/src/query_graph_builder/write/nested/update_nested.rs index 9870f21f25b0..0eeff35ff0a7 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/update_nested.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/update_nested.rs @@ -30,6 +30,7 @@ use write_args_parser::*; #[tracing::instrument(skip(graph, parent, parent_relation_field, value, child_model))] pub fn nested_update( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent: &NodeRef, parent_relation_field: &RelationFieldRef, value: ParsedInputValue, @@ -54,8 +55,13 @@ pub fn nested_update( let find_child_records_node = utils::insert_find_children_by_parent_node(graph, parent, parent_relation_field, filter)?; - let update_node = - update::update_record_node(graph, Filter::empty(), Arc::clone(child_model), data.try_into()?)?; + let update_node = update::update_record_node( + graph, + connector_ctx, + Filter::empty(), + Arc::clone(child_model), + data.try_into()?, + )?; let child_model_identifier = parent_relation_field.related_model().primary_identifier(); diff --git a/query-engine/core/src/query_graph_builder/write/nested/upsert_nested.rs b/query-engine/core/src/query_graph_builder/write/nested/upsert_nested.rs index eeec9878e0c9..2670d6ad76b3 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/upsert_nested.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/upsert_nested.rs @@ -87,6 +87,7 @@ use std::{convert::TryInto, sync::Arc}; #[tracing::instrument(skip(graph, parent_node, parent_relation_field, value))] pub fn nested_upsert( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, parent_node: NodeRef, parent_relation_field: &RelationFieldRef, value: ParsedInputValue, @@ -117,9 +118,11 @@ pub fn nested_upsert( utils::insert_find_children_by_parent_node(graph, &parent_node, parent_relation_field, filter)?; let if_node = graph.create_node(Flow::default_if()); - let create_node = create::create_record_node(graph, Arc::clone(&child_model), create_input.try_into()?)?; + let create_node = + create::create_record_node(graph, connector_ctx, Arc::clone(&child_model), create_input.try_into()?)?; let update_node = update::update_record_node( graph, + connector_ctx, Filter::empty(), Arc::clone(&child_model), update_input.try_into()?, diff --git a/query-engine/core/src/query_graph_builder/write/update.rs b/query-engine/core/src/query_graph_builder/write/update.rs index 06c01ccd8051..fbebe951375b 100644 --- a/query-engine/core/src/query_graph_builder/write/update.rs +++ b/query-engine/core/src/query_graph_builder/write/update.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{constants::args, query_graph_builder::write::write_args_parser::*}; +use crate::{constants::args, query_graph_builder::write::write_args_parser::*, ConnectorContext}; use crate::{ query_ast::*, query_graph::{Node, NodeRef, QueryGraph, QueryGraphDependency}, @@ -11,7 +11,12 @@ use std::{convert::TryInto, sync::Arc}; /// Creates an update record query and adds it to the query graph, together with it's nested queries and companion read query. #[tracing::instrument(skip(graph, model, field))] -pub fn update_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedField) -> QueryGraphBuilderResult<()> { +pub fn update_record( + graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, + model: ModelRef, + mut field: ParsedField, +) -> QueryGraphBuilderResult<()> { // "where" let where_arg: ParsedInputMap = field.arguments.lookup(args::WHERE).unwrap().value.try_into()?; let filter = extract_unique_filter(where_arg, &model)?; @@ -20,7 +25,7 @@ pub fn update_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedF let data_argument = field.arguments.lookup(args::DATA).unwrap(); let data_map: ParsedInputMap = data_argument.value.try_into()?; - let update_node = update_record_node(graph, filter, Arc::clone(&model), data_map)?; + let update_node = update_record_node(graph, connector_ctx, filter, Arc::clone(&model), data_map)?; let read_query = read::find_unique(field, model.clone())?; let read_node = graph.create_node(Query::Read(read_query)); @@ -55,6 +60,7 @@ pub fn update_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedF #[tracing::instrument(skip(graph, model, field))] pub fn update_many_records( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, model: ModelRef, mut field: ParsedField, ) -> QueryGraphBuilderResult<()> { @@ -87,6 +93,7 @@ pub fn update_many_records( #[tracing::instrument(skip(graph, filter, model, data_map))] pub fn update_record_node( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, filter: T, model: ModelRef, data_map: ParsedInputMap, @@ -111,7 +118,7 @@ where let node = graph.create_node(Query::Write(WriteQuery::UpdateRecord(ur))); for (relation_field, data_map) in update_args.nested { - nested::connect_nested_query(graph, node, relation_field, data_map)?; + nested::connect_nested_query(graph, connector_ctx, node, relation_field, data_map)?; } Ok(node) diff --git a/query-engine/core/src/query_graph_builder/write/upsert.rs b/query-engine/core/src/query_graph_builder/write/upsert.rs index 06af31db83c7..8406a04de137 100644 --- a/query-engine/core/src/query_graph_builder/write/upsert.rs +++ b/query-engine/core/src/query_graph_builder/write/upsert.rs @@ -3,14 +3,19 @@ use crate::{ constants::args, query_ast::*, query_graph::{Flow, Node, QueryGraph, QueryGraphDependency}, - ArgumentListLookup, ParsedField, ParsedInputMap, + ArgumentListLookup, ConnectorContext, ParsedField, ParsedInputMap, }; use connector::IdFilter; use prisma_models::ModelRef; use std::{convert::TryInto, sync::Arc}; #[tracing::instrument(skip(graph, model, field))] -pub fn upsert_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedField) -> QueryGraphBuilderResult<()> { +pub fn upsert_record( + graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, + model: ModelRef, + mut field: ParsedField, +) -> QueryGraphBuilderResult<()> { graph.flag_transactional(); let where_arg: ParsedInputMap = field.arguments.lookup(args::WHERE).unwrap().value.try_into()?; @@ -24,8 +29,20 @@ pub fn upsert_record(graph: &mut QueryGraph, model: ModelRef, mut field: ParsedF let read_parent_records = utils::read_ids_infallible(model.clone(), model_id.clone(), filter.clone()); let read_parent_records_node = graph.create_node(read_parent_records); - let create_node = create::create_record_node(graph, Arc::clone(&model), create_argument.value.try_into()?)?; - let update_node = update::update_record_node(graph, filter, Arc::clone(&model), update_argument.value.try_into()?)?; + let create_node = create::create_record_node( + graph, + connector_ctx, + Arc::clone(&model), + create_argument.value.try_into()?, + )?; + + let update_node = update::update_record_node( + graph, + connector_ctx, + filter, + Arc::clone(&model), + update_argument.value.try_into()?, + )?; let read_query = read::find_unique(field, Arc::clone(&model))?; let read_node_create = graph.create_node(Query::Read(read_query.clone())); diff --git a/query-engine/core/src/schema/query_schema.rs b/query-engine/core/src/schema/query_schema.rs index d6a979a6abfd..ddcbf1e9e16a 100644 --- a/query-engine/core/src/schema/query_schema.rs +++ b/query-engine/core/src/schema/query_schema.rs @@ -1,4 +1,6 @@ use super::*; +use datamodel::common::preview_features::PreviewFeature; +use datamodel_connector::ConnectorCapability; use fmt::Debug; use prisma_models::{InternalDataModelRef, ModelRef}; use std::{borrow::Borrow, fmt}; @@ -18,16 +20,39 @@ use std::{borrow::Borrow, fmt}; /// Using a QuerySchema should never involve dealing with the strong references. #[derive(Debug)] pub struct QuerySchema { + /// Root query object (read queries). pub query: OutputTypeRef, + + /// Root mutation object (write queries). pub mutation: OutputTypeRef, - /// Stores all strong refs to the input object types. + /// Internal abstraction over the datamodel AST. + pub internal_data_model: InternalDataModelRef, + + /// Information about the connector this schema was build for. + pub context: ConnectorContext, + + /// Internal. Stores all strong Arc refs to the input object types. input_object_types: Vec, - /// Stores all strong refs to the output object types. + /// Internal. Stores all strong Arc refs to the output object types. output_object_types: Vec, +} - pub internal_data_model: InternalDataModelRef, +/// Connector meta information, to be used in query execution if necessary. +#[derive(Debug)] +pub struct ConnectorContext { + /// Capabilities of the provider. + pub capabilities: Vec, + + /// Enabled preview features. + pub features: Vec, +} + +impl ConnectorContext { + pub fn new(capabilities: Vec, features: Vec) -> Self { + Self { capabilities, features } + } } impl QuerySchema { @@ -37,6 +62,8 @@ impl QuerySchema { input_object_types: Vec, output_object_types: Vec, internal_data_model: InternalDataModelRef, + capabilities: Vec, + features: Vec, ) -> Self { QuerySchema { query, @@ -44,6 +71,7 @@ impl QuerySchema { input_object_types, output_object_types, internal_data_model, + context: ConnectorContext::new(capabilities, features), } } @@ -76,6 +104,10 @@ impl QuerySchema { _ => unreachable!(), } } + + pub fn context(&self) -> &ConnectorContext { + &self.context + } } /// Designates a specific top-level operation on a corresponding model. diff --git a/query-engine/core/src/schema_builder/mod.rs b/query-engine/core/src/schema_builder/mod.rs index 152c5afad7fe..3ac8c4f09c12 100644 --- a/query-engine/core/src/schema_builder/mod.rs +++ b/query-engine/core/src/schema_builder/mod.rs @@ -164,7 +164,7 @@ pub fn build( internal_data_model, enable_raw_queries, capabilities, - preview_features, + preview_features.clone(), ); output_types::output_objects::initialize_model_object_type_cache(&mut ctx); @@ -185,6 +185,8 @@ pub fn build( input_objects, output_objects, ctx.internal_data_model, + ctx.capabilities.capabilities, + preview_features, ) } From e6f8c4828c4ef14e0860027c9eefd04363ac4814 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 7 Jun 2021 14:32:40 +0200 Subject: [PATCH 06/47] Experiments with deletion checks. --- libs/prisma-models/src/field/relation.rs | 6 +++++- libs/prisma-models/src/relation.rs | 9 +++++++++ .../core/src/query_graph_builder/write/delete.rs | 6 +----- .../core/src/query_graph_builder/write/utils.rs | 10 +++++++++- query-engine/core/src/schema_builder/mod.rs | 1 + query-engine/query-engine/src/context.rs | 1 - 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/libs/prisma-models/src/field/relation.rs b/libs/prisma-models/src/field/relation.rs index 98f0294ca3de..c4a2dd24708a 100644 --- a/libs/prisma-models/src/field/relation.rs +++ b/libs/prisma-models/src/field/relation.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use datamodel::{FieldArity, RelationInfo}; +use datamodel::{FieldArity, ReferentialAction, RelationInfo}; use once_cell::sync::OnceCell; use std::{ fmt::Debug, @@ -299,4 +299,8 @@ impl RelationField { pub fn db_names(&self) -> impl Iterator { self.scalar_fields().into_iter().map(|f| f.db_name().to_owned()) } + + pub fn on_delete(&self) -> Option<&ReferentialAction> { + self.relation_info.on_delete.as_ref() + } } diff --git a/libs/prisma-models/src/relation.rs b/libs/prisma-models/src/relation.rs index 0e5aea5c72e9..189d1d86b4f5 100644 --- a/libs/prisma-models/src/relation.rs +++ b/libs/prisma-models/src/relation.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use datamodel::ReferentialAction; use once_cell::sync::OnceCell; use std::{ fmt::Debug, @@ -181,4 +182,12 @@ impl Relation { .upgrade() .expect("InternalDataModel does not exist anymore. Parent internal_data_model is deleted without deleting the child internal_data_model.") } + + /// Retrieves the onDelete policy for this relation. + pub fn on_delete(&self) -> ReferentialAction { + let a = self.field_a().on_delete().cloned(); + let b = self.field_b().on_delete().cloned(); + + a.or(b).expect("No referential action found for relation.") + } } diff --git a/query-engine/core/src/query_graph_builder/write/delete.rs b/query-engine/core/src/query_graph_builder/write/delete.rs index 57d0275c3cd1..2cb2e2048d7e 100644 --- a/query-engine/core/src/query_graph_builder/write/delete.rs +++ b/query-engine/core/src/query_graph_builder/write/delete.rs @@ -36,11 +36,7 @@ pub fn delete_record( let delete_node = graph.create_node(delete_query); // Todo [RA] - if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) { - todo!() - } else { - utils::insert_deletion_checks(graph, &model, &read_node, &delete_node)?; - } + utils::insert_deletion_checks(graph, connector_ctx, &model, &read_node, &delete_node)?; graph.create_edge( &read_node, diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index 0c354679823c..e0f92b612d35 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -1,9 +1,10 @@ use crate::{ query_ast::*, query_graph::{Flow, Node, NodeRef, QueryGraph, QueryGraphDependency}, - ParsedInputValue, QueryGraphBuilderError, QueryGraphBuilderResult, + ConnectorContext, ParsedInputValue, QueryGraphBuilderError, QueryGraphBuilderResult, }; use connector::{Filter, WriteArgs}; +use datamodel::common::preview_features::PreviewFeature; use itertools::Itertools; use prisma_models::{ModelProjection, ModelRef, RelationFieldRef}; use std::sync::Arc; @@ -308,6 +309,7 @@ pub fn insert_existing_1to1_related_model_checks( #[tracing::instrument(skip(graph, model, parent_node, child_node))] pub fn insert_deletion_checks( graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, model: &ModelRef, parent_node: &NodeRef, child_node: &NodeRef, @@ -322,6 +324,12 @@ pub fn insert_deletion_checks( // We know that the relation can't be a list and must be required on the related model for `model` (see fields_requiring_model). // For all requiring models (RM), we use the field on `model` to query for existing RM records and error out if at least one exists. for rf in relation_fields { + if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) { + if !matches!(rf.relation().on_delete(), datamodel::ReferentialAction::EmulateRestrict) { + continue; + } + } + let relation_field = rf.related_field(); let child_model_identifier = relation_field.related_model().primary_identifier(); let read_node = insert_find_children_by_parent_node(graph, parent_node, &relation_field, Filter::empty())?; diff --git a/query-engine/core/src/schema_builder/mod.rs b/query-engine/core/src/schema_builder/mod.rs index 3ac8c4f09c12..4d2841ebcf1b 100644 --- a/query-engine/core/src/schema_builder/mod.rs +++ b/query-engine/core/src/schema_builder/mod.rs @@ -166,6 +166,7 @@ pub fn build( capabilities, preview_features.clone(), ); + output_types::output_objects::initialize_model_object_type_cache(&mut ctx); let (query_type, query_object_ref) = output_types::query_type::build(&mut ctx); diff --git a/query-engine/query-engine/src/context.rs b/query-engine/query-engine/src/context.rs index 1886e4dbdd56..75b40e9cbb69 100644 --- a/query-engine/query-engine/src/context.rs +++ b/query-engine/query-engine/src/context.rs @@ -58,7 +58,6 @@ impl PrismaContext { let url = data_source.load_url()?; // Load executor - let preview_features: Vec<_> = config.preview_features().cloned().collect(); let (db_name, executor) = exec_loader::load(&data_source, &preview_features, &url).await?; From 1f3bc2c1eadb4524c82afaee07ee24c52fa7f78b Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 7 Jun 2021 15:18:41 +0200 Subject: [PATCH 07/47] Default virtual actions --- .../connectors/datamodel-connector/src/lib.rs | 4 + libs/datamodel/connectors/dml/src/field.rs | 17 ++ .../mongodb-datamodel-connector/src/lib.rs | 5 + .../src/mysql_datamodel_connector.rs | 6 + .../core/src/transform/ast_to_dml/lift.rs | 2 + .../src/transform/helpers/value_validator.rs | 2 + .../relations/referential_actions.rs | 184 ++++++++++++++++-- 7 files changed, 199 insertions(+), 21 deletions(-) diff --git a/libs/datamodel/connectors/datamodel-connector/src/lib.rs b/libs/datamodel/connectors/datamodel-connector/src/lib.rs index c842898f0a17..668205b66a85 100644 --- a/libs/datamodel/connectors/datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/datamodel-connector/src/lib.rs @@ -24,6 +24,10 @@ pub trait Connector: Send + Sync { self.referential_actions().contains(action) } + fn virtual_referential_actions(&self) -> bool { + false + } + fn validate_field(&self, field: &Field) -> Result<(), ConnectorError>; fn validate_model(&self, model: &Model) -> Result<(), ConnectorError>; diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index 8d333feda761..f51cbc0f0b76 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -262,6 +262,9 @@ pub struct RelationField { /// Is `ON DELETE/UPDATE RESTRICT` allowed. pub supports_restrict_action: Option, + + /// Do we run the referential actions in the core instead of the database. + pub virtual_referential_actions: Option, } impl PartialEq for RelationField { @@ -312,6 +315,7 @@ impl RelationField { is_commented_out: false, is_ignored: false, supports_restrict_action: None, + virtual_referential_actions: None, } } @@ -320,6 +324,11 @@ impl RelationField { self.supports_restrict_action = Some(value); } + /// The referential actions should be handled by the core. + pub fn virtual_referential_actions(&mut self, value: bool) { + self.virtual_referential_actions = Some(value); + } + /// Creates a new field with the given name and type, marked as generated and optional. pub fn new_generated(name: &str, info: RelationInfo, required: bool) -> Self { let arity = if required { @@ -355,16 +364,24 @@ impl RelationField { } pub fn default_on_delete_action(&self) -> Option { + let is_virtual = self.virtual_referential_actions.unwrap_or(false); + self.supports_restrict_action.map(|restrict_ok| match self.arity { + FieldArity::Required if is_virtual => ReferentialAction::EmulateRestrict, FieldArity::Required if restrict_ok => ReferentialAction::Restrict, FieldArity::Required => ReferentialAction::NoAction, + _ if is_virtual => ReferentialAction::EmulateSetNull, _ => ReferentialAction::SetNull, }) } pub fn default_on_update_action(&self) -> ReferentialAction { + let is_virtual = self.virtual_referential_actions.unwrap_or(false); + match self.arity { + FieldArity::Required if is_virtual => ReferentialAction::EmulateRestrict, FieldArity::Required => ReferentialAction::Cascade, + _ if is_virtual => ReferentialAction::EmulateSetNull, _ => ReferentialAction::SetNull, } } diff --git a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs index 0b48243082f0..3199c67165c5 100644 --- a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs @@ -35,6 +35,7 @@ impl MongoDbDatamodelConnector { ConnectorCapability::CreateSkipDuplicates, ConnectorCapability::ScalarLists, ConnectorCapability::InsensitiveFilters, + ConnectorCapability::ReferentialActions, ]; let native_types = mongodb_types::available_types(); @@ -67,6 +68,10 @@ impl Connector for MongoDbDatamodelConnector { self.referential_actions } + fn virtual_referential_actions(&self) -> bool { + true + } + 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/src/mysql_datamodel_connector.rs b/libs/datamodel/connectors/sql-datamodel-connector/src/mysql_datamodel_connector.rs index b20a84968133..0405b96843fa 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 @@ -62,6 +62,7 @@ pub struct MySqlDatamodelConnector { capabilities: Vec, constructors: Vec, referential_actions: BitFlags, + is_planetscale: bool, } impl MySqlDatamodelConnector { @@ -168,6 +169,7 @@ impl MySqlDatamodelConnector { capabilities, constructors, referential_actions, + is_planetscale, } } } @@ -197,6 +199,10 @@ impl Connector for MySqlDatamodelConnector { self.referential_actions } + fn virtual_referential_actions(&self) -> bool { + self.is_planetscale + } + 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/core/src/transform/ast_to_dml/lift.rs b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs index fc02371450aa..a4e414db1a6c 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs @@ -165,6 +165,8 @@ impl<'a> LiftAstToDml<'a> { .active_connector .supports_referential_action(ReferentialAction::Restrict), ); + + field.virtual_referential_actions(source.active_connector.virtual_referential_actions()); } field.documentation = ast_field.documentation.clone().map(|comment| comment.text); diff --git a/libs/datamodel/core/src/transform/helpers/value_validator.rs b/libs/datamodel/core/src/transform/helpers/value_validator.rs index 0df1f4d7ba70..ec61bb1cfd03 100644 --- a/libs/datamodel/core/src/transform/helpers/value_validator.rs +++ b/libs/datamodel/core/src/transform/helpers/value_validator.rs @@ -173,6 +173,8 @@ impl ValueValidator { "NoAction" => Ok(ReferentialAction::NoAction), "SetNull" => Ok(ReferentialAction::SetNull), "SetDefault" => Ok(ReferentialAction::SetDefault), + "EmulateRestrict" => Ok(ReferentialAction::EmulateRestrict), + "EmulateSetNull" => Ok(ReferentialAction::EmulateSetNull), s => { let message = format!("Invalid referential action: `{}`", s); diff --git a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs index 9c12b5705e76..f852a75771cb 100644 --- a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs +++ b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs @@ -58,6 +58,80 @@ fn on_update_actions() { } } +#[test] +fn virtual_actions_on_mongo() { + let actions = &[EmulateRestrict, EmulateSetNull]; + + for action in actions { + let dml = formatdoc!( + 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], onDelete: {action}, onUpdate: {action}) + }} + "#, + action = action + ); + + parse(&dml) + .assert_has_model("B") + .assert_has_relation_field("a") + .assert_relation_delete_strategy(*action) + .assert_relation_update_strategy(*action); + } +} + +#[test] +fn virtual_actions_on_planetscale() { + let actions = &[EmulateRestrict, EmulateSetNull]; + + for action in actions { + let dml = formatdoc!( + r#" + datasource db {{ + provider = "mysql" + planetScaleMode = true + url = "mysql://root:prisma@localhost:3306/mydb" + }} + + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["planetScaleMode"] + }} + + model A {{ + id Int @id + bs B[] + }} + + model B {{ + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onDelete: {action}, onUpdate: {action}) + }} + "#, + action = action + ); + + parse(&dml) + .assert_has_model("B") + .assert_has_relation_field("a") + .assert_relation_delete_strategy(*action) + .assert_relation_update_strategy(*action); + } +} + #[test] fn invalid_on_delete_action() { let dml = indoc! { r#" @@ -132,29 +206,97 @@ fn restrict_should_not_work_on_sql_server() { } #[test] -fn nothing_should_work_on_mongo() { - let dml = indoc! {r#" - datasource db { - provider = "mongodb" - url = "mongodb://" - } +fn concrete_actions_should_not_work_on_mongo() { + let actions = &[ + (Cascade, 237), + (Restrict, 238), + (NoAction, 238), + (SetNull, 237), + (SetDefault, 240), + ]; - model A { - id Int @id @map("_id") - bs B[] - } + for (action, span) in actions { + let dml = formatdoc!( + r#" + datasource db {{ + provider = "mongodb" + url = "mongodb://" + }} - model B { - id Int @id @map("_id") - aId Int - a A @relation(fields: [aId], references: [id], onUpdate: Cascade, onDelete: Cascade) - } - "#}; + model A {{ + id Int @id @map("_id") + bs B[] + }} - let message = "Referential actions are not supported for current connector."; + model B {{ + id Int @id @map("_id") + aId Int + a A @relation(fields: [aId], references: [id], onDelete: {}) + }} + "#, + action + ); - 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)), - ]); + let message = format!( + "Invalid referential action: `{}`. Allowed values: (`EmulateSetNull`, `EmulateRestrict`)", + action + ); + + parse_error(&dml).assert_are(&[DatamodelError::new_attribute_validation_error( + &message, + "relation", + Span::new(171, *span), + )]); + } +} + +#[test] +fn concrete_actions_should_not_work_on_planetscale() { + let actions = &[ + (Cascade, 389), + (Restrict, 390), + (NoAction, 390), + (SetNull, 389), + (SetDefault, 392), + ]; + + for (action, span) in actions { + let dml = formatdoc!( + r#" + datasource db {{ + provider = "mysql" + planetScaleMode = true + url = "mysql://root:prisma@localhost:3306/mydb" + }} + + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["planetScaleMode"] + }} + + 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], onDelete: {}) + }} + "#, + action + ); + + let message = format!( + "Invalid referential action: `{}`. Allowed values: (`EmulateSetNull`, `EmulateRestrict`)", + action + ); + + parse_error(&dml).assert_are(&[DatamodelError::new_attribute_validation_error( + &message, + "relation", + Span::new(323, *span), + )]); + } } From 9e6ea453101004e93030872e114b92be78a67bc6 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 7 Jun 2021 15:57:54 +0200 Subject: [PATCH 08/47] Working db propagation. --- datamodel_v2.prisma | 119 +++--------------- .../src/query_graph_builder/write/delete.rs | 4 +- .../write/nested/delete_nested.rs | 24 +++- .../src/query_graph_builder/write/utils.rs | 10 +- 4 files changed, 50 insertions(+), 107 deletions(-) diff --git a/datamodel_v2.prisma b/datamodel_v2.prisma index 12ae3fbbcbcc..16deb57779f1 100644 --- a/datamodel_v2.prisma +++ b/datamodel_v2.prisma @@ -1,115 +1,34 @@ -datasource chinook { - provider = "sqlite" - url = "file:./db/Chinook.db?connection_limit=1&socket_timeout=20" +datasource db { + provider = "postgres" + url = "postgresql://postgres:prisma@localhost:5432" } generator js { provider = "prisma-client-js" - previewFeatures = ["microsoftSqlServer", "mongodb", "orderByRelation", "napi", "selectRelationCount", "orderByAggregateGroup"] + previewFeatures = ["microsoftSqlServer", "mongodb", "orderByRelation", "napi", "selectRelationCount", "orderByAggregateGroup", "referentialActions"] } -model Album { - id Int @id @map("AlbumId") - Title String @default("TestDefaultTitle") - ArtistId Int - Tracks Track[] - Artist Artist @relation(fields: [ArtistId], references: [id]) -} - -model Track { - id Int @id @map("TrackId") - Name String - Composer String? - Milliseconds Int - UnitPrice Float - AlbumId Int? - GenreId Int? - MediaTypeId Int - MediaType MediaType @relation(fields: [MediaTypeId], references: [id]) - Genre Genre? @relation(fields: [GenreId], references: [id]) - Album Album? @relation(fields: [AlbumId], references: [id]) - InvoiceLines InvoiceLine[] -} - -model MediaType { - id Int @id @map("MediaTypeId") - Name String? - Track Track[] -} +model A { + id String @id + gql String? -model Genre { - id Int @id @map("GenreId") - Name String? - Tracks Track[] -} + bs B[] -model Artist { - id Int @id @map("ArtistId") - Name String? - Albums Album[] + c_id String + c C @relation(fields: [c_id], onDelete: Cascade, references: [id]) } -model Customer { - id Int @id @map("CustomerId") - FirstName String - LastName String - Company String? - Address String? - City String? - State String? - Country String? - PostalCode String? - Phone String? - Fax String? - Email String - SupportRepId Int? - SupportRep Employee? @relation(fields: [SupportRepId], references: [id]) - Invoices Invoice[] -} +model B { + id String @id + gql String? -model Employee { - id Int @id @map("EmployeeId") - FirstName String - LastName String - Title String? - BirthDate DateTime? - HireDate DateTime? - Address String? - City String? - State String? - Country String? - PostalCode String? - Phone String? - Fax String? - Email String? - Customers Customer[] + a_id String + a A @relation(fields: [a_id], references: [id], onDelete: Cascade) } -model Invoice { - id Int @id @map("InvoiceId") - InvoiceDate DateTime - BillingAddress String? - BillingCity String? - BillingState String? - BillingCountry String? - BillingPostalCode String? - Total Float - CustomerId Int - Customer Customer @relation(fields: [CustomerId], references: [id]) - Lines InvoiceLine[] -} - -model InvoiceLine { - id Int @id @map("InvoiceLineId") - UnitPrice Float - Quantity Int - InvoiceId Int - TrackId Int - Invoice Invoice @relation(fields: [InvoiceId], references: [id]) - Track Track @relation(fields: [TrackId], references: [id]) -} +model C { + id String @id + gql String? -model Playlist { - id Int @id @map("PlaylistId") - Name String? + a A? } diff --git a/query-engine/core/src/query_graph_builder/write/delete.rs b/query-engine/core/src/query_graph_builder/write/delete.rs index 2cb2e2048d7e..149e5032d1c2 100644 --- a/query-engine/core/src/query_graph_builder/write/delete.rs +++ b/query-engine/core/src/query_graph_builder/write/delete.rs @@ -6,7 +6,6 @@ use crate::{ ArgumentListLookup, ConnectorContext, FilteredQuery, ParsedField, }; use connector::filter::Filter; -use datamodel::common::preview_features::PreviewFeature; use prisma_models::ModelRef; use std::{convert::TryInto, sync::Arc}; @@ -86,7 +85,8 @@ pub fn delete_many_records( let delete_many_node = graph.create_node(Query::Write(delete_many)); // Todo [RA] - utils::insert_deletion_checks(graph, &model, &read_query_node, &delete_many_node)?; + utils::insert_deletion_checks(graph, connector_ctx, &model, &read_query_node, &delete_many_node)?; + graph.create_edge( &read_query_node, &delete_many_node, diff --git a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs index 712b2273f36a..6e063d941ac6 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs @@ -52,7 +52,13 @@ pub fn nested_delete( utils::insert_find_children_by_parent_node(graph, parent_node, parent_relation_field, or_filter)?; // Todo [RA] - utils::insert_deletion_checks(graph, child_model, &find_child_records_node, &delete_many_node)?; + utils::insert_deletion_checks( + graph, + connector_ctx, + child_model, + &find_child_records_node, + &delete_many_node, + )?; let relation_name = parent_relation_field.relation().name.clone(); let parent_name = parent_relation_field.model().name.clone(); @@ -93,7 +99,13 @@ pub fn nested_delete( record_filter: None, }))); - utils::insert_deletion_checks(graph, child_model, &find_child_records_node, &delete_record_node)?; + utils::insert_deletion_checks( + graph, + connector_ctx, + child_model, + &find_child_records_node, + &delete_record_node, + )?; let relation_name = parent_relation_field.relation().name.clone(); let child_model_name = child_model.name.clone(); @@ -152,7 +164,13 @@ pub fn nested_delete_many( let delete_many_node = graph.create_node(Query::Write(delete_many)); // Todo [RA] - utils::insert_deletion_checks(graph, child_model, &find_child_records_node, &delete_many_node)?; + utils::insert_deletion_checks( + graph, + connector_ctx, + child_model, + &find_child_records_node, + &delete_many_node, + )?; graph.create_edge( &find_child_records_node, diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index e0f92b612d35..dbe476732c3b 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -6,6 +6,7 @@ use crate::{ use connector::{Filter, WriteArgs}; use datamodel::common::preview_features::PreviewFeature; use itertools::Itertools; +use once_cell::sync::OnceCell; use prisma_models::{ModelProjection, ModelRef, RelationFieldRef}; use std::sync::Arc; @@ -318,8 +319,10 @@ pub fn insert_deletion_checks( let relation_fields = internal_model.fields_requiring_model(model); let mut check_nodes = vec![]; + let once = OnceCell::new(); + if !relation_fields.is_empty() { - let noop_node = graph.create_node(Node::Empty); + // let noop_node = graph.create_node(Node::Empty); // We know that the relation can't be a list and must be required on the related model for `model` (see fields_requiring_model). // For all requiring models (RM), we use the field on `model` to query for existing RM records and error out if at least one exists. @@ -330,6 +333,7 @@ pub fn insert_deletion_checks( } } + let noop_node = once.get_or_init(|| graph.create_node(Node::Empty)); let relation_field = rf.related_field(); let child_model_identifier = relation_field.related_model().primary_identifier(); let read_node = insert_find_children_by_parent_node(graph, parent_node, &relation_field, Filter::empty())?; @@ -362,7 +366,9 @@ pub fn insert_deletion_checks( }); // Edge from empty node to the child (delete). - graph.create_edge(&noop_node, child_node, QueryGraphDependency::ExecutionOrder)?; + if let Some(noop_node) = once.get() { + graph.create_edge(&noop_node, child_node, QueryGraphDependency::ExecutionOrder)?; + } } Ok(()) From a4032c5294b1a3976c7a04e72e203e9f523a17da Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 7 Jun 2021 19:48:33 +0200 Subject: [PATCH 09/47] Add new prisma-fmt command referential-actions Call it with starting prisma-fmt with subcommand referential actions. STDIN takes the data model and stdout lists all allowed actions. --- prisma-fmt/src/actions.rs | 33 +++++++++++++++++++++++++++++++++ prisma-fmt/src/main.rs | 4 ++++ 2 files changed, 37 insertions(+) create mode 100644 prisma-fmt/src/actions.rs diff --git a/prisma-fmt/src/actions.rs b/prisma-fmt/src/actions.rs new file mode 100644 index 000000000000..66c26f89ddbd --- /dev/null +++ b/prisma-fmt/src/actions.rs @@ -0,0 +1,33 @@ +use std::io::{self, Read}; + +pub fn run() { + let mut datamodel_string = String::new(); + + io::stdin() + .read_to_string(&mut datamodel_string) + .expect("Unable to read from stdin."); + + let datamodel_result = datamodel::parse_configuration(&datamodel_string); + + match datamodel_result { + Ok(validated_configuration) => { + if validated_configuration.subject.datasources.len() != 1 { + print!("[]") + } else if let Some(datasource) = validated_configuration.subject.datasources.first() { + let available_referential_actions = datasource + .active_connector + .referential_actions() + .iter() + .map(|act| format!("{:?}", act)) + .collect::>(); + + let json = serde_json::to_string(&available_referential_actions).expect("Failed to render JSON"); + + print!("{}", json) + } else { + print!("[]") + } + } + _ => print!("[]"), + } +} diff --git a/prisma-fmt/src/main.rs b/prisma-fmt/src/main.rs index adc43ecf8720..2a938ccb3b82 100644 --- a/prisma-fmt/src/main.rs +++ b/prisma-fmt/src/main.rs @@ -1,3 +1,4 @@ +mod actions; mod format; mod lint; mod native; @@ -45,6 +46,8 @@ pub enum FmtOpts { Format(FormatOpts), /// Specifies Native Types mode NativeTypes, + /// List of available referential actions + ReferentialActions, /// Specifies preview features mode PreviewFeatures(PreviewFeaturesOpts), } @@ -62,6 +65,7 @@ fn main() { FmtOpts::Lint(opts) => lint::run(opts), FmtOpts::Format(opts) => format::run(opts), FmtOpts::NativeTypes => native::run(), + FmtOpts::ReferentialActions => actions::run(), FmtOpts::PreviewFeatures(opts) => preview::run(opts), } } From 4f4f04f29c49fd5c91c6f222bb448f004f567d54 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 8 Jun 2021 18:18:31 +0200 Subject: [PATCH 10/47] Get the default actions from dml (if possible) --- libs/datamodel/core/src/walkers.rs | 8 ++++++++ .../sql_schema_calculator_flavour.rs | 19 ++++++++----------- .../sql_schema_calculator_flavour/mssql.rs | 11 +++++++---- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/libs/datamodel/core/src/walkers.rs b/libs/datamodel/core/src/walkers.rs index 9c9a3e190466..54c13280830c 100644 --- a/libs/datamodel/core/src/walkers.rs +++ b/libs/datamodel/core/src/walkers.rs @@ -349,6 +349,14 @@ impl<'a> RelationFieldWalker<'a> { pub fn on_delete_action(&self) -> Option { self.get().relation_info.on_delete } + + pub fn default_on_update_action(&self) -> ReferentialAction { + self.get().default_on_update_action() + } + + pub fn default_on_delete_action(&self) -> Option { + self.get().default_on_delete_action() + } } #[derive(Debug, Clone, Copy)] 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 d453b11aa772..b4015b276473 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 @@ -39,22 +39,19 @@ pub(crate) trait SqlSchemaCalculatorFlavour { } 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) + .unwrap_or_else(|| convert_referential_action(rf.default_on_update_action())) } 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!(), + let default = || { + rf.default_on_delete_action() + .map(convert_referential_action) + .unwrap_or_else(|| match rf.arity() { + FieldArity::Required => sql::ForeignKeyAction::Restrict, + _ => sql::ForeignKeyAction::SetNull, + }) }; rf.on_delete_action() 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 0c4fec66ceae..a2c89c9c670b 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 @@ -22,10 +22,13 @@ 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!(), + let default = || { + rf.default_on_delete_action() + .map(super::convert_referential_action) + .unwrap_or_else(|| match rf.arity() { + FieldArity::Required => sql::ForeignKeyAction::NoAction, + _ => sql::ForeignKeyAction::SetNull, + }) }; rf.on_delete_action() From 7bef40ae34a515ee0649149c30f8e950981422ed Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 8 Jun 2021 18:56:05 +0200 Subject: [PATCH 11/47] onUpdate should always CASCADE. --- .../introspection-engine-tests/tests/re_introspection/mod.rs | 4 ++-- libs/datamodel/connectors/dml/src/field.rs | 3 +-- .../migration-engine-tests/tests/migrations/postgres.rs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) 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 e47e13b0d0c2..c91eb79c49da 100644 --- a/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs @@ -1856,12 +1856,12 @@ async fn default_optional_actions(api: &TestApi) -> TestResult { match family { SqlFamily::Mssql => { t.inject_custom( - "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_optional_actions.a(id) ON DELETE SET NULL ON UPDATE SET NULL", + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_optional_actions.a(id) ON DELETE SET NULL ON UPDATE CASCADE", ); } _ => { t.inject_custom( - "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE SET NULL ON UPDATE SET NULL", + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE SET NULL ON UPDATE CASCADE", ); } } diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index f51cbc0f0b76..e78565d07bad 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -380,9 +380,8 @@ impl RelationField { match self.arity { FieldArity::Required if is_virtual => ReferentialAction::EmulateRestrict, - FieldArity::Required => ReferentialAction::Cascade, _ if is_virtual => ReferentialAction::EmulateSetNull, - _ => ReferentialAction::SetNull, + _ => ReferentialAction::Cascade, } } } diff --git a/migration-engine/migration-engine-tests/tests/migrations/postgres.rs b/migration-engine/migration-engine-tests/tests/migrations/postgres.rs index 9821be75f4a5..3061414e24a9 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/postgres.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/postgres.rs @@ -167,7 +167,7 @@ fn uuids_do_not_generate_drift_issue_5282(api: TestApi) { 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) ON DELETE SET NULL ON UPDATE SET NULL); + 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 CASCADE); "# ); From ecf9b5b9d14f789a8985e396a652d016a9647436 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Wed, 9 Jun 2021 16:20:28 +0200 Subject: [PATCH 12/47] Minor schema fix for update --- .../query-engine-tests/tests/new/mod.rs | 1 + .../tests/new/ref_actions/mod.rs | 1 + .../tests/new/ref_actions/to_one.rs | 64 +++++++++++++++++++ .../input_types/objects/update_one_objects.rs | 13 ++++ 4 files changed, 79 insertions(+) create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/mod.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/to_one.rs diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/mod.rs index 73e346653b62..9ad887944cee 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/mod.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/mod.rs @@ -1,3 +1,4 @@ mod create_many; mod cursor; mod disconnect; +mod ref_actions; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/mod.rs new file mode 100644 index 000000000000..eb32d3684bf0 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/mod.rs @@ -0,0 +1 @@ +mod to_one; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/to_one.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/to_one.rs new file mode 100644 index 000000000000..7cf301ae5863 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/to_one.rs @@ -0,0 +1,64 @@ +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(schema(schema))] +mod to_one { + fn schema() -> String { + let schema = indoc! { + r#" + model Parent { + id String @id @map("_id") + p String @unique + p_1 String + p_2 String + childOpt Child? + non_unique String? + + @@unique([p_1, p_2]) + } + + model Child { + id String @id @default(cuid()) @map("_id") + c String @unique + c_1 String + c_2 String + parentOpt Parent? @relation(fields: [parentRef], references: [p]) + parentRef String? + non_unique String? + + @@unique([c_1, c_2]) + } + "# + }; + + schema.to_owned() + } + + #[connector_test] + async fn vanilla(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: "1", p: "p1", p_1: "p", p_2: "1" childOpt: { create: {c: "c1", c_1: "foo", c_2: "bar"} } }){ p childOpt{ c } }}"#), + @r###"{"data":{"createOneParent":{"p":"p1","childOpt":{"c":"c1"}}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { upsertOneParent( where: { p: "p1" } update:{ p: { set: "p2" } childOpt: {delete: true} } create:{id: "whatever" ,p: "Should not matter", p_1: "no", p_2: "yes"} ){ childOpt { c } }}"#), + @r###"{"data":{"upsertOneParent":{"childOpt":null}}}"### + ); + + Ok(()) + } + + async fn create_test_data(runner: &Runner) -> TestResult<()> { + create_row(runner, r#"{ uniqueField: 1, nonUniqFieldA: "A", nonUniqFieldB: "A"}"#).await?; + + Ok(()) + } + + async fn create_row(runner: &Runner, data: &str) -> TestResult<()> { + runner + .query(format!("mutation {{ createOneTestModel(data: {}) {{ id }} }}", data)) + .await?; + Ok(()) + } +} diff --git a/query-engine/core/src/schema_builder/input_types/objects/update_one_objects.rs b/query-engine/core/src/schema_builder/input_types/objects/update_one_objects.rs index 3e5e6d28d426..4e1fdfdd62be 100644 --- a/query-engine/core/src/schema_builder/input_types/objects/update_one_objects.rs +++ b/query-engine/core/src/schema_builder/input_types/objects/update_one_objects.rs @@ -114,11 +114,24 @@ pub(super) fn scalar_input_fields_for_unchecked_update( vec![] }; + let id_fields = model.fields().id(); let scalar_fields: Vec = model .fields() .scalar() .into_iter() .filter(|sf| !linking_fields.contains(sf)) + .filter(|sf| { + if let Some(ref id_fields) = id_fields { + // Exclude @@id or @id fields if not updatable + if id_fields.contains(sf) { + ctx.capabilities.contains(ConnectorCapability::UpdateableId) + } else { + false + } + } else { + true + } + }) .collect(); input_fields::scalar_input_fields( From 7eaaaf0d7b6830f33c667214d03b24d9dc8e78a8 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Wed, 9 Jun 2021 16:31:44 +0200 Subject: [PATCH 13/47] Core graph debugging --- .envrc | 4 ++-- .../NestedDeleteMutationInsideUpsertSpec.scala | 2 +- .../src/query_graph_builder/write/delete.rs | 3 --- .../src/query_graph_builder/write/utils.rs | 18 +++++++++--------- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.envrc b/.envrc index fe5abd452215..74a3a65ea45f 100644 --- a/.envrc +++ b/.envrc @@ -1,11 +1,11 @@ export SERVER_ROOT=$(pwd) export RUST_LOG_FORMAT=devel export RUST_BACKTRACE=1 -export RUST_LOG=query_engine=debug,query_core=trace,query_connector=debug,sql_query_connector=debug,prisma_models=debug,engineer=info,sql_introspection_connector=debug +export RUST_LOG=query_engine_tests=trace,query_engine=debug,query_core=trace,query_connector=debug,sql_query_connector=debug,prisma_models=debug,engineer=info,sql_introspection_connector=debug export SKIP_CONNECTORS="vitess_5_7,vitess_8_0" # Controls Scala test kit verbosity. Levels are trace, debug, info, error, warning -export LOG_LEVEL=debug +export LOG_LEVEL=trace # Controls Scala test kit test complexity ("simple", "complex") export TEST_MODE=simple diff --git a/query-engine/connector-test-kit/src/test/scala/writes/nestedMutations/alreadyConverted/NestedDeleteMutationInsideUpsertSpec.scala b/query-engine/connector-test-kit/src/test/scala/writes/nestedMutations/alreadyConverted/NestedDeleteMutationInsideUpsertSpec.scala index 6d03ef7f9830..98a500a2c642 100644 --- a/query-engine/connector-test-kit/src/test/scala/writes/nestedMutations/alreadyConverted/NestedDeleteMutationInsideUpsertSpec.scala +++ b/query-engine/connector-test-kit/src/test/scala/writes/nestedMutations/alreadyConverted/NestedDeleteMutationInsideUpsertSpec.scala @@ -28,7 +28,7 @@ class NestedDeleteMutationInsideUpsertSpec extends FlatSpec with Matchers with A | } | }){ | ${t.parent.selection} - | childReq{ + | childReq { | ${t.child.selection} | } | } diff --git a/query-engine/core/src/query_graph_builder/write/delete.rs b/query-engine/core/src/query_graph_builder/write/delete.rs index 149e5032d1c2..94e8df4240e9 100644 --- a/query-engine/core/src/query_graph_builder/write/delete.rs +++ b/query-engine/core/src/query_graph_builder/write/delete.rs @@ -33,8 +33,6 @@ pub fn delete_record( })); let delete_node = graph.create_node(delete_query); - - // Todo [RA] utils::insert_deletion_checks(graph, connector_ctx, &model, &read_node, &delete_node)?; graph.create_edge( @@ -84,7 +82,6 @@ pub fn delete_many_records( let read_query_node = graph.create_node(read_query); let delete_many_node = graph.create_node(Query::Write(delete_many)); - // Todo [RA] utils::insert_deletion_checks(graph, connector_ctx, &model, &read_query_node, &delete_many_node)?; graph.create_edge( diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index dbe476732c3b..3f8f33f1136a 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -322,16 +322,16 @@ pub fn insert_deletion_checks( let once = OnceCell::new(); if !relation_fields.is_empty() { - // let noop_node = graph.create_node(Node::Empty); + let noop_node = graph.create_node(Node::Empty); // We know that the relation can't be a list and must be required on the related model for `model` (see fields_requiring_model). // For all requiring models (RM), we use the field on `model` to query for existing RM records and error out if at least one exists. for rf in relation_fields { - if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) { - if !matches!(rf.relation().on_delete(), datamodel::ReferentialAction::EmulateRestrict) { - continue; - } - } + // if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) { + // if !matches!(rf.relation().on_delete(), datamodel::ReferentialAction::EmulateRestrict) { + // continue; + // } + // } let noop_node = once.get_or_init(|| graph.create_node(Node::Empty)); let relation_field = rf.related_field(); @@ -366,9 +366,9 @@ pub fn insert_deletion_checks( }); // Edge from empty node to the child (delete). - if let Some(noop_node) = once.get() { - graph.create_edge(&noop_node, child_node, QueryGraphDependency::ExecutionOrder)?; - } + // if let Some(noop_node) = once.get() { + graph.create_edge(&noop_node, child_node, QueryGraphDependency::ExecutionOrder)?; + // } } Ok(()) From eb2ff28a019fc6e14fd83665044afeb1635f84f6 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Wed, 9 Jun 2021 18:44:53 +0200 Subject: [PATCH 14/47] Emulation defaults --- libs/datamodel/connectors/dml/src/field.rs | 7 ++++--- libs/datamodel/connectors/dml/src/relation_info.rs | 3 +++ .../connectors/mongodb-datamodel-connector/src/lib.rs | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index e78565d07bad..f1d77b4a6710 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -368,9 +368,11 @@ impl RelationField { self.supports_restrict_action.map(|restrict_ok| match self.arity { FieldArity::Required if is_virtual => ReferentialAction::EmulateRestrict, + FieldArity::Optional if is_virtual => ReferentialAction::EmulateSetNull, + FieldArity::Required if restrict_ok => ReferentialAction::Restrict, FieldArity::Required => ReferentialAction::NoAction, - _ if is_virtual => ReferentialAction::EmulateSetNull, + _ => ReferentialAction::SetNull, }) } @@ -379,8 +381,7 @@ impl RelationField { let is_virtual = self.virtual_referential_actions.unwrap_or(false); match self.arity { - FieldArity::Required if is_virtual => ReferentialAction::EmulateRestrict, - _ if is_virtual => ReferentialAction::EmulateSetNull, + _ if is_virtual => ReferentialAction::EmulateNoAction, _ => ReferentialAction::Cascade, } } diff --git a/libs/datamodel/connectors/dml/src/relation_info.rs b/libs/datamodel/connectors/dml/src/relation_info.rs index 1c1f6cfd373d..76b0542ab1ec 100644 --- a/libs/datamodel/connectors/dml/src/relation_info.rs +++ b/libs/datamodel/connectors/dml/src/relation_info.rs @@ -75,6 +75,8 @@ pub enum ReferentialAction { EmulateSetNull, /// An emulated version of `Restrict` for databases without foreign keys. EmulateRestrict, + /// An emulated version of `NoAction` for databases without foreign keys. + EmulateNoAction, } impl fmt::Display for ReferentialAction { @@ -87,6 +89,7 @@ impl fmt::Display for ReferentialAction { ReferentialAction::SetDefault => write!(f, "SetDefault"), ReferentialAction::EmulateSetNull => write!(f, "EmulateSetNull"), ReferentialAction::EmulateRestrict => write!(f, "EmulateRestrict"), + ReferentialAction::EmulateNoAction => write!(f, "EmulateNoAction"), } } } diff --git a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs index 72dae3a6f0ed..bd957baad11d 100644 --- a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs @@ -39,7 +39,7 @@ impl MongoDbDatamodelConnector { ]; let native_types = mongodb_types::available_types(); - let referential_actions = EmulateRestrict | EmulateSetNull; + let referential_actions = EmulateRestrict | EmulateSetNull | EmulateNoAction; Self { capabilities, @@ -95,7 +95,7 @@ impl Connector for MongoDbDatamodelConnector { }?; } - // If the field is _not_ a native-type-annotated field and it has a `dbgenerated` defult, we error. + // If the field is _not_ a native-type-annotated field and it has a `dbgenerated` default, we error. if !matches!(field.field_type(), FieldType::NativeType(_, _)) && matches!(field.default_value(), Some(DefaultValue::Expression(expr)) if expr.is_dbgenerated()) { From bf26779a841c3a199511056f0623511eb057051a Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Fri, 11 Jun 2021 14:56:33 +0200 Subject: [PATCH 15/47] No virtuals, foreign key capability, validations --- .../src/introspection.rs | 1 + .../connectors/datamodel-connector/src/lib.rs | 2 +- libs/datamodel/connectors/dml/src/field.rs | 31 +++++----- .../connectors/dml/src/relation_info.rs | 6 -- .../mongodb-datamodel-connector/src/lib.rs | 3 +- .../src/mssql_datamodel_connector.rs | 2 +- .../src/mysql_datamodel_connector.rs | 9 ++- .../src/postgres_datamodel_connector.rs | 2 +- .../src/sqlite_datamodel_connector.rs | 2 +- .../core/src/transform/ast_to_dml/validate.rs | 23 +++++--- .../core/src/transform/attributes/relation.rs | 7 +-- .../src/transform/helpers/value_validator.rs | 2 - libs/datamodel/core/src/walkers.rs | 2 +- .../relations/referential_actions.rs | 58 ++++++++++++------- .../sql_schema_calculator_flavour.rs | 14 +---- .../sql_schema_calculator_flavour/mssql.rs | 22 ++++--- 16 files changed, 94 insertions(+), 92 deletions(-) diff --git a/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs b/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs index 0c45b4d6e529..56afbf634c80 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/introspection.rs @@ -43,6 +43,7 @@ pub fn introspect( version_check.uses_on_delete(foreign_key, table); let mut relation_field = calculate_relation_field(schema, table, foreign_key)?; + relation_field.supports_restrict_action(!sql_family.is_mssql()); model.add_field(Field::RelationField(relation_field)); diff --git a/libs/datamodel/connectors/datamodel-connector/src/lib.rs b/libs/datamodel/connectors/datamodel-connector/src/lib.rs index 668205b66a85..df719cb546e1 100644 --- a/libs/datamodel/connectors/datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/datamodel-connector/src/lib.rs @@ -183,7 +183,7 @@ pub enum ConnectorCapability { AutoIncrementMultipleAllowed, AutoIncrementNonIndexedAllowed, RelationFieldsInArbitraryOrder, - ReferentialActions, + ForeignKeys, // start of Query Engine Capabilities InsensitiveFilters, diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index e78565d07bad..bf3714949cf4 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -278,12 +278,15 @@ impl PartialEq for RelationField { && self.is_ignored == other.is_ignored && self.relation_info == other.relation_info; - let this_on_delete = self.relation_info.on_delete.or_else(|| self.default_on_delete_action()); + let this_on_delete = self + .relation_info + .on_delete + .unwrap_or_else(|| self.default_on_delete_action()); let other_on_delete = other .relation_info .on_delete - .or_else(|| other.default_on_delete_action()); + .unwrap_or_else(|| other.default_on_delete_action()); let on_delete_matches = this_on_delete == other_on_delete; @@ -363,25 +366,23 @@ impl RelationField { self.arity.is_optional() } - pub fn default_on_delete_action(&self) -> Option { - let is_virtual = self.virtual_referential_actions.unwrap_or(false); + pub fn default_on_delete_action(&self) -> ReferentialAction { + use ReferentialAction::*; - self.supports_restrict_action.map(|restrict_ok| match self.arity { - FieldArity::Required if is_virtual => ReferentialAction::EmulateRestrict, - FieldArity::Required if restrict_ok => ReferentialAction::Restrict, - FieldArity::Required => ReferentialAction::NoAction, - _ if is_virtual => ReferentialAction::EmulateSetNull, - _ => ReferentialAction::SetNull, - }) + match self.arity { + FieldArity::Required if self.supports_restrict_action.unwrap_or(true) => Restrict, + FieldArity::Required => NoAction, + _ => SetNull, + } } pub fn default_on_update_action(&self) -> ReferentialAction { - let is_virtual = self.virtual_referential_actions.unwrap_or(false); + use ReferentialAction::*; match self.arity { - FieldArity::Required if is_virtual => ReferentialAction::EmulateRestrict, - _ if is_virtual => ReferentialAction::EmulateSetNull, - _ => ReferentialAction::Cascade, + _ if !self.virtual_referential_actions.unwrap_or(false) => Cascade, + FieldArity::Required => Restrict, + _ => SetNull, } } } diff --git a/libs/datamodel/connectors/dml/src/relation_info.rs b/libs/datamodel/connectors/dml/src/relation_info.rs index 1c1f6cfd373d..a1894ca08013 100644 --- a/libs/datamodel/connectors/dml/src/relation_info.rs +++ b/libs/datamodel/connectors/dml/src/relation_info.rs @@ -71,10 +71,6 @@ pub enum ReferentialAction { /// of relation. Will always result in a runtime error if no defaults are /// provided for any relation scalar fields. SetDefault, - /// An emulated version of `SetNull` for databases without foreign keys. - EmulateSetNull, - /// An emulated version of `Restrict` for databases without foreign keys. - EmulateRestrict, } impl fmt::Display for ReferentialAction { @@ -85,8 +81,6 @@ impl fmt::Display for ReferentialAction { ReferentialAction::NoAction => write!(f, "NoAction"), ReferentialAction::SetNull => write!(f, "SetNull"), ReferentialAction::SetDefault => write!(f, "SetDefault"), - ReferentialAction::EmulateSetNull => write!(f, "EmulateSetNull"), - ReferentialAction::EmulateRestrict => write!(f, "EmulateRestrict"), } } } diff --git a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs index 72dae3a6f0ed..543819e2d242 100644 --- a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs @@ -35,11 +35,10 @@ impl MongoDbDatamodelConnector { ConnectorCapability::CreateSkipDuplicates, ConnectorCapability::ScalarLists, ConnectorCapability::InsensitiveFilters, - ConnectorCapability::ReferentialActions, ]; let native_types = mongodb_types::available_types(); - let referential_actions = EmulateRestrict | EmulateSetNull; + let referential_actions = Restrict | SetNull; Self { capabilities, 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 c615a25a799b..9540e04a8549 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 @@ -65,7 +65,7 @@ impl MsSqlDatamodelConnector { ConnectorCapability::MultipleIndexesWithSameName, ConnectorCapability::AutoIncrement, ConnectorCapability::CompoundIds, - ConnectorCapability::ReferentialActions, + ConnectorCapability::ForeignKeys, ]; let constructors: Vec = vec![ 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 0405b96843fa..f10c38eaedeb 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 @@ -69,7 +69,7 @@ impl MySqlDatamodelConnector { pub fn new(is_planetscale: bool) -> MySqlDatamodelConnector { use ReferentialAction::*; - let capabilities = vec![ + let mut capabilities = vec![ ConnectorCapability::RelationsOverNonUniqueCriteria, ConnectorCapability::Enums, ConnectorCapability::Json, @@ -84,9 +84,12 @@ impl MySqlDatamodelConnector { ConnectorCapability::CreateManyWriteableAutoIncId, ConnectorCapability::AutoIncrement, ConnectorCapability::CompoundIds, - ConnectorCapability::ReferentialActions, ]; + if !is_planetscale { + capabilities.push(ConnectorCapability::ForeignKeys); + } + let int = NativeTypeConstructor::without_args(INT_TYPE_NAME, vec![ScalarType::Int]); let unsigned_int = NativeTypeConstructor::without_args(UNSIGNED_INT_TYPE_NAME, vec![ScalarType::Int]); let small_int = NativeTypeConstructor::without_args(SMALL_INT_TYPE_NAME, vec![ScalarType::Int]); @@ -160,7 +163,7 @@ impl MySqlDatamodelConnector { ]; let referential_actions = if is_planetscale { - EmulateRestrict | EmulateSetNull + Restrict | SetNull } else { Restrict | Cascade | SetNull | NoAction | SetDefault }; 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 71df566c18ad..9805ef0f1d79 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 @@ -69,7 +69,7 @@ impl PostgresDatamodelConnector { ConnectorCapability::CreateManyWriteableAutoIncId, ConnectorCapability::AutoIncrement, ConnectorCapability::CompoundIds, - ConnectorCapability::ReferentialActions, + ConnectorCapability::ForeignKeys, ]; let small_int = NativeTypeConstructor::without_args(SMALL_INT_TYPE_NAME, vec![ScalarType::Int]); 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 aaadebbd39d8..eaa0a292a396 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 @@ -21,7 +21,7 @@ impl SqliteDatamodelConnector { ConnectorCapability::UpdateableId, ConnectorCapability::AutoIncrement, ConnectorCapability::CompoundIds, - ConnectorCapability::ReferentialActions, + ConnectorCapability::ForeignKeys, ]; let constructors: Vec = vec![]; 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 1272c35ad832..18e50ea682ed 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/validate.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/validate.rs @@ -6,7 +6,6 @@ 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}; @@ -517,13 +516,7 @@ impl<'a> Validator<'a> { 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) { + if !connector.supports_referential_action(*action) { let allowed_values: Vec<_> = connector .referential_actions() .iter() @@ -906,6 +899,20 @@ impl<'a> Validator<'a> { )); } + if field.is_list() + && !related_field.is_list() + && (!rel_info.on_delete.is_none() || !rel_info.on_update.is_none()) + { + errors.push_error(DatamodelError::new_attribute_validation_error( + &format!( + "The relation field `{}` on Model `{}` must not specify the `onDelete` or `onUpdate` argument in the {} attribute. You must only specify it on the opposite field `{}` on model `{}`, or in case of a many to many relation, in an explicit join table.", + &field.name, &model.name, RELATION_ATTRIBUTE_NAME_WITH_AT, &related_field.name, &related_model.name + ), + RELATION_ATTRIBUTE_NAME, + field_span, + )); + } + // ONE TO ONE if field.is_singular() && related_field.is_singular() { if rel_info.fields.is_empty() && related_field_rel_info.fields.is_empty() { diff --git a/libs/datamodel/core/src/transform/attributes/relation.rs b/libs/datamodel/core/src/transform/attributes/relation.rs index 25041ef511d1..2a6e25c59a50 100644 --- a/libs/datamodel/core/src/transform/attributes/relation.rs +++ b/libs/datamodel/core/src/transform/attributes/relation.rs @@ -102,12 +102,7 @@ impl AttributeValidator for RelationAttributeValidator { } if let Some(ref_action) = relation_info.on_delete { - let is_default = rf - .default_on_delete_action() - .map(|default| default == ref_action) - .unwrap_or(false); - - if !is_default { + if rf.default_on_delete_action() != ref_action { let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); args.push(ast::Argument::new("onDelete", expression)); } diff --git a/libs/datamodel/core/src/transform/helpers/value_validator.rs b/libs/datamodel/core/src/transform/helpers/value_validator.rs index ec61bb1cfd03..0df1f4d7ba70 100644 --- a/libs/datamodel/core/src/transform/helpers/value_validator.rs +++ b/libs/datamodel/core/src/transform/helpers/value_validator.rs @@ -173,8 +173,6 @@ impl ValueValidator { "NoAction" => Ok(ReferentialAction::NoAction), "SetNull" => Ok(ReferentialAction::SetNull), "SetDefault" => Ok(ReferentialAction::SetDefault), - "EmulateRestrict" => Ok(ReferentialAction::EmulateRestrict), - "EmulateSetNull" => Ok(ReferentialAction::EmulateSetNull), s => { let message = format!("Invalid referential action: `{}`", s); diff --git a/libs/datamodel/core/src/walkers.rs b/libs/datamodel/core/src/walkers.rs index 54c13280830c..96c755df7490 100644 --- a/libs/datamodel/core/src/walkers.rs +++ b/libs/datamodel/core/src/walkers.rs @@ -354,7 +354,7 @@ impl<'a> RelationFieldWalker<'a> { self.get().default_on_update_action() } - pub fn default_on_delete_action(&self) -> Option { + pub fn default_on_delete_action(&self) -> ReferentialAction { self.get().default_on_delete_action() } } diff --git a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs index f852a75771cb..8af36c8f3ecf 100644 --- a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs +++ b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs @@ -59,8 +59,8 @@ fn on_update_actions() { } #[test] -fn virtual_actions_on_mongo() { - let actions = &[EmulateRestrict, EmulateSetNull]; +fn actions_on_mongo() { + let actions = &[Restrict, SetNull]; for action in actions { let dml = formatdoc!( @@ -93,8 +93,8 @@ fn virtual_actions_on_mongo() { } #[test] -fn virtual_actions_on_planetscale() { - let actions = &[EmulateRestrict, EmulateSetNull]; +fn actions_on_planetscale() { + let actions = &[Restrict, SetNull]; for action in actions { let dml = formatdoc!( @@ -207,13 +207,7 @@ fn restrict_should_not_work_on_sql_server() { #[test] fn concrete_actions_should_not_work_on_mongo() { - let actions = &[ - (Cascade, 237), - (Restrict, 238), - (NoAction, 238), - (SetNull, 237), - (SetDefault, 240), - ]; + let actions = &[(Cascade, 237), (NoAction, 238), (SetDefault, 240)]; for (action, span) in actions { let dml = formatdoc!( @@ -238,7 +232,7 @@ fn concrete_actions_should_not_work_on_mongo() { ); let message = format!( - "Invalid referential action: `{}`. Allowed values: (`EmulateSetNull`, `EmulateRestrict`)", + "Invalid referential action: `{}`. Allowed values: (`Restrict`, `SetNull`)", action ); @@ -252,13 +246,7 @@ fn concrete_actions_should_not_work_on_mongo() { #[test] fn concrete_actions_should_not_work_on_planetscale() { - let actions = &[ - (Cascade, 389), - (Restrict, 390), - (NoAction, 390), - (SetNull, 389), - (SetDefault, 392), - ]; + let actions = &[(Cascade, 389), (NoAction, 390), (SetDefault, 392)]; for (action, span) in actions { let dml = formatdoc!( @@ -289,7 +277,7 @@ fn concrete_actions_should_not_work_on_planetscale() { ); let message = format!( - "Invalid referential action: `{}`. Allowed values: (`EmulateSetNull`, `EmulateRestrict`)", + "Invalid referential action: `{}`. Allowed values: (`Restrict`, `SetNull`)", action ); @@ -300,3 +288,33 @@ fn concrete_actions_should_not_work_on_planetscale() { )]); } } + +#[test] +fn on_delete_cannot_be_defined_on_the_wrong_side() { + let dml = indoc! { r#" + datasource db { + provider = "mysql" + url = "mysql://" + } + + model A { + id Int @id + bs B[] @relation(onDelete: Restrict) + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onDelete: Restrict) + } + "#}; + + let message = + "The relation field `bs` on Model `A` must not specify the `onDelete` or `onUpdate` argument in the @relation attribute. You must only specify it on the opposite field `a` on model `B`, or in case of a many to many relation, in an explicit join table."; + + parse_error(dml).assert_are(&[DatamodelError::new_attribute_validation_error( + &message, + "relation", + Span::new(92, 129), + )]); +} 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 b4015b276473..53a639816285 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 @@ -45,18 +45,9 @@ pub(crate) trait SqlSchemaCalculatorFlavour { } fn on_delete_action(&self, rf: &RelationFieldWalker<'_>) -> sql::ForeignKeyAction { - let default = || { - rf.default_on_delete_action() - .map(convert_referential_action) - .unwrap_or_else(|| match rf.arity() { - FieldArity::Required => sql::ForeignKeyAction::Restrict, - _ => sql::ForeignKeyAction::SetNull, - }) - }; - rf.on_delete_action() .map(convert_referential_action) - .unwrap_or_else(default) + .unwrap_or_else(|| convert_referential_action(rf.default_on_delete_action())) } fn m2m_foreign_key_action(&self, _model_a: &ModelWalker<'_>, _model_b: &ModelWalker<'_>) -> sql::ForeignKeyAction { @@ -76,8 +67,5 @@ fn convert_referential_action(action: ReferentialAction) -> sql::ForeignKeyActio ReferentialAction::NoAction => sql::ForeignKeyAction::NoAction, ReferentialAction::SetNull => sql::ForeignKeyAction::SetNull, ReferentialAction::SetDefault => sql::ForeignKeyAction::SetDefault, - // These will be only used for databases with no foreign keys. - ReferentialAction::EmulateSetNull => unreachable!("EmulateSetNull conversion"), - ReferentialAction::EmulateRestrict => unreachable!("EmulateRestrict conversion"), } } 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 a2c89c9c670b..08a12e03fd7d 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 @@ -2,7 +2,7 @@ use super::SqlSchemaCalculatorFlavour; use crate::flavour::MssqlFlavour; use datamodel::{ walkers::{ModelWalker, RelationFieldWalker}, - FieldArity, ScalarType, + ScalarType, }; use datamodel_connector::Connector; use sql_schema_describer::{self as sql, ForeignKeyAction}; @@ -22,18 +22,16 @@ impl SqlSchemaCalculatorFlavour for MssqlFlavour { } fn on_delete_action(&self, rf: &RelationFieldWalker<'_>) -> sql::ForeignKeyAction { - let default = || { - rf.default_on_delete_action() - .map(super::convert_referential_action) - .unwrap_or_else(|| match rf.arity() { - FieldArity::Required => sql::ForeignKeyAction::NoAction, - _ => sql::ForeignKeyAction::SetNull, - }) - }; - - rf.on_delete_action() + let action = rf + .on_delete_action() .map(super::convert_referential_action) - .unwrap_or_else(default) + .unwrap_or_else(|| super::convert_referential_action(rf.default_on_delete_action())); + + if action == ForeignKeyAction::Restrict { + ForeignKeyAction::NoAction + } else { + action + } } fn single_field_index_name(&self, model_name: &str, field_name: &str) -> String { From 486e285cc20ff2eb35290029e1abcc02a165655c Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Fri, 11 Jun 2021 14:59:29 +0200 Subject: [PATCH 16/47] Add test for onUpdate on the wrong side --- .../relations/referential_actions.rs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs index 8af36c8f3ecf..b511ecc1908b 100644 --- a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs +++ b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs @@ -318,3 +318,33 @@ fn on_delete_cannot_be_defined_on_the_wrong_side() { Span::new(92, 129), )]); } + +#[test] +fn on_update_cannot_be_defined_on_the_wrong_side() { + let dml = indoc! { r#" + datasource db { + provider = "mysql" + url = "mysql://" + } + + model A { + id Int @id + bs B[] @relation(onUpdate: Restrict) + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onUpdate: Restrict) + } + "#}; + + let message = + "The relation field `bs` on Model `A` must not specify the `onDelete` or `onUpdate` argument in the @relation attribute. You must only specify it on the opposite field `a` on model `B`, or in case of a many to many relation, in an explicit join table."; + + parse_error(dml).assert_are(&[DatamodelError::new_attribute_validation_error( + &message, + "relation", + Span::new(92, 129), + )]); +} From 40cd6b214e0d90a76aef9fc313153b47df78f365 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Fri, 11 Jun 2021 15:41:49 +0200 Subject: [PATCH 17/47] First tests for onDelete --- datamodel_v2.prisma | 6 + libs/prisma-models/src/datamodel_converter.rs | 2 + libs/prisma-models/src/field/relation.rs | 7 + libs/prisma-models/src/relation.rs | 5 + .../sql_schema_calculator_flavour.rs | 1 + .../tests/new/ref_actions/mod.rs | 3 +- .../tests/new/ref_actions/on_delete/mod.rs | 3 + .../new/ref_actions/on_delete/no_action.rs | 262 ++++++++++++++++++ .../new/ref_actions/on_delete/restrict.rs | 262 ++++++++++++++++++ .../new/ref_actions/on_delete/set_null.rs | 178 ++++++++++++ .../tests/new/ref_actions/on_update/mod.rs | 0 .../tests/new/ref_actions/to_one.rs | 64 ----- .../src/query_graph_builder/write/delete.rs | 4 +- .../write/nested/delete_nested.rs | 7 +- .../src/query_graph_builder/write/utils.rs | 22 +- 15 files changed, 744 insertions(+), 82 deletions(-) create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/mod.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_null.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs delete mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/to_one.rs diff --git a/datamodel_v2.prisma b/datamodel_v2.prisma index 16deb57779f1..c956161a9d16 100644 --- a/datamodel_v2.prisma +++ b/datamodel_v2.prisma @@ -29,6 +29,12 @@ model B { model C { id String @id gql String? + ds D[] @relation(onDelete: Cascade) a A? } + +model D { + id String @id + cs C[] +} diff --git a/libs/prisma-models/src/datamodel_converter.rs b/libs/prisma-models/src/datamodel_converter.rs index 826a9fd5fa00..26dae5e0f188 100644 --- a/libs/prisma-models/src/datamodel_converter.rs +++ b/libs/prisma-models/src/datamodel_converter.rs @@ -98,6 +98,8 @@ impl<'a> DatamodelConverter<'a> { relation_name: relation.name.clone(), relation_side: relation.relation_side(rf), relation_info: rf.relation_info.clone(), + on_delete_default: rf.default_on_delete_action().unwrap(), + on_update_default: rf.default_on_update_action(), })) } dml::Field::ScalarField(sf) => { diff --git a/libs/prisma-models/src/field/relation.rs b/libs/prisma-models/src/field/relation.rs index c4a2dd24708a..3df44cbdbb1a 100644 --- a/libs/prisma-models/src/field/relation.rs +++ b/libs/prisma-models/src/field/relation.rs @@ -21,6 +21,8 @@ pub struct RelationFieldTemplate { pub relation_name: String, pub relation_side: RelationSide, pub relation_info: RelationInfo, + pub on_delete_default: ReferentialAction, + pub on_update_default: ReferentialAction, } #[derive(Clone)] @@ -33,6 +35,9 @@ pub struct RelationField { pub relation: OnceCell, pub relation_info: RelationInfo, + pub on_delete_default: ReferentialAction, + pub on_update_default: ReferentialAction, + pub model: ModelWeakRef, pub(crate) fields: OnceCell>, } @@ -112,6 +117,8 @@ impl RelationFieldTemplate { relation: OnceCell::new(), relation_info: self.relation_info, fields: OnceCell::new(), + on_delete_default: self.on_delete_default, + on_update_default: self.on_update_default, }) } } diff --git a/libs/prisma-models/src/relation.rs b/libs/prisma-models/src/relation.rs index 189d1d86b4f5..0f12b4adeef0 100644 --- a/libs/prisma-models/src/relation.rs +++ b/libs/prisma-models/src/relation.rs @@ -188,6 +188,11 @@ impl Relation { let a = self.field_a().on_delete().cloned(); let b = self.field_b().on_delete().cloned(); + dbg!(a); + dbg!(b); + dbg!(&self.field_a().on_delete_default); + dbg!(&self.field_b().on_delete_default); + a.or(b).expect("No referential action found for relation.") } } 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 b4015b276473..edee8b05232e 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 @@ -79,5 +79,6 @@ fn convert_referential_action(action: ReferentialAction) -> sql::ForeignKeyActio // These will be only used for databases with no foreign keys. ReferentialAction::EmulateSetNull => unreachable!("EmulateSetNull conversion"), ReferentialAction::EmulateRestrict => unreachable!("EmulateRestrict conversion"), + ReferentialAction::EmulateNoAction => unreachable!("EmulateRestrict conversion"), } } diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/mod.rs index eb32d3684bf0..af76b00948de 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/mod.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/mod.rs @@ -1 +1,2 @@ -mod to_one; +mod on_delete; +mod on_update; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/mod.rs new file mode 100644 index 000000000000..ec813d3d6629 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/mod.rs @@ -0,0 +1,3 @@ +mod no_action; +mod restrict; +mod set_null; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs new file mode 100644 index 000000000000..e96879b65197 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs @@ -0,0 +1,262 @@ +//! SQL Server doesn't support Restrict. + +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(schema(required), exclude(MongoDb))] +mod one2one_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: NoAction) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected. + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb))] +mod one2one_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: NoAction) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected. + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent succeeds if no child is connected. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteManyParent(where: { id: 2 }) { count }}"), + @r###"{"data":{"deleteManyParent":{"count":1}}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(required), exclude(MongoDb))] +mod one2many_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: NoAction) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected. + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent succeeds if no child is connected. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteManyParent(where: { id: 2 }) { count }}"), + @r###"{"data":{"deleteManyParent":{"count":1}}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb))] +mod one2many_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: NoAction) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected. + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent succeeds if no child is connected. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteManyParent(where: { id: 2 }) { count }}"), + @r###"{"data":{"deleteManyParent":{"count":1}}}"### + ); + + Ok(()) + } +} diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs new file mode 100644 index 000000000000..6f5b8211b6aa --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs @@ -0,0 +1,262 @@ +//! SQL Server doesn't support Restrict. + +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(schema(required), exclude(MongoDb, SqlServer))] +mod one2one_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: Restrict) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected. + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb, SqlServer))] +mod one2one_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: Restrict) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected. + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent succeeds if no child is connected. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteManyParent(where: { id: 2 }) { count }}"), + @r###"{"data":{"deleteManyParent":{"count":1}}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(required), exclude(MongoDb, SqlServer))] +mod one2many_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: Restrict) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected. + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent succeeds if no child is connected. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteManyParent(where: { id: 2 }) { count }}"), + @r###"{"data":{"deleteManyParent":{"count":1}}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb, SqlServer))] +mod one2many_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: Restrict) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected. + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent succeeds if no child is connected. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteManyParent(where: { id: 2 }) { count }}"), + @r###"{"data":{"deleteManyParent":{"count":1}}}"### + ); + + Ok(()) + } +} diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_null.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_null.rs new file mode 100644 index 000000000000..134683d19b4b --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_null.rs @@ -0,0 +1,178 @@ +//! Only Postgres allows SetNull on a non-nullable FK at all, rest fail during migration. + +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(schema(required), only(Postgres))] +mod one2one_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetNull) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected (because of null key violation). + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // `onDelete: SetNull` would cause `null` on `parent_id`, throwing an error. + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2011, + "Null constraint violation on the fields: (`parent_id`)" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2011, + "Null constraint violation on the fields: (`parent_id`)" + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb))] +mod one2one_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: SetNull) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent suceeds and sets the FK null. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent_id }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent_id":null}]}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(required), only(Postgres))] +mod one2many_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetNull) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent must fail if a child is connected (because of null key violation). + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // `onDelete: SetNull` would cause `null` on `parent_id`, throwing an error. + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2011, + "Null constraint violation on the fields: (`parent_id`)" + ); + + assert_error!( + runner, + "mutation { deleteManyParent(where: { id: 1 }) { count }}", + 2011, + "Null constraint violation on the fields: (`parent_id`)" + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb))] +mod one2many_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: SetNull) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent suceeds and sets the FK null. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent_id }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent_id":null}]}}"### + ); + + Ok(()) + } +} diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/to_one.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/to_one.rs deleted file mode 100644 index 7cf301ae5863..000000000000 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/to_one.rs +++ /dev/null @@ -1,64 +0,0 @@ -use indoc::indoc; -use query_engine_tests::*; - -#[test_suite(schema(schema))] -mod to_one { - fn schema() -> String { - let schema = indoc! { - r#" - model Parent { - id String @id @map("_id") - p String @unique - p_1 String - p_2 String - childOpt Child? - non_unique String? - - @@unique([p_1, p_2]) - } - - model Child { - id String @id @default(cuid()) @map("_id") - c String @unique - c_1 String - c_2 String - parentOpt Parent? @relation(fields: [parentRef], references: [p]) - parentRef String? - non_unique String? - - @@unique([c_1, c_2]) - } - "# - }; - - schema.to_owned() - } - - #[connector_test] - async fn vanilla(runner: &Runner) -> TestResult<()> { - insta::assert_snapshot!( - run_query!(runner, r#"mutation { createOneParent(data: { id: "1", p: "p1", p_1: "p", p_2: "1" childOpt: { create: {c: "c1", c_1: "foo", c_2: "bar"} } }){ p childOpt{ c } }}"#), - @r###"{"data":{"createOneParent":{"p":"p1","childOpt":{"c":"c1"}}}}"### - ); - - insta::assert_snapshot!( - run_query!(runner, r#"mutation { upsertOneParent( where: { p: "p1" } update:{ p: { set: "p2" } childOpt: {delete: true} } create:{id: "whatever" ,p: "Should not matter", p_1: "no", p_2: "yes"} ){ childOpt { c } }}"#), - @r###"{"data":{"upsertOneParent":{"childOpt":null}}}"### - ); - - Ok(()) - } - - async fn create_test_data(runner: &Runner) -> TestResult<()> { - create_row(runner, r#"{ uniqueField: 1, nonUniqFieldA: "A", nonUniqFieldB: "A"}"#).await?; - - Ok(()) - } - - async fn create_row(runner: &Runner, data: &str) -> TestResult<()> { - runner - .query(format!("mutation {{ createOneTestModel(data: {}) {{ id }} }}", data)) - .await?; - Ok(()) - } -} diff --git a/query-engine/core/src/query_graph_builder/write/delete.rs b/query-engine/core/src/query_graph_builder/write/delete.rs index 94e8df4240e9..9f6ed9788520 100644 --- a/query-engine/core/src/query_graph_builder/write/delete.rs +++ b/query-engine/core/src/query_graph_builder/write/delete.rs @@ -33,7 +33,7 @@ pub fn delete_record( })); let delete_node = graph.create_node(delete_query); - utils::insert_deletion_checks(graph, connector_ctx, &model, &read_node, &delete_node)?; + utils::insert_deletion_restrict_checks(graph, connector_ctx, &model, &read_node, &delete_node)?; graph.create_edge( &read_node, @@ -82,7 +82,7 @@ pub fn delete_many_records( let read_query_node = graph.create_node(read_query); let delete_many_node = graph.create_node(Query::Write(delete_many)); - utils::insert_deletion_checks(graph, connector_ctx, &model, &read_query_node, &delete_many_node)?; + utils::insert_deletion_restrict_checks(graph, connector_ctx, &model, &read_query_node, &delete_many_node)?; graph.create_edge( &read_query_node, diff --git a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs index 6e063d941ac6..e476374616f1 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs @@ -51,8 +51,7 @@ pub fn nested_delete( let find_child_records_node = utils::insert_find_children_by_parent_node(graph, parent_node, parent_relation_field, or_filter)?; - // Todo [RA] - utils::insert_deletion_checks( + utils::insert_deletion_restrict_checks( graph, connector_ctx, child_model, @@ -99,7 +98,7 @@ pub fn nested_delete( record_filter: None, }))); - utils::insert_deletion_checks( + utils::insert_deletion_restrict_checks( graph, connector_ctx, child_model, @@ -164,7 +163,7 @@ pub fn nested_delete_many( let delete_many_node = graph.create_node(Query::Write(delete_many)); // Todo [RA] - utils::insert_deletion_checks( + utils::insert_deletion_restrict_checks( graph, connector_ctx, child_model, diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index 3f8f33f1136a..8ff5e60f436f 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -267,7 +267,7 @@ pub fn insert_existing_1to1_related_model_checks( Ok(read_existing_children) } -/// Inserts checks into the graph that check all required, non-list relations pointing to +/// Inserts emulated restrict checks into the graph that checks all _required_ marked relations pointing to /// the given `model`. Those checks fail at runtime (edges to the `Empty` node) if one or more /// records are found. Checks are inserted between `parent_node` and `child_node`. /// @@ -308,7 +308,7 @@ pub fn insert_existing_1to1_related_model_checks( /// └────────────────────┘ /// ``` #[tracing::instrument(skip(graph, model, parent_node, child_node))] -pub fn insert_deletion_checks( +pub fn insert_deletion_restrict_checks( graph: &mut QueryGraph, connector_ctx: &ConnectorContext, model: &ModelRef, @@ -322,16 +322,16 @@ pub fn insert_deletion_checks( let once = OnceCell::new(); if !relation_fields.is_empty() { - let noop_node = graph.create_node(Node::Empty); + // let noop_node = graph.create_node(Node::Empty); // We know that the relation can't be a list and must be required on the related model for `model` (see fields_requiring_model). // For all requiring models (RM), we use the field on `model` to query for existing RM records and error out if at least one exists. for rf in relation_fields { - // if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) { - // if !matches!(rf.relation().on_delete(), datamodel::ReferentialAction::EmulateRestrict) { - // continue; - // } - // } + if dbg!(connector_ctx.features.contains(&PreviewFeature::ReferentialActions)) { + if !matches!(rf.relation().on_delete(), datamodel::ReferentialAction::EmulateRestrict) { + continue; + } + } let noop_node = once.get_or_init(|| graph.create_node(Node::Empty)); let relation_field = rf.related_field(); @@ -366,9 +366,9 @@ pub fn insert_deletion_checks( }); // Edge from empty node to the child (delete). - // if let Some(noop_node) = once.get() { - graph.create_edge(&noop_node, child_node, QueryGraphDependency::ExecutionOrder)?; - // } + if let Some(noop_node) = once.get() { + graph.create_edge(&noop_node, child_node, QueryGraphDependency::ExecutionOrder)?; + } } Ok(()) From a824868c1c53ffbdad35f4480a461027566bd25c Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 14 Jun 2021 11:38:47 +0200 Subject: [PATCH 18/47] Missing onDelete mode tests --- .../new/ref_actions/on_delete/cascade.rs | 166 ++++++++ .../tests/new/ref_actions/on_delete/mod.rs | 2 + .../new/ref_actions/on_delete/no_action.rs | 2 - .../new/ref_actions/on_delete/set_default.rs | 400 ++++++++++++++++++ 4 files changed, 568 insertions(+), 2 deletions(-) create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs new file mode 100644 index 000000000000..e0d90838b26b --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs @@ -0,0 +1,166 @@ +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(schema(required), exclude(MongoDb))] +mod one2one_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: Cascade) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent deletes child as well. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "query { findManyChild { id }}"), + @r###"{"data":{"findManyChild":[]}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb))] +mod one2one_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: Cascade) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent deletes child as well. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "query { findManyChild { id }}"), + @r###"{"data":{"findManyChild":[]}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(required), exclude(MongoDb))] +mod one2many_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: Cascade) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent deletes all children. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: [ { id: 1 }, { id: 2 } ] }}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "query { findManyChild { id }}"), + @r###"{"data":{"findManyChild":[]}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb))] +mod one2many_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: Cascade) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent deletes all children. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: [ { id: 1 }, { id: 2 } ] }}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "query { findManyChild { id }}"), + @r###"{"data":{"findManyChild":[]}}"### + ); + + Ok(()) + } +} diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/mod.rs index ec813d3d6629..1b8d08fa7328 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/mod.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/mod.rs @@ -1,3 +1,5 @@ +mod cascade; mod no_action; mod restrict; +mod set_default; mod set_null; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs index e96879b65197..b02a86a735ef 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs @@ -1,5 +1,3 @@ -//! SQL Server doesn't support Restrict. - use indoc::indoc; use query_engine_tests::*; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs new file mode 100644 index 000000000000..75b160837b8d --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs @@ -0,0 +1,400 @@ +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(exclude(MongoDb))] +mod one2one_req { + fn required_with_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int @default(2) + parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + }"# + }; + + schema.to_owned() + } + + fn required_without_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent reconnects the child to the default. + #[connector_test(schema(required_with_default))] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // The default + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent { id } }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent":{"id":2}}]}}"### + ); + + Ok(()) + } + + /// Deleting the parent reconnects the child to the default and fails (the default doesn't exist). + #[connector_test(schema(required_with_default))] + async fn delete_parent_no_exist_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent with no default for SetDefault fails. + /// Only postgres allows setting no default for a SetDefault FK. + #[connector_test(schema(required_without_default), only(Postgres))] + async fn delete_parent_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2011, + "Null constraint violation on the fields" + ); + + Ok(()) + } +} + +#[test_suite(exclude(MongoDb))] +mod one2one_opt { + fn optional_with_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int? @default(2) + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + }"# + }; + + schema.to_owned() + } + + fn optional_without_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent reconnects the child to the default. + #[connector_test(schema(optional_with_default))] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // The default + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent { id } }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent":{"id":2}}]}}"### + ); + + Ok(()) + } + + /// Deleting the parent reconnects the child to the default and fails (the default doesn't exist). + #[connector_test(schema(optional_with_default))] + async fn delete_parent_no_exist_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent with no default for SetDefault nulls the FK. + #[connector_test(schema(optional_without_default), only(Postgres))] + async fn delete_parent_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild(where: { id: 1 }) { id parent_id }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent_id":1}]}}"### + ); + + Ok(()) + } +} + +#[test_suite(exclude(MongoDb))] +mod one2many_req { + fn required_with_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int @default(2) + parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + }"# + }; + + schema.to_owned() + } + + fn required_without_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent reconnects the children to the default. + #[connector_test(schema(required_with_default))] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // The default + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent { id } }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent":{"id":2}}]}}"### + ); + + Ok(()) + } + + /// Deleting the parent reconnects the child to the default and fails (the default doesn't exist). + #[connector_test(schema(required_with_default))] + async fn delete_parent_no_exist_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent with no default for SetDefault fails. + /// Only postgres allows setting no default for a SetDefault FK. + #[connector_test(schema(required_without_default), only(Postgres))] + async fn delete_parent_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2011, + "Null constraint violation on the fields" + ); + + Ok(()) + } +} + +#[test_suite(exclude(MongoDb))] +mod one2many_opt { + fn optional_with_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int @default(2) + parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + }"# + }; + + schema.to_owned() + } + + fn optional_without_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent reconnects the child to the default. + #[connector_test(schema(optional_with_default))] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // The default + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2 }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent { id } }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent":{"id":2}}]}}"### + ); + + Ok(()) + } + + /// Deleting the parent reconnects the child to the default and fails (the default doesn't exist). + #[connector_test(schema(optional_with_default))] + async fn delete_parent_no_exist_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Deleting the parent with no default for SetDefault nulls the FK. + #[connector_test(schema(optional_without_default), only(Postgres))] + async fn delete_parent_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild(where: { id: 1 }) { id parent_id }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent_id":1}]}}"### + ); + + Ok(()) + } +} From 63f2fa89ed657b5fa351176df0dc84321343802c Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 14 Jun 2021 12:08:52 +0200 Subject: [PATCH 19/47] Disable emulation correctly. --- .../core/src/query_graph_builder/write/utils.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index 5b21e6015d32..be5da1df7651 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -5,6 +5,7 @@ use crate::{ }; use connector::{Filter, WriteArgs}; use datamodel::common::preview_features::PreviewFeature; +use datamodel_connector::ConnectorCapability; use itertools::Itertools; use once_cell::sync::OnceCell; use prisma_models::{ModelProjection, ModelRef, RelationFieldRef}; @@ -327,11 +328,14 @@ pub fn insert_deletion_restrict_checks( // We know that the relation can't be a list and must be required on the related model for `model` (see fields_requiring_model). // For all requiring models (RM), we use the field on `model` to query for existing RM records and error out if at least one exists. for rf in relation_fields { - // if dbg!(connector_ctx.features.contains(&PreviewFeature::ReferentialActions)) { - // if !matches!(rf.relation().on_delete(), datamodel::ReferentialAction::EmulateRestrict) { - // continue; - // } - // } + if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) { + // If the connector supports foreign keys, we do not do any emulation. + if connector_ctx.capabilities.contains(&ConnectorCapability::ForeignKeys) + // && !matches!(rf.relation().on_delete(), datamodel::ReferentialAction::Restrict) + { + continue; + } + } let noop_node = once.get_or_init(|| graph.create_node(Node::Empty)); let relation_field = rf.related_field(); From ae2f024320ffb384e6addde34228d7301904e53b Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 14 Jun 2021 13:46:19 +0200 Subject: [PATCH 20/47] Clippy. Bug. --- .../core/src/transform/ast_to_dml/validate.rs | 2 +- .../new/ref_actions/on_update/cascade.rs | 166 ++++++++++++++++++ .../tests/new/ref_actions/on_update/mod.rs | 1 + .../input_types/objects/update_one_objects.rs | 4 +- 4 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/cascade.rs 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 18e50ea682ed..a4d6feea8e4f 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/validate.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/validate.rs @@ -901,7 +901,7 @@ impl<'a> Validator<'a> { if field.is_list() && !related_field.is_list() - && (!rel_info.on_delete.is_none() || !rel_info.on_update.is_none()) + && (rel_info.on_delete.is_some() || rel_info.on_update.is_some()) { errors.push_error(DatamodelError::new_attribute_validation_error( &format!( diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/cascade.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/cascade.rs new file mode 100644 index 000000000000..e0d90838b26b --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/cascade.rs @@ -0,0 +1,166 @@ +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(schema(required), exclude(MongoDb))] +mod one2one_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: Cascade) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent deletes child as well. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "query { findManyChild { id }}"), + @r###"{"data":{"findManyChild":[]}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb))] +mod one2one_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + child Child? + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: Cascade) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent deletes child as well. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "query { findManyChild { id }}"), + @r###"{"data":{"findManyChild":[]}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(required), exclude(MongoDb))] +mod one2many_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int + parent Parent @relation(fields: [parent_id], references: [id], onDelete: Cascade) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent deletes all children. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: [ { id: 1 }, { id: 2 } ] }}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "query { findManyChild { id }}"), + @r###"{"data":{"findManyChild":[]}}"### + ); + + Ok(()) + } +} + +#[test_suite(schema(optional), exclude(MongoDb))] +mod one2many_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: Cascade) + }"# + }; + + schema.to_owned() + } + + /// Deleting the parent deletes all children. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: [ { id: 1 }, { id: 2 } ] }}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, "query { findManyChild { id }}"), + @r###"{"data":{"findManyChild":[]}}"### + ); + + Ok(()) + } +} diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs index e69de29bb2d1..2651e9c38133 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs @@ -0,0 +1 @@ +mod cascade; diff --git a/query-engine/core/src/schema_builder/input_types/objects/update_one_objects.rs b/query-engine/core/src/schema_builder/input_types/objects/update_one_objects.rs index 4e1fdfdd62be..5d94a61a1692 100644 --- a/query-engine/core/src/schema_builder/input_types/objects/update_one_objects.rs +++ b/query-engine/core/src/schema_builder/input_types/objects/update_one_objects.rs @@ -121,12 +121,12 @@ pub(super) fn scalar_input_fields_for_unchecked_update( .into_iter() .filter(|sf| !linking_fields.contains(sf)) .filter(|sf| { - if let Some(ref id_fields) = id_fields { + if let Some(ref id_fields) = &id_fields { // Exclude @@id or @id fields if not updatable if id_fields.contains(sf) { ctx.capabilities.contains(ConnectorCapability::UpdateableId) } else { - false + true } } else { true From 912d03bbb10173220266c45ae671153a1b29cde7 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 14 Jun 2021 16:16:32 +0200 Subject: [PATCH 21/47] Fix test_suite macro --- .../new/ref_actions/on_delete/cascade.rs | 8 +- .../new/ref_actions/on_delete/no_action.rs | 8 +- .../new/ref_actions/on_delete/restrict.rs | 8 +- .../new/ref_actions/on_delete/set_default.rs | 8 +- .../new/ref_actions/on_delete/set_null.rs | 10 +- .../delete_many_relations.rs | 159 +++++++++--------- .../query-test-macros/src/test_suite.rs | 7 +- 7 files changed, 107 insertions(+), 101 deletions(-) diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs index e0d90838b26b..61810e1f63e0 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs @@ -1,7 +1,7 @@ use indoc::indoc; use query_engine_tests::*; -#[test_suite(schema(required), exclude(MongoDb))] +#[test_suite(suite = "cascade_onD_1to1_req", schema(required), exclude(MongoDb))] mod one2one_req { fn required() -> String { let schema = indoc! { @@ -42,7 +42,7 @@ mod one2one_req { } } -#[test_suite(schema(optional), exclude(MongoDb))] +#[test_suite(suite = "cascade_onD_1to1_opt", schema(optional), exclude(MongoDb))] mod one2one_opt { fn optional() -> String { let schema = indoc! { @@ -83,7 +83,7 @@ mod one2one_opt { } } -#[test_suite(schema(required), exclude(MongoDb))] +#[test_suite(suite = "cascade_onD_1toM_req", schema(required), exclude(MongoDb))] mod one2many_req { fn required() -> String { let schema = indoc! { @@ -124,7 +124,7 @@ mod one2many_req { } } -#[test_suite(schema(optional), exclude(MongoDb))] +#[test_suite(suite = "cascade_onD_1toM_opt", schema(optional), exclude(MongoDb))] mod one2many_opt { fn optional() -> String { let schema = indoc! { diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs index b02a86a735ef..9974175acf8c 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs @@ -1,7 +1,7 @@ use indoc::indoc; use query_engine_tests::*; -#[test_suite(schema(required), exclude(MongoDb))] +#[test_suite(suite = "noaction_onD_1to1_req", schema(required), exclude(MongoDb))] mod one2one_req { fn required() -> String { let schema = indoc! { @@ -46,7 +46,7 @@ mod one2one_req { } } -#[test_suite(schema(optional), exclude(MongoDb))] +#[test_suite(suite = "noaction_onD_1to1_opt", schema(optional), exclude(MongoDb))] mod one2one_opt { fn optional() -> String { let schema = indoc! { @@ -117,7 +117,7 @@ mod one2one_opt { } } -#[test_suite(schema(required), exclude(MongoDb))] +#[test_suite(suite = "noaction_onD_1toM_req", schema(required), exclude(MongoDb))] mod one2many_req { fn required() -> String { let schema = indoc! { @@ -188,7 +188,7 @@ mod one2many_req { } } -#[test_suite(schema(optional), exclude(MongoDb))] +#[test_suite(suite = "noaction_onD_1toM_opt", schema(optional), exclude(MongoDb))] mod one2many_opt { fn optional() -> String { let schema = indoc! { diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs index 6f5b8211b6aa..52515df7fbc9 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs @@ -3,7 +3,7 @@ use indoc::indoc; use query_engine_tests::*; -#[test_suite(schema(required), exclude(MongoDb, SqlServer))] +#[test_suite(suite = "restrict_onD_1to1_req", schema(required), exclude(MongoDb, SqlServer))] mod one2one_req { fn required() -> String { let schema = indoc! { @@ -48,7 +48,7 @@ mod one2one_req { } } -#[test_suite(schema(optional), exclude(MongoDb, SqlServer))] +#[test_suite(suite = "restrict_onD_1to1_opt", schema(optional), exclude(MongoDb, SqlServer))] mod one2one_opt { fn optional() -> String { let schema = indoc! { @@ -119,7 +119,7 @@ mod one2one_opt { } } -#[test_suite(schema(required), exclude(MongoDb, SqlServer))] +#[test_suite(suite = "restrict_onD_1toM_req", schema(required), exclude(MongoDb, SqlServer))] mod one2many_req { fn required() -> String { let schema = indoc! { @@ -190,7 +190,7 @@ mod one2many_req { } } -#[test_suite(schema(optional), exclude(MongoDb, SqlServer))] +#[test_suite(suite = "restrict_onD_1toM_opt", schema(optional), exclude(MongoDb, SqlServer))] mod one2many_opt { fn optional() -> String { let schema = indoc! { diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs index 75b160837b8d..310ebddcc0d0 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs @@ -1,7 +1,7 @@ use indoc::indoc; use query_engine_tests::*; -#[test_suite(exclude(MongoDb))] +#[test_suite(suite = "setdefault_onD_1to1_req", exclude(MongoDb))] mod one2one_req { fn required_with_default() -> String { let schema = indoc! { @@ -102,7 +102,7 @@ mod one2one_req { } } -#[test_suite(exclude(MongoDb))] +#[test_suite(suite = "setdefault_onD_1to1_opt", exclude(MongoDb))] mod one2one_opt { fn optional_with_default() -> String { let schema = indoc! { @@ -200,7 +200,7 @@ mod one2one_opt { } } -#[test_suite(exclude(MongoDb))] +#[test_suite(suite = "setdefault_onD_1toM_req", exclude(MongoDb))] mod one2many_req { fn required_with_default() -> String { let schema = indoc! { @@ -301,7 +301,7 @@ mod one2many_req { } } -#[test_suite(exclude(MongoDb))] +#[test_suite(suite = "setdefault_onD_1toM_opt", exclude(MongoDb))] mod one2many_opt { fn optional_with_default() -> String { let schema = indoc! { diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_null.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_null.rs index 134683d19b4b..5257a5a6b998 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_null.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_null.rs @@ -3,7 +3,7 @@ use indoc::indoc; use query_engine_tests::*; -#[test_suite(schema(required), only(Postgres))] +#[test_suite(suite = "setnull_onD_1to1_req", schema(required), only(Postgres))] mod one2one_req { fn required() -> String { let schema = indoc! { @@ -49,7 +49,7 @@ mod one2one_req { } } -#[test_suite(schema(optional), exclude(MongoDb))] +#[test_suite(suite = "setnull_onD_1to1_opt", schema(optional), exclude(MongoDb))] mod one2one_opt { fn optional() -> String { let schema = indoc! { @@ -90,7 +90,7 @@ mod one2one_opt { } } -#[test_suite(schema(required), only(Postgres))] +#[test_suite(suite = "setnull_onD_1toM_req", schema(required), only(Postgres))] mod one2many_req { fn required() -> String { let schema = indoc! { @@ -113,7 +113,7 @@ mod one2many_req { #[connector_test] async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { insta::assert_snapshot!( - run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), @r###"{"data":{"createOneParent":{"id":1}}}"### ); @@ -136,7 +136,7 @@ mod one2many_req { } } -#[test_suite(schema(optional), exclude(MongoDb))] +#[test_suite(suite = "setnull_onD_1toM_opt", schema(optional), exclude(MongoDb))] mod one2many_opt { fn optional() -> String { let schema = indoc! { diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/top_level_mutations/delete_many_relations.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/top_level_mutations/delete_many_relations.rs index ed02706c2e13..893cae7c5298 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/top_level_mutations/delete_many_relations.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/top_level_mutations/delete_many_relations.rs @@ -1,93 +1,94 @@ use query_engine_tests::*; // TODO: Finish porting this test suite. Needs a way to port the `schemaWithRelation` method +// Note: Referential actions require a rewrite of these tests. #[test_suite] mod delete_many_rels { - use indoc::indoc; - use query_engine_tests::{assert_error, run_query}; + // use indoc::indoc; + // use query_engine_tests::{assert_error, run_query}; - fn schema_1() -> String { - let schema = indoc! { - r#"model Parent{ - #id(id, Int, @id) - p String @unique - c Child[] - } - - model Child{ - #id(id, Int, @id) - c String @unique - parentId Int - parentReq Parent @relation(fields: [parentId], references: [id]) - }"# - }; + // fn schema_1() -> String { + // let schema = indoc! { + // r#"model Parent{ + // #id(id, Int, @id) + // p String @unique + // c Child[] + // } - schema.to_owned() - } + // model Child{ + // #id(id, Int, @id) + // c String @unique + // parentId Int + // parentReq Parent @relation(fields: [parentId], references: [id]) + // }"# + // }; - // "a PM to C1! relation " should "error when deleting the parent" - #[connector_test(schema(schema_1))] - async fn pm_c1_error_delete_parent(runner: &Runner) -> TestResult<()> { - run_query!( - runner, - r#"mutation { - createOneChild(data: { - id: 1, - c: "c1" - parentReq: { - create: {id: 1, p: "p1"} - } - }){ - id - } - }"# - ); + // schema.to_owned() + // } - assert_error!( - runner, - r#"mutation { - deleteManyParent( - where: { p: { equals: "p1" }} - ){ - count - } - }"#, - 2014, - "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." - ); + // // "a PM to C1! relation " should "error when deleting the parent" + // #[connector_test(schema(schema_1))] + // async fn pm_c1_error_delete_parent(runner: &Runner) -> TestResult<()> { + // run_query!( + // runner, + // r#"mutation { + // createOneChild(data: { + // id: 1, + // c: "c1" + // parentReq: { + // create: {id: 1, p: "p1"} + // } + // }){ + // id + // } + // }"# + // ); - Ok(()) - } + // assert_error!( + // runner, + // r#"mutation { + // deleteManyParent( + // where: { p: { equals: "p1" }} + // ){ + // count + // } + // }"#, + // 2014, + // "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + // ); - // "a PM to C1! relation " should "error when deleting the parent with empty filter" - #[connector_test(schema(schema_1))] - async fn pm_c1_error_delete_parent_empty_filter(runner: &Runner) -> TestResult<()> { - run_query!( - runner, - r#"mutation { - createOneChild(data: { - id: 1, - c: "c1" - parentReq: { - create: {id: 1, p: "p1"} - } - }){ - id - } - }"# - ); + // Ok(()) + // } - assert_error!( - runner, - r#"mutation { - deleteManyParent(where: {}){ - count - } - }"#, - 2014, - "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." - ); + // // "a PM to C1! relation " should "error when deleting the parent with empty filter" + // #[connector_test(schema(schema_1))] + // async fn pm_c1_error_delete_parent_empty_filter(runner: &Runner) -> TestResult<()> { + // run_query!( + // runner, + // r#"mutation { + // createOneChild(data: { + // id: 1, + // c: "c1" + // parentReq: { + // create: {id: 1, p: "p1"} + // } + // }){ + // id + // } + // }"# + // ); - Ok(()) - } + // assert_error!( + // runner, + // r#"mutation { + // deleteManyParent(where: {}){ + // count + // } + // }"#, + // 2014, + // "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + // ); + + // Ok(()) + // } } diff --git a/query-engine/connector-test-kit-rs/query-test-macros/src/test_suite.rs b/query-engine/connector-test-kit-rs/query-test-macros/src/test_suite.rs index 92a7c1e307d1..a51e6b2a2dbe 100644 --- a/query-engine/connector-test-kit-rs/query-test-macros/src/test_suite.rs +++ b/query-engine/connector-test-kit-rs/query-test-macros/src/test_suite.rs @@ -1,3 +1,5 @@ +use std::collections::hash_map::Entry; + use crate::{attr_map::NestedAttrMap, ConnectorTestArgs}; use darling::{FromMeta, ToTokens}; use proc_macro::TokenStream; @@ -54,7 +56,10 @@ pub fn test_suite_impl(attr: TokenStream, input: TokenStream) -> TokenStream { let suite_meta: Meta = parse_quote! { suite = #module_name }; let suite_nested_meta = NestedMeta::from(suite_meta); - module_attrs.insert("suite".to_owned(), suite_nested_meta); + + if let Entry::Vacant(entry) = module_attrs.entry("suite".to_owned()) { + entry.insert(suite_nested_meta); + }; if let Some((_, ref mut items)) = test_module.content { add_module_imports(items); From 74b9cd81912099a0a70fcf79ad949e628b885c1e Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 14 Jun 2021 16:56:35 +0200 Subject: [PATCH 22/47] Ignore SetDefault for MySQL --- .../tests/new/ref_actions/on_delete/set_default.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs index 310ebddcc0d0..6169f761c9cc 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs @@ -1,7 +1,8 @@ +//! MySQL doesn't support SetDefault for InnoDB (which is our only supported engine at the moment). use indoc::indoc; use query_engine_tests::*; -#[test_suite(suite = "setdefault_onD_1to1_req", exclude(MongoDb))] +#[test_suite(suite = "setdefault_onD_1to1_req", exclude(MongoDb, MySQL))] mod one2one_req { fn required_with_default() -> String { let schema = indoc! { From 21308fb08255a333937c3993fbfff25306a40307 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Tue, 15 Jun 2021 15:46:50 +0200 Subject: [PATCH 23/47] More nuanced restrict emulation --- libs/prisma-models/src/internal_data_model.rs | 11 +- libs/prisma-models/src/relation.rs | 14 +-- .../new/ref_actions/on_delete/no_action.rs | 88 ++++++++++++- .../new/ref_actions/on_delete/restrict.rs | 116 +++++++++--------- .../query-tests-setup/src/query_result.rs | 2 +- .../src/query_graph_builder/write/utils.rs | 22 ++-- 6 files changed, 171 insertions(+), 82 deletions(-) diff --git a/libs/prisma-models/src/internal_data_model.rs b/libs/prisma-models/src/internal_data_model.rs index 6a12296b7a24..cdbcf230a171 100644 --- a/libs/prisma-models/src/internal_data_model.rs +++ b/libs/prisma-models/src/internal_data_model.rs @@ -147,12 +147,15 @@ impl InternalDataModel { self.version.is_none() } - pub fn fields_requiring_model(&self, model: &ModelRef) -> Vec { + /// Finds all non-list relation fields pointing to the given model. + /// `required` may narrow down the returned fields to required fields only. Returns all on `false`. + pub fn fields_pointing_to_model(&self, model: &ModelRef, required: bool) -> Vec { self.relation_fields() .iter() - .filter(|rf| &rf.related_model() == model) - .filter(|f| f.is_required && !f.is_list) - .map(|f| Arc::clone(f)) + .filter(|rf| &rf.related_model() == model) // All relation fields pointing to `model`. + .filter(|rf| !rf.is_list) // Not a list. + .filter(|rf| (required && rf.is_required) || !required) // If only required fields should be returned + .map(Arc::clone) .collect() } diff --git a/libs/prisma-models/src/relation.rs b/libs/prisma-models/src/relation.rs index 0f12b4adeef0..e6fd5d017bcf 100644 --- a/libs/prisma-models/src/relation.rs +++ b/libs/prisma-models/src/relation.rs @@ -185,14 +185,10 @@ impl Relation { /// Retrieves the onDelete policy for this relation. pub fn on_delete(&self) -> ReferentialAction { - let a = self.field_a().on_delete().cloned(); - let b = self.field_b().on_delete().cloned(); - - dbg!(a); - dbg!(b); - dbg!(&self.field_a().on_delete_default); - dbg!(&self.field_b().on_delete_default); - - a.or(b).expect("No referential action found for relation.") + self.field_a() + .on_delete() + .cloned() + .or(self.field_b().on_delete().cloned()) + .unwrap_or(self.field_a().on_delete_default) } } diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs index 9974175acf8c..ff3b29a5bf61 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/no_action.rs @@ -1,7 +1,7 @@ use indoc::indoc; use query_engine_tests::*; -#[test_suite(suite = "noaction_onD_1to1_req", schema(required), exclude(MongoDb))] +#[test_suite(suite = "noaction_onD_1to1_req", schema(required))] mod one2one_req { fn required() -> String { let schema = indoc! { @@ -21,7 +21,7 @@ mod one2one_req { } /// Deleting the parent must fail if a child is connected. - #[connector_test] + #[connector_test(exclude(MongoDb))] async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { insta::assert_snapshot!( run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), @@ -44,6 +44,27 @@ mod one2one_req { Ok(()) } + + /// Deleting the parent leaves the data in a integrity-violating state. + #[connector_test(only(MongoDb))] + async fn delete_parent_violation(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { parent_id }}"#), + @r###"{"data":{"findManyChild":[{"parent_id":1}]}}"### + ); + + Ok(()) + } } #[test_suite(suite = "noaction_onD_1to1_opt", schema(optional), exclude(MongoDb))] @@ -115,6 +136,27 @@ mod one2one_opt { Ok(()) } + + /// Deleting the parent leaves the data in a integrity-violating state. + #[connector_test(only(MongoDb))] + async fn delete_parent_violation(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { parent_id }}"#), + @r###"{"data":{"findManyChild":[{"parent_id":1}]}}"### + ); + + Ok(()) + } } #[test_suite(suite = "noaction_onD_1toM_req", schema(required), exclude(MongoDb))] @@ -186,6 +228,27 @@ mod one2many_req { Ok(()) } + + /// Deleting the parent leaves the data in a integrity-violating state. + #[connector_test(only(MongoDb))] + async fn delete_parent_violation(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { parent_id }}"#), + @r###"{"data":{"findManyChild":[{"parent_id":1}]}}"### + ); + + Ok(()) + } } #[test_suite(suite = "noaction_onD_1toM_opt", schema(optional), exclude(MongoDb))] @@ -257,4 +320,25 @@ mod one2many_opt { Ok(()) } + + /// Deleting the parent leaves the data in a integrity-violating state. + #[connector_test(only(MongoDb))] + async fn delete_parent_violation(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { parent_id }}"#), + @r###"{"data":{"findManyChild":[{"parent_id":1}]}}"### + ); + + Ok(()) + } } diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs index 52515df7fbc9..8756b3f05dfa 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/restrict.rs @@ -3,7 +3,7 @@ use indoc::indoc; use query_engine_tests::*; -#[test_suite(suite = "restrict_onD_1to1_req", schema(required), exclude(MongoDb, SqlServer))] +#[test_suite(suite = "restrict_onD_1to1_req", schema(required), exclude(SqlServer))] mod one2one_req { fn required() -> String { let schema = indoc! { @@ -30,25 +30,26 @@ mod one2one_req { @r###"{"data":{"createOneParent":{"id":1}}}"### ); - assert_error!( - runner, - "mutation { deleteOneParent(where: { id: 1 }) { id }}", - 2003, - "Foreign key constraint failed on the field" - ); - - assert_error!( - runner, - "mutation { deleteManyParent(where: { id: 1 }) { count }}", - 2003, - "Foreign key constraint failed on the field" - ); + match runner.connector() { + ConnectorTag::MongoDb(_) => assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2014, + "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + ), + _ => assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ), + }; Ok(()) } } -#[test_suite(suite = "restrict_onD_1to1_opt", schema(optional), exclude(MongoDb, SqlServer))] +#[test_suite(suite = "restrict_onD_1to1_opt", schema(optional), exclude(SqlServer))] mod one2one_opt { fn optional() -> String { let schema = indoc! { @@ -75,19 +76,20 @@ mod one2one_opt { @r###"{"data":{"createOneParent":{"id":1}}}"### ); - assert_error!( - runner, - "mutation { deleteOneParent(where: { id: 1 }) { id }}", - 2003, - "Foreign key constraint failed on the field" - ); - - assert_error!( - runner, - "mutation { deleteManyParent(where: { id: 1 }) { count }}", - 2003, - "Foreign key constraint failed on the field" - ); + match runner.connector() { + ConnectorTag::MongoDb(_) => assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2014, + "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + ), + _ => assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ), + }; Ok(()) } @@ -119,7 +121,7 @@ mod one2one_opt { } } -#[test_suite(suite = "restrict_onD_1toM_req", schema(required), exclude(MongoDb, SqlServer))] +#[test_suite(suite = "restrict_onD_1toM_req", schema(required), exclude(SqlServer))] mod one2many_req { fn required() -> String { let schema = indoc! { @@ -146,19 +148,20 @@ mod one2many_req { @r###"{"data":{"createOneParent":{"id":1}}}"### ); - assert_error!( - runner, - "mutation { deleteOneParent(where: { id: 1 }) { id }}", - 2003, - "Foreign key constraint failed on the field" - ); - - assert_error!( - runner, - "mutation { deleteManyParent(where: { id: 1 }) { count }}", - 2003, - "Foreign key constraint failed on the field" - ); + match runner.connector() { + ConnectorTag::MongoDb(_) => assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2014, + "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + ), + _ => assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ), + }; Ok(()) } @@ -190,7 +193,7 @@ mod one2many_req { } } -#[test_suite(suite = "restrict_onD_1toM_opt", schema(optional), exclude(MongoDb, SqlServer))] +#[test_suite(suite = "restrict_onD_1toM_opt", schema(optional), exclude(SqlServer))] mod one2many_opt { fn optional() -> String { let schema = indoc! { @@ -217,19 +220,20 @@ mod one2many_opt { @r###"{"data":{"createOneParent":{"id":1}}}"### ); - assert_error!( - runner, - "mutation { deleteOneParent(where: { id: 1 }) { id }}", - 2003, - "Foreign key constraint failed on the field" - ); - - assert_error!( - runner, - "mutation { deleteManyParent(where: { id: 1 }) { count }}", - 2003, - "Foreign key constraint failed on the field" - ); + match runner.connector() { + ConnectorTag::MongoDb(_) => assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2014, + "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + ), + _ => assert_error!( + runner, + "mutation { deleteOneParent(where: { id: 1 }) { id }}", + 2003, + "Foreign key constraint failed on the field" + ), + }; Ok(()) } diff --git a/query-engine/connector-test-kit-rs/query-tests-setup/src/query_result.rs b/query-engine/connector-test-kit-rs/query-tests-setup/src/query_result.rs index 427a1e6cab8a..3e3c11045afd 100644 --- a/query-engine/connector-test-kit-rs/query-tests-setup/src/query_result.rs +++ b/query-engine/connector-test-kit-rs/query-tests-setup/src/query_result.rs @@ -32,7 +32,7 @@ impl QueryResult { let err_code = format!("P{}", err_code); let err_exists = self.errors().into_iter().any(|err| { - let code_matches = dbg!(err.code() == Some(&err_code)); + let code_matches = err.code() == Some(&err_code); let msg_matches = match msg_contains.as_ref() { Some(msg) => err.message().contains(msg), None => true, diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index be5da1df7651..3284b3217165 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -4,7 +4,7 @@ use crate::{ ConnectorContext, ParsedInputValue, QueryGraphBuilderError, QueryGraphBuilderResult, }; use connector::{Filter, WriteArgs}; -use datamodel::common::preview_features::PreviewFeature; +use datamodel::{common::preview_features::PreviewFeature, ReferentialAction}; use datamodel_connector::ConnectorCapability; use itertools::Itertools; use once_cell::sync::OnceCell; @@ -316,10 +316,16 @@ pub fn insert_deletion_restrict_checks( parent_node: &NodeRef, child_node: &NodeRef, ) -> QueryGraphBuilderResult<()> { + let has_fks = connector_ctx.capabilities.contains(&ConnectorCapability::ForeignKeys); + // If the connector supports foreign keys, we do not do any checks / emulation. + if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) && has_fks { + return Ok(()); + } + + // If it's non-fk dbs, then the emulation will kick in. If it has Fks, then preserve the old behavior (only required ones). let internal_model = model.internal_data_model(); - let relation_fields = internal_model.fields_requiring_model(model); + let relation_fields = internal_model.fields_pointing_to_model(model, has_fks); let mut check_nodes = vec![]; - let once = OnceCell::new(); if !relation_fields.is_empty() { @@ -328,13 +334,9 @@ pub fn insert_deletion_restrict_checks( // We know that the relation can't be a list and must be required on the related model for `model` (see fields_requiring_model). // For all requiring models (RM), we use the field on `model` to query for existing RM records and error out if at least one exists. for rf in relation_fields { - if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) { - // If the connector supports foreign keys, we do not do any emulation. - if connector_ctx.capabilities.contains(&ConnectorCapability::ForeignKeys) - // && !matches!(rf.relation().on_delete(), datamodel::ReferentialAction::Restrict) - { - continue; - } + // We're only looking to emulate restrict here. + if rf.relation().on_delete() != ReferentialAction::Restrict { + continue; } let noop_node = once.get_or_init(|| graph.create_node(Node::Empty)); From 065e991a516bb76455890fa8fbd6d218ad23300c Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Wed, 16 Jun 2021 18:27:34 +0200 Subject: [PATCH 24/47] Skeleton work --- libs/prisma-models/src/internal_data_model.rs | 12 ++++ .../src/query_graph_builder/write/delete.rs | 4 +- .../write/nested/delete_nested.rs | 8 +-- .../src/query_graph_builder/write/utils.rs | 70 ++++++++----------- 4 files changed, 48 insertions(+), 46 deletions(-) diff --git a/libs/prisma-models/src/internal_data_model.rs b/libs/prisma-models/src/internal_data_model.rs index cdbcf230a171..a7c174df8b75 100644 --- a/libs/prisma-models/src/internal_data_model.rs +++ b/libs/prisma-models/src/internal_data_model.rs @@ -159,6 +159,18 @@ impl InternalDataModel { .collect() } + /// Finds all relation fields where the foreign key refers to the given field (as either singular or compound). + pub fn fields_refering_to_field(&self, field: &ScalarFieldRef) -> Vec { + let model_name = &field.model().name; + + self.relation_fields() + .iter() + .filter(|rf| &rf.relation_info.to == model_name) + .filter(|rf| rf.relation_info.references.contains(&field.name)) + .map(Arc::clone) + .collect() + } + pub fn relation_fields(&self) -> &[RelationFieldRef] { self.relation_fields .get_or_init(|| { diff --git a/query-engine/core/src/query_graph_builder/write/delete.rs b/query-engine/core/src/query_graph_builder/write/delete.rs index 9f6ed9788520..4299bc1b142b 100644 --- a/query-engine/core/src/query_graph_builder/write/delete.rs +++ b/query-engine/core/src/query_graph_builder/write/delete.rs @@ -33,7 +33,7 @@ pub fn delete_record( })); let delete_node = graph.create_node(delete_query); - utils::insert_deletion_restrict_checks(graph, connector_ctx, &model, &read_node, &delete_node)?; + utils::insert_emulated_on_delete(graph, connector_ctx, &model, &read_node, &delete_node)?; graph.create_edge( &read_node, @@ -82,7 +82,7 @@ pub fn delete_many_records( let read_query_node = graph.create_node(read_query); let delete_many_node = graph.create_node(Query::Write(delete_many)); - utils::insert_deletion_restrict_checks(graph, connector_ctx, &model, &read_query_node, &delete_many_node)?; + utils::insert_emulated_on_delete(graph, connector_ctx, &model, &read_query_node, &delete_many_node)?; graph.create_edge( &read_query_node, diff --git a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs index e476374616f1..6fbc3b263ed1 100644 --- a/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs +++ b/query-engine/core/src/query_graph_builder/write/nested/delete_nested.rs @@ -51,7 +51,7 @@ pub fn nested_delete( let find_child_records_node = utils::insert_find_children_by_parent_node(graph, parent_node, parent_relation_field, or_filter)?; - utils::insert_deletion_restrict_checks( + utils::insert_emulated_on_delete( graph, connector_ctx, child_model, @@ -98,7 +98,7 @@ pub fn nested_delete( record_filter: None, }))); - utils::insert_deletion_restrict_checks( + utils::insert_emulated_on_delete( graph, connector_ctx, child_model, @@ -161,9 +161,7 @@ pub fn nested_delete_many( }); let delete_many_node = graph.create_node(Query::Write(delete_many)); - - // Todo [RA] - utils::insert_deletion_restrict_checks( + utils::insert_emulated_on_delete( graph, connector_ctx, child_model, diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index 3284b3217165..d5edbf018de1 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -268,48 +268,18 @@ pub fn insert_existing_1to1_related_model_checks( Ok(read_existing_children) } -/// Inserts emulated restrict checks into the graph that checks all _required_ marked relations pointing to -/// the given `model`. Those checks fail at runtime (edges to the `Empty` node) if one or more -/// records are found. Checks are inserted between `parent_node` and `child_node`. +/// Inserts emulated referential actions for `onDelete` into the graph. +/// All relations that refer to the `model` row(s) being deleted are checked for their desired emulation and inserted accordingly. +/// Right now, supported modes are `Restrict` and `SetNull` (cascade will follow). +/// Those checks fail at runtime and are inserted between `parent_node` and `child_node`. /// /// This function is usually part of a delete (`deleteOne` or `deleteMany`). /// Expects `parent_node` to return one or more IDs (for records of `model`) to be checked. /// -/// ## Example for a standard delete scenario -/// - We have 2 relations, from `A` and `B` to `model`. -/// - This function inserts the nodes and edges in between `Find Record IDs` (`parent_node`) and -/// `Delete` (`child_node`) into the graph (but not the edge from `Find` to `Delete`, assumed already existing here). -/// -/// ```text -/// ┌────────────────────┐ -/// │ Find Record IDs to │ -/// ┌──│ Delete │ -/// │ └────────────────────┘ -/// │ │ -/// │ ▼ -/// │ ┌────────────────────┐ -/// ├─▶│Find Connected Model│ -/// │ │ A │──┐ -/// │ └────────────────────┘ │ -/// │ │ │ -/// │ ▼ │ -/// │ ┌────────────────────┐ │ -/// ├─▶│Find Connected Model│ │ Fail if > 0 -/// │ │ B │ │ -/// │ └────────────────────┘ │ -/// │ │Fail if > 0 │ -/// │ ▼ │ -/// │ ┌────────────────────┐ │ -/// ├─▶│ Empty │◀─┘ -/// │ └────────────────────┘ -/// │ │ -/// │ ▼ -/// │ ┌────────────────────┐ -/// └─▶│ Delete │ -/// └────────────────────┘ -/// ``` +/// The old behavior (pre-referential actions) is preserved for if the ReferentialActions feature flag is disabled, +/// which was basically only the `Restrict` part of #[tracing::instrument(skip(graph, model, parent_node, child_node))] -pub fn insert_deletion_restrict_checks( +pub fn insert_emulated_on_delete( graph: &mut QueryGraph, connector_ctx: &ConnectorContext, model: &ModelRef, @@ -317,14 +287,25 @@ pub fn insert_deletion_restrict_checks( child_node: &NodeRef, ) -> QueryGraphBuilderResult<()> { let has_fks = connector_ctx.capabilities.contains(&ConnectorCapability::ForeignKeys); - // If the connector supports foreign keys, we do not do any checks / emulation. + + // If the connector supports foreign keys and the new mode is enabled (preview feature), we do not do any checks / emulation. if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) && has_fks { return Ok(()); } - // If it's non-fk dbs, then the emulation will kick in. If it has Fks, then preserve the old behavior (only required ones). + // If it's non-fk dbs, then the emulation will kick in. If it has Fks, then preserve the old behavior (`has_fks` -> only required ones). let internal_model = model.internal_data_model(); let relation_fields = internal_model.fields_pointing_to_model(model, has_fks); + + for rf in relation_fields { + match rf.relation().on_delete() { + ReferentialAction::Restrict => emulate_restrict(graph, &rf, connector_ctx, model, parent_node, child_node), + ReferentialAction::SetNull => todo!(), + ReferentialAction::Cascade => todo!(), + x => panic!("Unsupported referential action emulation: {}", x), + }; + } + let mut check_nodes = vec![]; let once = OnceCell::new(); @@ -379,3 +360,14 @@ pub fn insert_deletion_restrict_checks( Ok(()) } + +pub fn emulate_restrict( + graph: &mut QueryGraph, + relation_field: &RelationFieldRef, + connector_ctx: &ConnectorContext, + model: &ModelRef, + parent_node: &NodeRef, + child_node: &NodeRef, +) -> QueryGraphBuilderResult<()> { + todo!() +} From a28c9ef0a9b810fa06b7744eab24d383c0bf2199 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 15 Jun 2021 14:51:08 +0300 Subject: [PATCH 25/47] Feature-gating referential actions for IE/ME --- Cargo.lock | 1 + .../src/test_api.rs | 4 +- libs/datamodel/core/Cargo.toml | 1 + .../core/src/ast/reformat/reformatter.rs | 8 ++-- .../core/src/common/preview_features.rs | 2 + .../core/src/diagnostics/validated.rs | 4 +- libs/datamodel/core/src/lib.rs | 13 +++--- .../transform/ast_to_dml/datasource_loader.rs | 6 +-- .../core/src/transform/ast_to_dml/lift.rs | 18 +++++--- .../ast_to_dml/standardise_parsing.rs | 45 ++++++++++++++++--- .../core/src/transform/ast_to_dml/validate.rs | 44 +++++++++++++----- .../ast_to_dml/validation_pipeline.rs | 22 ++++++--- .../core/src/transform/attributes/mod.rs | 13 +++--- .../core/src/transform/attributes/relation.rs | 41 +++++++++++------ .../core/src/transform/dml_to_ast/lower.rs | 7 ++- libs/test-macros/src/lib.rs | 23 ++++++++-- libs/test-setup/src/test_api_args.rs | 8 +++- .../sql-migration-connector/src/flavour.rs | 18 +++++--- .../src/flavour/mssql.rs | 13 ++++-- .../src/flavour/mysql.rs | 12 +++-- .../src/flavour/postgres.rs | 13 ++++-- .../src/flavour/sqlite.rs | 7 +++ .../sql-migration-connector/src/lib.rs | 16 ++++--- .../src/sql_schema_differ.rs | 19 +++++--- migration-engine/core/src/lib.rs | 22 +++++---- migration-engine/core/src/qe_setup.rs | 6 +-- .../src/multi_engine_test_api.rs | 9 ++++ .../migration-engine-tests/src/test_api.rs | 15 +++++-- .../query-engine/src/tests/test_api.rs | 20 +++++++-- 29 files changed, 317 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 663f57f61d70..fac802efec5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -971,6 +971,7 @@ dependencies = [ "colored 1.9.3", "datamodel-connector", "dml", + "enumflags2", "indoc", "itertools 0.8.2", "mongodb-datamodel-connector", diff --git a/introspection-engine/introspection-engine-tests/src/test_api.rs b/introspection-engine/introspection-engine-tests/src/test_api.rs index ef537b588e93..a4241bbd7898 100644 --- a/introspection-engine/introspection-engine-tests/src/test_api.rs +++ b/introspection-engine/introspection-engine-tests/src/test_api.rs @@ -27,7 +27,9 @@ impl TestApi { let connection_string = args.database_url(); let (database, connection_string): (Quaint, String) = if tags.intersects(Tags::Vitess) { - let me = SqlMigrationConnector::new(&connection_string, None).await.unwrap(); + let me = SqlMigrationConnector::new(&connection_string, BitFlags::all(), None) + .await + .unwrap(); me.reset().await.unwrap(); ( diff --git a/libs/datamodel/core/Cargo.toml b/libs/datamodel/core/Cargo.toml index 779f54945b56..f2763803498f 100644 --- a/libs/datamodel/core/Cargo.toml +++ b/libs/datamodel/core/Cargo.toml @@ -22,6 +22,7 @@ regex = "1.3.7" serde = { version = "1.0.90", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order", "float_roundtrip"] } thiserror = "1.0" +enumflags2 = "0.7" [dev-dependencies] clap = "2.33" diff --git a/libs/datamodel/core/src/ast/reformat/reformatter.rs b/libs/datamodel/core/src/ast/reformat/reformatter.rs index 6379a80c4a0f..36371aa399e7 100644 --- a/libs/datamodel/core/src/ast/reformat/reformatter.rs +++ b/libs/datamodel/core/src/ast/reformat/reformatter.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use super::helpers::*; use crate::ast::helper::get_sort_index_of_attribute; use crate::diagnostics::ValidatedMissingFields; @@ -33,7 +35,7 @@ impl<'a> Reformatter<'a> { let schema_ast = crate::parse_schema_ast(&schema_string)?; let validated_datamodel = crate::parse_datamodel_for_formatter(&schema_string)?; - let lowerer = crate::transform::dml_to_ast::LowerDmlToAst::new(None); + let lowerer = crate::transform::dml_to_ast::LowerDmlToAst::new(None, &HashSet::new()); let mut result = Vec::new(); diagnostics.append_warning_vec(validated_datamodel.warnings); @@ -67,7 +69,7 @@ impl<'a> Reformatter<'a> { let validated_datamodel = crate::parse_datamodel_for_formatter(&schema_string)?; diagnostics.append_warning_vec(validated_datamodel.warnings); - let lowerer = crate::transform::dml_to_ast::LowerDmlToAst::new(None); + let lowerer = crate::transform::dml_to_ast::LowerDmlToAst::new(None, &HashSet::new()); let mut missing_field_attributes = Vec::new(); for model in validated_datamodel.subject.models() { @@ -104,7 +106,7 @@ impl<'a> Reformatter<'a> { let validated_datamodel = crate::parse_datamodel_for_formatter(&schema_string)?; diagnostics.append_warning_vec(validated_datamodel.warnings); - let lowerer = crate::transform::dml_to_ast::LowerDmlToAst::new(None); + let lowerer = crate::transform::dml_to_ast::LowerDmlToAst::new(None, &HashSet::new()); let mut missing_relation_attribute_args = Vec::new(); for model in validated_datamodel.subject.models() { diff --git a/libs/datamodel/core/src/common/preview_features.rs b/libs/datamodel/core/src/common/preview_features.rs index 901fbfa14dc4..9d9ed1409a47 100644 --- a/libs/datamodel/core/src/common/preview_features.rs +++ b/libs/datamodel/core/src/common/preview_features.rs @@ -4,6 +4,8 @@ use PreviewFeature::*; macro_rules! features { ($( $variant:ident $(,)? ),*) => { + #[enumflags2::bitflags] + #[repr(u32)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum PreviewFeature { $( diff --git a/libs/datamodel/core/src/diagnostics/validated.rs b/libs/datamodel/core/src/diagnostics/validated.rs index e9d99d7da285..ebc04d373ee5 100644 --- a/libs/datamodel/core/src/diagnostics/validated.rs +++ b/libs/datamodel/core/src/diagnostics/validated.rs @@ -18,10 +18,10 @@ pub type ValidatedGenerators = Validated>; pub type ValidatedMissingFields = Validated>; impl ValidatedGenerators { - pub(crate) fn preview_features(&self) -> HashSet<&PreviewFeature> { + pub(crate) fn preview_features(&self) -> HashSet { self.subject .iter() - .flat_map(|gen| gen.preview_features.iter()) + .flat_map(|gen| gen.preview_features.iter().map(Clone::clone)) .collect() } } diff --git a/libs/datamodel/core/src/lib.rs b/libs/datamodel/core/src/lib.rs index c7c6d81428ad..8acf7361774a 100644 --- a/libs/datamodel/core/src/lib.rs +++ b/libs/datamodel/core/src/lib.rs @@ -139,9 +139,9 @@ fn parse_datamodel_internal( let ast = ast::parse_schema(datamodel_string)?; let generators = GeneratorLoader::load_generators_from_ast(&ast)?; - let preview_features = generators.preview_features(); - let sources = load_sources(&ast, &&preview_features)?; - let validator = ValidationPipeline::new(&sources.subject); + let sources = load_sources(&ast, &generators.preview_features())?; + let features = generators.preview_features(); + let validator = ValidationPipeline::new(&sources.subject, &features); diagnostics.append_warning_vec(sources.warnings); diagnostics.append_warning_vec(generators.warnings); @@ -193,7 +193,7 @@ pub fn parse_configuration(schema: &str) -> Result, + preview_features: &HashSet, ) -> Result { let source_loader = DatasourceLoader::new(); source_loader.load_datasources_from_ast(&schema_ast, preview_features) @@ -225,7 +225,7 @@ pub fn render_datamodel_to( datamodel: &dml::Datamodel, datasource: Option<&Datasource>, ) { - let lowered = LowerDmlToAst::new(datasource).lower(datamodel); + let lowered = LowerDmlToAst::new(datasource, &HashSet::new()).lower(datamodel); render_schema_ast_to(stream, &lowered, 2); } @@ -247,7 +247,8 @@ fn render_datamodel_and_config_to( datamodel: &dml::Datamodel, config: &configuration::Configuration, ) { - let mut lowered = LowerDmlToAst::new(config.datasources.first()).lower(datamodel); + let features = config.preview_features().map(Clone::clone).collect(); + let mut lowered = LowerDmlToAst::new(config.datasources.first(), &features).lower(datamodel); DatasourceSerializer::add_sources_to_ast(config.datasources.as_slice(), &mut lowered); GeneratorSerializer::add_generators_to_ast(&config.generators, &mut lowered); diff --git a/libs/datamodel/core/src/transform/ast_to_dml/datasource_loader.rs b/libs/datamodel/core/src/transform/ast_to_dml/datasource_loader.rs index ae0f5514aa10..fa3a1963b5bc 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/datasource_loader.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/datasource_loader.rs @@ -37,7 +37,7 @@ impl DatasourceLoader { pub fn load_datasources_from_ast( &self, ast_schema: &ast::SchemaAst, - preview_features: &HashSet<&PreviewFeature>, + preview_features: &HashSet, ) -> Result { let mut sources = vec![]; let mut diagnostics = Diagnostics::new(); @@ -92,7 +92,7 @@ impl DatasourceLoader { fn lift_datasource( &self, ast_source: &ast::SourceConfig, - preview_features: &HashSet<&PreviewFeature>, + preview_features: &HashSet, ) -> Result { let source_name = &ast_source.name.name; let args: HashMap<_, _> = ast_source @@ -215,7 +215,7 @@ generator client { fn get_planet_scale_mode_arg( args: &HashMap<&str, ValueValidator>, - preview_features: &HashSet<&PreviewFeature>, + preview_features: &HashSet, source: &SourceConfig, ) -> Result { let arg = args.get("planetScaleMode"); diff --git a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs index a4e414db1a6c..eec1e23520d7 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs @@ -1,13 +1,16 @@ +use std::collections::HashSet; use std::str::FromStr; use super::super::attributes::AllAttributes; -use crate::transform::helpers::ValueValidator; -use crate::{ast, configuration, dml, Field, FieldType}; use crate::{ - ast::Identifier, + ast::{self, Identifier}, + common::preview_features::PreviewFeature, + configuration, diagnostics::{DatamodelError, Diagnostics}, + dml::{self, ScalarType}, + transform::helpers::ValueValidator, + Datasource, Field, FieldType, }; -use crate::{dml::ScalarType, Datasource}; use ::dml::relation_info::ReferentialAction; use datamodel_connector::connector_error::{ConnectorError, ErrorKind}; use itertools::Itertools; @@ -28,9 +31,12 @@ impl<'a> LiftAstToDml<'a> { /// the attributes defined by the given sources registered. /// /// The attributes defined by the given sources will be namespaced. - pub fn new(source: Option<&'a configuration::Datasource>) -> LiftAstToDml<'a> { + pub fn new( + source: Option<&'a configuration::Datasource>, + preview_features: &HashSet, + ) -> LiftAstToDml<'a> { LiftAstToDml { - attributes: AllAttributes::new(), + attributes: AllAttributes::new(preview_features), source, } } diff --git a/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs b/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs index 395b113a77b5..75ec403a2e75 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs @@ -1,23 +1,58 @@ +use std::collections::HashSet; + +use ::dml::{field::FieldArity, relation_info::ReferentialAction}; + use super::common::*; -use crate::{common::RelationNames, diagnostics::Diagnostics, dml, Field}; +use crate::{ + common::{preview_features::PreviewFeature, RelationNames}, + diagnostics::Diagnostics, + dml, Field, +}; /// Helper for standardising a datamodel during parsing. /// /// This will add relation names and M2M references contents -pub struct StandardiserForParsing {} +pub struct StandardiserForParsing<'a> { + preview_features: &'a HashSet, +} -impl StandardiserForParsing { +impl<'a> StandardiserForParsing<'a> { /// Creates a new instance, with all builtin attributes registered. - pub fn new() -> Self { - StandardiserForParsing {} + pub fn new(preview_features: &'a HashSet) -> Self { + Self { preview_features } } pub fn standardise(&self, schema: &mut dml::Datamodel) -> Result<(), Diagnostics> { self.name_unnamed_relations(schema); self.set_relation_to_field_to_id_if_missing_for_m2m_relations(schema); + self.set_default_referential_actions(schema); + Ok(()) } + fn set_default_referential_actions(&self, schema: &mut dml::Datamodel) { + if self.preview_features.contains(&PreviewFeature::ReferentialActions) { + return; + } + + for model in schema.models_mut() { + for field in model.fields_mut() { + match field { + Field::RelationField(field) if field.is_singular() => { + field.relation_info.on_update = Some(ReferentialAction::Cascade); + field.relation_info.on_delete = Some({ + match field.arity { + FieldArity::Required => ReferentialAction::Cascade, + _ => ReferentialAction::SetNull, + } + }) + } + _ => (), + } + } + } + } + /// For M2M relations set the references to the @id fields of the foreign model. fn set_relation_to_field_to_id_if_missing_for_m2m_relations(&self, schema: &mut dml::Datamodel) { let schema_copy = schema.clone(); 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 a4d6feea8e4f..3e40a73d1ff7 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/validate.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/validate.rs @@ -1,5 +1,7 @@ use crate::{ - ast, configuration, + ast, + common::preview_features::PreviewFeature, + configuration, diagnostics::{DatamodelError, Diagnostics}, dml, walkers::ModelWalker, @@ -15,6 +17,7 @@ use std::collections::{HashMap, HashSet}; /// When validating, we check if the datamodel is valid, and generate errors otherwise. pub struct Validator<'a> { source: Option<&'a configuration::Datasource>, + preview_features: &'a HashSet, } /// State error message. Seeing this error means something went really wrong internally. It's the datamodel equivalent of a bluescreen. @@ -25,8 +28,14 @@ const PRISMA_FORMAT_HINT: &str = "You can run `prisma format` to fix this automa impl<'a> Validator<'a> { /// Creates a new instance, with all builtin attributes registered. - pub fn new(source: Option<&'a configuration::Datasource>) -> Validator<'a> { - Self { source } + pub fn new( + source: Option<&'a configuration::Datasource>, + preview_features: &'a HashSet, + ) -> Validator<'a> { + Self { + source, + preview_features, + } } pub fn validate(&self, ast_schema: &ast::SchemaAst, schema: &mut dml::Datamodel) -> Result<(), Diagnostics> { @@ -899,18 +908,33 @@ impl<'a> Validator<'a> { )); } - if field.is_list() - && !related_field.is_list() + if !self.preview_features.contains(&PreviewFeature::ReferentialActions) && (rel_info.on_delete.is_some() || rel_info.on_update.is_some()) { + let message = &format!( + "The relation field `{}` on Model `{}` must not specify the `onDelete` or `onUpdate` argument in the {} attribute without enabling the referentialActions preview feature.", + &field.name, &model.name, RELATION_ATTRIBUTE_NAME_WITH_AT + ); + errors.push_error(DatamodelError::new_attribute_validation_error( - &format!( + message, + RELATION_ATTRIBUTE_NAME, + field_span, + )) + } else if field.is_list() + && !related_field.is_list() + && (rel_info.on_delete.is_some() || rel_info.on_update.is_some()) + { + let message = &format!( "The relation field `{}` on Model `{}` must not specify the `onDelete` or `onUpdate` argument in the {} attribute. You must only specify it on the opposite field `{}` on model `{}`, or in case of a many to many relation, in an explicit join table.", &field.name, &model.name, RELATION_ATTRIBUTE_NAME_WITH_AT, &related_field.name, &related_model.name - ), - RELATION_ATTRIBUTE_NAME, - field_span, - )); + ); + + errors.push_error(DatamodelError::new_attribute_validation_error( + message, + RELATION_ATTRIBUTE_NAME, + field_span, + )); } // ONE TO ONE diff --git a/libs/datamodel/core/src/transform/ast_to_dml/validation_pipeline.rs b/libs/datamodel/core/src/transform/ast_to_dml/validation_pipeline.rs index e4fcae98478d..7bc87d889353 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/validation_pipeline.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/validation_pipeline.rs @@ -1,24 +1,32 @@ +use std::collections::HashSet; + use super::*; -use crate::transform::ast_to_dml::standardise_parsing::StandardiserForParsing; -use crate::{ast, configuration, diagnostics::Diagnostics, ValidatedDatamodel}; +use crate::{ + ast, common::preview_features::PreviewFeature, configuration, diagnostics::Diagnostics, + transform::ast_to_dml::standardise_parsing::StandardiserForParsing, ValidatedDatamodel, +}; /// Is responsible for loading and validating the Datamodel defined in an AST. /// Wrapper for all lift and validation steps pub struct ValidationPipeline<'a> { lifter: LiftAstToDml<'a>, validator: Validator<'a>, - standardiser_for_parsing: StandardiserForParsing, standardiser_for_formatting: StandardiserForFormatting, + standardiser_for_parsing: StandardiserForParsing<'a>, } impl<'a, 'b> ValidationPipeline<'a> { - pub fn new(sources: &'a [configuration::Datasource]) -> ValidationPipeline<'a> { + pub fn new( + sources: &'a [configuration::Datasource], + preview_features: &'a HashSet, + ) -> ValidationPipeline<'a> { let source = sources.first(); + ValidationPipeline { - lifter: LiftAstToDml::new(source), - validator: Validator::new(source), + lifter: LiftAstToDml::new(source, preview_features), + validator: Validator::new(source, preview_features), standardiser_for_formatting: StandardiserForFormatting::new(), - standardiser_for_parsing: StandardiserForParsing::new(), + standardiser_for_parsing: StandardiserForParsing::new(preview_features), } } diff --git a/libs/datamodel/core/src/transform/attributes/mod.rs b/libs/datamodel/core/src/transform/attributes/mod.rs index e75920efd8fb..d46b7611e7b3 100644 --- a/libs/datamodel/core/src/transform/attributes/mod.rs +++ b/libs/datamodel/core/src/transform/attributes/mod.rs @@ -8,9 +8,10 @@ mod relation; mod unique_and_index; mod updated_at; -use crate::dml; +use crate::{common::preview_features::PreviewFeature, dml}; use attribute_list_validator::AttributeListValidator; use attribute_validator::AttributeValidator; +use std::collections::HashSet; /// This is the facade for all attribute validations. It is used within the `ValidationPipeline`. pub struct AllAttributes { @@ -21,9 +22,9 @@ pub struct AllAttributes { } impl AllAttributes { - pub fn new() -> AllAttributes { + pub fn new(preview_features: &HashSet) -> AllAttributes { AllAttributes { - field: new_builtin_field_attributes(), + field: new_builtin_field_attributes(preview_features), model: new_builtin_model_attributes(), enm: new_builtin_enum_attributes(), enm_value: new_builtin_enum_value_attributes(), @@ -31,7 +32,7 @@ impl AllAttributes { } } -fn new_builtin_field_attributes() -> AttributeListValidator { +fn new_builtin_field_attributes(preview_features: &HashSet) -> AttributeListValidator { let mut validator = AttributeListValidator::::new(); // this order of field attributes is used in the formatter as well @@ -40,7 +41,9 @@ fn new_builtin_field_attributes() -> AttributeListValidator { validator.add(Box::new(default::DefaultAttributeValidator {})); validator.add(Box::new(updated_at::UpdatedAtAttributeValidator {})); validator.add(Box::new(map::MapAttributeValidatorForField {})); - validator.add(Box::new(relation::RelationAttributeValidator {})); + validator.add(Box::new(relation::RelationAttributeValidator::new( + preview_features.clone(), + ))); validator.add(Box::new(ignore::IgnoreAttributeValidatorForField {})); validator diff --git a/libs/datamodel/core/src/transform/attributes/relation.rs b/libs/datamodel/core/src/transform/attributes/relation.rs index 2a6e25c59a50..b98f78eacd18 100644 --- a/libs/datamodel/core/src/transform/attributes/relation.rs +++ b/libs/datamodel/core/src/transform/attributes/relation.rs @@ -1,10 +1,23 @@ +use std::collections::HashSet; + use super::{super::helpers::*, AttributeValidator}; -use crate::common::RelationNames; -use crate::diagnostics::DatamodelError; -use crate::{ast, dml, Field}; +use crate::{ + ast, + common::{preview_features::PreviewFeature, RelationNames}, + diagnostics::DatamodelError, + dml, Field, +}; /// Prismas builtin `@relation` attribute. -pub struct RelationAttributeValidator {} +pub struct RelationAttributeValidator { + preview_features: HashSet, +} + +impl RelationAttributeValidator { + pub fn new(preview_features: HashSet) -> Self { + Self { preview_features } + } +} impl AttributeValidator for RelationAttributeValidator { fn attribute_name(&self) -> &'static str { @@ -101,17 +114,19 @@ impl AttributeValidator for RelationAttributeValidator { } } - if let Some(ref_action) = relation_info.on_delete { - if rf.default_on_delete_action() != ref_action { - let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); - args.push(ast::Argument::new("onDelete", expression)); + if self.preview_features.contains(&PreviewFeature::ReferentialActions) { + if let Some(ref_action) = relation_info.on_delete { + if rf.default_on_delete_action() != ref_action { + 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 { - if rf.default_on_update_action() != ref_action { - let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); - args.push(ast::Argument::new("onUpdate", expression)); + if let Some(ref_action) = relation_info.on_update { + if rf.default_on_update_action() != ref_action { + let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty()); + args.push(ast::Argument::new("onUpdate", expression)); + } } } 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 cf3b0b87bd4e..0539d19b999d 100644 --- a/libs/datamodel/core/src/transform/dml_to_ast/lower.rs +++ b/libs/datamodel/core/src/transform/dml_to_ast/lower.rs @@ -1,4 +1,7 @@ +use std::collections::HashSet; + use super::super::attributes::AllAttributes; +use crate::common::preview_features::PreviewFeature; use crate::{ ast::{self, Attribute, Span}, dml, Datasource, @@ -11,9 +14,9 @@ pub struct LowerDmlToAst<'a> { impl<'a> LowerDmlToAst<'a> { /// Creates a new instance, with all builtin attributes registered. - pub fn new(datasource: Option<&'a Datasource>) -> Self { + pub fn new(datasource: Option<&'a Datasource>, preview_features: &HashSet) -> Self { Self { - attributes: AllAttributes::new(), + attributes: AllAttributes::new(preview_features), datasource, } } diff --git a/libs/test-macros/src/lib.rs b/libs/test-macros/src/lib.rs index 3676a1cb27ad..8cad9af30cc5 100644 --- a/libs/test-macros/src/lib.rs +++ b/libs/test-macros/src/lib.rs @@ -35,7 +35,7 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { // Then the function body // We take advantage of the function body being the last token tree (surrounded by braces). - let (sig, body): (syn::Signature, proc_macro2::TokenStream) = { + let (sig, body): (Signature, proc_macro2::TokenStream) = { let sig_tokens = input .clone() .into_iter() @@ -54,6 +54,8 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { let include_tagged = &attrs.include_tagged; let exclude_tagged = &attrs.exclude_tagged; let capabilities = &attrs.capabilities; + let preview_features = &attrs.preview_features; + let test_function_name = &sig.ident; let test_function_name_lit = sig.ident.to_string(); let (arg_name, arg_type) = match extract_api_arg(&sig) { @@ -72,7 +74,8 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { #[test] #ignore_attr fn #test_function_name() { - let args = test_setup::TestApiArgs::new(#test_function_name_lit); + let preview_features = &[#(#preview_features,)*]; + let args = test_setup::TestApiArgs::new(#test_function_name_lit, preview_features); if test_setup::should_skip_test( &args, @@ -94,7 +97,8 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { #[test] #ignore_attr fn #test_function_name() { - let args = test_setup::TestApiArgs::new(#test_function_name_lit); + let preview_features = &[#(#preview_features,)*]; + let args = test_setup::TestApiArgs::new(#test_function_name_lit, preview_features); if test_setup::should_skip_test( &args, @@ -118,6 +122,7 @@ struct TestConnectorAttrs { include_tagged: Vec, exclude_tagged: Vec, capabilities: Vec, + preview_features: Vec, ignore_reason: Option, } @@ -127,6 +132,18 @@ impl TestConnectorAttrs { p if p.is_ident("tags") => &mut self.include_tagged, p if p.is_ident("exclude") => &mut self.exclude_tagged, p if p.is_ident("capabilities") => &mut self.capabilities, + p if p.is_ident("preview_features") => { + self.preview_features.reserve(list.nested.len()); + + for item in list.nested { + match item { + NestedMeta::Lit(Lit::Str(s)) => self.preview_features.push(s), + other => return Err(syn::Error::new_spanned(other, "Unexpected argument")), + } + } + + return Ok(()); + } p if p.is_ident("logs") => return Ok(()), // TODO other => return Err(syn::Error::new_spanned(other, "Unexpected argument")), }; diff --git a/libs/test-setup/src/test_api_args.rs b/libs/test-setup/src/test_api_args.rs index 59d4d4c4f7fb..a99ccb208825 100644 --- a/libs/test-setup/src/test_api_args.rs +++ b/libs/test-setup/src/test_api_args.rs @@ -102,17 +102,23 @@ pub(crate) fn db_under_test() -> &'static DbUnderTest { #[derive(Debug)] pub struct TestApiArgs { test_function_name: &'static str, + preview_features: &'static [&'static str], db: &'static DbUnderTest, } impl TestApiArgs { - pub fn new(test_function_name: &'static str) -> Self { + pub fn new(test_function_name: &'static str, preview_features: &'static [&'static str]) -> Self { TestApiArgs { test_function_name, + preview_features, db: db_under_test(), } } + pub fn preview_features(&self) -> &'static [&'static str] { + &self.preview_features + } + pub fn test_function_name(&self) -> &'static str { self.test_function_name } diff --git a/migration-engine/connectors/sql-migration-connector/src/flavour.rs b/migration-engine/connectors/sql-migration-connector/src/flavour.rs index 23c459c55e2e..5362a86ab777 100644 --- a/migration-engine/connectors/sql-migration-connector/src/flavour.rs +++ b/migration-engine/connectors/sql-migration-connector/src/flavour.rs @@ -7,6 +7,7 @@ mod mysql; mod postgres; mod sqlite; +use enumflags2::BitFlags; pub(crate) use mssql::MssqlFlavour; pub(crate) use mysql::MysqlFlavour; pub(crate) use postgres::PostgresFlavour; @@ -17,7 +18,7 @@ use crate::{ sql_renderer::SqlRenderer, sql_schema_calculator::SqlSchemaCalculatorFlavour, sql_schema_differ::SqlSchemaDifferFlavour, SqlMigrationConnector, }; -use datamodel::Datamodel; +use datamodel::{common::preview_features::PreviewFeature, Datamodel}; use migration_connector::{ConnectorError, ConnectorResult, MigrationDirectory}; use quaint::prelude::{ConnectionInfo, Table}; use sql_schema_describer::SqlSchema; @@ -28,15 +29,19 @@ use std::fmt::Debug; /// reference: https://dev.mysql.com/doc/refman/5.7/en/identifier-length.html pub(crate) const MYSQL_IDENTIFIER_SIZE_LIMIT: usize = 64; -pub(crate) fn from_connection_info(connection_info: &ConnectionInfo) -> Box { +pub(crate) fn from_connection_info( + connection_info: &ConnectionInfo, + preview_features: BitFlags, +) -> Box { match connection_info { - ConnectionInfo::Mysql(url) => Box::new(MysqlFlavour::new(url.clone())), - ConnectionInfo::Postgres(url) => Box::new(PostgresFlavour::new(url.clone())), + ConnectionInfo::Mysql(url) => Box::new(MysqlFlavour::new(url.clone(), preview_features)), + ConnectionInfo::Postgres(url) => Box::new(PostgresFlavour::new(url.clone(), preview_features)), ConnectionInfo::Sqlite { file_path, db_name } => Box::new(SqliteFlavour { file_path: file_path.clone(), attached_name: db_name.clone(), + preview_features, }), - ConnectionInfo::Mssql(url) => Box::new(MssqlFlavour::new(url.clone())), + ConnectionInfo::Mssql(url) => Box::new(MssqlFlavour::new(url.clone(), preview_features)), ConnectionInfo::InMemorySqlite { .. } => unreachable!("SqlFlavour for in-memory SQLite"), } } @@ -92,6 +97,9 @@ pub(crate) trait SqlFlavour: connector: &SqlMigrationConnector, ) -> ConnectorResult; + /// The preview features in use. + fn preview_features(&self) -> BitFlags; + /// Table to store applied migrations, the name part. fn migrations_table_name(&self) -> &'static str { "_prisma_migrations" diff --git a/migration-engine/connectors/sql-migration-connector/src/flavour/mssql.rs b/migration-engine/connectors/sql-migration-connector/src/flavour/mssql.rs index 30ceac7ffb07..dc128a8eaae3 100644 --- a/migration-engine/connectors/sql-migration-connector/src/flavour/mssql.rs +++ b/migration-engine/connectors/sql-migration-connector/src/flavour/mssql.rs @@ -2,6 +2,8 @@ use crate::{ connect, connection_wrapper::Connection, error::quaint_error_to_connector_error, SqlFlavour, SqlMigrationConnector, }; use connection_string::JdbcString; +use datamodel::common::preview_features::PreviewFeature; +use enumflags2::BitFlags; use indoc::formatdoc; use migration_connector::{ConnectorError, ConnectorResult, MigrationDirectory}; use quaint::{connector::MssqlUrl, prelude::Table}; @@ -11,6 +13,7 @@ use user_facing_errors::{introspection_engine::DatabaseSchemaInconsistent, Known pub(crate) struct MssqlFlavour { url: MssqlUrl, + preview_features: BitFlags, } impl std::fmt::Debug for MssqlFlavour { @@ -20,8 +23,8 @@ impl std::fmt::Debug for MssqlFlavour { } impl MssqlFlavour { - pub fn new(url: MssqlUrl) -> Self { - Self { url } + pub fn new(url: MssqlUrl, preview_features: BitFlags) -> Self { + Self { url, preview_features } } fn is_running_on_azure_sql(&self) -> bool { @@ -118,6 +121,10 @@ impl SqlFlavour for MssqlFlavour { (self.schema_name(), self.migrations_table_name()).into() } + fn preview_features(&self) -> BitFlags { + self.preview_features + } + async fn create_database(&self, jdbc_string: &str) -> ConnectorResult { let (db_name, master_uri) = Self::master_url(jdbc_string)?; let conn = connect(&master_uri.to_string()).await?; @@ -384,7 +391,7 @@ mod tests { fn debug_impl_does_not_leak_connection_info() { let url = "sqlserver://myserver:8765;database=master;schema=mydbname;user=SA;password=;trustServerCertificate=true;socket_timeout=60;isolationLevel=READ UNCOMMITTED"; - let flavour = MssqlFlavour::new(MssqlUrl::new(&url).unwrap()); + let flavour = MssqlFlavour::new(MssqlUrl::new(&url).unwrap(), BitFlags::empty()); let debugged = format!("{:?}", flavour); let words = &["myname", "mypassword", "myserver", "8765", "mydbname"]; diff --git a/migration-engine/connectors/sql-migration-connector/src/flavour/mysql.rs b/migration-engine/connectors/sql-migration-connector/src/flavour/mysql.rs index 6e7754c0c96e..996d76d982b9 100644 --- a/migration-engine/connectors/sql-migration-connector/src/flavour/mysql.rs +++ b/migration-engine/connectors/sql-migration-connector/src/flavour/mysql.rs @@ -5,7 +5,7 @@ use crate::{ error::{quaint_error_to_connector_error, SystemDatabase}, SqlMigrationConnector, }; -use datamodel::{walkers::walk_scalar_fields, Datamodel}; +use datamodel::{common::preview_features::PreviewFeature, walkers::walk_scalar_fields, Datamodel}; use enumflags2::BitFlags; use indoc::indoc; use migration_connector::{ConnectorError, ConnectorResult, MigrationDirectory}; @@ -23,6 +23,7 @@ pub(crate) struct MysqlFlavour { url: MysqlUrl, /// See the [Circumstances] enum. circumstances: AtomicU8, + preview_features: BitFlags, } impl std::fmt::Debug for MysqlFlavour { @@ -32,10 +33,11 @@ impl std::fmt::Debug for MysqlFlavour { } impl MysqlFlavour { - pub(crate) fn new(url: MysqlUrl) -> Self { + pub(crate) fn new(url: MysqlUrl, preview_features: BitFlags) -> Self { MysqlFlavour { url, circumstances: Default::default(), + preview_features, } } @@ -303,6 +305,10 @@ impl SqlFlavour for MysqlFlavour { Ok(()) } + fn preview_features(&self) -> BitFlags { + self.preview_features + } + fn scan_migration_script(&self, script: &str) { for capture in QUALIFIED_NAME_RE .captures_iter(script) @@ -396,7 +402,7 @@ mod tests { fn debug_impl_does_not_leak_connection_info() { let url = "mysql://myname:mypassword@myserver:8765/mydbname"; - let flavour = MysqlFlavour::new(MysqlUrl::new(url.parse().unwrap()).unwrap()); + let flavour = MysqlFlavour::new(MysqlUrl::new(url.parse().unwrap()).unwrap(), BitFlags::empty()); // unwrap this let debugged = format!("{:?}", flavour); let words = &["myname", "mypassword", "myserver", "8765", "mydbname"]; diff --git a/migration-engine/connectors/sql-migration-connector/src/flavour/postgres.rs b/migration-engine/connectors/sql-migration-connector/src/flavour/postgres.rs index 95611558d4a1..372a26787384 100644 --- a/migration-engine/connectors/sql-migration-connector/src/flavour/postgres.rs +++ b/migration-engine/connectors/sql-migration-connector/src/flavour/postgres.rs @@ -1,6 +1,8 @@ use crate::{ connect, connection_wrapper::Connection, error::quaint_error_to_connector_error, SqlFlavour, SqlMigrationConnector, }; +use datamodel::common::preview_features::PreviewFeature; +use enumflags2::BitFlags; use indoc::indoc; use migration_connector::{ConnectorError, ConnectorResult, MigrationDirectory}; use quaint::{connector::PostgresUrl, error::ErrorKind as QuaintKind}; @@ -17,6 +19,7 @@ const ADVISORY_LOCK_TIMEOUT: std::time::Duration = std::time::Duration::from_sec pub(crate) struct PostgresFlavour { url: PostgresUrl, + preview_features: BitFlags, } impl std::fmt::Debug for PostgresFlavour { @@ -26,8 +29,8 @@ impl std::fmt::Debug for PostgresFlavour { } impl PostgresFlavour { - pub fn new(url: PostgresUrl) -> Self { - Self { url } + pub fn new(url: PostgresUrl, preview_features: BitFlags) -> Self { + Self { url, preview_features } } pub(crate) fn schema_name(&self) -> &str { @@ -275,6 +278,10 @@ impl SqlFlavour for PostgresFlavour { Ok(()) } + fn preview_features(&self) -> BitFlags { + self.preview_features + } + #[tracing::instrument(skip(self, migrations, connection, connector))] async fn sql_schema_from_migration_history( &self, @@ -380,7 +387,7 @@ mod tests { fn debug_impl_does_not_leak_connection_info() { let url = "postgresql://myname:mypassword@myserver:8765/mydbname"; - let flavour = PostgresFlavour::new(PostgresUrl::new(url.parse().unwrap()).unwrap()); + let flavour = PostgresFlavour::new(PostgresUrl::new(url.parse().unwrap()).unwrap(), BitFlags::all()); let debugged = format!("{:?}", flavour); let words = &["myname", "mypassword", "myserver", "8765", "mydbname"]; diff --git a/migration-engine/connectors/sql-migration-connector/src/flavour/sqlite.rs b/migration-engine/connectors/sql-migration-connector/src/flavour/sqlite.rs index 6f0a2b68dda5..01c3ad3fdf71 100644 --- a/migration-engine/connectors/sql-migration-connector/src/flavour/sqlite.rs +++ b/migration-engine/connectors/sql-migration-connector/src/flavour/sqlite.rs @@ -2,6 +2,8 @@ use crate::{ connect, connection_wrapper::Connection, error::quaint_error_to_connector_error, flavour::SqlFlavour, SqlMigrationConnector, }; +use datamodel::common::preview_features::PreviewFeature; +use enumflags2::BitFlags; use indoc::indoc; use migration_connector::{ConnectorError, ConnectorResult, MigrationDirectory}; use quaint::prelude::ConnectionInfo; @@ -12,6 +14,7 @@ use std::path::Path; pub(crate) struct SqliteFlavour { pub(super) file_path: String, pub(super) attached_name: String, + pub(super) preview_features: BitFlags, } #[async_trait::async_trait] @@ -154,4 +157,8 @@ impl SqlFlavour for SqliteFlavour { Ok(sql_schema) } + + fn preview_features(&self) -> BitFlags { + self.preview_features + } } diff --git a/migration-engine/connectors/sql-migration-connector/src/lib.rs b/migration-engine/connectors/sql-migration-connector/src/lib.rs index 044af8e7b583..10a8488a03f6 100644 --- a/migration-engine/connectors/sql-migration-connector/src/lib.rs +++ b/migration-engine/connectors/sql-migration-connector/src/lib.rs @@ -16,7 +16,8 @@ mod sql_schema_calculator; mod sql_schema_differ; use connection_wrapper::Connection; -use datamodel::{walkers::walk_models, Configuration, Datamodel}; +use datamodel::{common::preview_features::PreviewFeature, walkers::walk_models, Configuration, Datamodel}; +use enumflags2::BitFlags; use error::quaint_error_to_connector_error; use flavour::SqlFlavour; use migration_connector::*; @@ -37,10 +38,11 @@ impl SqlMigrationConnector { /// Construct and initialize the SQL migration connector. pub async fn new( connection_string: &str, + preview_features: BitFlags, shadow_database_connection_string: Option, ) -> ConnectorResult { let connection = connect(connection_string).await?; - let flavour = flavour::from_connection_info(connection.connection_info()); + let flavour = flavour::from_connection_info(connection.connection_info(), preview_features); flavour.ensure_connection_validity(&connection).await?; @@ -54,14 +56,14 @@ impl SqlMigrationConnector { /// Create the database corresponding to the connection string, without initializing the connector. pub async fn create_database(database_str: &str) -> ConnectorResult { let connection_info = ConnectionInfo::from_url(database_str).map_err(ConnectorError::url_parse_error)?; - let flavour = flavour::from_connection_info(&connection_info); + let flavour = flavour::from_connection_info(&connection_info, BitFlags::empty()); flavour.create_database(database_str).await } /// Drop the database corresponding to the connection string, without initializing the connector. pub async fn drop_database(database_str: &str) -> ConnectorResult<()> { let connection_info = ConnectionInfo::from_url(database_str).map_err(ConnectorError::url_parse_error)?; - let flavour = flavour::from_connection_info(&connection_info); + let flavour = flavour::from_connection_info(&connection_info, BitFlags::empty()); flavour.drop_database(database_str).await } @@ -70,7 +72,7 @@ impl SqlMigrationConnector { pub async fn qe_setup(database_str: &str) -> ConnectorResult<()> { let connection_info = ConnectionInfo::from_url(database_str).map_err(ConnectorError::url_parse_error)?; - let flavour = flavour::from_connection_info(&connection_info); + let flavour = flavour::from_connection_info(&connection_info, BitFlags::empty()); flavour.qe_setup(database_str).await } @@ -107,7 +109,6 @@ impl SqlMigrationConnector { let source_schema = self.flavour.describe_schema(connection).await?; let target_schema = SqlSchema::empty(); - let mut steps = Vec::new(); // We drop views here, not in the normal migration process to not @@ -157,7 +158,8 @@ impl SqlMigrationConnector { to: (&Configuration, &Datamodel), ) -> SqlMigration { let connection_info = ConnectionInfo::from_url(&from.0.datasources[0].load_url().unwrap()).unwrap(); - let flavour = flavour::from_connection_info(&connection_info); + let flavour = flavour::from_connection_info(&connection_info, BitFlags::empty()); + let from_sql = sql_schema_calculator::calculate_sql_schema(from, flavour.as_ref()); let to_sql = sql_schema_calculator::calculate_sql_schema(to, flavour.as_ref()); 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 ecbfab844152..f54778f8c6ec 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 @@ -6,6 +6,7 @@ mod sql_schema_differ_flavour; mod table; pub(crate) use column::{ColumnChange, ColumnChanges}; +use datamodel::common::preview_features::PreviewFeature; pub(crate) use sql_schema_differ_flavour::SqlSchemaDifferFlavour; use self::differ_database::DifferDatabase; @@ -542,16 +543,20 @@ 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 + let matches = 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 + && references_same_columns; + + if flavour.preview_features().contains(PreviewFeature::ReferentialActions) { + 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(); + + matches && same_on_delete_action && same_on_update_action + } else { + matches + } } fn enums_match(previous: &EnumWalker<'_>, next: &EnumWalker<'_>) -> bool { diff --git a/migration-engine/core/src/lib.rs b/migration-engine/core/src/lib.rs index 344ebc78d878..2a8e4244db90 100644 --- a/migration-engine/core/src/lib.rs +++ b/migration-engine/core/src/lib.rs @@ -13,10 +13,14 @@ pub use commands::SchemaPushInput; pub use core_error::{CoreError, CoreResult}; use datamodel::{ - common::provider_names::{MSSQL_SOURCE_NAME, MYSQL_SOURCE_NAME, POSTGRES_SOURCE_NAME, SQLITE_SOURCE_NAME}, + common::{ + preview_features::PreviewFeature, + provider_names::{MSSQL_SOURCE_NAME, MYSQL_SOURCE_NAME, POSTGRES_SOURCE_NAME, SQLITE_SOURCE_NAME}, + }, dml::Datamodel, Configuration, Datasource, }; +use enumflags2::BitFlags; use migration_connector::ConnectorError; use sql_migration_connector::SqlMigrationConnector; use user_facing_errors::{common::InvalidDatabaseString, KnownError}; @@ -28,7 +32,7 @@ use mongodb_migration_connector::MongoDbMigrationConnector; /// Top-level constructor for the migration engine API. pub async fn migration_api(datamodel: &str) -> CoreResult> { - let (source, url, shadow_database_url) = parse_configuration(datamodel)?; + let (source, url, preview_features, shadow_database_url) = parse_configuration(datamodel)?; match source.active_provider.as_str() { #[cfg(feature = "sql")] @@ -58,13 +62,13 @@ pub async fn migration_api(datamodel: &str) -> CoreResult { - let connector = SqlMigrationConnector::new(&url, shadow_database_url).await?; + let connector = SqlMigrationConnector::new(&url, preview_features, shadow_database_url).await?; Ok(Box::new(connector)) } @@ -76,7 +80,7 @@ pub async fn migration_api(datamodel: &str) -> CoreResult CoreResult { - let (source, url, _shadow_database_url) = parse_configuration(schema)?; + let (source, url, _preview_features, _shadow_database_url) = parse_configuration(schema)?; match &source.active_provider { provider @@ -96,7 +100,7 @@ pub async fn create_database(schema: &str) -> CoreResult { /// Drop the database referenced by the passed in Prisma schema. pub async fn drop_database(schema: &str) -> CoreResult<()> { - let (source, url, _shadow_database_url) = parse_configuration(schema)?; + let (source, url, _preview_features, _shadow_database_url) = parse_configuration(schema)?; match &source.active_provider { provider @@ -114,7 +118,7 @@ pub async fn drop_database(schema: &str) -> CoreResult<()> { } } -fn parse_configuration(datamodel: &str) -> CoreResult<(Datasource, String, Option)> { +fn parse_configuration(datamodel: &str) -> CoreResult<(Datasource, String, BitFlags, Option)> { let config = datamodel::parse_configuration(&datamodel) .map(|validated_config| validated_config.subject) .map_err(|err| CoreError::new_schema_parser_error(err.to_pretty_string("schema.prisma", datamodel)))?; @@ -127,13 +131,15 @@ fn parse_configuration(datamodel: &str) -> CoreResult<(Datasource, String, Optio .load_shadow_database_url() .map_err(|err| CoreError::new_schema_parser_error(err.to_pretty_string("schema.prisma", datamodel)))?; + let preview_features = config.preview_features().map(Clone::clone).collect(); + let source = config .datasources .into_iter() .next() .ok_or_else(|| CoreError::from_msg("There is no datasource in the schema.".into()))?; - Ok((source, url, shadow_database_url)) + Ok((source, url, preview_features, shadow_database_url)) } fn parse_schema(schema: &str) -> CoreResult<(Configuration, Datamodel)> { diff --git a/migration-engine/core/src/qe_setup.rs b/migration-engine/core/src/qe_setup.rs index 4d1db15d6f39..fcb6d8705e72 100644 --- a/migration-engine/core/src/qe_setup.rs +++ b/migration-engine/core/src/qe_setup.rs @@ -24,11 +24,11 @@ pub enum QueryEngineFlags { /// Database setup for connector-test-kit. pub async fn run(prisma_schema: &str, flags: BitFlags) -> CoreResult<()> { - let (source, url, _shadow_database_url) = super::parse_configuration(prisma_schema)?; + let (source, url, preview_features, _shadow_database_url) = super::parse_configuration(prisma_schema)?; let api: Box = match &source.active_provider { _ if flags.contains(QueryEngineFlags::DatabaseCreationNotAllowed) => { - let api = SqlMigrationConnector::new(&url, None).await?; + let api = SqlMigrationConnector::new(&url, preview_features, None).await?; api.reset().await?; Box::new(api) @@ -44,7 +44,7 @@ pub async fn run(prisma_schema: &str, flags: BitFlags) -> Core { // 1. creates schema & database SqlMigrationConnector::qe_setup(&url).await?; - Box::new(SqlMigrationConnector::new(&url, None).await?) + Box::new(SqlMigrationConnector::new(&url, preview_features, None).await?) } #[cfg(feature = "mongodb")] provider if provider == MONGODB_SOURCE_NAME => { diff --git a/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs b/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs index afdef9aca0cd..c1545d55f305 100644 --- a/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs +++ b/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs @@ -3,6 +3,7 @@ //! A TestApi that is initialized without IO or async code and can instantiate //! multiple migration engines. +use datamodel::common::preview_features::PreviewFeature; pub use test_macros::test_connector; pub use test_setup::{BitFlags, Capabilities, Tags}; @@ -35,9 +36,16 @@ impl TestApi { let (_, q, cs) = rt.block_on(args.create_postgres_database()); (q, cs) } else if tags.contains(Tags::Vitess) { + let preview_features = args + .preview_features() + .into_iter() + .flat_map(|s| PreviewFeature::parse_opt(s)) + .collect(); + let conn = rt .block_on(SqlMigrationConnector::new( args.database_url(), + preview_features, args.shadow_database_url().map(String::from), )) .unwrap(); @@ -164,6 +172,7 @@ impl TestApi { .rt .block_on(SqlMigrationConnector::new( &connection_string, + BitFlags::empty(), shadow_db_connection_string, )) .unwrap(); diff --git a/migration-engine/migration-engine-tests/src/test_api.rs b/migration-engine/migration-engine-tests/src/test_api.rs index c46283a7c700..9324f9a4dc24 100644 --- a/migration-engine/migration-engine-tests/src/test_api.rs +++ b/migration-engine/migration-engine-tests/src/test_api.rs @@ -11,6 +11,7 @@ mod schema_push; pub use apply_migrations::ApplyMigrations; pub use create_migration::CreateMigration; +use datamodel::common::preview_features::PreviewFeature; pub use dev_diagnostic::DevDiagnostic; pub use diagnose_migration_history::DiagnoseMigrationHistory; pub use evaluate_data_loss::EvaluateDataLoss; @@ -44,13 +45,21 @@ impl TestApi { pub async fn new(args: TestApiArgs) -> Self { let tags = args.tags(); + let shadow_database_url = args.shadow_database_url().map(String::from); + + let preview_features = args + .preview_features() + .into_iter() + .flat_map(|feat| PreviewFeature::parse_opt(feat)) + .collect(); + let connection_string = if tags.contains(Tags::Mysql | Tags::Vitess) { let connector = - SqlMigrationConnector::new(args.database_url(), args.shadow_database_url().map(String::from)) + SqlMigrationConnector::new(args.database_url(), preview_features, shadow_database_url.clone()) .await .unwrap(); - connector.reset().await.unwrap(); + connector.reset().await.unwrap(); args.database_url().to_owned() } else if tags.contains(Tags::Mysql) { args.create_mysql_database().await.1 @@ -67,7 +76,7 @@ impl TestApi { unreachable!() }; - let api = SqlMigrationConnector::new(&connection_string, args.shadow_database_url().map(String::from)) + let api = SqlMigrationConnector::new(&connection_string, preview_features, shadow_database_url) .await .unwrap(); diff --git a/query-engine/query-engine/src/tests/test_api.rs b/query-engine/query-engine/src/tests/test_api.rs index cf8387fd0d69..ef7389371c58 100644 --- a/query-engine/query-engine/src/tests/test_api.rs +++ b/query-engine/query-engine/src/tests/test_api.rs @@ -104,22 +104,34 @@ impl TestApi { pub(super) async fn mysql_migration_connector(args: &TestApiArgs) -> (SqlMigrationConnector, String) { let (_db_name, url) = args.create_mysql_database().await; - (SqlMigrationConnector::new(&url, None).await.unwrap(), url) + ( + SqlMigrationConnector::new(&url, BitFlags::all(), None).await.unwrap(), + url, + ) } pub(super) async fn mssql_migration_connector(db_name: &str, args: &TestApiArgs) -> (SqlMigrationConnector, String) { let (_, url) = test_setup::init_mssql_database(args.database_url(), db_name) .await .unwrap(); - (SqlMigrationConnector::new(&url, None).await.unwrap(), url) + ( + SqlMigrationConnector::new(&url, BitFlags::all(), None).await.unwrap(), + url, + ) } pub(super) async fn postgres_migration_connector(args: &TestApiArgs) -> (SqlMigrationConnector, String) { let (_db_name, _, url) = args.create_postgres_database().await; - (SqlMigrationConnector::new(&url, None).await.unwrap(), url) + ( + SqlMigrationConnector::new(&url, BitFlags::all(), None).await.unwrap(), + url, + ) } pub(super) async fn sqlite_migration_connector(db_name: &str) -> (SqlMigrationConnector, String) { let url = sqlite_test_url(db_name); - (SqlMigrationConnector::new(&url, None).await.unwrap(), url) + ( + SqlMigrationConnector::new(&url, BitFlags::all(), None).await.unwrap(), + url, + ) } From 15acaff80a7ccc6b35a84edd2b56b72d94db287a Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Thu, 17 Jun 2021 16:59:55 +0200 Subject: [PATCH 26/47] Implement onDelete cascades emulation. --- .../mongodb-datamodel-connector/src/lib.rs | 2 +- libs/prisma-models/src/internal_data_model.rs | 2 +- .../new/ref_actions/on_delete/cascade.rs | 12 +- .../src/query_graph_builder/write/utils.rs | 174 ++++++++++++------ 4 files changed, 122 insertions(+), 68 deletions(-) diff --git a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs index 303150ebb6cc..87b8214fa5f8 100644 --- a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs @@ -38,7 +38,7 @@ impl MongoDbDatamodelConnector { ]; let native_types = mongodb_types::available_types(); - let referential_actions = Restrict | SetNull | NoAction; + let referential_actions = Restrict | SetNull | NoAction | Cascade; Self { capabilities, diff --git a/libs/prisma-models/src/internal_data_model.rs b/libs/prisma-models/src/internal_data_model.rs index a7c174df8b75..9697f2b1ded6 100644 --- a/libs/prisma-models/src/internal_data_model.rs +++ b/libs/prisma-models/src/internal_data_model.rs @@ -153,7 +153,7 @@ impl InternalDataModel { self.relation_fields() .iter() .filter(|rf| &rf.related_model() == model) // All relation fields pointing to `model`. - .filter(|rf| !rf.is_list) // Not a list. + .filter(|rf| rf.is_inlined_on_enclosing_model()) // Not a list, not a virtual field. .filter(|rf| (required && rf.is_required) || !required) // If only required fields should be returned .map(Arc::clone) .collect() diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs index 61810e1f63e0..b285453e2852 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/cascade.rs @@ -1,7 +1,7 @@ use indoc::indoc; use query_engine_tests::*; -#[test_suite(suite = "cascade_onD_1to1_req", schema(required), exclude(MongoDb))] +#[test_suite(suite = "cascade_onD_1to1_req", schema(required))] mod one2one_req { fn required() -> String { let schema = indoc! { @@ -24,8 +24,8 @@ mod one2one_req { #[connector_test] async fn delete_parent(runner: &Runner) -> TestResult<()> { insta::assert_snapshot!( - run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), - @r###"{"data":{"createOneParent":{"id":1}}}"### + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### ); insta::assert_snapshot!( @@ -42,7 +42,7 @@ mod one2one_req { } } -#[test_suite(suite = "cascade_onD_1to1_opt", schema(optional), exclude(MongoDb))] +#[test_suite(suite = "cascade_onD_1to1_opt", schema(optional))] mod one2one_opt { fn optional() -> String { let schema = indoc! { @@ -83,7 +83,7 @@ mod one2one_opt { } } -#[test_suite(suite = "cascade_onD_1toM_req", schema(required), exclude(MongoDb))] +#[test_suite(suite = "cascade_onD_1toM_req", schema(required))] mod one2many_req { fn required() -> String { let schema = indoc! { @@ -124,7 +124,7 @@ mod one2many_req { } } -#[test_suite(suite = "cascade_onD_1toM_opt", schema(optional), exclude(MongoDb))] +#[test_suite(suite = "cascade_onD_1toM_opt", schema(optional))] mod one2many_opt { fn optional() -> String { let schema = indoc! { diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index d5edbf018de1..7e4968ab5695 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -3,7 +3,7 @@ use crate::{ query_graph::{Flow, Node, NodeRef, QueryGraph, QueryGraphDependency}, ConnectorContext, ParsedInputValue, QueryGraphBuilderError, QueryGraphBuilderResult, }; -use connector::{Filter, WriteArgs}; +use connector::{Filter, RecordFilter, WriteArgs}; use datamodel::{common::preview_features::PreviewFeature, ReferentialAction}; use datamodel_connector::ConnectorCapability; use itertools::Itertools; @@ -278,84 +278,49 @@ pub fn insert_existing_1to1_related_model_checks( /// /// The old behavior (pre-referential actions) is preserved for if the ReferentialActions feature flag is disabled, /// which was basically only the `Restrict` part of -#[tracing::instrument(skip(graph, model, parent_node, child_node))] +#[tracing::instrument(skip(graph, model_to_delete, parent_node, child_node))] pub fn insert_emulated_on_delete( graph: &mut QueryGraph, connector_ctx: &ConnectorContext, - model: &ModelRef, + model_to_delete: &ModelRef, parent_node: &NodeRef, child_node: &NodeRef, ) -> QueryGraphBuilderResult<()> { let has_fks = connector_ctx.capabilities.contains(&ConnectorCapability::ForeignKeys); + let has_ra_feature = connector_ctx.features.contains(&PreviewFeature::ReferentialActions); // If the connector supports foreign keys and the new mode is enabled (preview feature), we do not do any checks / emulation. - if connector_ctx.features.contains(&PreviewFeature::ReferentialActions) && has_fks { + if has_ra_feature && has_fks { return Ok(()); } // If it's non-fk dbs, then the emulation will kick in. If it has Fks, then preserve the old behavior (`has_fks` -> only required ones). - let internal_model = model.internal_data_model(); - let relation_fields = internal_model.fields_pointing_to_model(model, has_fks); + let internal_model = model_to_delete.internal_data_model(); + let relation_fields = internal_model.fields_pointing_to_model(model_to_delete, has_fks); for rf in relation_fields { match rf.relation().on_delete() { - ReferentialAction::Restrict => emulate_restrict(graph, &rf, connector_ctx, model, parent_node, child_node), - ReferentialAction::SetNull => todo!(), - ReferentialAction::Cascade => todo!(), - x => panic!("Unsupported referential action emulation: {}", x), - }; - } + // old behavior was to only insert restrict checks. + _ if !has_ra_feature => { + emulate_restrict(graph, &rf, connector_ctx, model_to_delete, parent_node, child_node)? + } - let mut check_nodes = vec![]; - let once = OnceCell::new(); + ReferentialAction::Cascade => { + emulate_cascade(graph, &rf, connector_ctx, model_to_delete, parent_node, child_node)? + } - if !relation_fields.is_empty() { - // let noop_node = graph.create_node(Node::Empty); + ReferentialAction::Restrict => { + emulate_restrict(graph, &rf, connector_ctx, model_to_delete, parent_node, child_node)? + } - // We know that the relation can't be a list and must be required on the related model for `model` (see fields_requiring_model). - // For all requiring models (RM), we use the field on `model` to query for existing RM records and error out if at least one exists. - for rf in relation_fields { - // We're only looking to emulate restrict here. - if rf.relation().on_delete() != ReferentialAction::Restrict { - continue; + ReferentialAction::SetNull => { + emulate_set_null(graph, &rf, connector_ctx, model_to_delete, parent_node, child_node)? } - let noop_node = once.get_or_init(|| graph.create_node(Node::Empty)); - let relation_field = rf.related_field(); - let child_model_identifier = relation_field.related_model().primary_identifier(); - let read_node = insert_find_children_by_parent_node(graph, parent_node, &relation_field, Filter::empty())?; - - graph.create_edge( - &read_node, - &noop_node, - QueryGraphDependency::ParentProjection( - child_model_identifier, - Box::new(move |noop_node, child_ids| { - if !child_ids.is_empty() { - return Err(QueryGraphBuilderError::RelationViolation((relation_field).into())); - } - - Ok(noop_node) - }), - ), - )?; - - check_nodes.push(read_node); - } - - // Connects all `Find Connected Model` nodes with execution order dependency from the example in the docs. - check_nodes.into_iter().fold1(|prev, next| { - graph - .create_edge(&prev, &next, QueryGraphDependency::ExecutionOrder) - .unwrap(); - - next - }); - - // Edge from empty node to the child (delete). - if let Some(noop_node) = once.get() { - graph.create_edge(&noop_node, child_node, QueryGraphDependency::ExecutionOrder)?; - } + ReferentialAction::NoAction => continue, // Explicitly do nothing. + + x => panic!("Unsupported referential action emulation: {}", x), + }; } Ok(()) @@ -364,10 +329,99 @@ pub fn insert_emulated_on_delete( pub fn emulate_restrict( graph: &mut QueryGraph, relation_field: &RelationFieldRef, + _connector_ctx: &ConnectorContext, + _model: &ModelRef, + parent_node: &NodeRef, + child_node: &NodeRef, +) -> QueryGraphBuilderResult<()> { + let noop_node = graph.create_node(Node::Empty); + let relation_field = relation_field.related_field(); + let child_model_identifier = relation_field.related_model().primary_identifier(); + let read_node = insert_find_children_by_parent_node(graph, parent_node, &relation_field, Filter::empty())?; + + graph.create_edge( + &read_node, + &noop_node, + QueryGraphDependency::ParentProjection( + child_model_identifier, + Box::new(move |noop_node, child_ids| { + if !child_ids.is_empty() { + return Err(QueryGraphBuilderError::RelationViolation((relation_field).into())); + } + + Ok(noop_node) + }), + ), + )?; + + // Edge from empty node to the child (delete). + graph.create_edge(&noop_node, child_node, QueryGraphDependency::ExecutionOrder)?; + + Ok(()) +} + +pub fn emulate_cascade( + graph: &mut QueryGraph, + relation_field: &RelationFieldRef, // This is the field _on the other model_ for cascade. connector_ctx: &ConnectorContext, - model: &ModelRef, + _model: &ModelRef, + parent_node: &NodeRef, + child_node: &NodeRef, +) -> QueryGraphBuilderResult<()> { + let dependent_model = relation_field.model(); + let parent_relation_field = relation_field.related_field(); + let child_model_identifier = relation_field.related_model().primary_identifier(); + + // Records that need to be deleted for the cascade. + let dependent_records_node = + insert_find_children_by_parent_node(graph, parent_node, &parent_relation_field, Filter::empty())?; + + let delete_query = WriteQuery::DeleteManyRecords(DeleteManyRecords { + model: dependent_model.clone(), + record_filter: RecordFilter::empty(), + }); + + let delete_dependents_node = graph.create_node(Query::Write(delete_query)); + + insert_emulated_on_delete( + graph, + connector_ctx, + &dependent_model, + &dependent_records_node, + &delete_dependents_node, + )?; + + graph.create_edge( + &dependent_records_node, + &delete_dependents_node, + QueryGraphDependency::ParentProjection( + child_model_identifier.clone(), + Box::new(move |mut delete_dependents_node, dependent_ids| { + if let Node::Query(Query::Write(WriteQuery::DeleteManyRecords(ref mut dmr))) = delete_dependents_node { + dmr.record_filter = dependent_ids.into(); + } + + Ok(delete_dependents_node) + }), + ), + )?; + + graph.create_edge( + &delete_dependents_node, + child_node, + QueryGraphDependency::ExecutionOrder, + )?; + + Ok(()) +} + +pub fn emulate_set_null( + graph: &mut QueryGraph, + relation_field: &RelationFieldRef, + _connector_ctx: &ConnectorContext, + _model: &ModelRef, parent_node: &NodeRef, child_node: &NodeRef, ) -> QueryGraphBuilderResult<()> { - todo!() + Ok(()) } From da6661d3176fe8e97debb282bef149fc110cc49b Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Thu, 17 Jun 2021 17:26:20 +0200 Subject: [PATCH 27/47] Clippy. Ignore SetDefault tests for MySQL --- libs/prisma-models/src/internal_data_model.rs | 2 +- libs/prisma-models/src/relation.rs | 2 +- .../tests/new/ref_actions/on_delete/set_default.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/prisma-models/src/internal_data_model.rs b/libs/prisma-models/src/internal_data_model.rs index 9697f2b1ded6..4bc677138b9f 100644 --- a/libs/prisma-models/src/internal_data_model.rs +++ b/libs/prisma-models/src/internal_data_model.rs @@ -154,7 +154,7 @@ impl InternalDataModel { .iter() .filter(|rf| &rf.related_model() == model) // All relation fields pointing to `model`. .filter(|rf| rf.is_inlined_on_enclosing_model()) // Not a list, not a virtual field. - .filter(|rf| (required && rf.is_required) || !required) // If only required fields should be returned + .filter(|rf| !required || rf.is_required) // If only required fields should be returned .map(Arc::clone) .collect() } diff --git a/libs/prisma-models/src/relation.rs b/libs/prisma-models/src/relation.rs index e6fd5d017bcf..4b855b9d3408 100644 --- a/libs/prisma-models/src/relation.rs +++ b/libs/prisma-models/src/relation.rs @@ -188,7 +188,7 @@ impl Relation { self.field_a() .on_delete() .cloned() - .or(self.field_b().on_delete().cloned()) + .or_else(|| self.field_b().on_delete().cloned()) .unwrap_or(self.field_a().on_delete_default) } } diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs index 6169f761c9cc..22c3efc474c3 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs @@ -103,7 +103,7 @@ mod one2one_req { } } -#[test_suite(suite = "setdefault_onD_1to1_opt", exclude(MongoDb))] +#[test_suite(suite = "setdefault_onD_1to1_opt", exclude(MongoDb, MySQL))] mod one2one_opt { fn optional_with_default() -> String { let schema = indoc! { @@ -201,7 +201,7 @@ mod one2one_opt { } } -#[test_suite(suite = "setdefault_onD_1toM_req", exclude(MongoDb))] +#[test_suite(suite = "setdefault_onD_1toM_req", exclude(MongoDb, MySQL))] mod one2many_req { fn required_with_default() -> String { let schema = indoc! { @@ -302,7 +302,7 @@ mod one2many_req { } } -#[test_suite(suite = "setdefault_onD_1toM_opt", exclude(MongoDb))] +#[test_suite(suite = "setdefault_onD_1toM_opt", exclude(MongoDb, MySQL))] mod one2many_opt { fn optional_with_default() -> String { let schema = indoc! { From 00ec7c1273a4c5255ac5b4fcee2bf867981c7671 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Thu, 17 Jun 2021 18:31:17 +0200 Subject: [PATCH 28/47] Add cascade onUpdate spec --- .../new/ref_actions/on_update/cascade.rs | 92 ++++++++++--------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/cascade.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/cascade.rs index e0d90838b26b..98f8495a88c4 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/cascade.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/cascade.rs @@ -1,164 +1,168 @@ use indoc::indoc; use query_engine_tests::*; -#[test_suite(schema(required), exclude(MongoDb))] +#[test_suite(suite = "cascade_onU_1to1_req", schema(required), exclude(MongoDb))] mod one2one_req { fn required() -> String { let schema = indoc! { r#"model Parent { #id(id, Int, @id) + uniq String @unique child Child? } model Child { #id(id, Int, @id) - parent_id Int - parent Parent @relation(fields: [parent_id], references: [id], onDelete: Cascade) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: Cascade) }"# }; schema.to_owned() } - /// Deleting the parent deletes child as well. + /// Updating the parent updates the child as well. #[connector_test] - async fn delete_parent(runner: &Runner) -> TestResult<()> { + async fn update_parent(runner: &Runner) -> TestResult<()> { insta::assert_snapshot!( - run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), - @r###"{"data":{"createOneParent":{"id":1}}}"### + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### ); insta::assert_snapshot!( - run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), - @r###"{"data":{"deleteOneParent":{"id":1}}}"### + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "1u" }) { uniq }}"#), + @r###"{"data":{"updateOneParent":{"uniq":"1u"}}}"### ); insta::assert_snapshot!( - run_query!(runner, "query { findManyChild { id }}"), - @r###"{"data":{"findManyChild":[]}}"### + run_query!(runner, "query { findManyParent { uniq child { parent_uniq } }}"), + @r###"{"data":{"findManyParent":[{"uniq":"1u","child":{"parent_uniq":"1u"}}]}}"### ); Ok(()) } } -#[test_suite(schema(optional), exclude(MongoDb))] +#[test_suite(suite = "cascade_onU_1to1_opt", schema(optional), exclude(MongoDb))] mod one2one_opt { fn optional() -> String { let schema = indoc! { r#"model Parent { #id(id, Int, @id) + uniq String @unique child Child? } model Child { #id(id, Int, @id) - parent_id Int? - parent Parent? @relation(fields: [parent_id], references: [id], onDelete: Cascade) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: Cascade) }"# }; schema.to_owned() } - /// Deleting the parent deletes child as well. + /// Updating the parent updates the child as well. #[connector_test] - async fn delete_parent(runner: &Runner) -> TestResult<()> { + async fn update_parent(runner: &Runner) -> TestResult<()> { insta::assert_snapshot!( - run_query!(runner, r#"mutation { createOneParent(data: { id: 1, child: { create: { id: 1 }}}) { id }}"#), - @r###"{"data":{"createOneParent":{"id":1}}}"### + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### ); insta::assert_snapshot!( - run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), - @r###"{"data":{"deleteOneParent":{"id":1}}}"### + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "1u" }) { uniq }}"#), + @r###"{"data":{"updateOneParent":{"uniq":"1u"}}}"### ); insta::assert_snapshot!( - run_query!(runner, "query { findManyChild { id }}"), - @r###"{"data":{"findManyChild":[]}}"### + run_query!(runner, "query { findManyParent { uniq child { parent_uniq } }}"), + @r###"{"data":{"findManyParent":[{"uniq":"1u","child":{"parent_uniq":"1u"}}]}}"### ); Ok(()) } } -#[test_suite(schema(required), exclude(MongoDb))] +#[test_suite(suite = "cascade_onU_1toM_req", schema(required), exclude(MongoDb))] mod one2many_req { fn required() -> String { let schema = indoc! { r#"model Parent { #id(id, Int, @id) + uniq String @unique children Child[] } model Child { #id(id, Int, @id) - parent_id Int - parent Parent @relation(fields: [parent_id], references: [id], onDelete: Cascade) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: Cascade) }"# }; schema.to_owned() } - /// Deleting the parent deletes all children. + /// Updating the parent updates the child as well. #[connector_test] - async fn delete_parent(runner: &Runner) -> TestResult<()> { + async fn update_parent(runner: &Runner) -> TestResult<()> { insta::assert_snapshot!( - run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: [ { id: 1 }, { id: 2 } ] }}) { id }}"#), - @r###"{"data":{"createOneParent":{"id":1}}}"### + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### ); insta::assert_snapshot!( - run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), - @r###"{"data":{"deleteOneParent":{"id":1}}}"### + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "1u" }) { uniq }}"#), + @r###"{"data":{"updateOneParent":{"uniq":"1u"}}}"### ); insta::assert_snapshot!( - run_query!(runner, "query { findManyChild { id }}"), - @r###"{"data":{"findManyChild":[]}}"### + run_query!(runner, "query { findManyParent { uniq children { parent_uniq } }}"), + @r###"{"data":{"findManyParent":[{"uniq":"1u","children":[{"parent_uniq":"1u"}]}]}}"### ); Ok(()) } } -#[test_suite(schema(optional), exclude(MongoDb))] +#[test_suite(suite = "cascade_onU_1toM_opt", schema(optional), exclude(MongoDb))] mod one2many_opt { fn optional() -> String { let schema = indoc! { r#"model Parent { #id(id, Int, @id) + uniq String @unique children Child[] } model Child { #id(id, Int, @id) - parent_id Int? - parent Parent? @relation(fields: [parent_id], references: [id], onDelete: Cascade) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: Cascade) }"# }; schema.to_owned() } - /// Deleting the parent deletes all children. + /// Updating the parent updates the child as well. #[connector_test] - async fn delete_parent(runner: &Runner) -> TestResult<()> { + async fn update_parent(runner: &Runner) -> TestResult<()> { insta::assert_snapshot!( - run_query!(runner, r#"mutation { createOneParent(data: { id: 1, children: { create: [ { id: 1 }, { id: 2 } ] }}) { id }}"#), - @r###"{"data":{"createOneParent":{"id":1}}}"### + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### ); insta::assert_snapshot!( - run_query!(runner, "mutation { deleteOneParent(where: { id: 1 }) { id }}"), - @r###"{"data":{"deleteOneParent":{"id":1}}}"### + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "1u" }) { uniq }}"#), + @r###"{"data":{"updateOneParent":{"uniq":"1u"}}}"### ); insta::assert_snapshot!( - run_query!(runner, "query { findManyChild { id }}"), - @r###"{"data":{"findManyChild":[]}}"### + run_query!(runner, "query { findManyParent { uniq children { parent_uniq } }}"), + @r###"{"data":{"findManyParent":[{"uniq":"1u","children":[{"parent_uniq":"1u"}]}]}}"### ); Ok(()) From b0b337acc063de2407bef0ce7bd2718f112f2457 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Fri, 18 Jun 2021 10:42:38 +0300 Subject: [PATCH 29/47] Green IE/PSL tests --- .../src/calculate_datamodel.rs | 4 + .../src/introspection_helpers.rs | 3 + .../src/test_api.rs | 3 +- .../tests/commenting_out/mod.rs | 12 +- .../tests/re_introspection/mod.rs | 127 ++++++++----- .../tests/relations/mod.rs | 179 +++++------------- .../tests/relations_with_compound_fk/mod.rs | 100 ++-------- .../tests/remapping_database_names/mod.rs | 35 +--- .../connectors/dml/src/relation_info.rs | 9 + .../ast_to_dml/standardise_formatting.rs | 2 + .../ast_to_dml/standardise_parsing.rs | 10 +- .../core/src/transform/ast_to_dml/validate.rs | 3 +- .../relations/referential_actions.rs | 97 ++++++++-- 13 files changed, 269 insertions(+), 315 deletions(-) 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 b80b01e666ec..75e996ea6a30 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs @@ -471,6 +471,7 @@ mod tests { name: "CityToUser".to_string(), on_delete: None, on_update: None, + legacy_referential_actions: false, }, )), ], @@ -559,6 +560,7 @@ mod tests { references: vec!["id".to_string(), "name".to_string()], on_delete: None, on_update: None, + legacy_referential_actions: false, }, )), ], @@ -857,6 +859,7 @@ mod tests { name: "CityToUser".to_string(), on_delete: None, on_update: None, + legacy_referential_actions: false, }, )), ], @@ -915,6 +918,7 @@ mod tests { references: vec!["id".to_string()], on_delete: None, on_update: None, + legacy_referential_actions: false, }, )), ], 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 4d00bf241870..905cd4d52d20 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs @@ -109,6 +109,7 @@ pub fn calculate_many_to_many_field( references: opposite_foreign_key.referenced_columns.clone(), on_delete: None, on_update: None, + legacy_referential_actions: false, }; let basename = opposite_foreign_key.referenced_table.clone(); @@ -190,6 +191,7 @@ pub(crate) fn calculate_relation_field( references: foreign_key.referenced_columns.clone(), on_delete: Some(map_action(foreign_key.on_delete_action)), on_update: Some(map_action(foreign_key.on_update_action)), + legacy_referential_actions: false, }; let columns: Vec<&Column> = foreign_key @@ -225,6 +227,7 @@ pub(crate) fn calculate_backrelation_field( references: vec![], on_delete: None, on_update: None, + legacy_referential_actions: false, }; // 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 a4241bbd7898..0c49cd566214 100644 --- a/introspection-engine/introspection-engine-tests/src/test_api.rs +++ b/introspection-engine/introspection-engine-tests/src/test_api.rs @@ -187,9 +187,10 @@ impl TestApi { #[track_caller] pub fn assert_eq_datamodels(&self, expected_without_header: &str, result_with_header: &str) { - let parsed_expected = datamodel::parse_datamodel(&self.dm_with_sources(expected_without_header)) + let parsed_expected = datamodel::parse_datamodel(&self.dm_with_sources(dbg!(expected_without_header))) .unwrap() .subject; + let parsed_result = datamodel::parse_datamodel(result_with_header).unwrap().subject; let reformatted_expected = 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 fdc75af7fc92..ac3d79325df9 100644 --- a/introspection-engine/introspection-engine-tests/tests/commenting_out/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/commenting_out/mod.rs @@ -25,7 +25,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], onDelete: Restrict, onUpdate: Restrict) + User User @relation(fields: [user_id], references: [id]) @@index([user_id], name: "user_id") @@ignore @@ -42,7 +42,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], onDelete: NoAction, onUpdate: NoAction) + User User @relation(fields: [user_id], references: [id]) @@index([user_id], name: "user_id") @@ignore @@ -59,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], onDelete: NoAction, onUpdate: NoAction) + User User @relation(fields: [user_id], references: [id]) @@ignore } @@ -96,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], onDelete: NoAction, onUpdate: NoAction) + User User @relation(fields: [user_id], references: [id]) @@ignore } @@ -291,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], onDelete: NoAction, onUpdate: NoAction) + User User @relation(fields: [user_ip], references: [ip]) } model User { @@ -412,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], onDelete: NoAction, onUpdate: NoAction) + User User @relation(fields: [user_ip], references: [ip]) @@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 c91eb79c49da..1c9af0a5ed10 100644 --- a/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs @@ -1,5 +1,4 @@ use barrel::types; -use datamodel::ReferentialAction; use indoc::formatdoc; use indoc::indoc; use introspection_engine_tests::{assert_eq_json, test_api::*}; @@ -131,17 +130,12 @@ 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], onDelete: {action}, onUpdate: {action}) + Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id]) {extra_index} }} @@ -152,7 +146,6 @@ async fn mapped_model_and_field_name(api: &TestApi) -> TestResult { @@map(name: "User") }} "#, - action = action, extra_index = extra_index ); @@ -161,7 +154,7 @@ 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], onDelete: {action}, onUpdate: {action}) + Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id]) {extra_index} }} @@ -176,7 +169,6 @@ async fn mapped_model_and_field_name(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) }} "#, - action = action, extra_index = extra_index ); @@ -244,7 +236,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], onDelete: NoAction, onUpdate: NoAction) + Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id]) {} }} @@ -263,7 +255,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], onDelete: NoAction, onUpdate: NoAction) + Custom_User Custom_User @relation(fields: [c_user_id], references: [c_id]) {} }} @@ -814,11 +806,6 @@ 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 {{ @@ -831,13 +818,12 @@ 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], onDelete: {action}, onUpdate: {action}) - Employee_EmployeeToSchedule_morningEmployeeId Employee @relation("EmployeeToSchedule_morningEmployeeId", fields: [morningEmployeeId], references: [id], onDelete: NoAction, onUpdate: {action}) + Employee_EmployeeToSchedule_eveningEmployeeId Employee @relation("EmployeeToSchedule_eveningEmployeeId", fields: [eveningEmployeeId], references: [id]) + Employee_EmployeeToSchedule_morningEmployeeId Employee @relation("EmployeeToSchedule_morningEmployeeId", fields: [morningEmployeeId], references: [id]) {idx1} {idx2} }} "#, - action = action, idx1 = idx1, idx2 = idx2, ); @@ -854,8 +840,8 @@ 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], onDelete: {action}, onUpdate: {action}) - Employee_EmployeeToSchedule_morningEmployeeId Employee @relation("EmployeeToSchedule_morningEmployeeId", fields: [morningEmployeeId], references: [id], onDelete: {action}, onUpdate: {action}) + Employee_EmployeeToSchedule_eveningEmployeeId Employee @relation("EmployeeToSchedule_eveningEmployeeId", fields: [eveningEmployeeId], references: [id]) + Employee_EmployeeToSchedule_morningEmployeeId Employee @relation("EmployeeToSchedule_morningEmployeeId", fields: [morningEmployeeId], references: [id]) {idx1} {idx2} }} @@ -864,7 +850,6 @@ async fn multiple_changed_relation_names(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) }} "#, - action = action, idx1 = idx1, idx2 = idx2, ); @@ -894,29 +879,24 @@ async fn custom_virtual_relation_field_names(api: &TestApi) -> TestResult { }) .await?; - 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], onDelete: {action}, onUpdate: {action}) + custom_User User @relation(fields: [user_id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) custom_Post Post? }} - "#, action = action}; + "#}; 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], onDelete: {action}, onUpdate: {action}) + custom_User User @relation(fields: [user_id], references: [id]) }} model User {{ @@ -927,7 +907,7 @@ async fn custom_virtual_relation_field_names(api: &TestApi) -> TestResult { model Unrelated {{ id Int @id @default(autoincrement()) }} - "#, action = action}; + "#}; api.assert_eq_datamodels(&final_dm, &api.re_introspect(&input_dm).await?); @@ -1138,18 +1118,13 @@ async fn multiple_changed_relation_names_due_to_mapped_models(api: &TestApi) -> }) .await?; - 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], onDelete: {action}, onUpdate: {action}) - custom_User2 Custom_User @relation("AnotherCustomRelationName", fields: [user_id2], references: [id], onDelete: {action}, onUpdate: {action}) + custom_User Custom_User @relation("CustomRelationName", fields: [user_id], references: [id]) + custom_User2 Custom_User @relation("AnotherCustomRelationName", fields: [user_id2], references: [id]) }} model Custom_User {{ @@ -1159,15 +1134,15 @@ async fn multiple_changed_relation_names_due_to_mapped_models(api: &TestApi) -> @@map("User") }} - "#, action = action}; + "#}; 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], onDelete: {action}, onUpdate: {action}) - custom_User2 Custom_User @relation("AnotherCustomRelationName", fields: [user_id2], references: [id], onDelete: {action}, onUpdate: {action}) + custom_User Custom_User @relation("CustomRelationName", fields: [user_id], references: [id]) + custom_User2 Custom_User @relation("AnotherCustomRelationName", fields: [user_id2], references: [id]) }} model Custom_User {{ @@ -1181,7 +1156,7 @@ async fn multiple_changed_relation_names_due_to_mapped_models(api: &TestApi) -> model Unrelated {{ id Int @id @default(autoincrement()) }} - "#, action = action}; + "#}; api.assert_eq_datamodels(&final_dm, &api.re_introspect(&input_dm).await?); @@ -1610,7 +1585,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, onDelete: NoAction, onUpdate: NoAction) + tag Tag @relation("post_to_tag", fields:[tag_id], references: id) } model Tag { @@ -1625,7 +1600,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, onDelete: NoAction, onUpdate: NoAction) + tag Tag @relation("post_to_tag", fields:[tag_id], references: id) } model Tag { @@ -1753,8 +1728,56 @@ async fn do_not_try_to_keep_custom_many_to_many_self_relation_names(api: &TestAp Ok(()) } +#[test_connector] +async fn referential_actions(api: &TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("a", |t| { + t.add_column("id", types::primary()); + }); + + migration.create_table("b", |t| { + t.add_column("id", types::primary()); + t.add_column("a_id", types::integer().nullable(false)); + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE SET NULL ON UPDATE SET NULL", + ); + }); + }) + .await?; + + let extra_index = if api.sql_family().is_mysql() { + r#"@@index([a_id], name: "asdf")"# + } else { + "" + }; + + let input_dm = formatdoc! {r#" + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + + model a {{ + id Int @id @default(autoincrement()) + bs b[] + }} + + model b {{ + id Int @id @default(autoincrement()) + a_id Int + a a @relation(fields: [a_id], references: [id], onDelete: SetNull, onUpdate: SetNull) + {} + }} + "#, extra_index}; + + api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?); + + Ok(()) +} + #[test_connector(tags(Postgres, Mysql, Sqlite))] -async fn default_required_actions_with_restrict(api: &TestApi) -> TestResult { +async fn default_referential_actions_with_restrict(api: &TestApi) -> TestResult { api.barrel() .execute(|migration| { migration.create_table("a", |t| { @@ -1778,6 +1801,11 @@ async fn default_required_actions_with_restrict(api: &TestApi) -> TestResult { }; let input_dm = formatdoc! {r#" + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + model a {{ id Int @id @default(autoincrement()) bs b[] @@ -1797,7 +1825,7 @@ async fn default_required_actions_with_restrict(api: &TestApi) -> TestResult { } #[test_connector(tags(Mssql))] -async fn default_required_actions_without_restrict(api: &TestApi) -> TestResult { +async fn default_referential_actions_without_restrict(api: &TestApi) -> TestResult { api.barrel() .execute(|migration| { migration.create_table("a", |t| { @@ -1821,6 +1849,11 @@ async fn default_required_actions_without_restrict(api: &TestApi) -> TestResult }; let input_dm = formatdoc! {r#" + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + model a {{ id Int @id @default(autoincrement()) bs b[] diff --git a/introspection-engine/introspection-engine-tests/tests/relations/mod.rs b/introspection-engine/introspection-engine-tests/tests/relations/mod.rs index 6d4f3f8f9fdc..9a15797864a5 100644 --- a/introspection-engine/introspection-engine-tests/tests/relations/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/relations/mod.rs @@ -1,5 +1,4 @@ use barrel::types; -use datamodel::ReferentialAction; use indoc::formatdoc; use indoc::indoc; use introspection_engine_tests::test_api::*; @@ -25,23 +24,18 @@ async fn one_to_one_req_relation(api: &TestApi) -> TestResult { ) .await?; - 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], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) Post Post? }} - "##, action = action}; + "##}; api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -66,22 +60,17 @@ async fn one_to_one_relation_on_a_singular_primary_key(api: &TestApi) -> TestRes ) .await?; - 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], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) Post Post? }} - "##, action = action}; + "##}; api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -126,26 +115,21 @@ async fn two_one_to_one_relations_between_the_same_models(api: &TestApi) -> Test ) .await?; - 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], onDelete: {action}, onUpdate: {action}) + User_Post_user_idToUser User @relation("Post_user_idToUser", fields: [user_id], references: [id]) User_PostToUser_post_id User? @relation("PostToUser_post_id") }} 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], onDelete: {action}, onUpdate: {action}) + Post_PostToUser_post_id Post @relation("PostToUser_post_id", fields: [post_id], references: [id]) Post_Post_user_idToUser Post? @relation("Post_user_idToUser") }} - "##, action = action}; + "##}; api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -171,23 +155,18 @@ async fn a_one_to_one_relation(api: &TestApi) -> TestResult { ) .await?; - 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], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) Post Post? }} - "##, action = action}; + "##}; api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -220,16 +199,11 @@ 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 {native_type} - User User? @relation(fields: [user_email], references: [email], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_email], references: [email]) }} model User {{ @@ -237,7 +211,7 @@ async fn a_one_to_one_relation_referencing_non_id(api: &TestApi) -> TestResult { email String? @unique {native_type} Post Post? }} - "##, action = action, native_type = native_type}; + "##, native_type = native_type}; api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -263,18 +237,13 @@ 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 => { formatdoc! {r##" model Post {{ id Int @id @default(autoincrement()) user_id Int? - User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_id], references: [id]) @@index([user_id], name: "user_id") }} @@ -282,21 +251,21 @@ async fn a_one_to_many_relation(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) Post Post[] }} - "##, action = action} + "##} } _ => { formatdoc! {r##" model Post {{ id Int @id @default(autoincrement()) user_id Int? - User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) Post Post[] }} - "##, action = action} + "##} } }; @@ -324,18 +293,13 @@ 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 => { formatdoc! {r##" model Post {{ id Int @id @default(autoincrement()) user_id Int - User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id], references: [id]) @@index([user_id], name: "user_id") }} @@ -343,21 +307,21 @@ async fn a_one_req_to_many_relation(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) Post Post[] }} - "##, action = action} + "##} } _ => { formatdoc! {r##" model Post {{ id Int @id @default(autoincrement()) user_id Int - User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) Post Post[] }} - "##, action = action} + "##} } }; @@ -437,11 +401,6 @@ 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 => { formatdoc! {r##" @@ -454,8 +413,8 @@ async fn a_many_to_many_relation_with_an_id(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) user_id Int post_id Int - Post Post @relation(fields: [post_id], references: [id], onDelete: {action}, onUpdate: {action}) - User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + Post Post @relation(fields: [post_id], references: [id]) + User User @relation(fields: [user_id], references: [id]) @@index([post_id], name: "post_id") @@index([user_id], name: "user_id") }} @@ -464,7 +423,7 @@ async fn a_many_to_many_relation_with_an_id(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) PostsToUsers PostsToUsers[] }} - "##, action = action} + "##} } _ => { formatdoc! {r##" @@ -477,15 +436,15 @@ async fn a_many_to_many_relation_with_an_id(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) user_id Int post_id Int - Post Post @relation(fields: [post_id], references: [id], onDelete: {action}, onUpdate: {action}) - User User @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + Post Post @relation(fields: [post_id], references: [id]) + User User @relation(fields: [user_id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) PostsToUsers PostsToUsers[] }} - "##, action = action} + "##} } }; @@ -512,11 +471,6 @@ 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 => { formatdoc! {r##" @@ -524,14 +478,14 @@ async fn a_self_relation(api: &TestApi) -> TestResult { 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], onDelete: {action}, onUpdate: {action}) - User_UserToUser_recruited_by User? @relation("UserToUser_recruited_by", fields: [recruited_by], references: [id], onDelete: {action}, onUpdate: {action}) + 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]) 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} + "##} } _ => { formatdoc! {r##" @@ -539,12 +493,12 @@ async fn a_self_relation(api: &TestApi) -> TestResult { 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], onDelete: {action}, onUpdate: {action}) - User_UserToUser_recruited_by User? @relation("UserToUser_recruited_by", fields: [recruited_by], references: [id], onDelete: {action}, onUpdate: {action}) + 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]) other_User_UserToUser_direct_report User[] @relation("UserToUser_direct_report") other_User_UserToUser_recruited_by User[] @relation("UserToUser_recruited_by") }} - "##, action = action} + "##} } }; @@ -572,22 +526,17 @@ async fn id_fields_with_foreign_key(api: &TestApi) -> TestResult { ) .await?; - 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], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) Post Post? }} - "##, action = action}; + "##}; api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -618,18 +567,13 @@ 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 => { formatdoc! {r##" model Post {{ id Int @id @default(autoincrement()) user_id Int? - User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_id], references: [id]) @@index([user_id], name: "user_id") }} @@ -637,21 +581,21 @@ async fn duplicate_fks_should_ignore_one_of_them(api: &TestApi) -> TestResult { id Int @id @default(autoincrement()) Post Post[] }} - "##, action = action} + "##} } _ => { formatdoc! {r##" model Post {{ id Int @id @default(autoincrement()) user_id Int? - User User? @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) Post Post[] }} - "##, action = action} + "##} } }; @@ -675,23 +619,18 @@ async fn default_values_on_relations(api: &TestApi) -> TestResult { }) .await?; - 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], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_id], references: [id]) }} model User {{ id Int @id @default(autoincrement()) Post Post[] }} - "##, action = action}; + "##}; api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -764,18 +703,13 @@ 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 => { formatdoc! {r##" model x {{ id Int @id @default(autoincrement()) y Int - y_xToy y @relation(fields: [y], references: [id], onDelete: NoAction, onUpdate: NoAction) + y_xToy y @relation(fields: [y], references: [id]) }} model y {{ @@ -790,7 +724,7 @@ async fn relations_should_avoid_name_clashes(api: &TestApi) -> TestResult { model x {{ id Int @id y Int - y_xToy y @relation(fields: [y], references: [id], onDelete: {action}, onUpdate: {action}) + y_xToy y @relation(fields: [y], references: [id]) @@index([y], name: "y") }} @@ -799,14 +733,14 @@ async fn relations_should_avoid_name_clashes(api: &TestApi) -> TestResult { x Int x_xToy x[] }} - "##, action = action} + "##} } _ => { formatdoc! {r##" model x {{ id Int @id y Int - y_xToy y @relation(fields: [y], references: [id], onDelete: {action}, onUpdate: {action}) + y_xToy y @relation(fields: [y], references: [id]) }} model y {{ @@ -814,7 +748,7 @@ async fn relations_should_avoid_name_clashes(api: &TestApi) -> TestResult { x Int x_xToy x[] }} - "##, action = action} + "##} } }; @@ -864,18 +798,13 @@ 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 => { formatdoc! { r##" model x {{ id Int @id @default(autoincrement()) y Int - y_x_yToy y @relation("x_yToy", fields: [y], references: [id], onDelete: {action}, onUpdate: {action}) + y_x_yToy y @relation("x_yToy", fields: [y], references: [id]) 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") @@ -886,18 +815,18 @@ async fn relations_should_avoid_name_clashes_2(api: &TestApi) -> TestResult { 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], onDelete: {action}, onUpdate: {action}) + 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_x_yToy x[] @relation("x_yToy") @@index([fk_x_1, fk_x_2], name: "fk_x_1") }} - "##, action = action} + "##} } _ => { formatdoc! { r##" model x {{ id Int @id @default(autoincrement()) y Int - y_x_yToy y @relation("x_yToy", fields: [y], references: [id], onDelete: NoAction, onUpdate: NoAction) + y_x_yToy y @relation("x_yToy", fields: [y], references: [id]) y_xToy_fk_x_1_fk_x_2 y[] @relation("xToy_fk_x_1_fk_x_2") @@unique([id, y], name: "unique_y_id") }} @@ -907,10 +836,10 @@ async fn relations_should_avoid_name_clashes_2(api: &TestApi) -> TestResult { 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], onDelete: {action}, onUpdate: {action}) + 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_x_yToy x[] @relation("x_yToy") }} - "##, action = action} + "##} } }; @@ -961,17 +890,12 @@ 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], onDelete: {action}, onUpdate: {action}) + User_EventToUser User @relation(fields: [host_id], references: [id]) User_EventToUserManyToMany User[] @relation("EventToUserManyToMany") {extra_index} }} @@ -982,7 +906,6 @@ async fn one_to_many_relation_field_names_do_not_conflict_with_many_to_many_rela Event_EventToUserManyToMany Event[] @relation("EventToUserManyToMany") }} "#, - action = action, extra_index = extra_index, ); 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 f100b5852181..d4c4dd2588d0 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,8 +1,6 @@ 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] @@ -34,18 +32,13 @@ 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], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_id, user_age], references: [id, age]) @@unique([user_id, user_age], name: "{constraint_name}") }} @@ -59,7 +52,6 @@ async fn compound_foreign_keys_for_one_to_one_relations(api: &TestApi) -> TestRe }} "#, constraint_name = constraint_name, - action = action, ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -96,18 +88,13 @@ 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], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id, user_age], references: [id, age]) @@unique([user_id, user_age], name: "{constraint_name}") }} @@ -121,7 +108,6 @@ async fn compound_foreign_keys_for_required_one_to_one_relations(api: &TestApi) }} "#, constraint_name = constraint_name, - action = action ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -156,18 +142,13 @@ 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], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_id, user_age], references: [id, age]) {extra_index} }} @@ -179,7 +160,6 @@ async fn compound_foreign_keys_for_one_to_many_relations(api: &TestApi) -> TestR @@unique([id, age], name: "user_unique") }} "#, - action = action, extra_index = extra_index, ); @@ -215,18 +195,13 @@ 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], onDelete: {action}, onUpdate: {action}) + User User? @relation(fields: [user_id, user_age], references: [id, age]) {extra_index} }} @@ -238,7 +213,6 @@ async fn compound_foreign_keys_for_one_to_many_relations_with_mixed_requiredness @@unique([id, age], name: "user_unique") }} "#, - action = action, extra_index = extra_index, ); @@ -274,18 +248,13 @@ 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], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id, user_age], references: [id, age]) {extra_index} }} @@ -297,7 +266,6 @@ async fn compound_foreign_keys_for_required_one_to_many_relations(api: &TestApi) @@unique([id, age], name: "user_unique") }} "#, - action = action, extra_index = extra_index ); @@ -334,11 +302,6 @@ 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 {{ @@ -346,14 +309,13 @@ 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], onDelete: {action}, onUpdate: {action}) + Person Person @relation("PersonToPerson_partner_id_partner_age", fields: [partner_id, partner_age], references: [id, age]) other_Person Person[] @relation("PersonToPerson_partner_id_partner_age") @@unique([id, age], name: "{constraint_name}") {extra_index} }} "#, - action = action, constraint_name = constraint_name, extra_index = extra_index, ); @@ -391,11 +353,6 @@ 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 {{ @@ -403,14 +360,13 @@ 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], onDelete: {action}, onUpdate: {action}) + Person Person? @relation("PersonToPerson_partner_id_partner_age", fields: [partner_id, partner_age], references: [id, age]) other_Person Person[] @relation("PersonToPerson_partner_id_partner_age") @@unique([id, age], name: "{constraint_name}") {extra_index} }} "#, - action = action, constraint_name = constraint_name, extra_index = extra_index ); @@ -448,11 +404,6 @@ 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 {{ @@ -460,14 +411,13 @@ 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], onDelete: {action}, onUpdate: {action}) + Person Person @relation("PersonToPerson_partner_id_partner_age", fields: [partner_id, partner_age], references: [id, age]) other_Person Person[] @relation("PersonToPerson_partner_id_partner_age") @@unique([id, age], name: "{constraint_name}") {extra_index} }} "#, - action = action, constraint_name = constraint_name, extra_index = extra_index ); @@ -510,18 +460,13 @@ 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], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id, user_age], references: [id, age]) {extra_index} }} @@ -533,7 +478,6 @@ async fn compound_foreign_keys_for_one_to_many_relations_with_non_unique_index(a @@unique([id, age], name: "{constraint_name}") }} "#, - action = action, extra_index = extra_index, constraint_name = constraint_name ); @@ -568,11 +512,6 @@ 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 {{ @@ -588,11 +527,10 @@ async fn repro_matt_references_on_wrong_side(api: &TestApi) -> TestResult { one Int two Int - a a @relation(fields: [one, two], references: [one, two], onDelete: {action}, onUpdate: {action}) + a a @relation(fields: [one, two], references: [one, two]) {extra_index} }} "#, - action = action, extra_index = extra_index, ); @@ -628,11 +566,6 @@ 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 {{ @@ -647,13 +580,12 @@ 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], onDelete: {action}, onUpdate: {action}) + a a @relation(fields: [one, two], references: [one, two]) @@id([dummy, one, two]) {extra_index} }} "#, - action = action, extra_index = extra_index, ); @@ -701,11 +633,6 @@ 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 {{ @@ -714,8 +641,8 @@ 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], 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}) + 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]) {extra_index} }} @@ -728,7 +655,6 @@ async fn compound_foreign_keys_for_duplicate_one_to_many_relations(api: &TestApi @@unique([id, age], name: "{constraint_name}") }} "#, - action = action, extra_index = extra_index, constraint_name = constraint_name ); 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 b85d1db5445b..2d26cd29bf27 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,9 +1,8 @@ use barrel::types; -use datamodel::ReferentialAction; use indoc::formatdoc; use indoc::indoc; use introspection_engine_tests::test_api::*; -use quaint::prelude::{Queryable, SqlFamily}; +use quaint::prelude::Queryable; use test_macros::test_connector; #[test_connector(tags(Postgres))] @@ -128,17 +127,12 @@ async fn remapping_models_in_relations(api: &TestApi) -> TestResult { }) .await?; - 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_with_Space User_with_Space @relation(fields: [user_id], references: [id], onDelete: {action}, onUpdate: {action}) + User_with_Space User_with_Space @relation(fields: [user_id], references: [id]) }} model User_with_Space {{ @@ -148,7 +142,6 @@ async fn remapping_models_in_relations(api: &TestApi) -> TestResult { @@map("User with Space") }} "#, - action = action ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -174,17 +167,12 @@ async fn remapping_models_in_relations_should_not_map_virtual_fields(api: &TestA }) .await?; - 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], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id], references: [id]) @@map("Post With Space") }} @@ -194,7 +182,6 @@ async fn remapping_models_in_relations_should_not_map_virtual_fields(api: &TestA Post_With_Space Post_With_Space? }} "#, - action = action ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -240,18 +227,13 @@ 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], onDelete: {action}, onUpdate: {action}) + User_with_Space User_with_Space @relation(fields: [user_id, user_age], references: [id, age]) @@unique([user_id, user_age], name: "{post_constraint}") }} @@ -267,7 +249,6 @@ async fn remapping_models_in_compound_relations(api: &TestApi) -> TestResult { "#, post_constraint = post_constraint, user_constraint = user_constraint, - action = action ); api.assert_eq_datamodels(&dm, &api.introspect().await?); @@ -316,18 +297,13 @@ 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], onDelete: {action}, onUpdate: {action}) + User User @relation(fields: [user_id, user_age], references: [id, age_that_is_invalid]) @@unique([user_id, user_age], name: "{user_post_constraint}") }} @@ -342,7 +318,6 @@ async fn remapping_fields_in_compound_relations(api: &TestApi) -> TestResult { "#, 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/dml/src/relation_info.rs b/libs/datamodel/connectors/dml/src/relation_info.rs index a1894ca08013..913156e95c64 100644 --- a/libs/datamodel/connectors/dml/src/relation_info.rs +++ b/libs/datamodel/connectors/dml/src/relation_info.rs @@ -18,6 +18,8 @@ pub struct RelationInfo { /// A strategy indicating what happens when /// a related node is deleted. pub on_update: Option, + /// Set true if referential actions feature is not in use. + pub legacy_referential_actions: bool, } impl PartialEq for RelationInfo { @@ -38,8 +40,15 @@ impl RelationInfo { name: String::new(), on_delete: None, on_update: None, + legacy_referential_actions: false, } } + + /// Set referential action legacy mode, skipping the validation errors on + /// automatically set actions. + pub fn legacy_referential_actions(&mut self) { + self.legacy_referential_actions = true; + } } /// Describes what happens when related nodes are deleted. 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 ea2f6929f155..2998fe280581 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 @@ -212,6 +212,7 @@ impl StandardiserForFormatting { name: rel_info.name.clone(), on_delete: None, on_update: None, + legacy_referential_actions: false, }; let mut opposite_relation_field = dml::RelationField::new_generated(&model.name, relation_info, false); @@ -290,6 +291,7 @@ impl StandardiserForFormatting { name: rel_info.name.clone(), on_delete: None, on_update: None, + legacy_referential_actions: false, }; let is_required = all_existing_underlying_fields_on_opposite_model_are_required diff --git a/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs b/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs index 75ec403a2e75..767a5691cc9a 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs @@ -39,13 +39,21 @@ impl<'a> StandardiserForParsing<'a> { for field in model.fields_mut() { match field { Field::RelationField(field) if field.is_singular() => { + if field.relation_info.on_delete.is_some() || field.relation_info.on_update.is_some() { + continue; + } + field.relation_info.on_update = Some(ReferentialAction::Cascade); field.relation_info.on_delete = Some({ match field.arity { FieldArity::Required => ReferentialAction::Cascade, _ => ReferentialAction::SetNull, } - }) + }); + // So our validator won't get a stroke when seeing the + // values set without having the preview feature + // enabled. Remove this before GA. + field.relation_info.legacy_referential_actions(); } _ => (), } 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 3e40a73d1ff7..801df4741dad 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/validate.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/validate.rs @@ -910,9 +910,10 @@ impl<'a> Validator<'a> { if !self.preview_features.contains(&PreviewFeature::ReferentialActions) && (rel_info.on_delete.is_some() || rel_info.on_update.is_some()) + && !rel_info.legacy_referential_actions { let message = &format!( - "The relation field `{}` on Model `{}` must not specify the `onDelete` or `onUpdate` argument in the {} attribute without enabling the referentialActions preview feature.", + "The relation field `{}` on Model `{}` must not specify the `onDelete` or `onUpdate` argument in the {} attribute without enabling the `referentialActions` preview feature.", &field.name, &model.name, RELATION_ATTRIBUTE_NAME_WITH_AT ); diff --git a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs index b511ecc1908b..110b9549f3e6 100644 --- a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs +++ b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs @@ -9,6 +9,11 @@ fn on_delete_actions() { for action in actions { let dml = formatdoc!( r#" + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + model A {{ id Int @id bs B[] @@ -37,6 +42,11 @@ fn on_update_actions() { for action in actions { let dml = formatdoc!( r#" + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + model A {{ id Int @id bs B[] @@ -70,6 +80,11 @@ fn actions_on_mongo() { url = "mongodb://" }} + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + model A {{ id Int @id @map("_id") bs B[] @@ -107,7 +122,7 @@ fn actions_on_planetscale() { generator client {{ provider = "prisma-client-js" - previewFeatures = ["planetScaleMode"] + previewFeatures = ["planetScaleMode", "referentialActions"] }} model A {{ @@ -135,6 +150,11 @@ fn actions_on_planetscale() { #[test] fn invalid_on_delete_action() { let dml = indoc! { r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id bs B[] @@ -150,13 +170,18 @@ fn invalid_on_delete_action() { parse_error(dml).assert_is(DatamodelError::new_attribute_validation_error( "Invalid referential action: `MeowMeow`", "relation", - Span::new(137, 145), + Span::new(238, 246), )); } #[test] fn invalid_on_update_action() { let dml = indoc! { r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id bs B[] @@ -172,7 +197,7 @@ fn invalid_on_update_action() { parse_error(dml).assert_is(DatamodelError::new_attribute_validation_error( "Invalid referential action: `MeowMeow`", "relation", - Span::new(137, 145), + Span::new(238, 246), )); } @@ -184,6 +209,11 @@ fn restrict_should_not_work_on_sql_server() { url = "sqlserver://" } + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id bs B[] @@ -200,14 +230,14 @@ fn restrict_should_not_work_on_sql_server() { "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)), + DatamodelError::new_attribute_validation_error(&message, "relation", Span::new(252, 339)), + DatamodelError::new_attribute_validation_error(&message, "relation", Span::new(252, 339)), ]); } #[test] -fn concrete_actions_should_not_work_on_mongo() { - let actions = &[(Cascade, 237), (NoAction, 238), (SetDefault, 240)]; +fn non_emulated_actions_should_not_work_on_mongo() { + let actions = &[(Cascade, 338), (SetDefault, 341)]; for (action, span) in actions { let dml = formatdoc!( @@ -217,6 +247,11 @@ fn concrete_actions_should_not_work_on_mongo() { url = "mongodb://" }} + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + model A {{ id Int @id @map("_id") bs B[] @@ -232,21 +267,21 @@ fn concrete_actions_should_not_work_on_mongo() { ); let message = format!( - "Invalid referential action: `{}`. Allowed values: (`Restrict`, `SetNull`)", + "Invalid referential action: `{}`. Allowed values: (`Restrict`, `NoAction`, `SetNull`)", action ); parse_error(&dml).assert_are(&[DatamodelError::new_attribute_validation_error( &message, "relation", - Span::new(171, *span), + Span::new(272, *span), )]); } } #[test] fn concrete_actions_should_not_work_on_planetscale() { - let actions = &[(Cascade, 389), (NoAction, 390), (SetDefault, 392)]; + let actions = &[(Cascade, 411), (NoAction, 412), (SetDefault, 414)]; for (action, span) in actions { let dml = formatdoc!( @@ -259,7 +294,7 @@ fn concrete_actions_should_not_work_on_planetscale() { generator client {{ provider = "prisma-client-js" - previewFeatures = ["planetScaleMode"] + previewFeatures = ["planetScaleMode", "referentialActions"] }} model A {{ @@ -284,7 +319,7 @@ fn concrete_actions_should_not_work_on_planetscale() { parse_error(&dml).assert_are(&[DatamodelError::new_attribute_validation_error( &message, "relation", - Span::new(323, *span), + Span::new(345, *span), )]); } } @@ -297,6 +332,11 @@ fn on_delete_cannot_be_defined_on_the_wrong_side() { url = "mysql://" } + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id bs B[] @relation(onDelete: Restrict) @@ -315,7 +355,7 @@ fn on_delete_cannot_be_defined_on_the_wrong_side() { parse_error(dml).assert_are(&[DatamodelError::new_attribute_validation_error( &message, "relation", - Span::new(92, 129), + Span::new(193, 230), )]); } @@ -327,6 +367,11 @@ fn on_update_cannot_be_defined_on_the_wrong_side() { url = "mysql://" } + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id bs B[] @relation(onUpdate: Restrict) @@ -345,6 +390,30 @@ fn on_update_cannot_be_defined_on_the_wrong_side() { parse_error(dml).assert_are(&[DatamodelError::new_attribute_validation_error( &message, "relation", - Span::new(92, 129), + Span::new(193, 230), + )]); +} + +#[test] +fn referential_actions_without_preview_feature_should_error() { + 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: Restrict) + } + "#}; + + let message = "The relation field `a` on Model `B` must not specify the `onDelete` or `onUpdate` argument in the @relation attribute without enabling the `referentialActions` preview feature."; + + parse_error(dml).assert_are(&[DatamodelError::new_attribute_validation_error( + &message, + "relation", + Span::new(80, 147), )]); } From adcc20b87f041368bedaf2294f9f2082c3291114 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Fri, 18 Jun 2021 12:11:08 +0300 Subject: [PATCH 30/47] WIP: Fixing ME tests TODO: a way to pass RA preview flag correctly (everywhere) --- libs/test-macros/src/lib.rs | 20 +------ libs/test-setup/src/test_api_args.rs | 8 +-- .../migration-engine-tests/src/assertions.rs | 3 + .../src/multi_engine_test_api.rs | 11 +--- .../migration-engine-tests/src/test_api.rs | 11 +--- .../tests/migrations/relations.rs | 57 ++++++++++++++++++- 6 files changed, 66 insertions(+), 44 deletions(-) diff --git a/libs/test-macros/src/lib.rs b/libs/test-macros/src/lib.rs index 8cad9af30cc5..8a609bb276e1 100644 --- a/libs/test-macros/src/lib.rs +++ b/libs/test-macros/src/lib.rs @@ -54,7 +54,6 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { let include_tagged = &attrs.include_tagged; let exclude_tagged = &attrs.exclude_tagged; let capabilities = &attrs.capabilities; - let preview_features = &attrs.preview_features; let test_function_name = &sig.ident; let test_function_name_lit = sig.ident.to_string(); @@ -74,8 +73,7 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { #[test] #ignore_attr fn #test_function_name() { - let preview_features = &[#(#preview_features,)*]; - let args = test_setup::TestApiArgs::new(#test_function_name_lit, preview_features); + let args = test_setup::TestApiArgs::new(#test_function_name_lit); if test_setup::should_skip_test( &args, @@ -97,8 +95,7 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { #[test] #ignore_attr fn #test_function_name() { - let preview_features = &[#(#preview_features,)*]; - let args = test_setup::TestApiArgs::new(#test_function_name_lit, preview_features); + let args = test_setup::TestApiArgs::new(#test_function_name_lit); if test_setup::should_skip_test( &args, @@ -122,7 +119,6 @@ struct TestConnectorAttrs { include_tagged: Vec, exclude_tagged: Vec, capabilities: Vec, - preview_features: Vec, ignore_reason: Option, } @@ -132,18 +128,6 @@ impl TestConnectorAttrs { p if p.is_ident("tags") => &mut self.include_tagged, p if p.is_ident("exclude") => &mut self.exclude_tagged, p if p.is_ident("capabilities") => &mut self.capabilities, - p if p.is_ident("preview_features") => { - self.preview_features.reserve(list.nested.len()); - - for item in list.nested { - match item { - NestedMeta::Lit(Lit::Str(s)) => self.preview_features.push(s), - other => return Err(syn::Error::new_spanned(other, "Unexpected argument")), - } - } - - return Ok(()); - } p if p.is_ident("logs") => return Ok(()), // TODO other => return Err(syn::Error::new_spanned(other, "Unexpected argument")), }; diff --git a/libs/test-setup/src/test_api_args.rs b/libs/test-setup/src/test_api_args.rs index a99ccb208825..59d4d4c4f7fb 100644 --- a/libs/test-setup/src/test_api_args.rs +++ b/libs/test-setup/src/test_api_args.rs @@ -102,23 +102,17 @@ pub(crate) fn db_under_test() -> &'static DbUnderTest { #[derive(Debug)] pub struct TestApiArgs { test_function_name: &'static str, - preview_features: &'static [&'static str], db: &'static DbUnderTest, } impl TestApiArgs { - pub fn new(test_function_name: &'static str, preview_features: &'static [&'static str]) -> Self { + pub fn new(test_function_name: &'static str) -> Self { TestApiArgs { test_function_name, - preview_features, db: db_under_test(), } } - pub fn preview_features(&self) -> &'static [&'static str] { - &self.preview_features - } - pub fn test_function_name(&self) -> &'static str { self.test_function_name } diff --git a/migration-engine/migration-engine-tests/src/assertions.rs b/migration-engine/migration-engine-tests/src/assertions.rs index e76c4de29a73..9a67ec9d68c8 100644 --- a/migration-engine/migration-engine-tests/src/assertions.rs +++ b/migration-engine/migration-engine-tests/src/assertions.rs @@ -581,6 +581,7 @@ impl<'a> ForeignKeyAssertion<'a> { Self { fk, tags } } + #[track_caller] pub fn assert_references(self, table: &str, columns: &[&str]) -> Self { assert!( self.is_same_table_name(&self.fk.referenced_table, table) && self.fk.referenced_columns == columns, @@ -594,6 +595,7 @@ impl<'a> ForeignKeyAssertion<'a> { self } + #[track_caller] pub fn assert_referential_action_on_delete(self, action: ForeignKeyAction) -> Self { assert!( self.fk.on_delete_action == action, @@ -605,6 +607,7 @@ impl<'a> ForeignKeyAssertion<'a> { self } + #[track_caller] pub fn assert_referential_action_on_update(self, action: ForeignKeyAction) -> Self { assert!( self.fk.on_update_action == action, diff --git a/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs b/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs index c1545d55f305..49d4393140d3 100644 --- a/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs +++ b/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs @@ -3,7 +3,6 @@ //! A TestApi that is initialized without IO or async code and can instantiate //! multiple migration engines. -use datamodel::common::preview_features::PreviewFeature; pub use test_macros::test_connector; pub use test_setup::{BitFlags, Capabilities, Tags}; @@ -36,16 +35,10 @@ impl TestApi { let (_, q, cs) = rt.block_on(args.create_postgres_database()); (q, cs) } else if tags.contains(Tags::Vitess) { - let preview_features = args - .preview_features() - .into_iter() - .flat_map(|s| PreviewFeature::parse_opt(s)) - .collect(); - let conn = rt .block_on(SqlMigrationConnector::new( args.database_url(), - preview_features, + BitFlags::all(), args.shadow_database_url().map(String::from), )) .unwrap(); @@ -172,7 +165,7 @@ impl TestApi { .rt .block_on(SqlMigrationConnector::new( &connection_string, - BitFlags::empty(), + BitFlags::all(), shadow_db_connection_string, )) .unwrap(); diff --git a/migration-engine/migration-engine-tests/src/test_api.rs b/migration-engine/migration-engine-tests/src/test_api.rs index 9324f9a4dc24..b3337c8d8f0d 100644 --- a/migration-engine/migration-engine-tests/src/test_api.rs +++ b/migration-engine/migration-engine-tests/src/test_api.rs @@ -11,7 +11,6 @@ mod schema_push; pub use apply_migrations::ApplyMigrations; pub use create_migration::CreateMigration; -use datamodel::common::preview_features::PreviewFeature; pub use dev_diagnostic::DevDiagnostic; pub use diagnose_migration_history::DiagnoseMigrationHistory; pub use evaluate_data_loss::EvaluateDataLoss; @@ -47,15 +46,9 @@ impl TestApi { let shadow_database_url = args.shadow_database_url().map(String::from); - let preview_features = args - .preview_features() - .into_iter() - .flat_map(|feat| PreviewFeature::parse_opt(feat)) - .collect(); - let connection_string = if tags.contains(Tags::Mysql | Tags::Vitess) { let connector = - SqlMigrationConnector::new(args.database_url(), preview_features, shadow_database_url.clone()) + SqlMigrationConnector::new(args.database_url(), BitFlags::all(), shadow_database_url.clone()) .await .unwrap(); @@ -76,7 +69,7 @@ impl TestApi { unreachable!() }; - let api = SqlMigrationConnector::new(&connection_string, preview_features, shadow_database_url) + let api = SqlMigrationConnector::new(&connection_string, BitFlags::all(), shadow_database_url) .await .unwrap(); diff --git a/migration-engine/migration-engine-tests/tests/migrations/relations.rs b/migration-engine/migration-engine-tests/tests/migrations/relations.rs index c74fea30ac90..f4618aca2ab2 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/relations.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/relations.rs @@ -65,6 +65,11 @@ fn adding_a_many_to_many_relation_with_custom_name_must_work(api: TestApi) { #[test_connector] fn adding_an_inline_relation_must_result_in_a_foreign_key_in_the_model_table(api: TestApi) { let dm1 = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id bid Int @@ -124,6 +129,11 @@ fn specifying_a_db_name_for_an_inline_relation_must_work(api: TestApi) { #[test_connector] fn adding_an_inline_relation_to_a_model_with_an_exotic_id_type(api: TestApi) { let dm1 = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id b_id String @@ -191,6 +201,11 @@ fn removing_an_inline_relation_must_work(api: TestApi) { #[test_connector] fn compound_foreign_keys_should_work_in_correct_order(api: TestApi) { let dm1 = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id b Int @@ -223,6 +238,11 @@ fn compound_foreign_keys_should_work_in_correct_order(api: TestApi) { #[test_connector] fn moving_an_inline_relation_to_the_other_side_must_work(api: TestApi) { let dm1 = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id b_id Int @@ -245,6 +265,11 @@ fn moving_an_inline_relation_to_the_other_side_must_work(api: TestApi) { }); let dm2 = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id b B[] @@ -460,6 +485,11 @@ fn on_delete_referential_actions_should_work(api: TestApi) { for (ra, fka) in actions { let dm = format!( r#" + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + model A {{ id Int @id @default(autoincrement()) b B[] @@ -492,6 +522,11 @@ fn on_delete_referential_actions_should_work(api: TestApi) { #[test_connector(exclude(Mysql56, Mysql57, Mariadb, Mssql))] fn on_delete_set_default_should_work(api: TestApi) { let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id b B[] @@ -517,6 +552,11 @@ fn on_delete_set_default_should_work(api: TestApi) { #[test_connector(exclude(Mssql))] fn on_delete_restrict_should_work(api: TestApi) { let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id b B[] @@ -542,14 +582,19 @@ fn on_delete_restrict_should_work(api: TestApi) { #[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), + (ReferentialAction::Cascade, ForeignKeyAction::Cascade), ]; for (ra, fka) in actions { let dm = format!( r#" + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + model A {{ id Int @id @default(autoincrement()) b B[] @@ -580,6 +625,11 @@ fn on_update_referential_actions_should_work(api: TestApi) { #[test_connector(exclude(Mysql56, Mysql57, Mariadb, Mssql))] fn on_update_set_default_should_work(api: TestApi) { let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id b B[] @@ -605,6 +655,11 @@ fn on_update_set_default_should_work(api: TestApi) { #[test_connector(exclude(Mssql))] fn on_update_restrict_should_work(api: TestApi) { let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + model A { id Int @id b B[] From 2d8e33ffa0b1db930125b2aa5625a9457eff8a7e Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Fri, 18 Jun 2021 16:14:27 +0200 Subject: [PATCH 31/47] onUpdate tests --- .../new/ref_actions/on_delete/set_default.rs | 22 +- .../tests/new/ref_actions/on_update/mod.rs | 4 + .../new/ref_actions/on_update/no_action.rs | 350 +++++++++++++++ .../new/ref_actions/on_update/restrict.rs | 270 +++++++++++ .../new/ref_actions/on_update/set_default.rs | 419 ++++++++++++++++++ .../new/ref_actions/on_update/set_null.rs | 182 ++++++++ 6 files changed, 1241 insertions(+), 6 deletions(-) create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/no_action.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/restrict.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_default.rs create mode 100644 query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_null.rs diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs index 22c3efc474c3..f5932ce10416 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_delete/set_default.rs @@ -192,9 +192,14 @@ mod one2one_opt { @r###"{"data":{"createOneParent":{"id":1}}}"### ); + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + insta::assert_snapshot!( run_query!(runner, r#"query { findManyChild(where: { id: 1 }) { id parent_id }}"#), - @r###"{"data":{"findManyChild":[{"id":1,"parent_id":1}]}}"### + @r###"{"data":{"findManyChild":[{"id":1,"parent_id":null}]}}"### ); Ok(()) @@ -313,8 +318,8 @@ mod one2many_opt { model Child { #id(id, Int, @id) - parent_id Int @default(2) - parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + parent_id Int? @default(2) + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: SetDefault) }"# }; @@ -330,8 +335,8 @@ mod one2many_opt { model Child { #id(id, Int, @id) - parent_id Int - parent Parent @relation(fields: [parent_id], references: [id], onDelete: SetDefault) + parent_id Int? + parent Parent? @relation(fields: [parent_id], references: [id], onDelete: SetDefault) }"# }; @@ -391,9 +396,14 @@ mod one2many_opt { @r###"{"data":{"createOneParent":{"id":1}}}"### ); + insta::assert_snapshot!( + run_query!(runner, r#"mutation { deleteOneParent(where: { id: 1 }) { id }}"#), + @r###"{"data":{"deleteOneParent":{"id":1}}}"### + ); + insta::assert_snapshot!( run_query!(runner, r#"query { findManyChild(where: { id: 1 }) { id parent_id }}"#), - @r###"{"data":{"findManyChild":[{"id":1,"parent_id":1}]}}"### + @r###"{"data":{"findManyChild":[{"id":1,"parent_id":null}]}}"### ); Ok(()) diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs index 2651e9c38133..1b8d08fa7328 100644 --- a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/mod.rs @@ -1 +1,5 @@ mod cascade; +mod no_action; +mod restrict; +mod set_default; +mod set_null; diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/no_action.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/no_action.rs new file mode 100644 index 000000000000..a04234423173 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/no_action.rs @@ -0,0 +1,350 @@ +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(suite = "noaction_onU_1to1_req", schema(required))] +mod one2one_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: NoAction) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected. + #[connector_test(exclude(MongoDb))] + async fn update_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + r#"mutation { updateManyParent(where: { id: 1 }, data: { uniq: "u1" }) { count }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Updating the parent leaves the data in a integrity-violating state. + /// All supported dbs except mongo throw key violations. + #[connector_test(only(MongoDb))] + async fn update_parent_violation(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { parent_uniq }}"#), + @r###"{"data":{"findManyChild":[{"parent_uniq":"1"}]}}"### + ); + + Ok(()) + } +} + +#[test_suite(suite = "noaction_onU_1to1_opt", schema(optional), exclude(MongoDb))] +mod one2one_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: NoAction) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected. + #[connector_test] + async fn update_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + r#"mutation { updateManyParent(where: { id: 1 }, data: { uniq: "u1" }) { count }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Updating the parent succeeds if no child is connected. + #[connector_test] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateManyParent(where: { id: 1 }, data: { uniq: "u1" }) { count }}"#), + @r###"{"data":{"updateManyParent":{"count":1}}}"### + ); + + Ok(()) + } + + /// Updating the parent leaves the data in a integrity-violating state. + /// All supported dbs except mongo throw key violations. + #[connector_test(only(MongoDb))] + async fn update_parent_violation(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { parent_uniq }}"#), + @r###"{"data":{"findManyChild":[{"parent_uniq":"1"}]}}"### + ); + + Ok(()) + } +} + +#[test_suite(suite = "noaction_onU_1toM_req", schema(required), exclude(MongoDb))] +mod one2many_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: NoAction) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected. + #[connector_test] + async fn update_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + r#"mutation { updateManyParent(where: { id: 1 }, data: { uniq: "u1" }) { count }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Updating the parent succeeds if no child is connected. + #[connector_test] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateManyParent(where: { id: 2 }, data: { uniq: "u2" }) { count }}"#), + @r###"{"data":{"updateManyParent":{"count":1}}}"### + ); + + Ok(()) + } + + /// Updating the parent leaves the data in a integrity-violating state. + #[connector_test(only(MongoDb))] + async fn update_parent_violation(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { parent_uniq }}"#), + @r###"{"data":{"findManyChild":[{"parent_uniq":"1"}]}}"### + ); + + Ok(()) + } +} + +#[test_suite(suite = "noaction_onU_1toM_opt", schema(optional), exclude(MongoDb))] +mod one2many_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: NoAction) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected. + #[connector_test] + async fn update_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + assert_error!( + runner, + r#"mutation { updateManyParent(where: { id: 1 }, data: { uniq: "u1" }) { count }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Updating the parent succeeds if no child is connected. + #[connector_test] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateManyParent(where: { id: 2 }, data: { uniq: "u2" }) { count }}"#), + @r###"{"data":{"updateManyParent":{"count":1}}}"### + ); + + Ok(()) + } + + /// Updating the parent leaves the data in a integrity-violating state. + #[connector_test(only(MongoDb))] + async fn update_parent_violation(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { parent_uniq }}"#), + @r###"{"data":{"findManyChild":[{"parent_uniq":"1"}]}}"### + ); + + Ok(()) + } +} diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/restrict.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/restrict.rs new file mode 100644 index 000000000000..f6fe4215ff6a --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/restrict.rs @@ -0,0 +1,270 @@ +//! SQL Server doesn't support Restrict. + +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(suite = "restrict_onU_1to1_req", schema(required), exclude(SqlServer, MongoDb))] +mod one2one_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: Restrict) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected. + #[connector_test] + async fn update_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, , uniq: ""uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + match runner.connector() { + // ConnectorTag::MongoDb(_) => assert_error!( + // runner, + // r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + // 2014, + // "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + // ), + _ => assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ), + }; + + Ok(()) + } +} + +#[test_suite(suite = "restrict_onU_1to1_opt", schema(optional), exclude(SqlServer, MongoDb))] +mod one2one_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: Restrict) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected. + #[connector_test] + async fn update_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + match runner.connector() { + // ConnectorTag::MongoDb(_) => assert_error!( + // runner, + // r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + // 2014, + // "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + // ), + _ => assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ), + }; + + Ok(()) + } + + /// Updating the parent succeeds if no child is connected. + #[connector_test] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + r#run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateManyParent(where: { id: 2 }, data: { uniq: "u2" }) { count }}"#), + @r###"{"data":{"updateManyParent":{"count":1}}}"### + ); + + Ok(()) + } +} + +#[test_suite(suite = "restrict_onU_1toM_req", schema(required), exclude(SqlServer, MongoDb))] +mod one2many_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: Restrict) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected. + #[connector_test] + async fn update_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + match runner.connector() { + // ConnectorTag::MongoDb(_) => assert_error!( + // runner, + // r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + // 2014, + // "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + // ), + _ => assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ), + }; + + Ok(()) + } + + /// Updating the parent succeeds if no child is connected. + #[connector_test] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + r#run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateManyParent(where: { id: 2 }, data: { uniq: "u2" }) { count }}"#), + @r###"{"data":{"updateManyParent":{"count":1}}}"### + ); + + Ok(()) + } +} + +#[test_suite(suite = "restrict_onU_1toM_opt", schema(optional), exclude(SqlServer, MongoDb))] +mod one2many_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: Restrict) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected. + #[connector_test] + async fn update_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, , uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + match runner.connector() { + // ConnectorTag::MongoDb(_) => assert_error!( + // runner, + // r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + // 2014, + // "The change you are trying to make would violate the required relation 'ChildToParent' between the `Child` and `Parent` models." + // ), + _ => assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ), + }; + + Ok(()) + } + + /// Updating the parent succeeds if no child is connected. + #[connector_test] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + r#run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateManyParent(where: { id: 2 }, data: { uniq: "u2" }) { count }}"#), + @r###"{"data":{"updateManyParent":{"count":1}}}"### + ); + + Ok(()) + } +} diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_default.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_default.rs new file mode 100644 index 000000000000..ce39539eca16 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_default.rs @@ -0,0 +1,419 @@ +//! MySQL doesn't support SetDefault for InnoDB (which is our only supported engine at the moment). +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(suite = "setdefault_onU_1to1_req", exclude(MongoDb, MySQL))] +mod one2one_req { + fn required_with_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String @default("2") + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetDefault) + }"# + }; + + schema.to_owned() + } + + fn required_without_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetDefault) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent reconnects the child to the default. + #[connector_test(schema(required_with_default))] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // The default + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent { id } }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent":{"id":2}}]}}"### + ); + + Ok(()) + } + + /// Updating the parent reconnects the child to the default and fails (the default doesn't exist). + #[connector_test(schema(required_with_default))] + async fn update_parent_no_exist_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Updating the parent with no default for SetDefault fails. + /// Only postgres allows setting no default for a SetDefault FK. + #[connector_test(schema(required_without_default), only(Postgres))] + async fn update_parent_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2011, + "Null constraint violation on the fields" + ); + + Ok(()) + } +} + +#[test_suite(suite = "setdefault_onU_1to1_opt", exclude(MongoDb, MySQL))] +mod one2one_opt { + fn optional_with_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String? @default("2") + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetDefault) + }"# + }; + + schema.to_owned() + } + + fn optional_without_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetDefault) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent reconnects the child to the default. + #[connector_test(schema(optional_with_default))] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // The default + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent { id } }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent":{"id":2}}]}}"### + ); + + Ok(()) + } + + /// Updating the parent reconnects the child to the default and fails (the default doesn't exist). + #[connector_test(schema(optional_with_default))] + async fn update_parent_no_exist_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 } data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Updating the parent with no default for SetDefault nulls the FK. + #[connector_test(schema(optional_without_default), only(Postgres))] + async fn update_parent_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild(where: { id: 1 }) { id parent_uniq }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent_uniq":null}]}}"### + ); + + Ok(()) + } +} + +#[test_suite(suite = "setdefault_onU_1toM_req", exclude(MongoDb, MySQL))] +mod one2many_req { + fn required_with_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String @default("2") + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetDefault) + }"# + }; + + schema.to_owned() + } + + fn required_without_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetDefault) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent reconnects the children to the default. + #[connector_test(schema(required_with_default))] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // The default + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent { id } }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent":{"id":2}}]}}"### + ); + + Ok(()) + } + + /// Updating the parent reconnects the child to the default and fails (the default doesn't exist). + #[connector_test(schema(required_with_default))] + async fn update_parent_no_exist_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Updating the parent with no default for SetDefault fails. + /// Only postgres allows setting no default for a SetDefault FK. + #[connector_test(schema(required_without_default), only(Postgres))] + async fn update_parent_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2011, + "Null constraint violation on the fields" + ); + + Ok(()) + } +} + +#[test_suite(suite = "setdefault_onU_1toM_opt", exclude(MongoDb, MySQL))] +mod one2many_opt { + fn optional_with_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String? @default("2") + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetDefault) + }"# + }; + + schema.to_owned() + } + + fn optional_without_default() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetDefault) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent reconnects the child to the default. + #[connector_test(schema(optional_with_default))] + async fn update_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // The default + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 2, uniq: "2" }) { id }}"#), + @r###"{"data":{"createOneParent":{"id":2}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent { id } }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent":{"id":2}}]}}"### + ); + + Ok(()) + } + + /// Updating the parent reconnects the child to the default and fails (the default doesn't exist). + #[connector_test(schema(optional_with_default))] + async fn update_parent_no_exist_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2003, + "Foreign key constraint failed on the field" + ); + + Ok(()) + } + + /// Updating the parent with no default for SetDefault nulls the FK. + #[connector_test(schema(optional_without_default), only(Postgres))] + async fn update_parent_fail(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild(where: { id: 1 }) { id parent_uniq }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent_uniq":null}]}}"### + ); + + Ok(()) + } +} diff --git a/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_null.rs b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_null.rs new file mode 100644 index 000000000000..987641e5f8c9 --- /dev/null +++ b/query-engine/connector-test-kit-rs/query-engine-tests/tests/new/ref_actions/on_update/set_null.rs @@ -0,0 +1,182 @@ +//! Only Postgres allows SetNull on a non-nullable FK at all, rest fail during migration. + +use indoc::indoc; +use query_engine_tests::*; + +#[test_suite(suite = "setnull_onU_1to1_req", schema(required), only(Postgres))] +mod one2one_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetNull) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected (because of null key violation). + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // `onUpdate: SetNull` would cause `null` on `parent_uniq`, throwing an error. + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2011, + "Null constraint violation on the fields: (`parent_uniq`)" + ); + + assert_error!( + runner, + r#"mutation { updateManyParent(where: { id: 1 }, data: { uniq: "u1" }) { count }}"#, + 2011, + "Null constraint violation on the fields: (`parent_uniq`)" + ); + + Ok(()) + } +} + +#[test_suite(suite = "setnull_onU_1to1_opt", schema(optional), exclude(MongoDb))] +mod one2one_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String? @unique + child Child? + } + + model Child { + #id(id, Int, @id) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetNull) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent suceeds and sets the FK null. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", child: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent_uniq }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent_uniq":null}]}}"### + ); + + Ok(()) + } +} + +#[test_suite(suite = "setnull_onU_1toM_req", schema(required), only(Postgres))] +mod one2many_req { + fn required() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String + parent Parent @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetNull) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent must fail if a child is connected (because of null key violation). + #[connector_test] + async fn delete_parent_failure(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + // `onUpdate: SetNull` would cause `null` on `parent_uniq`, throwing an error. + assert_error!( + runner, + r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#, + 2011, + "Null constraint violation on the fields: (`parent_uniq`)" + ); + + assert_error!( + runner, + r#"mutation { updateManyParent(where: { id: 1 }, data: { uniq: "u1" }) { count }}"#, + 2011, + "Null constraint violation on the fields: (`parent_uniq`)" + ); + + Ok(()) + } +} + +#[test_suite(suite = "setnull_onU_1toM_opt", schema(optional), exclude(MongoDb))] +mod one2many_opt { + fn optional() -> String { + let schema = indoc! { + r#"model Parent { + #id(id, Int, @id) + uniq String? @unique + children Child[] + } + + model Child { + #id(id, Int, @id) + parent_uniq String? + parent Parent? @relation(fields: [parent_uniq], references: [uniq], onUpdate: SetNull) + }"# + }; + + schema.to_owned() + } + + /// Updating the parent suceeds and sets the FK null. + #[connector_test] + async fn delete_parent(runner: &Runner) -> TestResult<()> { + insta::assert_snapshot!( + run_query!(runner, r#"mutation { createOneParent(data: { id: 1, uniq: "1", children: { create: { id: 1 }}}) { id }}"#), + @r###"{"data":{"createOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"mutation { updateOneParent(where: { id: 1 }, data: { uniq: "u1" }) { id }}"#), + @r###"{"data":{"updateOneParent":{"id":1}}}"### + ); + + insta::assert_snapshot!( + run_query!(runner, r#"query { findManyChild { id parent_uniq }}"#), + @r###"{"data":{"findManyChild":[{"id":1,"parent_uniq":null}]}}"### + ); + + Ok(()) + } +} From 4b1bbffb5465968791f9800ca3abcab96fb309ff Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Sat, 19 Jun 2021 10:45:27 +0200 Subject: [PATCH 32/47] Setting up --- .../src/query_graph_builder/write/utils.rs | 377 +++++++++++++++++- 1 file changed, 362 insertions(+), 15 deletions(-) diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index 7e4968ab5695..4a400e0121e5 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -277,7 +277,41 @@ pub fn insert_existing_1to1_related_model_checks( /// Expects `parent_node` to return one or more IDs (for records of `model`) to be checked. /// /// The old behavior (pre-referential actions) is preserved for if the ReferentialActions feature flag is disabled, -/// which was basically only the `Restrict` part of +/// which was basically only the `Restrict` part of this code. +/// +/// Resulting graph (all emulations): +/// ```text +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// Parent │ +/// ┌ ─│ (ids to delete) ─────────────────┬─────────────────────────────┬────────────────────────────────────────┐ +/// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ │ +/// │ │ │ │ │ +/// ▼ ▼ ▼ ▼ +/// │ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ +/// │Find Connected Model│ │Find Connected Model│ │Find Connected Model│ │Find Connected Model│ +/// │ │ A (Restrict) │ │ B (Restrict) │ ┌──│ C (SetNull) │ ┌──│ D (Cascade) │ +/// └────────────────────┘ └────────────────────┘ │ └────────────────────┘ │ └────────────────────┘ +/// │ │ │ │ │ │ │ +/// Fail if│> 0 Fail if│> 0 │ ▼ │ │ +/// │ │ │ │┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ▼ +/// ▼ ▼ │ ┌────────────────────┐ │ │┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// │ ┌────────────────────┐ ┌────────────────────┐ ││ │ Insert onUpdate │ │ ┌────────────────────┐ │ +/// │ Empty │ │ Empty │ │ │ emulation subtree │ │ ││ │ Insert onDelete │ +/// │ └────────────────────┘ └────────────────────┘ ││ │for relations using │ │ │ emulation subtree │ │ +/// │ │ │ │the foreign key that│ │ ││ │ for all relations │ +/// │ │ │ ││ │ was updated. │ │ │ pointing to D. │ │ +/// │ │ │ └────────────────────┘ │ ││ └────────────────────┘ +/// │ │ │ │└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +/// │ │ │ │ │ │ +/// │ │ │ │ │ │ │ +/// ▼ │ │ ▼ │ ▼ +/// │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ ┌────────────────────┐ │ ┌────────────────────┐ +/// ─▶ Delete │◀────────────────┘ │ │ Update Cs (set FK │ └─▶│ Delete Cs │ +/// └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └─▶│ null) │ └────────────────────┘ +/// ▲ └────────────────────┘ │ +/// │ │ │ +/// └─────────────────────────────────────────────────────────┴────────────────────────────────────────┘ +/// ``` #[tracing::instrument(skip(graph, model_to_delete, parent_node, child_node))] pub fn insert_emulated_on_delete( graph: &mut QueryGraph, @@ -301,24 +335,18 @@ pub fn insert_emulated_on_delete( for rf in relation_fields { match rf.relation().on_delete() { // old behavior was to only insert restrict checks. - _ if !has_ra_feature => { - emulate_restrict(graph, &rf, connector_ctx, model_to_delete, parent_node, child_node)? - } + _ if !has_ra_feature => emulate_restrict(graph, &rf, parent_node, child_node)?, + ReferentialAction::Restrict => emulate_restrict(graph, &rf, parent_node, child_node)?, + ReferentialAction::NoAction => continue, // Explicitly do nothing. ReferentialAction::Cascade => { - emulate_cascade(graph, &rf, connector_ctx, model_to_delete, parent_node, child_node)? - } - - ReferentialAction::Restrict => { - emulate_restrict(graph, &rf, connector_ctx, model_to_delete, parent_node, child_node)? + emulate_on_delete_cascade(graph, &rf, connector_ctx, model_to_delete, parent_node, child_node)? } ReferentialAction::SetNull => { emulate_set_null(graph, &rf, connector_ctx, model_to_delete, parent_node, child_node)? } - ReferentialAction::NoAction => continue, // Explicitly do nothing. - x => panic!("Unsupported referential action emulation: {}", x), }; } @@ -326,11 +354,37 @@ pub fn insert_emulated_on_delete( Ok(()) } +/// Inserts restrict emulations into the graph between `parent_node` and `child_node`. +/// `relation_field` is the relation field pointing to the model to be deleted/updated. +/// +/// +/// ```text +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// Parent │ +/// ┌ ─│ (ids to del/upd) +/// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +/// │ │ +/// ▼ +/// │ ┌────────────────────┐ +/// │Find Connected Model│ +/// │ │ (Restrict) │ +/// └────────────────────┘ +/// │ │ +/// Fail if│> 0 +/// │ │ +/// ▼ +/// │ ┌────────────────────┐ +/// │ Empty │ +/// │ └────────────────────┘ +/// │ +/// │ ▼ +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// └ ▶ Delete / Update │ +/// └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// ``` pub fn emulate_restrict( graph: &mut QueryGraph, relation_field: &RelationFieldRef, - _connector_ctx: &ConnectorContext, - _model: &ModelRef, parent_node: &NodeRef, child_node: &NodeRef, ) -> QueryGraphBuilderResult<()> { @@ -360,7 +414,43 @@ pub fn emulate_restrict( Ok(()) } -pub fn emulate_cascade( +/// Inserts cascade emulations into the graph between `parent_node` and `child_node`. +/// `relation_field` is the relation field pointing to the model to be deleted. +/// Recurses into the deletion emulation to ensure that subsequent deletions are handled correctly as well. +/// +/// ```text +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// Parent │ +/// │ (ids to delete) ─ ┐ +/// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +/// │ │ +/// ▼ +/// ┌────────────────────┐ │ +/// │Find Connected Model│ +/// ┌──│ (Cascade) │ │ +/// │ └────────────────────┘ +/// │ │ │ +/// │ ▼ +/// │┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ +/// │ ┌────────────────────┐ │ +/// ││ │ Insert onDelete │ │ +/// │ │ emulation subtree │ │ +/// ││ │ for all relations │ │ +/// │ │ pointing to D. │ │ +/// ││ └────────────────────┘ │ +/// │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +/// │ │ │ +/// │ ▼ +/// │ ┌────────────────────┐ │ +/// └─▶│ Delete children │ +/// └────────────────────┘ │ +/// │ +/// ▼ │ +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// Delete │◀ ┘ +/// └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// ``` +pub fn emulate_on_delete_cascade( graph: &mut QueryGraph, relation_field: &RelationFieldRef, // This is the field _on the other model_ for cascade. connector_ctx: &ConnectorContext, @@ -415,13 +505,270 @@ pub fn emulate_cascade( Ok(()) } +/// Inserts set null emulations into the graph between `parent_node` and `child_node`. +/// `relation_field` is the relation field pointing to the model to be deleted. +/// Recurses into the deletion emulation to ensure that subsequent deletions are handled correctly as well. +/// +/// ```text +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// Parent │ +/// │ (ids to del/upd) ─ ┐ +/// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +/// │ │ +/// ▼ +/// ┌────────────────────┐ │ +/// │Find Connected Model│ +/// ┌──│ (SetNull) │ │ +/// │ └────────────────────┘ +/// │ │ │ +/// │ ▼ +/// │┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ +/// │ ┌────────────────────┐ │ +/// ││ │ Insert onUpdate │ │ +/// │ │ emulation subtree │ │ +/// ││ │for relations using │ │ +/// │ │the foreign key that│ │ +/// ││ │ was updated. │ │ +/// │ └────────────────────┘ │ +/// │└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ +/// │ │ +/// │ ▼ │ +/// │ ┌────────────────────┐ +/// │ │Update children (set│ │ +/// └─▶│ FK null) │ +/// └────────────────────┘ │ +/// │ +/// ▼ │ +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// Delete / Update │◀ ┘ +/// └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// ``` pub fn emulate_set_null( graph: &mut QueryGraph, relation_field: &RelationFieldRef, - _connector_ctx: &ConnectorContext, + connector_ctx: &ConnectorContext, + model: &ModelRef, + parent_node: &NodeRef, + child_node: &NodeRef, +) -> QueryGraphBuilderResult<()> { + // let dependent_model = relation_field.model(); + // let parent_relation_field = relation_field.related_field(); + // let child_model_identifier = relation_field.related_model().primary_identifier(); + + // // Records that need to be deleted for the cascade. + // let dependent_records_node = + // insert_find_children_by_parent_node(graph, parent_node, &parent_relation_field, Filter::empty())?; + + // let delete_query = WriteQuery::DeleteManyRecords(DeleteManyRecords { + // model: dependent_model.clone(), + // record_filter: RecordFilter::empty(), + // }); + + // let delete_dependents_node = graph.create_node(Query::Write(delete_query)); + + // insert_emulated_on_delete( + // graph, + // connector_ctx, + // &dependent_model, + // &dependent_records_node, + // &delete_dependents_node, + // )?; + + // graph.create_edge( + // &dependent_records_node, + // &delete_dependents_node, + // QueryGraphDependency::ParentProjection( + // child_model_identifier.clone(), + // Box::new(move |mut delete_dependents_node, dependent_ids| { + // if let Node::Query(Query::Write(WriteQuery::DeleteManyRecords(ref mut dmr))) = delete_dependents_node { + // dmr.record_filter = dependent_ids.into(); + // } + + // Ok(delete_dependents_node) + // }), + // ), + // )?; + + // graph.create_edge( + // &delete_dependents_node, + // child_node, + // QueryGraphDependency::ExecutionOrder, + // )?; + + Ok(()) +} + +/// Inserts emulated referential actions for `onUpdate` into the graph. +/// All relations that refer to the `model` row(s) being deleted are checked for their desired emulation and inserted accordingly. +/// Right now, supported modes are `Restrict` and `SetNull` (cascade will follow). +/// Those checks fail at runtime and are inserted between `parent_node` and `child_node`. +/// +/// This function is usually part of a delete (`deleteOne` or `deleteMany`). +/// Expects `parent_node` to return one or more IDs (for records of `model`) to be checked. +/// +/// The old behavior (pre-referential actions) is preserved for if the ReferentialActions feature flag is disabled, +/// which was basically only the `Restrict` part of this code. +/// +/// Resulting graph (all emulations): +/// ```text +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// Parent │ +/// ┌ ─│ (ids to update) ─────────────────┬─────────────────────────────┬────────────────────────────────────────┐ +/// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ │ +/// │ │ │ │ │ +/// ▼ ▼ ▼ ▼ +/// │ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ +/// │Find Connected Model│ │Find Connected Model│ │Find Connected Model│ │Find Connected Model│ +/// │ │ A (Restrict) │ │ B (Restrict) │ ┌──│ C (SetNull) │ ┌──│ D (Cascade) │ +/// └────────────────────┘ └────────────────────┘ │ └────────────────────┘ │ └────────────────────┘ +/// │ │ │ │ │ │ │ +/// Fail if│> 0 Fail if│> 0 │ ▼ │ │ +/// │ │ │ │┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ▼ +/// ▼ ▼ │ ┌────────────────────┐ │ │┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// │ ┌────────────────────┐ ┌────────────────────┐ ││ │ Insert onUpdate │ │ ┌────────────────────┐ │ +/// │ Empty │ │ Empty │ │ │ emulation subtree │ │ ││ │ Insert onUpdate │ +/// │ └────────────────────┘ └────────────────────┘ ││ │for relations using │ │ │ emulation subtree │ │ +/// │ │ │ │the foreign key that│ │ ││ │ for all relations │ +/// │ │ │ ││ │ was updated. │ │ │ pointing to D. │ │ +/// │ │ │ └────────────────────┘ │ ││ └────────────────────┘ +/// │ │ │ │└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +/// │ │ │ │ │ │ +/// │ │ │ │ │ │ │ +/// ▼ │ │ ▼ │ ▼ +/// │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ ┌────────────────────┐ │ ┌────────────────────┐ +/// ─▶ Update │◀────────────────┘ │ │ Update Cs (set FK │ └─▶│ Update Cs │ +/// └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └─▶│ null) │ └────────────────────┘ +/// ▲ └────────────────────┘ │ +/// │ │ │ +/// └─────────────────────────────────────────────────────────┴────────────────────────────────────────┘ +/// ``` +#[tracing::instrument(skip(graph, model_to_update, parent_node, child_node))] +pub fn insert_emulated_on_update( + graph: &mut QueryGraph, + connector_ctx: &ConnectorContext, + model_to_update: &ModelRef, + parent_node: &NodeRef, + child_node: &NodeRef, +) -> QueryGraphBuilderResult<()> { + let has_fks = connector_ctx.capabilities.contains(&ConnectorCapability::ForeignKeys); + let has_ra_feature = connector_ctx.features.contains(&PreviewFeature::ReferentialActions); + + // If the connector supports foreign keys and the new mode is enabled (preview feature), we do not do any checks / emulation. + if has_ra_feature && has_fks { + return Ok(()); + } + + // If it's non-fk dbs, then the emulation will kick in. If it has Fks, then preserve the old behavior (`has_fks` -> only required ones). + let internal_model = model_to_update.internal_data_model(); + let relation_fields = internal_model.fields_pointing_to_model(model_to_update, has_fks); + + for rf in relation_fields { + match rf.relation().on_delete() { + ReferentialAction::Restrict => emulate_restrict(graph, &rf, parent_node, child_node)?, + ReferentialAction::NoAction => continue, // Explicitly do nothing. + + ReferentialAction::Cascade => { + emulate_on_update_cascade(graph, &rf, connector_ctx, model_to_update, parent_node, child_node)? + } + + ReferentialAction::SetNull => { + emulate_set_null(graph, &rf, connector_ctx, model_to_update, parent_node, child_node)? + } + + x => panic!("Unsupported referential action emulation: {}", x), + }; + } + + Ok(()) +} + +/// Inserts cascade emulations into the graph between `parent_node` and `child_node`. +/// `relation_field` is the relation field pointing to the model to be deleted. +/// Recurses into the deletion emulation to ensure that subsequent deletions are handled correctly as well. +/// +/// ```text +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// Parent │ +/// │ (ids to update) ─ ┐ +/// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +/// │ │ +/// ▼ +/// ┌────────────────────┐ │ +/// │Find Connected Model│ +/// ┌──│ (Cascade) │ │ +/// │ └────────────────────┘ +/// │ │ │ +/// │ ▼ +/// │┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ +/// │ ┌────────────────────┐ │ +/// ││ │ Insert onUpdate │ │ +/// │ │ emulation subtree │ │ +/// ││ │ for all relations │ │ +/// │ │ pointing to D. │ │ +/// ││ └────────────────────┘ │ +/// │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +/// │ │ │ +/// │ ▼ +/// │ ┌────────────────────┐ │ +/// └─▶│ Update children │ +/// └────────────────────┘ │ +/// │ +/// ▼ │ +/// ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// Update │◀ ┘ +/// └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +/// ``` +pub fn emulate_on_update_cascade( + graph: &mut QueryGraph, + relation_field: &RelationFieldRef, // This is the field _on the other model_ for cascade. + connector_ctx: &ConnectorContext, _model: &ModelRef, parent_node: &NodeRef, child_node: &NodeRef, ) -> QueryGraphBuilderResult<()> { + let dependent_model = relation_field.model(); + let parent_relation_field = relation_field.related_field(); + let child_model_identifier = relation_field.related_model().primary_identifier(); + + // Records that need to be deleted for the cascade. + let dependent_records_node = + insert_find_children_by_parent_node(graph, parent_node, &parent_relation_field, Filter::empty())?; + + let update_query = WriteQuery::UpdateManyRecords(UpdateManyRecords { + model: dependent_model.clone(), + record_filter: RecordFilter::empty(), + }); + + let update_dependents_node = graph.create_node(Query::Write(delete_query)); + + insert_emulated_on_update( + graph, + connector_ctx, + &dependent_model, + &dependent_records_node, + &update_dependents_node, + )?; + + graph.create_edge( + &dependent_records_node, + &update_dependents_node, + QueryGraphDependency::ParentProjection( + child_model_identifier.clone(), + Box::new(move |mut update_dependents_node, dependent_ids| { + if let Node::Query(Query::Write(WriteQuery::DeleteManyRecords(ref mut dmr))) = delete_dependents_node { + dmr.record_filter = dependent_ids.into(); + } + + Ok(delete_dependents_node) + }), + ), + )?; + + graph.create_edge( + &delete_dependents_node, + child_node, + QueryGraphDependency::ExecutionOrder, + )?; + Ok(()) } From 3c4c400ebc5730aa30c8815cc9e237ea7706b5ad Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 21 Jun 2021 11:19:39 +0300 Subject: [PATCH 33/47] Add preview_features test params for ME --- libs/test-macros/src/lib.rs | 18 ++++++- libs/test-setup/src/test_api_args.rs | 8 ++- .../src/multi_engine_test_api.rs | 13 ++++- .../migration-engine-tests/src/test_api.rs | 11 +++- .../create_migration_tests.rs | 2 +- .../tests/migration_tests.rs | 4 +- .../tests/migrations/foreign_keys.rs | 2 +- .../tests/migrations/relations.rs | 52 ++++++------------- 8 files changed, 63 insertions(+), 47 deletions(-) diff --git a/libs/test-macros/src/lib.rs b/libs/test-macros/src/lib.rs index 8a609bb276e1..2c18733f6cf1 100644 --- a/libs/test-macros/src/lib.rs +++ b/libs/test-macros/src/lib.rs @@ -54,6 +54,7 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { let include_tagged = &attrs.include_tagged; let exclude_tagged = &attrs.exclude_tagged; let capabilities = &attrs.capabilities; + let preview_features = &attrs.preview_features; let test_function_name = &sig.ident; let test_function_name_lit = sig.ident.to_string(); @@ -73,7 +74,7 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { #[test] #ignore_attr fn #test_function_name() { - let args = test_setup::TestApiArgs::new(#test_function_name_lit); + let args = test_setup::TestApiArgs::new(#test_function_name_lit, &[#(#preview_features,)*]); if test_setup::should_skip_test( &args, @@ -95,7 +96,7 @@ pub fn test_connector(attr: TokenStream, input: TokenStream) -> TokenStream { #[test] #ignore_attr fn #test_function_name() { - let args = test_setup::TestApiArgs::new(#test_function_name_lit); + let args = test_setup::TestApiArgs::new(#test_function_name_lit, &[#(#preview_features,)*]); if test_setup::should_skip_test( &args, @@ -119,6 +120,7 @@ struct TestConnectorAttrs { include_tagged: Vec, exclude_tagged: Vec, capabilities: Vec, + preview_features: Vec, ignore_reason: Option, } @@ -128,6 +130,18 @@ impl TestConnectorAttrs { p if p.is_ident("tags") => &mut self.include_tagged, p if p.is_ident("exclude") => &mut self.exclude_tagged, p if p.is_ident("capabilities") => &mut self.capabilities, + p if p.is_ident("preview_features") => { + self.preview_features.reserve(list.nested.len()); + + for item in list.nested { + match item { + NestedMeta::Lit(Lit::Str(s)) => self.preview_features.push(s), + other => return Err(syn::Error::new_spanned(other, "Unexpected argument")), + } + } + + return Ok(()); + } p if p.is_ident("logs") => return Ok(()), // TODO other => return Err(syn::Error::new_spanned(other, "Unexpected argument")), }; diff --git a/libs/test-setup/src/test_api_args.rs b/libs/test-setup/src/test_api_args.rs index 59d4d4c4f7fb..a99ccb208825 100644 --- a/libs/test-setup/src/test_api_args.rs +++ b/libs/test-setup/src/test_api_args.rs @@ -102,17 +102,23 @@ pub(crate) fn db_under_test() -> &'static DbUnderTest { #[derive(Debug)] pub struct TestApiArgs { test_function_name: &'static str, + preview_features: &'static [&'static str], db: &'static DbUnderTest, } impl TestApiArgs { - pub fn new(test_function_name: &'static str) -> Self { + pub fn new(test_function_name: &'static str, preview_features: &'static [&'static str]) -> Self { TestApiArgs { test_function_name, + preview_features, db: db_under_test(), } } + pub fn preview_features(&self) -> &'static [&'static str] { + &self.preview_features + } + pub fn test_function_name(&self) -> &'static str { self.test_function_name } diff --git a/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs b/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs index 49d4393140d3..878399b74188 100644 --- a/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs +++ b/migration-engine/migration-engine-tests/src/multi_engine_test_api.rs @@ -3,6 +3,7 @@ //! A TestApi that is initialized without IO or async code and can instantiate //! multiple migration engines. +use datamodel::common::preview_features::PreviewFeature; pub use test_macros::test_connector; pub use test_setup::{BitFlags, Capabilities, Tags}; @@ -23,6 +24,7 @@ pub struct TestApi { connection_string: String, pub(crate) admin_conn: Quaint, pub(crate) rt: tokio::runtime::Runtime, + preview_features: BitFlags, } impl TestApi { @@ -31,6 +33,12 @@ impl TestApi { let rt = test_setup::runtime::test_tokio_runtime(); let tags = args.tags(); + let preview_features = args + .preview_features() + .iter() + .flat_map(|f| PreviewFeature::parse_opt(f)) + .collect(); + let (admin_conn, connection_string) = if tags.contains(Tags::Postgres) { let (_, q, cs) = rt.block_on(args.create_postgres_database()); (q, cs) @@ -38,7 +46,7 @@ impl TestApi { let conn = rt .block_on(SqlMigrationConnector::new( args.database_url(), - BitFlags::all(), + preview_features, args.shadow_database_url().map(String::from), )) .unwrap(); @@ -70,6 +78,7 @@ impl TestApi { connection_string, admin_conn, rt, + preview_features, } } @@ -165,7 +174,7 @@ impl TestApi { .rt .block_on(SqlMigrationConnector::new( &connection_string, - BitFlags::all(), + self.preview_features, shadow_db_connection_string, )) .unwrap(); diff --git a/migration-engine/migration-engine-tests/src/test_api.rs b/migration-engine/migration-engine-tests/src/test_api.rs index b3337c8d8f0d..8fb03b01c89a 100644 --- a/migration-engine/migration-engine-tests/src/test_api.rs +++ b/migration-engine/migration-engine-tests/src/test_api.rs @@ -11,6 +11,7 @@ mod schema_push; pub use apply_migrations::ApplyMigrations; pub use create_migration::CreateMigration; +use datamodel::common::preview_features::PreviewFeature; pub use dev_diagnostic::DevDiagnostic; pub use diagnose_migration_history::DiagnoseMigrationHistory; pub use evaluate_data_loss::EvaluateDataLoss; @@ -46,9 +47,15 @@ impl TestApi { let shadow_database_url = args.shadow_database_url().map(String::from); + let preview_features = args + .preview_features() + .iter() + .flat_map(|f| PreviewFeature::parse_opt(f)) + .collect(); + let connection_string = if tags.contains(Tags::Mysql | Tags::Vitess) { let connector = - SqlMigrationConnector::new(args.database_url(), BitFlags::all(), shadow_database_url.clone()) + SqlMigrationConnector::new(args.database_url(), preview_features, shadow_database_url.clone()) .await .unwrap(); @@ -69,7 +76,7 @@ impl TestApi { unreachable!() }; - let api = SqlMigrationConnector::new(&connection_string, BitFlags::all(), shadow_database_url) + let api = SqlMigrationConnector::new(&connection_string, preview_features, shadow_database_url) .await .unwrap(); 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 9bb0bc538ff6..d77ac1c07d0f 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 RESTRICT ON UPDATE CASCADE; + ALTER TABLE "Collar" ADD FOREIGN KEY ("id") REFERENCES "Cat"("id") ON DELETE CASCADE 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 f3756e7eb75b..177b1397fc98 100644 --- a/migration-engine/migration-engine-tests/tests/migration_tests.rs +++ b/migration-engine/migration-engine-tests/tests/migration_tests.rs @@ -1087,8 +1087,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], onDelete: Cascade) - human Human @relation(fields: [human_firstName, human_lastName], references: [firstName, lastName], onDelete: Cascade) + cat Cat @relation(fields: [cat_id], references: [id]) + human Human @relation(fields: [human_firstName, human_lastName], references: [firstName, lastName]) @@unique([cat_id, human_firstName, human_lastName], name: "joinTableUnique") @@index([human_firstName, human_lastName], name: "joinTableIndex") 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 b5c7f69cbc8a..0d55476a0a7a 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/foreign_keys.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/foreign_keys.rs @@ -203,7 +203,7 @@ 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], onDelete: Cascade) + b_rel B @relation(fields: [b], references: [id]) } model B { diff --git a/migration-engine/migration-engine-tests/tests/migrations/relations.rs b/migration-engine/migration-engine-tests/tests/migrations/relations.rs index f4618aca2ab2..4561f7fb2030 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/relations.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/relations.rs @@ -65,17 +65,12 @@ fn adding_a_many_to_many_relation_with_custom_name_must_work(api: TestApi) { #[test_connector] fn adding_an_inline_relation_must_result_in_a_foreign_key_in_the_model_table(api: TestApi) { let dm1 = r#" - generator client { - provider = "prisma-client-js" - previewFeatures = ["referentialActions"] - } - model A { id Int @id bid Int cid Int? - b B @relation(fields: [bid], references: [id], onDelete: NoAction) - c C? @relation(fields: [cid], references: [id], onDelete: NoAction) + b B @relation(fields: [bid], references: [id]) + c C? @relation(fields: [cid], references: [id]) } model B { @@ -97,7 +92,7 @@ fn adding_an_inline_relation_must_result_in_a_foreign_key_in_the_model_table(api .assert_fk_on_columns(&["bid"], |fk| { fk.assert_references("B", &["id"]) .assert_referential_action_on_update(ForeignKeyAction::Cascade) - .assert_referential_action_on_delete(ForeignKeyAction::NoAction) + .assert_referential_action_on_delete(ForeignKeyAction::Cascade) }) .assert_fk_on_columns(&["cid"], |fk| fk.assert_references("C", &["id"])) }); @@ -126,7 +121,7 @@ fn specifying_a_db_name_for_an_inline_relation_must_work(api: TestApi) { }); } -#[test_connector] +#[test_connector(preview_features("referentialActions"))] fn adding_an_inline_relation_to_a_model_with_an_exotic_id_type(api: TestApi) { let dm1 = r#" generator client { @@ -201,17 +196,12 @@ fn removing_an_inline_relation_must_work(api: TestApi) { #[test_connector] fn compound_foreign_keys_should_work_in_correct_order(api: TestApi) { let dm1 = r#" - generator client { - provider = "prisma-client-js" - previewFeatures = ["referentialActions"] - } - model A { id Int @id b Int a Int d Int - bb B @relation(fields: [a, b, d], references: [a_id, b_id, d_id], onDelete: NoAction) + bb B @relation(fields: [a, b, d], references: [a_id, b_id, d_id]) } model B { @@ -228,7 +218,7 @@ fn compound_foreign_keys_should_work_in_correct_order(api: TestApi) { api.assert_schema().assert_table("A", |t| { t.assert_foreign_keys_count(1) .assert_fk_on_columns(&["a", "b", "d"], |fk| { - fk.assert_referential_action_on_delete(ForeignKeyAction::NoAction) + fk.assert_referential_action_on_delete(ForeignKeyAction::Cascade) .assert_referential_action_on_update(ForeignKeyAction::Cascade) .assert_references("B", &["a_id", "b_id", "d_id"]) }) @@ -238,15 +228,10 @@ fn compound_foreign_keys_should_work_in_correct_order(api: TestApi) { #[test_connector] fn moving_an_inline_relation_to_the_other_side_must_work(api: TestApi) { let dm1 = r#" - generator client { - provider = "prisma-client-js" - previewFeatures = ["referentialActions"] - } - model A { id Int @id b_id Int - b B @relation(fields: [b_id], references: [id], onDelete: NoAction) + b B @relation(fields: [b_id], references: [id]) } model B { @@ -258,18 +243,13 @@ 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("A", |t| { t.assert_foreign_keys_count(1).assert_fk_on_columns(&["b_id"], |fk| { - fk.assert_referential_action_on_delete(ForeignKeyAction::NoAction) + fk.assert_referential_action_on_delete(ForeignKeyAction::Cascade) .assert_referential_action_on_update(ForeignKeyAction::Cascade) .assert_references("B", &["id"]) }) }); let dm2 = r#" - generator client { - provider = "prisma-client-js" - previewFeatures = ["referentialActions"] - } - model A { id Int @id b B[] @@ -278,7 +258,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], onDelete: NoAction) + a A @relation(fields: [a_id], references: [id]) } "#; @@ -289,7 +269,7 @@ fn moving_an_inline_relation_to_the_other_side_must_work(api: TestApi) { .assert_foreign_keys_count(1) .assert_fk_on_columns(&["a_id"], |fk| { fk.assert_references("A", &["id"]) - .assert_referential_action_on_delete(ForeignKeyAction::NoAction) + .assert_referential_action_on_delete(ForeignKeyAction::Cascade) .assert_referential_action_on_update(ForeignKeyAction::Cascade) }) }) @@ -474,7 +454,7 @@ fn relations_with_mappings_on_referencing_side_can_reference_multiple_fields(api }); } -#[test_connector] +#[test_connector(preview_features("referentialActions"))] fn on_delete_referential_actions_should_work(api: TestApi) { let actions = &[ (ReferentialAction::SetNull, ForeignKeyAction::SetNull), @@ -519,7 +499,7 @@ fn on_delete_referential_actions_should_work(api: TestApi) { // 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))] +#[test_connector(exclude(Mysql56, Mysql57, Mariadb, Mssql), preview_features("referentialActions"))] fn on_delete_set_default_should_work(api: TestApi) { let dm = r#" generator client { @@ -549,7 +529,7 @@ fn on_delete_set_default_should_work(api: TestApi) { }); } -#[test_connector(exclude(Mssql))] +#[test_connector(exclude(Mssql), preview_features("referentialActions"))] fn on_delete_restrict_should_work(api: TestApi) { let dm = r#" generator client { @@ -579,7 +559,7 @@ fn on_delete_restrict_should_work(api: TestApi) { }); } -#[test_connector] +#[test_connector(preview_features("referentialActions"))] fn on_update_referential_actions_should_work(api: TestApi) { let actions = &[ (ReferentialAction::NoAction, ForeignKeyAction::NoAction), @@ -622,7 +602,7 @@ fn on_update_referential_actions_should_work(api: TestApi) { // 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))] +#[test_connector(exclude(Mysql56, Mysql57, Mariadb, Mssql), preview_features("referentialActions"))] fn on_update_set_default_should_work(api: TestApi) { let dm = r#" generator client { @@ -652,7 +632,7 @@ fn on_update_set_default_should_work(api: TestApi) { }); } -#[test_connector(exclude(Mssql))] +#[test_connector(exclude(Mssql), preview_features("referentialActions"))] fn on_update_restrict_should_work(api: TestApi) { let dm = r#" generator client { From 06c16b28cbcaa68f79ab9329ebe9a74942edc703 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 21 Jun 2021 12:08:58 +0200 Subject: [PATCH 34/47] onUpdate work. --- .../src/query_graph_builder/write/utils.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index 4a400e0121e5..04135f9f39fd 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -8,7 +8,7 @@ use datamodel::{common::preview_features::PreviewFeature, ReferentialAction}; use datamodel_connector::ConnectorCapability; use itertools::Itertools; use once_cell::sync::OnceCell; -use prisma_models::{ModelProjection, ModelRef, RelationFieldRef}; +use prisma_models::{InternalDataModelRef, ModelProjection, ModelRef, RelationFieldRef}; use std::sync::Arc; /// Coerces single values (`ParsedInputValue::Single` and `ParsedInputValue::Map`) into a vector. @@ -662,6 +662,9 @@ pub fn insert_emulated_on_update( let internal_model = model_to_update.internal_data_model(); let relation_fields = internal_model.fields_pointing_to_model(model_to_update, has_fks); + // Unwraps are safe as in this stage, no node content can be replaced. + let parent_update_args = extract_update_args(graph.node_content(parent_node).unwrap()); + for rf in relation_fields { match rf.relation().on_delete() { ReferentialAction::Restrict => emulate_restrict(graph, &rf, parent_node, child_node)?, @@ -682,6 +685,18 @@ pub fn insert_emulated_on_update( Ok(()) } +fn extract_update_args(parent_node: &Node) -> &WriteArgs { + if let Node::Query(Query::Write(q)) = parent_node { + match q { + WriteQuery::UpdateRecord(one) => &one.args, + WriteQuery::UpdateManyRecords(many) => &many.args, + _ => panic!("Parent operation for update emulation is not an update."), + } + } else { + panic!("Parent operation for update emulation is not a query.") + } +} + /// Inserts cascade emulations into the graph between `parent_node` and `child_node`. /// `relation_field` is the relation field pointing to the model to be deleted. /// Recurses into the deletion emulation to ensure that subsequent deletions are handled correctly as well. @@ -737,6 +752,7 @@ pub fn emulate_on_update_cascade( let update_query = WriteQuery::UpdateManyRecords(UpdateManyRecords { model: dependent_model.clone(), record_filter: RecordFilter::empty(), + args: (), }); let update_dependents_node = graph.create_node(Query::Write(delete_query)); From ea7a0106fa293c36aeca1cf8b56f4058b3226e98 Mon Sep 17 00:00:00 2001 From: Dominic Petrick Date: Mon, 21 Jun 2021 13:15:35 +0200 Subject: [PATCH 35/47] Compilation --- .../src/query_graph_builder/write/utils.rs | 59 ++----------------- 1 file changed, 6 insertions(+), 53 deletions(-) diff --git a/query-engine/core/src/query_graph_builder/write/utils.rs b/query-engine/core/src/query_graph_builder/write/utils.rs index 04135f9f39fd..d8ce3c855491 100644 --- a/query-engine/core/src/query_graph_builder/write/utils.rs +++ b/query-engine/core/src/query_graph_builder/write/utils.rs @@ -6,9 +6,7 @@ use crate::{ use connector::{Filter, RecordFilter, WriteArgs}; use datamodel::{common::preview_features::PreviewFeature, ReferentialAction}; use datamodel_connector::ConnectorCapability; -use itertools::Itertools; -use once_cell::sync::OnceCell; -use prisma_models::{InternalDataModelRef, ModelProjection, ModelRef, RelationFieldRef}; +use prisma_models::{ModelProjection, ModelRef, RelationFieldRef}; use std::sync::Arc; /// Coerces single values (`ParsedInputValue::Single` and `ParsedInputValue::Map`) into a vector. @@ -734,57 +732,12 @@ fn extract_update_args(parent_node: &Node) -> &WriteArgs { /// └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ /// ``` pub fn emulate_on_update_cascade( - graph: &mut QueryGraph, - relation_field: &RelationFieldRef, // This is the field _on the other model_ for cascade. - connector_ctx: &ConnectorContext, + _graph: &mut QueryGraph, + _relation_field: &RelationFieldRef, // This is the field _on the other model_ for cascade. + _connector_ctx: &ConnectorContext, _model: &ModelRef, - parent_node: &NodeRef, - child_node: &NodeRef, + _parent_node: &NodeRef, + _child_node: &NodeRef, ) -> QueryGraphBuilderResult<()> { - let dependent_model = relation_field.model(); - let parent_relation_field = relation_field.related_field(); - let child_model_identifier = relation_field.related_model().primary_identifier(); - - // Records that need to be deleted for the cascade. - let dependent_records_node = - insert_find_children_by_parent_node(graph, parent_node, &parent_relation_field, Filter::empty())?; - - let update_query = WriteQuery::UpdateManyRecords(UpdateManyRecords { - model: dependent_model.clone(), - record_filter: RecordFilter::empty(), - args: (), - }); - - let update_dependents_node = graph.create_node(Query::Write(delete_query)); - - insert_emulated_on_update( - graph, - connector_ctx, - &dependent_model, - &dependent_records_node, - &update_dependents_node, - )?; - - graph.create_edge( - &dependent_records_node, - &update_dependents_node, - QueryGraphDependency::ParentProjection( - child_model_identifier.clone(), - Box::new(move |mut update_dependents_node, dependent_ids| { - if let Node::Query(Query::Write(WriteQuery::DeleteManyRecords(ref mut dmr))) = delete_dependents_node { - dmr.record_filter = dependent_ids.into(); - } - - Ok(delete_dependents_node) - }), - ), - )?; - - graph.create_edge( - &delete_dependents_node, - child_node, - QueryGraphDependency::ExecutionOrder, - )?; - Ok(()) } From 68badc907e102a544c681aba01c149231a530727 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 21 Jun 2021 16:04:23 +0300 Subject: [PATCH 36/47] IE/ME clippy --- Cargo.lock | 1 - Cargo.toml | 3 ++ .../sql-introspection-connector/src/lib.rs | 2 +- .../introspection-engine-tests/src/lib.rs | 2 +- .../tests/commenting_out/mod.rs | 19 +----------- .../tests/re_introspection/mod.rs | 31 ++++++++++++++----- libs/datamodel/core/src/lib.rs | 2 +- .../sql-migration-connector/src/lib.rs | 2 +- .../src/sql_schema_differ.rs | 2 +- .../src/sql_schema_differ/column.rs | 4 +-- .../src/sql_schema_differ/table.rs | 12 +++---- 11 files changed, 41 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92f69e9177ac..b88576d8d752 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -434,7 +434,6 @@ dependencies = [ [[package]] name = "barrel" version = "0.6.6-alpha.0" -source = "git+https://github.com/prisma/barrel.git?branch=mssql-support#5d6a731665acdab0d459be23843c2c54112ff099" [[package]] name = "base-x" diff --git a/Cargo.toml b/Cargo.toml index da7ea62643b2..ee26d917e81a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,3 +44,6 @@ opt-level = 3 codegen-units = 1 opt-level = 'z' # Optimize for size. #strip="symbols" + +[patch."https://github.com/prisma/barrel.git"] +barrel = { path = "../barrel" } diff --git a/introspection-engine/connectors/sql-introspection-connector/src/lib.rs b/introspection-engine/connectors/sql-introspection-connector/src/lib.rs index fc70288c5a46..686837db2260 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/lib.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::clippy::vec_init_then_push)] +#![allow(clippy::vec_init_then_push)] pub mod calculate_datamodel; // only exported to be able to unit test it mod commenting_out_guardrails; diff --git a/introspection-engine/introspection-engine-tests/src/lib.rs b/introspection-engine/introspection-engine-tests/src/lib.rs index f43a10f2a997..fe2c1eb2537b 100644 --- a/introspection-engine/introspection-engine-tests/src/lib.rs +++ b/introspection-engine/introspection-engine-tests/src/lib.rs @@ -64,7 +64,7 @@ impl BarrelMigrationExecutor { return Ok(()); } - self.database.raw_cmd(&full_sql).await?; + self.database.raw_cmd(dbg!(&full_sql)).await?; Ok(()) } 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 ac3d79325df9..cae0b64cda54 100644 --- a/introspection-engine/introspection-engine-tests/tests/commenting_out/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/commenting_out/mod.rs @@ -19,24 +19,7 @@ async fn a_table_without_uniques_should_ignore(api: &TestApi) -> TestResult { }) .await?; - 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]) - - @@index([user_id], name: "user_id") - @@ignore - } - - model User { - id Int @id @default(autoincrement()) - Post Post[] @ignore - } - "#} - } else if api.sql_family().is_mysql() { + let dm = 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 { 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 1c9af0a5ed10..dc3f99851943 100644 --- a/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs @@ -1730,18 +1730,30 @@ async fn do_not_try_to_keep_custom_many_to_many_self_relation_names(api: &TestAp #[test_connector] async fn referential_actions(api: &TestApi) -> TestResult { + let family = api.sql_family(); + api.barrel() - .execute(|migration| { + .execute(move |migration| { migration.create_table("a", |t| { t.add_column("id", types::primary()); }); - migration.create_table("b", |t| { + migration.create_table("b", move |t| { t.add_column("id", types::primary()); t.add_column("a_id", types::integer().nullable(false)); - t.inject_custom( - "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE SET NULL ON UPDATE SET NULL", - ); + + match family { + SqlFamily::Mssql => { + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES referential_actions.a(id) ON DELETE CASCADE ON UPDATE NO ACTION", + ); + } + _ => { + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE CASCADE ON UPDATE NO ACTION", + ); + } + } }); }) .await?; @@ -1766,7 +1778,7 @@ async fn referential_actions(api: &TestApi) -> TestResult { model b {{ id Int @id @default(autoincrement()) a_id Int - a a @relation(fields: [a_id], references: [id], onDelete: SetNull, onUpdate: SetNull) + a a @relation(fields: [a_id], references: [id], onDelete: Cascade, onUpdate: NoAction) {} }} "#, extra_index}; @@ -1836,7 +1848,7 @@ async fn default_referential_actions_without_restrict(api: &TestApi) -> TestResu t.add_column("id", types::primary()); t.add_column("a_id", types::integer().nullable(false)); t.inject_custom( - "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_required_actions_without_restrict.a(id) ON DELETE NO ACTION ON UPDATE CASCADE", + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_referential_actions_without_restrict.a(id) ON DELETE NO ACTION ON UPDATE CASCADE", ); }); }) @@ -1909,6 +1921,11 @@ async fn default_optional_actions(api: &TestApi) -> TestResult { }; let input_dm = formatdoc! {r#" + generator client {{ + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + }} + model a {{ id Int @id @default(autoincrement()) bs b[] diff --git a/libs/datamodel/core/src/lib.rs b/libs/datamodel/core/src/lib.rs index 3270ac3c6278..97ad2b62ce55 100644 --- a/libs/datamodel/core/src/lib.rs +++ b/libs/datamodel/core/src/lib.rs @@ -183,8 +183,8 @@ pub fn parse_configuration(schema: &str) -> Result impl Iterator, flavour: &dyn SqlFlavou let redefine_indexes = if differ.flavour.can_alter_index() { Vec::new() } else { - std::mem::replace(&mut alter_indexes, Vec::new()) + std::mem::take(&mut alter_indexes) }; differ.drop_tables(&mut steps); diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ/column.rs b/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ/column.rs index 0a9c60a1f90e..54057ccbe0c7 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ/column.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ/column.rs @@ -172,11 +172,11 @@ impl ColumnChanges { } pub(crate) fn only_default_changed(&self) -> bool { - self.changes == BitFlags::from(ColumnChange::Default) + self.changes == ColumnChange::Default } pub(crate) fn only_type_changed(&self) -> bool { - self.changes == BitFlags::from(ColumnChange::TypeChanged) + self.changes == ColumnChange::TypeChanged } pub(crate) fn column_was_renamed(&self) -> bool { diff --git a/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ/table.rs b/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ/table.rs index 6686459a0ab9..673925a1adfd 100644 --- a/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ/table.rs +++ b/migration-engine/connectors/sql-migration-connector/src/sql_schema_differ/table.rs @@ -36,17 +36,17 @@ impl<'schema, 'b> TableDiffer<'schema, 'b> { pub(crate) fn created_foreign_keys<'a>(&'a self) -> impl Iterator> + 'a { self.next_foreign_keys().filter(move |next_fk| { - self.previous_foreign_keys() - .find(|previous_fk| super::foreign_keys_match(Pair::new(previous_fk, next_fk), self.flavour)) - .is_none() + !self + .previous_foreign_keys() + .any(|previous_fk| super::foreign_keys_match(Pair::new(&previous_fk, next_fk), self.flavour)) }) } pub(crate) fn dropped_foreign_keys<'a>(&'a self) -> impl Iterator> + 'a { self.previous_foreign_keys().filter(move |previous_fk| { - self.next_foreign_keys() - .find(|next_fk| super::foreign_keys_match(Pair::new(previous_fk, next_fk), self.flavour)) - .is_none() + !self + .next_foreign_keys() + .any(|next_fk| super::foreign_keys_match(Pair::new(previous_fk, &next_fk), self.flavour)) }) } From 357ce5c81ae4fbdbf19314f953589aa8aa541f96 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 21 Jun 2021 16:14:33 +0300 Subject: [PATCH 37/47] Do not patch barrel --- Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ee26d917e81a..da7ea62643b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,3 @@ opt-level = 3 codegen-units = 1 opt-level = 'z' # Optimize for size. #strip="symbols" - -[patch."https://github.com/prisma/barrel.git"] -barrel = { path = "../barrel" } From 67a49399b415bd8131237a9c2a7d663fcae7dd87 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 21 Jun 2021 16:49:20 +0300 Subject: [PATCH 38/47] More clippy --- Cargo.lock | 1 + .../tests/datamodel_converter_tests.rs | 6 ++-- .../mongodb-query-connector/src/cursor.rs | 4 +-- .../mongodb-query-connector/src/lib.rs | 2 +- .../query-connector/src/query_arguments.rs | 2 +- .../src/cursor_condition.rs | 5 ++-- .../src/database/operations/read.rs | 2 +- .../connectors/sql-query-connector/src/lib.rs | 2 +- .../inmemory_record_processor.rs | 2 +- .../query_interpreters/nested_read.rs | 1 + query-engine/core/src/query_graph/mod.rs | 2 +- query-engine/core/src/response_ir/internal.rs | 2 +- .../output_types/mutation_type.rs | 4 +-- .../request-handlers/src/dmmf/schema/mod.rs | 2 +- .../src/tests/dmmf/aggregation.rs | 30 +++++++++---------- .../src/tests/dmmf/ignored.rs | 2 +- 16 files changed, 35 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b88576d8d752..204632d36842 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -434,6 +434,7 @@ dependencies = [ [[package]] name = "barrel" version = "0.6.6-alpha.0" +source = "git+https://github.com/prisma/barrel.git?branch=mssql-support#4e84cf3d5013b4c92eb81d7ba90cd1c1c01c6805" [[package]] name = "base-x" diff --git a/libs/prisma-models/tests/datamodel_converter_tests.rs b/libs/prisma-models/tests/datamodel_converter_tests.rs index 4a7841388890..b46a079bb2cf 100644 --- a/libs/prisma-models/tests/datamodel_converter_tests.rs +++ b/libs/prisma-models/tests/datamodel_converter_tests.rs @@ -5,9 +5,9 @@ use std::sync::Arc; #[test] fn an_empty_datamodel_must_work() { let datamodel = convert(""); - assert_eq!(datamodel.enums.is_empty(), true); - assert_eq!(datamodel.models().is_empty(), true); - assert_eq!(datamodel.relations().is_empty(), true); + assert!(datamodel.enums.is_empty()); + assert!(datamodel.models().is_empty()); + assert!(datamodel.relations().is_empty()); } #[test] diff --git a/query-engine/connectors/mongodb-query-connector/src/cursor.rs b/query-engine/connectors/mongodb-query-connector/src/cursor.rs index f267daf7c95c..096b57d06208 100644 --- a/query-engine/connectors/mongodb-query-connector/src/cursor.rs +++ b/query-engine/connectors/mongodb-query-connector/src/cursor.rs @@ -92,11 +92,11 @@ fn cursor_conditions(mut order_data: Vec, reverse: bool) -> Documen and_conditions.push(map_equality_condition(order_data)); } + let order_data = tail.first().unwrap(); + if head.len() == num_orderings - 1 { - let order_data = tail.first().unwrap(); and_conditions.push(map_orderby_condition(order_data, reverse, true)); } else { - let order_data = tail.first().unwrap(); and_conditions.push(map_orderby_condition(order_data, reverse, false)); } diff --git a/query-engine/connectors/mongodb-query-connector/src/lib.rs b/query-engine/connectors/mongodb-query-connector/src/lib.rs index 484650a3181a..4736edca49ac 100644 --- a/query-engine/connectors/mongodb-query-connector/src/lib.rs +++ b/query-engine/connectors/mongodb-query-connector/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::clippy::vec_init_then_push)] +#![allow(clippy::vec_init_then_push)] mod cursor; mod error; diff --git a/query-engine/connectors/query-connector/src/query_arguments.rs b/query-engine/connectors/query-connector/src/query_arguments.rs index 8730a8f4564c..0b10f621d0ba 100644 --- a/query-engine/connectors/query-connector/src/query_arguments.rs +++ b/query-engine/connectors/query-connector/src/query_arguments.rs @@ -126,7 +126,7 @@ impl QueryArguments { } pub fn take_abs(&self) -> Option { - self.take.clone().map(|t| if t < 0 { -t } else { t }) + self.take.map(|t| if t < 0 { -t } else { t }) } pub fn should_batch(&self) -> bool { diff --git a/query-engine/connectors/sql-query-connector/src/cursor_condition.rs b/query-engine/connectors/sql-query-connector/src/cursor_condition.rs index 8692e5d4c0c5..ee0586f9731b 100644 --- a/query-engine/connectors/sql-query-connector/src/cursor_condition.rs +++ b/query-engine/connectors/sql-query-connector/src/cursor_condition.rs @@ -167,6 +167,8 @@ pub fn build( )); } + let order_definition = tail.first().unwrap(); + if head.len() == len - 1 { // Special case where we build lte / gte, not lt / gt. // - We use the combination of all order-by fields as comparator for the the cursor. @@ -192,11 +194,8 @@ pub fn build( // // Said differently, we handle all the cases in which the prefixes are equal to len - 1 to account for possible identical comparators, // but everything else must come strictly "after" the cursor. - let order_definition = tail.first().unwrap(); - and_conditions.push(map_orderby_condition(order_definition, reverse, true)); } else { - let order_definition = tail.first().unwrap(); and_conditions.push(map_orderby_condition(order_definition, reverse, false)); } diff --git a/query-engine/connectors/sql-query-connector/src/database/operations/read.rs b/query-engine/connectors/sql-query-connector/src/database/operations/read.rs index dd455c0a90b0..7c8b6e7fe7d3 100644 --- a/query-engine/connectors/sql-query-connector/src/database/operations/read.rs +++ b/query-engine/connectors/sql-query-connector/src/database/operations/read.rs @@ -81,7 +81,7 @@ pub async fn get_many_records( // The should_batch has been adjusted to reflect that as a band-aid, but deeper investigation is necessary. if query_arguments.should_batch() { // We don't need to order in the database due to us ordering in this function. - let order = std::mem::replace(&mut query_arguments.order_by, vec![]); + let order = std::mem::take(&mut query_arguments.order_by); let batches = query_arguments.batched(); let mut futures = FuturesUnordered::new(); diff --git a/query-engine/connectors/sql-query-connector/src/lib.rs b/query-engine/connectors/sql-query-connector/src/lib.rs index ce6722438512..368c23f13cc2 100644 --- a/query-engine/connectors/sql-query-connector/src/lib.rs +++ b/query-engine/connectors/sql-query-connector/src/lib.rs @@ -1,6 +1,6 @@ #![allow( clippy::wrong_self_convention, - clippy::clippy::upper_case_acronyms, + clippy::upper_case_acronyms, clippy::needless_question_mark )] diff --git a/query-engine/core/src/interpreter/query_interpreters/inmemory_record_processor.rs b/query-engine/core/src/interpreter/query_interpreters/inmemory_record_processor.rs index 819a64bc859a..bd2195cd63f0 100644 --- a/query-engine/core/src/interpreter/query_interpreters/inmemory_record_processor.rs +++ b/query-engine/core/src/interpreter/query_interpreters/inmemory_record_processor.rs @@ -32,7 +32,7 @@ impl InMemoryRecordProcessor { } fn take_abs(&self) -> Option { - self.take.clone().map(|t| if t < 0 { -t } else { t }) + self.take.map(|t| if t < 0 { -t } else { t }) } /// Checks whether or not we need to take records going backwards in the record list, diff --git a/query-engine/core/src/interpreter/query_interpreters/nested_read.rs b/query-engine/core/src/interpreter/query_interpreters/nested_read.rs index edc1baef40e4..d8cb7842e08d 100644 --- a/query-engine/core/src/interpreter/query_interpreters/nested_read.rs +++ b/query-engine/core/src/interpreter/query_interpreters/nested_read.rs @@ -138,6 +138,7 @@ pub async fn m2m<'a, 'b>( selected_fields, processor ))] +#[allow(clippy::too_many_arguments)] pub async fn one2m<'a, 'b>( tx: &'a ConnectionLike<'a, 'b>, parent_field: &RelationFieldRef, diff --git a/query-engine/core/src/query_graph/mod.rs b/query-engine/core/src/query_graph/mod.rs index 048e9861800b..f7193812d332 100644 --- a/query-engine/core/src/query_graph/mod.rs +++ b/query-engine/core/src/query_graph/mod.rs @@ -523,7 +523,7 @@ impl QueryGraph { trace!("[Graph][Swap] Before shape: {}", self); } - let mut marked = std::mem::replace(&mut self.marked_node_pairs, vec![]); + let mut marked = std::mem::take(&mut self.marked_node_pairs); marked.reverse(); // Todo: Marked operation order is currently breaking if done bottom-up. Investigate how to fix it. for (parent_node, child_node) in marked { diff --git a/query-engine/core/src/response_ir/internal.rs b/query-engine/core/src/response_ir/internal.rs index b8defaf11a6e..eed6fce17cca 100644 --- a/query-engine/core/src/response_ir/internal.rs +++ b/query-engine/core/src/response_ir/internal.rs @@ -289,7 +289,7 @@ fn serialize_objects( ) -> crate::Result { // The way our query execution works, we only need to look at nested + lists if we hit an object. // Move nested out of result for separate processing. - let nested = std::mem::replace(&mut result.nested, Vec::new()); + let nested = std::mem::take(&mut result.nested); // { -> { parent ID -> items } } let mut nested_mapping: HashMap = process_nested_results(nested, &typ)?; diff --git a/query-engine/core/src/schema_builder/output_types/mutation_type.rs b/query-engine/core/src/schema_builder/output_types/mutation_type.rs index 18a3704e67e5..ccf2e1a1184b 100644 --- a/query-engine/core/src/schema_builder/output_types/mutation_type.rs +++ b/query-engine/core/src/schema_builder/output_types/mutation_type.rs @@ -48,8 +48,8 @@ pub(crate) fn build(ctx: &mut BuilderContext) -> (OutputType, ObjectTypeStrongRe // implementation note: these need to be in the same function, because these vecs interact: the create inputs will enqueue update inputs, and vice versa. #[tracing::instrument(skip(ctx))] fn create_nested_inputs(ctx: &mut BuilderContext) { - let mut nested_create_inputs_queue = std::mem::replace(&mut ctx.nested_create_inputs_queue, Vec::new()); - let mut nested_update_inputs_queue = std::mem::replace(&mut ctx.nested_update_inputs_queue, Vec::new()); + let mut nested_create_inputs_queue = std::mem::take(&mut ctx.nested_create_inputs_queue); + let mut nested_update_inputs_queue = std::mem::take(&mut ctx.nested_update_inputs_queue); while !(nested_create_inputs_queue.is_empty() && nested_update_inputs_queue.is_empty()) { // Create inputs. diff --git a/query-engine/request-handlers/src/dmmf/schema/mod.rs b/query-engine/request-handlers/src/dmmf/schema/mod.rs index b6bc1537cd79..b2c9d5d4fa46 100644 --- a/query-engine/request-handlers/src/dmmf/schema/mod.rs +++ b/query-engine/request-handlers/src/dmmf/schema/mod.rs @@ -29,7 +29,7 @@ impl QuerySchemaRenderer<(DmmfSchema, DmmfOperationMappings)> for DmmfQuerySchem ctx.mark_to_be_rendered(&query_schema); while !ctx.next_pass.is_empty() { - let renderers = std::mem::replace(&mut ctx.next_pass, Vec::new()); + let renderers = std::mem::take(&mut ctx.next_pass); for renderer in renderers { renderer.render(&mut ctx) diff --git a/query-engine/request-handlers/src/tests/dmmf/aggregation.rs b/query-engine/request-handlers/src/tests/dmmf/aggregation.rs index 4dda502899d1..7562ed8fa4ac 100644 --- a/query-engine/request-handlers/src/tests/dmmf/aggregation.rs +++ b/query-engine/request-handlers/src/tests/dmmf/aggregation.rs @@ -38,27 +38,27 @@ fn nullable_fields_should_be_nullable_in_group_by_output_types() { parent_type.name.as_str(), ) { (TypeLocation::Scalar, false, _) => match field.name.as_str() { - "required_id" => assert_eq!(is_nullable, false), - "optional_string" => assert_eq!(is_nullable, true), - "required_string" => assert_eq!(is_nullable, false), - "optional_int" => assert_eq!(is_nullable, true), - "required_int" => assert_eq!(is_nullable, false), + "required_id" => assert!(is_nullable), + "optional_string" => assert!(is_nullable), + "required_string" => assert!(is_nullable), + "optional_int" => assert!(is_nullable), + "required_int" => assert!(is_nullable), _ => (), }, (TypeLocation::Scalar, true, "BlogCountAggregateOutputType") => match field.name.as_str() { - "required_id" => assert_eq!(is_nullable, false), - "optional_string" => assert_eq!(is_nullable, false), - "required_string" => assert_eq!(is_nullable, false), - "optional_int" => assert_eq!(is_nullable, false), - "required_int" => assert_eq!(is_nullable, false), + "required_id" => assert!(is_nullable), + "optional_string" => assert!(is_nullable), + "required_string" => assert!(is_nullable), + "optional_int" => assert!(is_nullable), + "required_int" => assert!(is_nullable), _ => (), }, (TypeLocation::Scalar, true, _) => match field.name.as_str() { - "required_id" => assert_eq!(is_nullable, true), - "optional_string" => assert_eq!(is_nullable, true), - "required_string" => assert_eq!(is_nullable, true), - "optional_int" => assert_eq!(is_nullable, true), - "required_int" => assert_eq!(is_nullable, true), + "required_id" => assert!(is_nullable), + "optional_string" => assert!(is_nullable), + "required_string" => assert!(is_nullable), + "optional_int" => assert!(is_nullable), + "required_int" => assert!(is_nullable), _ => (), }, _ => (), diff --git a/query-engine/request-handlers/src/tests/dmmf/ignored.rs b/query-engine/request-handlers/src/tests/dmmf/ignored.rs index 6ca1302b3a35..5f37a434e73b 100644 --- a/query-engine/request-handlers/src/tests/dmmf/ignored.rs +++ b/query-engine/request-handlers/src/tests/dmmf/ignored.rs @@ -351,7 +351,7 @@ fn ignored_models_should_be_filtered() { let mutation = find_output_type(&dmmf, PRISMA_NAMESPACE, "Mutation"); let has_no_inputs = dmmf.schema.input_object_types.get(PRISMA_NAMESPACE).is_none(); - assert_eq!(has_no_inputs, true); + assert!(has_no_inputs); assert_eq!(query.fields.len(), 0); assert_eq!(mutation.fields.len(), 0); } From d526152d99804318aef6f13bdfa29b08436654cc Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 21 Jun 2021 18:52:18 +0300 Subject: [PATCH 39/47] Make legacy compound defaults work again --- libs/datamodel/connectors/dml/src/field.rs | 7 +++ .../ast_to_dml/standardise_parsing.rs | 47 +++++++++++++------ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index bf3714949cf4..d2a22a17f540 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -112,6 +112,13 @@ impl Field { } } + pub fn as_relation_field_mut(&mut self) -> Option<&mut RelationField> { + match self { + Field::RelationField(ref mut rf) => Some(rf), + _ => None, + } + } + pub fn as_scalar_field(&self) -> Option<&ScalarField> { match self { Field::ScalarField(sf) => Some(sf), diff --git a/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs b/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs index 767a5691cc9a..a4032e125b37 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use ::dml::{field::FieldArity, relation_info::ReferentialAction}; +use ::dml::relation_info::ReferentialAction; use super::common::*; use crate::{ @@ -35,30 +35,49 @@ impl<'a> StandardiserForParsing<'a> { return; } - for model in schema.models_mut() { - for field in model.fields_mut() { + let mut modifications = Vec::new(); + + for (model_id, model) in schema.models().enumerate() { + for (field_id, field) in model.fields().enumerate() { match field { Field::RelationField(field) if field.is_singular() => { if field.relation_info.on_delete.is_some() || field.relation_info.on_update.is_some() { continue; } - field.relation_info.on_update = Some(ReferentialAction::Cascade); - field.relation_info.on_delete = Some({ - match field.arity { - FieldArity::Required => ReferentialAction::Cascade, - _ => ReferentialAction::SetNull, - } - }); - // So our validator won't get a stroke when seeing the - // values set without having the preview feature - // enabled. Remove this before GA. - field.relation_info.legacy_referential_actions(); + let some_required = field + .relation_info + .fields + .iter() + .flat_map(|name| model.find_field(name)) + .any(|field| field.arity().is_required()); + + let on_delete = if some_required { + ReferentialAction::Cascade + } else { + ReferentialAction::SetNull + }; + + modifications.push((model_id, field_id, on_delete)); } _ => (), } } } + + for (model_id, field_id, on_delete) in modifications { + let mut field = schema.models[model_id].fields[field_id] + .as_relation_field_mut() + .unwrap(); + + field.relation_info.on_update = Some(ReferentialAction::Cascade); + field.relation_info.on_delete = Some(on_delete); + + // So our validator won't get a stroke when seeing the + // values set without having the preview feature + // enabled. Remove this before GA. + field.relation_info.legacy_referential_actions(); + } } /// For M2M relations set the references to the @id fields of the foreign model. From 7931c4c2060b45e13b6a41b4617c84a5d6fd3336 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 22 Jun 2021 10:09:01 +0300 Subject: [PATCH 40/47] Set referential action for compounds correctly If even one underlying scalar field is required, the default referential action should not be `SetNull`. --- .../src/introspection_helpers.rs | 16 +- .../tests/re_introspection/mod.rs | 55 ++++ libs/datamodel/connectors/dml/src/field.rs | 13 +- .../core/src/transform/ast_to_dml/lift.rs | 3 +- .../ast_to_dml/standardise_parsing.rs | 59 ++-- .../tests/migrations/relations.rs | 266 +++++++++++++++++- 6 files changed, 375 insertions(+), 37 deletions(-) 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 905cd4d52d20..8a888264b378 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/introspection_helpers.rs @@ -119,7 +119,7 @@ pub fn calculate_many_to_many_field( false => basename, }; - RelationField::new(&name, FieldArity::List, relation_info) + RelationField::new(&name, FieldArity::List, FieldArity::List, relation_info) } pub(crate) fn calculate_index(index: &Index) -> IndexDefinition { @@ -205,7 +205,17 @@ pub(crate) fn calculate_relation_field( false => FieldArity::Required, }; - Ok(RelationField::new(&foreign_key.referenced_table, arity, relation_info)) + let calculated_arity = match columns.iter().any(|c| c.is_required()) { + true => FieldArity::Required, + false => arity, + }; + + Ok(RelationField::new( + &foreign_key.referenced_table, + arity, + calculated_arity, + relation_info, + )) } pub(crate) fn calculate_backrelation_field( @@ -250,7 +260,7 @@ pub(crate) fn calculate_backrelation_field( model.name.clone() }; - Ok(RelationField::new(&name, arity, new_relation_info)) + Ok(RelationField::new(&name, arity, arity, new_relation_info)) } } } 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 dc3f99851943..e4e1e79f0b7b 100644 --- a/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/re_introspection/mod.rs @@ -1728,6 +1728,61 @@ async fn do_not_try_to_keep_custom_many_to_many_self_relation_names(api: &TestAp Ok(()) } +#[test_connector] +async fn legacy_referential_actions(api: &TestApi) -> TestResult { + let family = api.sql_family(); + + api.barrel() + .execute(move |migration| { + migration.create_table("a", |t| { + t.add_column("id", types::primary()); + }); + + migration.create_table("b", move |t| { + t.add_column("id", types::primary()); + t.add_column("a_id", types::integer().nullable(false)); + + match family { + SqlFamily::Mssql => { + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES legacy_referential_actions.a(id) ON DELETE NO ACTION ON UPDATE NO ACTION", + ); + } + _ => { + t.inject_custom( + "CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE NO ACTION ON UPDATE NO ACTION", + ); + } + } + }); + }) + .await?; + + let extra_index = if api.sql_family().is_mysql() { + r#"@@index([a_id], name: "asdf")"# + } else { + "" + }; + + let input_dm = formatdoc! {r#" + model a {{ + id Int @id @default(autoincrement()) + bs b[] @relation("changed") + }} + + model b {{ + id Int @id @default(autoincrement()) + a_id Int + a a @relation("changed", fields: [a_id], references: [id]) + {} + }} + "#, extra_index}; + + api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?); + + Ok(()) +} + #[test_connector] async fn referential_actions(api: &TestApi) -> TestResult { let family = api.sql_family(); diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index d2a22a17f540..e7458667f541 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -255,6 +255,9 @@ pub struct RelationField { /// The field's arity. pub arity: FieldArity, + /// The arity of underlying fields for referential actions. + pub referential_arity: FieldArity, + /// Comments associated with this field. pub documentation: Option, @@ -279,6 +282,7 @@ impl PartialEq for RelationField { fn eq(&self, other: &Self) -> bool { let this_matches = self.name == other.name && self.arity == other.arity + && self.referential_arity == other.referential_arity && self.documentation == other.documentation && self.is_generated == other.is_generated && self.is_commented_out == other.is_commented_out @@ -315,10 +319,11 @@ impl PartialEq for RelationField { impl RelationField { /// Creates a new field with the given name and type. - pub fn new(name: &str, arity: FieldArity, relation_info: RelationInfo) -> Self { + pub fn new(name: &str, arity: FieldArity, calculated_arity: FieldArity, relation_info: RelationInfo) -> Self { RelationField { name: String::from(name), arity, + referential_arity: calculated_arity, relation_info, documentation: None, is_generated: false, @@ -347,7 +352,7 @@ impl RelationField { FieldArity::Optional }; - let mut field = Self::new(name, arity, info); + let mut field = Self::new(name, arity, arity, info); field.is_generated = true; field @@ -376,7 +381,7 @@ impl RelationField { pub fn default_on_delete_action(&self) -> ReferentialAction { use ReferentialAction::*; - match self.arity { + match self.referential_arity { FieldArity::Required if self.supports_restrict_action.unwrap_or(true) => Restrict, FieldArity::Required => NoAction, _ => SetNull, @@ -386,7 +391,7 @@ impl RelationField { pub fn default_on_update_action(&self) -> ReferentialAction { use ReferentialAction::*; - match self.arity { + match self.referential_arity { _ if !self.virtual_referential_actions.unwrap_or(false) => Cascade, FieldArity::Required => Restrict, _ => SetNull, diff --git a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs index ac46c9b764e2..b5a0edc61793 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs @@ -167,7 +167,8 @@ impl<'a> LiftAstToDml<'a> { let mut field = match field_type { FieldType::Relation(info) => { let arity = self.lift_field_arity(&ast_field.arity); - let mut field = dml::RelationField::new(&ast_field.name.name, arity, info); + + let mut field = dml::RelationField::new(&ast_field.name.name, arity, arity, info); if let Some(ref source) = self.source { field.supports_restrict_action( diff --git a/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs b/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs index a4032e125b37..86958b0e70e4 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/standardise_parsing.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use ::dml::relation_info::ReferentialAction; +use ::dml::{field::FieldArity, relation_info::ReferentialAction}; use super::common::*; use crate::{ @@ -25,26 +25,19 @@ impl<'a> StandardiserForParsing<'a> { pub fn standardise(&self, schema: &mut dml::Datamodel) -> Result<(), Diagnostics> { self.name_unnamed_relations(schema); self.set_relation_to_field_to_id_if_missing_for_m2m_relations(schema); + self.set_referential_arities(schema); self.set_default_referential_actions(schema); Ok(()) } - fn set_default_referential_actions(&self, schema: &mut dml::Datamodel) { - if self.preview_features.contains(&PreviewFeature::ReferentialActions) { - return; - } - + fn set_referential_arities(&self, schema: &mut dml::Datamodel) { let mut modifications = Vec::new(); for (model_id, model) in schema.models().enumerate() { for (field_id, field) in model.fields().enumerate() { match field { Field::RelationField(field) if field.is_singular() => { - if field.relation_info.on_delete.is_some() || field.relation_info.on_update.is_some() { - continue; - } - let some_required = field .relation_info .fields @@ -52,31 +45,55 @@ impl<'a> StandardiserForParsing<'a> { .flat_map(|name| model.find_field(name)) .any(|field| field.arity().is_required()); - let on_delete = if some_required { - ReferentialAction::Cascade + let arity = if some_required { + FieldArity::Required } else { - ReferentialAction::SetNull + field.arity }; - modifications.push((model_id, field_id, on_delete)); + modifications.push((model_id, field_id, arity)); } _ => (), } } } - for (model_id, field_id, on_delete) in modifications { + for (model_id, field_id, arity) in modifications { let mut field = schema.models[model_id].fields[field_id] .as_relation_field_mut() .unwrap(); - field.relation_info.on_update = Some(ReferentialAction::Cascade); - field.relation_info.on_delete = Some(on_delete); + field.referential_arity = arity; + } + } - // So our validator won't get a stroke when seeing the - // values set without having the preview feature - // enabled. Remove this before GA. - field.relation_info.legacy_referential_actions(); + fn set_default_referential_actions(&self, schema: &mut dml::Datamodel) { + if self.preview_features.contains(&PreviewFeature::ReferentialActions) { + return; + } + + for model in schema.models_mut() { + for field in model.fields_mut() { + match field { + Field::RelationField(field) if field.is_singular() => { + if field.relation_info.on_delete.is_some() || field.relation_info.on_update.is_some() { + continue; + } + + field.relation_info.on_update = Some(ReferentialAction::Cascade); + field.relation_info.on_delete = Some(match field.referential_arity { + FieldArity::Required => ReferentialAction::Cascade, + _ => ReferentialAction::SetNull, + }); + + // So our validator won't get a stroke when seeing the + // values set without having the preview feature + // enabled. Remove this before GA. + field.relation_info.legacy_referential_actions(); + } + _ => (), + } + } } } diff --git a/migration-engine/migration-engine-tests/tests/migrations/relations.rs b/migration-engine/migration-engine-tests/tests/migrations/relations.rs index 4561f7fb2030..7b4e0a7058d2 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/relations.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/relations.rs @@ -121,18 +121,13 @@ fn specifying_a_db_name_for_an_inline_relation_must_work(api: TestApi) { }); } -#[test_connector(preview_features("referentialActions"))] +#[test_connector] fn adding_an_inline_relation_to_a_model_with_an_exotic_id_type(api: TestApi) { let dm1 = r#" - generator client { - provider = "prisma-client-js" - previewFeatures = ["referentialActions"] - } - model A { id Int @id b_id String - b B @relation(fields: [b_id], references: [id], onDelete: NoAction) + b B @relation(fields: [b_id], references: [id]) } model B { @@ -148,7 +143,7 @@ fn adding_an_inline_relation_to_a_model_with_an_exotic_id_type(api: TestApi) { .assert_fk_on_columns(&["b_id"], |fk| { fk.assert_references("B", &["id"]) .assert_referential_action_on_update(ForeignKeyAction::Cascade) - .assert_referential_action_on_delete(ForeignKeyAction::NoAction) + .assert_referential_action_on_delete(ForeignKeyAction::Cascade) }) }); } @@ -661,3 +656,258 @@ fn on_update_restrict_should_work(api: TestApi) { }) }); } + +#[test_connector(exclude(Mssql), preview_features("referentialActions"))] +fn on_delete_required_default_action(api: TestApi) { + let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model A { + id Int @id + b B[] + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id]) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table("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(tags(Mssql), preview_features("referentialActions"))] +fn on_delete_required_default_action_with_no_restrict(api: TestApi) { + let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model A { + id Int @id + b B[] + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id]) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table("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::NoAction) + }) + }); +} + +#[test_connector(preview_features("referentialActions"))] +fn on_delete_optional_default_action(api: TestApi) { + let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model A { + id Int @id + b B[] + } + + model B { + id Int @id + aId Int? + a A? @relation(fields: [aId], references: [id]) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table("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::SetNull) + }) + }); +} + +#[test_connector(preview_features("referentialActions"))] +fn on_delete_compound_optional_optional_default_action(api: TestApi) { + let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model A { + id Int @id + id2 Int + b B[] + @@unique([id, id2]) + } + + model B { + id Int @id + aId1 Int? + aId2 Int? + a A? @relation(fields: [aId1, aId2], references: [id, id2]) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table("B", |table| { + table + .assert_foreign_keys_count(1) + .assert_fk_on_columns(&["aId1", "aId2"], |fk| { + fk.assert_references("A", &["id", "id2"]) + .assert_referential_action_on_delete(ForeignKeyAction::SetNull) + }) + }); +} + +#[test_connector(exclude(Mssql), preview_features("referentialActions"))] +fn on_delete_compound_required_optional_default_action_with_restrict(api: TestApi) { + let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model A { + id Int @id + id2 Int + b B[] + @@unique([id, id2]) + } + + model B { + id Int @id + aId1 Int? + aId2 Int + a A? @relation(fields: [aId1, aId2], references: [id, id2]) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table("B", |table| { + table + .assert_foreign_keys_count(1) + .assert_fk_on_columns(&["aId1", "aId2"], |fk| { + fk.assert_references("A", &["id", "id2"]) + .assert_referential_action_on_delete(ForeignKeyAction::Restrict) + }) + }); +} + +#[test_connector(tags(Mssql), preview_features("referentialActions"))] +fn on_delete_compound_required_optional_default_action_without_restrict(api: TestApi) { + let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model A { + id Int @id + id2 Int + b B[] + @@unique([id, id2]) + } + + model B { + id Int @id + aId1 Int? + aId2 Int + a A? @relation(fields: [aId1, aId2], references: [id, id2]) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table("B", |table| { + table + .assert_foreign_keys_count(1) + .assert_fk_on_columns(&["aId1", "aId2"], |fk| { + fk.assert_references("A", &["id", "id2"]) + .assert_referential_action_on_delete(ForeignKeyAction::NoAction) + }) + }); +} + +#[test_connector(preview_features("referentialActions"))] +fn on_update_optional_default_action(api: TestApi) { + let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model A { + id Int @id + b B[] + } + + model B { + id Int @id + aId Int? + a A? @relation(fields: [aId], references: [id]) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table("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::Cascade) + }) + }); +} + +#[test_connector(preview_features("referentialActions"))] +fn on_update_required_default_action(api: TestApi) { + let dm = r#" + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model A { + id Int @id + b B[] + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id]) + } + "#; + + api.schema_push(dm).send_sync().assert_green_bang(); + + api.assert_schema().assert_table("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::Cascade) + }) + }); +} From b5bd2f1b41b7a571d6a46f48202ede24a85b306e Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 22 Jun 2021 10:39:53 +0300 Subject: [PATCH 41/47] Nyeh nyeh test fix --- .../src/calculate_datamodel.rs | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) 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 ad65b1715e93..20f46af8b622 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs @@ -62,7 +62,7 @@ mod tests { use super::*; use datamodel::{ dml, Datamodel, DefaultValue as DMLDefault, Field, FieldArity, FieldType, Model, NativeTypeInstance, - RelationField, RelationInfo, ScalarField, ScalarType, ValueGenerator, + ReferentialAction, RelationField, RelationInfo, ScalarField, ScalarType, ValueGenerator, }; use native_types::{NativeType, PostgresType}; use pretty_assertions::assert_eq; @@ -551,20 +551,26 @@ mod tests { is_commented_out: false, is_ignored: false, }), - Field::RelationField(RelationField::new( - "City", - FieldArity::Required, - FieldArity::Required, - RelationInfo { + Field::RelationField(RelationField { + name: "City".into(), + arity: FieldArity::Required, + referential_arity: FieldArity::Required, + documentation: None, + is_generated: false, + is_commented_out: false, + is_ignored: false, + supports_restrict_action: Some(true), + virtual_referential_actions: None, + relation_info: RelationInfo { name: "CityToUser".to_string(), to: "City".to_string(), fields: vec!["city_id".to_string(), "city_name".to_string()], references: vec!["id".to_string(), "name".to_string()], - on_delete: None, - on_update: None, + on_delete: Some(ReferentialAction::NoAction), + on_update: Some(ReferentialAction::NoAction), legacy_referential_actions: false, }, - )), + }), ], is_generated: false, indices: vec![], @@ -911,20 +917,26 @@ mod tests { }, ), )), - Field::RelationField(RelationField::new( - "City", - FieldArity::Required, - FieldArity::Required, - RelationInfo { + Field::RelationField(RelationField { + name: "City".into(), + arity: FieldArity::Required, + referential_arity: FieldArity::Required, + documentation: None, + is_generated: false, + is_commented_out: false, + is_ignored: false, + supports_restrict_action: Some(true), + virtual_referential_actions: None, + relation_info: RelationInfo { name: "CityToUser".to_string(), to: "City".to_string(), fields: vec!["city_id".to_string()], references: vec!["id".to_string()], - on_delete: None, - on_update: None, + on_delete: Some(ReferentialAction::NoAction), + on_update: Some(ReferentialAction::NoAction), legacy_referential_actions: false, }, - )), + }), ], is_generated: false, indices: vec![], From d1b654cad35c39c19516d5877c00ea39e3f1475c Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 22 Jun 2021 11:11:37 +0300 Subject: [PATCH 42/47] LAST BROKEN TESTS --- .../relations/referential_actions.rs | 44 ------------------- .../tests/render_to_dmmf/files/general.json | 19 ++++---- .../files/without_relation_name.json | 5 +-- 3 files changed, 10 insertions(+), 58 deletions(-) diff --git a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs index 110b9549f3e6..e1a385862dfc 100644 --- a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs +++ b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs @@ -235,50 +235,6 @@ fn restrict_should_not_work_on_sql_server() { ]); } -#[test] -fn non_emulated_actions_should_not_work_on_mongo() { - let actions = &[(Cascade, 338), (SetDefault, 341)]; - - for (action, span) in actions { - let dml = formatdoc!( - r#" - datasource db {{ - provider = "mongodb" - url = "mongodb://" - }} - - generator client {{ - provider = "prisma-client-js" - previewFeatures = ["referentialActions"] - }} - - 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], onDelete: {}) - }} - "#, - action - ); - - let message = format!( - "Invalid referential action: `{}`. Allowed values: (`Restrict`, `NoAction`, `SetNull`)", - action - ); - - parse_error(&dml).assert_are(&[DatamodelError::new_attribute_validation_error( - &message, - "relation", - Span::new(272, *span), - )]); - } -} - #[test] fn concrete_actions_should_not_work_on_planetscale() { let actions = &[(Cascade, 411), (NoAction, 412), (SetDefault, 414)]; diff --git a/libs/datamodel/core/tests/render_to_dmmf/files/general.json b/libs/datamodel/core/tests/render_to_dmmf/files/general.json index ae1cf2e26a04..1b9a5eb9f120 100644 --- a/libs/datamodel/core/tests/render_to_dmmf/files/general.json +++ b/libs/datamodel/core/tests/render_to_dmmf/files/general.json @@ -90,7 +90,6 @@ "relationName": "author", "relationFromFields": [], "relationToFields": [], - "relationOnDelete": "NONE", "isGenerated": false, "isUpdatedAt": false }, @@ -107,7 +106,7 @@ "relationName": "ProfileToUser", "relationFromFields": [], "relationToFields": [], - "relationOnDelete": "NONE", + "relationOnDelete": "SetNull", "isGenerated": false, "isUpdatedAt": false } @@ -165,7 +164,7 @@ "relationToFields": [ "id" ], - "relationOnDelete": "NONE", + "relationOnDelete": "Cascade", "isGenerated": false, "isUpdatedAt": false }, @@ -304,7 +303,7 @@ "relationToFields": [ "id" ], - "relationOnDelete": "NONE", + "relationOnDelete": "Cascade", "isGenerated": false, "isUpdatedAt": false }, @@ -321,7 +320,6 @@ "relationName": "PostToPostToCategory", "relationFromFields": [], "relationToFields": [], - "relationOnDelete": "NONE", "isGenerated": false, "isUpdatedAt": false } @@ -378,7 +376,6 @@ "relationName": "CategoryToPostToCategory", "relationFromFields": [], "relationToFields": [], - "relationOnDelete": "NONE", "isGenerated": false, "isUpdatedAt": false }, @@ -477,7 +474,7 @@ "title", "createdAt" ], - "relationOnDelete": "NONE", + "relationOnDelete": "Cascade", "isGenerated": false, "isUpdatedAt": false }, @@ -498,7 +495,7 @@ "relationToFields": [ "id" ], - "relationOnDelete": "NONE", + "relationOnDelete": "Cascade", "isGenerated": false, "isUpdatedAt": false } @@ -559,7 +556,7 @@ "relationToFields": [ "id" ], - "relationOnDelete": "NONE", + "relationOnDelete": "Cascade", "isGenerated": false, "isUpdatedAt": false } @@ -600,7 +597,7 @@ "relationName": "AToB", "relationFromFields": [], "relationToFields": [], - "relationOnDelete": "NONE", + "relationOnDelete": "SetNull", "isGenerated": false, "isUpdatedAt": false } @@ -611,4 +608,4 @@ "uniqueIndexes": [] } ] -} \ No newline at end of file +} diff --git a/libs/datamodel/core/tests/render_to_dmmf/files/without_relation_name.json b/libs/datamodel/core/tests/render_to_dmmf/files/without_relation_name.json index 020ff07769ba..5ba52cd80d02 100644 --- a/libs/datamodel/core/tests/render_to_dmmf/files/without_relation_name.json +++ b/libs/datamodel/core/tests/render_to_dmmf/files/without_relation_name.json @@ -32,7 +32,6 @@ "relationName": "PostToUser", "relationFromFields": [], "relationToFields": [], - "relationOnDelete": "NONE", "isGenerated": false, "isUpdatedAt": false } @@ -90,7 +89,7 @@ "relationToFields": [ "id" ], - "relationOnDelete": "NONE", + "relationOnDelete": "Cascade", "isGenerated": false, "isUpdatedAt": false } @@ -101,4 +100,4 @@ "uniqueIndexes": [] } ] -} \ No newline at end of file +} From ed65a0ecc226bddfe64e2c2533f209bd09193941 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 22 Jun 2021 11:26:57 +0300 Subject: [PATCH 43/47] LAST LAST BROKEN TESTS --- .../src/tests/dmmf/aggregation.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/query-engine/request-handlers/src/tests/dmmf/aggregation.rs b/query-engine/request-handlers/src/tests/dmmf/aggregation.rs index 7562ed8fa4ac..ad8b48e751c0 100644 --- a/query-engine/request-handlers/src/tests/dmmf/aggregation.rs +++ b/query-engine/request-handlers/src/tests/dmmf/aggregation.rs @@ -38,19 +38,19 @@ fn nullable_fields_should_be_nullable_in_group_by_output_types() { parent_type.name.as_str(), ) { (TypeLocation::Scalar, false, _) => match field.name.as_str() { - "required_id" => assert!(is_nullable), + "required_id" => assert!(!is_nullable), "optional_string" => assert!(is_nullable), - "required_string" => assert!(is_nullable), + "required_string" => assert!(!is_nullable), "optional_int" => assert!(is_nullable), - "required_int" => assert!(is_nullable), + "required_int" => assert!(!is_nullable), _ => (), }, (TypeLocation::Scalar, true, "BlogCountAggregateOutputType") => match field.name.as_str() { - "required_id" => assert!(is_nullable), - "optional_string" => assert!(is_nullable), - "required_string" => assert!(is_nullable), - "optional_int" => assert!(is_nullable), - "required_int" => assert!(is_nullable), + "required_id" => assert!(!is_nullable), + "optional_string" => assert!(!is_nullable), + "required_string" => assert!(!is_nullable), + "optional_int" => assert!(!is_nullable), + "required_int" => assert!(!is_nullable), _ => (), }, (TypeLocation::Scalar, true, _) => match field.name.as_str() { From de24f6bf8a116035d17b2001f8573f59aaaafad8 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 22 Jun 2021 15:03:13 +0300 Subject: [PATCH 44/47] First round of review fixes --- .../src/calculate_datamodel.rs | 4 ++-- .../introspection-engine-tests/src/lib.rs | 2 +- .../introspection-engine-tests/src/test_api.rs | 2 +- .../connectors/datamodel-connector/src/lib.rs | 2 +- libs/datamodel/connectors/dml/src/field.rs | 10 +++++----- libs/datamodel/connectors/dml/src/relation_info.rs | 6 +++--- .../connectors/mongodb-datamodel-connector/src/lib.rs | 2 +- .../src/mysql_datamodel_connector.rs | 8 +------- libs/datamodel/core/src/transform/ast_to_dml/lift.rs | 2 +- 9 files changed, 16 insertions(+), 22 deletions(-) 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 a93e22179a5e..42d55d6a492f 100644 --- a/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs +++ b/introspection-engine/connectors/sql-introspection-connector/src/calculate_datamodel.rs @@ -568,7 +568,7 @@ mod tests { is_commented_out: false, is_ignored: false, supports_restrict_action: Some(true), - virtual_referential_actions: None, + emulates_referential_actions: None, relation_info: RelationInfo { name: "CityToUser".to_string(), to: "City".to_string(), @@ -941,7 +941,7 @@ mod tests { is_commented_out: false, is_ignored: false, supports_restrict_action: Some(true), - virtual_referential_actions: None, + emulates_referential_actions: None, relation_info: RelationInfo { name: "CityToUser".to_string(), to: "City".to_string(), diff --git a/introspection-engine/introspection-engine-tests/src/lib.rs b/introspection-engine/introspection-engine-tests/src/lib.rs index fe2c1eb2537b..f43a10f2a997 100644 --- a/introspection-engine/introspection-engine-tests/src/lib.rs +++ b/introspection-engine/introspection-engine-tests/src/lib.rs @@ -64,7 +64,7 @@ impl BarrelMigrationExecutor { return Ok(()); } - self.database.raw_cmd(dbg!(&full_sql)).await?; + self.database.raw_cmd(&full_sql).await?; Ok(()) } diff --git a/introspection-engine/introspection-engine-tests/src/test_api.rs b/introspection-engine/introspection-engine-tests/src/test_api.rs index 0c49cd566214..8805a28bd3fa 100644 --- a/introspection-engine/introspection-engine-tests/src/test_api.rs +++ b/introspection-engine/introspection-engine-tests/src/test_api.rs @@ -187,7 +187,7 @@ impl TestApi { #[track_caller] pub fn assert_eq_datamodels(&self, expected_without_header: &str, result_with_header: &str) { - let parsed_expected = datamodel::parse_datamodel(&self.dm_with_sources(dbg!(expected_without_header))) + let parsed_expected = datamodel::parse_datamodel(&self.dm_with_sources(expected_without_header)) .unwrap() .subject; diff --git a/libs/datamodel/connectors/datamodel-connector/src/lib.rs b/libs/datamodel/connectors/datamodel-connector/src/lib.rs index 3aeebbab1629..c22d6b446052 100644 --- a/libs/datamodel/connectors/datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/datamodel-connector/src/lib.rs @@ -28,7 +28,7 @@ pub trait Connector: Send + Sync { self.referential_actions().contains(action) } - fn virtual_referential_actions(&self) -> bool { + fn emulates_referential_actions(&self) -> bool { false } diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index b4e7da440a05..6eb469aa3fd8 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -269,7 +269,7 @@ pub struct RelationField { pub supports_restrict_action: Option, /// Do we run the referential actions in the core instead of the database. - pub virtual_referential_actions: Option, + pub emulates_referential_actions: Option, } impl PartialEq for RelationField { @@ -325,7 +325,7 @@ impl RelationField { is_commented_out: false, is_ignored: false, supports_restrict_action: None, - virtual_referential_actions: None, + emulates_referential_actions: None, } } @@ -335,8 +335,8 @@ impl RelationField { } /// The referential actions should be handled by the core. - pub fn virtual_referential_actions(&mut self, value: bool) { - self.virtual_referential_actions = Some(value); + pub fn emulates_referential_actions(&mut self, value: bool) { + self.emulates_referential_actions = Some(value); } /// Creates a new field with the given name and type, marked as generated and optional. @@ -387,7 +387,7 @@ impl RelationField { use ReferentialAction::*; match self.referential_arity { - _ if !self.virtual_referential_actions.unwrap_or(false) => Cascade, + _ if !self.emulates_referential_actions.unwrap_or(false) => Cascade, FieldArity::Required => Restrict, _ => SetNull, } diff --git a/libs/datamodel/connectors/dml/src/relation_info.rs b/libs/datamodel/connectors/dml/src/relation_info.rs index 913156e95c64..c296f1b78956 100644 --- a/libs/datamodel/connectors/dml/src/relation_info.rs +++ b/libs/datamodel/connectors/dml/src/relation_info.rs @@ -16,9 +16,10 @@ pub struct RelationInfo { /// a related node is deleted. pub on_delete: Option, /// A strategy indicating what happens when - /// a related node is deleted. + /// a related node is updated. pub on_update: Option, /// Set true if referential actions feature is not in use. + /// This prevents the datamodel validator nagging about the missing preview feature, when automatically setting the values. pub legacy_referential_actions: bool, } @@ -59,8 +60,7 @@ 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. + /// records are connected. Cascade, /// Prevents operation (both updates and deletes) from succeeding if any /// records are connected. This behavior will always result in a runtime diff --git a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs index 63866387f5eb..e09c6677cf1c 100644 --- a/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs +++ b/libs/datamodel/connectors/mongodb-datamodel-connector/src/lib.rs @@ -67,7 +67,7 @@ impl Connector for MongoDbDatamodelConnector { self.referential_actions } - fn virtual_referential_actions(&self) -> bool { + fn emulates_referential_actions(&self) -> bool { true } 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 9bb29faab35f..fd93e5eee8dc 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 @@ -202,7 +202,7 @@ impl Connector for MySqlDatamodelConnector { self.referential_actions } - fn virtual_referential_actions(&self) -> bool { + fn emulates_referential_actions(&self) -> bool { self.is_planetscale } @@ -453,9 +453,3 @@ impl Connector for MySqlDatamodelConnector { Ok(()) } } - -impl Default for MySqlDatamodelConnector { - fn default() -> Self { - Self::new(false) - } -} diff --git a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs index 91e04b56f87f..fda705db27a1 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/lift.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/lift.rs @@ -177,7 +177,7 @@ impl<'a> LiftAstToDml<'a> { .supports_referential_action(ReferentialAction::Restrict), ); - field.virtual_referential_actions(source.active_connector.virtual_referential_actions()); + field.emulates_referential_actions(source.active_connector.emulates_referential_actions()); } field.documentation = ast_field.documentation.clone().map(|comment| comment.text); From 288f9693e4f4273babaa59d39dc21c046656fc15 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 22 Jun 2021 15:15:05 +0300 Subject: [PATCH 45/47] Rename variable --- libs/datamodel/connectors/dml/src/field.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index 6eb469aa3fd8..aac746fe890a 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -314,11 +314,11 @@ impl PartialEq for RelationField { impl RelationField { /// Creates a new field with the given name and type. - pub fn new(name: &str, arity: FieldArity, calculated_arity: FieldArity, relation_info: RelationInfo) -> Self { + pub fn new(name: &str, arity: FieldArity, referential_arity: FieldArity, relation_info: RelationInfo) -> Self { RelationField { name: String::from(name), arity, - referential_arity: calculated_arity, + referential_arity, relation_info, documentation: None, is_generated: false, From ea849b2c7f9c0c97e2a833ae4a2a74d9df2b40ea Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 22 Jun 2021 16:51:43 +0300 Subject: [PATCH 46/47] Clone -> cloned --- libs/datamodel/core/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/datamodel/core/src/lib.rs b/libs/datamodel/core/src/lib.rs index 97ad2b62ce55..39af451045ba 100644 --- a/libs/datamodel/core/src/lib.rs +++ b/libs/datamodel/core/src/lib.rs @@ -265,6 +265,6 @@ fn render_schema_ast_to(stream: &mut dyn std::fmt::Write, schema: &ast::SchemaAs fn preview_features(generators: &[Generator]) -> HashSet { generators .iter() - .flat_map(|gen| gen.preview_features.iter().map(Clone::clone)) + .flat_map(|gen| gen.preview_features.iter().cloned()) .collect() } From de3b045c251b86b3de0eddd3aeef26410c8ea0ea Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 22 Jun 2021 19:20:02 +0300 Subject: [PATCH 47/47] Validation error if ref actions on both sides --- .../core/src/transform/ast_to_dml/validate.rs | 16 ++++++++ .../relations/referential_actions.rs | 37 +++++++++++++++++++ 2 files changed, 53 insertions(+) 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 86303ec9e8b9..2206c970ea46 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/validate.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/validate.rs @@ -973,6 +973,22 @@ impl<'a> Validator<'a> { )); } + if self.preview_features.contains(&PreviewFeature::ReferentialActions) + && (rel_info.on_delete.is_some() || rel_info.on_update.is_some()) + && (related_field_rel_info.on_delete.is_some() || related_field_rel_info.on_update.is_some()) + { + let message = format!( + "The relation fields `{}` on Model `{}` and `{}` on Model `{}` both provide the `onDelete` or `onUpdate` argument in the {} attribute. You have to provide it only on one of the two fields.", + &field.name, &model.name, &related_field.name, &related_model.name, RELATION_ATTRIBUTE_NAME_WITH_AT + ); + + errors.push_error(DatamodelError::new_attribute_validation_error( + &message, + RELATION_ATTRIBUTE_NAME, + field_span, + )); + } + if !rel_info.fields.is_empty() && !related_field_rel_info.fields.is_empty() { errors.push_error(DatamodelError::new_attribute_validation_error( &format!( diff --git a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs index e1a385862dfc..538d76d1e397 100644 --- a/libs/datamodel/core/tests/attributes/relations/referential_actions.rs +++ b/libs/datamodel/core/tests/attributes/relations/referential_actions.rs @@ -235,6 +235,43 @@ fn restrict_should_not_work_on_sql_server() { ]); } +#[test] +fn actions_should_be_defined_only_from_one_side() { + let dml = indoc! { r#" + datasource db { + provider = "sqlserver" + url = "sqlserver://" + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialActions"] + } + + model A { + id Int @id + b B? @relation(onUpdate: NoAction, onDelete: NoAction) + } + + model B { + id Int @id + aId Int + a A @relation(fields: [aId], references: [id], onUpdate: NoAction, onDelete: NoAction) + } + "#}; + + let message1 = + "The relation fields `b` on Model `A` and `a` on Model `B` both provide the `onDelete` or `onUpdate` argument in the @relation attribute. You have to provide it only on one of the two fields."; + + let message2 = + "The relation fields `a` on Model `B` and `b` on Model `A` both provide the `onDelete` or `onUpdate` argument in the @relation attribute. You have to provide it only on one of the two fields."; + + parse_error(dml).assert_are(&[ + DatamodelError::new_attribute_validation_error(&message1, "relation", Span::new(201, 256)), + DatamodelError::new_attribute_validation_error(&message2, "relation", Span::new(300, 387)), + ]); +} + #[test] fn concrete_actions_should_not_work_on_planetscale() { let actions = &[(Cascade, 411), (NoAction, 412), (SetDefault, 414)];