Skip to content

Commit

Permalink
Merge pull request #3298 from prisma/fix/set-null-validation
Browse files Browse the repository at this point in the history
feat(validation): disallow `SetNull` referential action on required referenced fields
  • Loading branch information
jkomyno committed Oct 20, 2022
2 parents bc452eb + 2f30b37 commit abd71fc
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 240 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ async fn referential_actions_are_kept_intact(api: &TestApi) -> TestResult {
CREATE TABLE `B` (
id INT AUTO_INCREMENT PRIMARY KEY,
aId INT NOT NULL
aId INT NULL
);
"#};

Expand All @@ -403,9 +403,9 @@ async fn referential_actions_are_kept_intact(api: &TestApi) -> TestResult {
}
model B {
id Int @id @default(autoincrement())
aId Int
a A @relation(fields: [aId], references: [id], onDelete: SetNull)
id Int @id @default(autoincrement())
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: Restrict)
}
"#};

Expand All @@ -416,9 +416,9 @@ async fn referential_actions_are_kept_intact(api: &TestApi) -> TestResult {
}
model B {
id Int @id @default(autoincrement())
aId Int
a A @relation(fields: [aId], references: [id], onDelete: SetNull)
id Int @id @default(autoincrement())
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: Restrict)
}
"#]];

Expand Down
3 changes: 3 additions & 0 deletions psl/psl-core/src/validate/validation_pipeline/validations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ pub(super) fn validate(ctx: &mut Context<'_>) {
relations::field_arity(relation, ctx);
relations::referencing_scalar_field_types(relation, ctx);
relations::has_a_unique_constraint_name(&names, relation, ctx);
relations::required_relation_cannot_use_set_null(relation, ctx);

if relation.is_one_to_one() {
relations::one_to_one::both_sides_are_defined(relation, ctx);
Expand All @@ -170,6 +171,7 @@ pub(super) fn validate(ctx: &mut Context<'_>) {
relations::one_to_many::referential_actions(relation, ctx);
}
}

RefinedRelationWalker::ImplicitManyToMany(relation) => {
use relations::many_to_many::implicit;

Expand All @@ -178,6 +180,7 @@ pub(super) fn validate(ctx: &mut Context<'_>) {
implicit::validate_no_referential_actions(relation, ctx);
implicit::cannot_define_references_argument(relation, ctx);
}

RefinedRelationWalker::TwoWayEmbeddedManyToMany(relation) => {
use relations::many_to_many::embedded;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ mod visited_relation;
use super::constraint_namespace::ConstraintName;
use crate::datamodel_connector::{walker_ext_traits::*, Connector, ConnectorCapability, RelationMode};
use crate::{diagnostics::DatamodelError, validate::validation_pipeline::context::Context};
use indoc::indoc;
use itertools::Itertools;
use parser_database::ReferentialAction;
use parser_database::{
ast::WithSpan,
walkers::{CompleteInlineRelationWalker, InlineRelationWalker, RelationFieldWalker},
Expand Down Expand Up @@ -525,3 +527,39 @@ fn is_empty_fields<T>(fields: Option<impl ExactSizeIterator<Item = T>>) -> bool
Some(fields) => fields.len() == 0,
}
}

/// There cannot be any required field in a relation where one of the referential actions is SetNull.
pub(crate) fn required_relation_cannot_use_set_null(relation: InlineRelationWalker<'_>, ctx: &mut Context<'_>) {
// return early if there's no relation field on the referencing model
let forward = match relation.forward_relation_field() {
Some(forward) => forward,
None => return,
};

// return early if no referencing field is required
if forward
.referencing_fields()
.map(|mut fields| fields.all(|f| !f.ast_field().arity.is_required()))
.unwrap_or_default()
{
return;
}

if let Some(ReferentialAction::SetNull) = forward.explicit_on_delete() {
ctx.push_error(DatamodelError::new_attribute_validation_error(
indoc! {"The `onDelete` referential action of a relation must not be set to `SetNull` when a referenced field is required.
Either choose another referential action, or make the referenced fields optional."},
RELATION_ATTRIBUTE_NAME,
forward.ast_field().span(),
))
}

if let Some(ReferentialAction::SetNull) = forward.explicit_on_update() {
ctx.push_error(DatamodelError::new_attribute_validation_error(
indoc! {"The `onUpdate` referential action of a relation must not be set to `SetNull` when a referenced field is required.
Either choose another referential action, or make the referenced fields optional."},
RELATION_ATTRIBUTE_NAME,
forward.ast_field().span(),
))
}
}
54 changes: 27 additions & 27 deletions psl/psl/tests/attributes/relations/referential_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ fn on_delete_actions() {
}}
model B {{
id Int @id
aId Int
a A @relation(fields: [aId], references: [id], onDelete: {})
id Int @id
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: {})
}}
"#,
action
Expand All @@ -47,9 +47,9 @@ fn on_update_actions() {
}}
model B {{
id Int @id
aId Int
a A @relation(fields: [aId], references: [id], onUpdate: {})
id Int @id
aId Int?
a A? @relation(fields: [aId], references: [id], onUpdate: {})
}}
"#,
action
Expand Down Expand Up @@ -80,9 +80,9 @@ fn actions_on_mongo() {
}}
model B {{
id Int @id @map("_id")
aId Int
a A @relation(fields: [aId], references: [id], onDelete: {action})
id Int @id @map("_id")
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: {action})
}}
"#,
action = action
Expand Down Expand Up @@ -119,9 +119,9 @@ fn actions_on_mysql_with_prisma_relation_mode() {
}}
model B {{
id Int @id
aId Int
a A @relation(fields: [aId], references: [id], onDelete: {action})
id Int @id
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: {action})
}}
"#,
action = action
Expand Down Expand Up @@ -158,9 +158,9 @@ fn actions_on_sqlserver_with_prisma_relation_mode() {
}}
model B {{
id Int @id
aId Int
a A @relation(fields: [aId], references: [id], onDelete: {action})
id Int @id
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: {action})
}}
"#,
action = action
Expand Down Expand Up @@ -197,9 +197,9 @@ fn actions_on_cockroachdb_with_prisma_relation_mode() {
}}
model B {{
id Int @id
aId Int
a A @relation(fields: [aId], references: [id], onDelete: {action})
id Int @id
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: {action})
}}
"#,
action = action
Expand Down Expand Up @@ -236,9 +236,9 @@ fn actions_on_postgres_with_prisma_relation_mode() {
}}
model B {{
id Int @id
aId Int
a A @relation(fields: [aId], references: [id], onDelete: {action})
id Int @id
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: {action})
}}
"#,
action = action
Expand Down Expand Up @@ -275,9 +275,9 @@ fn actions_on_sqlite_with_prisma_relation_mode() {
}}
model B {{
id Int @id
aId Int
a A @relation(fields: [aId], references: [id], onDelete: {action})
id Int @id
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: {action})
}}
"#,
action = action
Expand Down Expand Up @@ -314,9 +314,9 @@ fn on_delete_actions_should_work_on_prisma_relation_mode() {
}}
model B {{
id Int @id
aId Int
a A @relation(fields: [aId], references: [id], onDelete: {action})
id Int @id
aId Int?
a A? @relation(fields: [aId], references: [id], onDelete: {action})
}}
"#,
action = action
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
model SomeUser {
id Int @id
ref Int
profile Profile?
@@unique([id, ref])
}

model Profile {
id Int @id
user SomeUser? @relation(fields: [user_id, user_ref], references: [id, ref], onUpdate: SetNull, onDelete: SetNull)
user_id Int?
user_ref Int
@@unique([user_id, user_ref])
}
// error: Error parsing attribute "@relation": The `onDelete` referential action of a relation must not be set to `SetNull` when a referenced field is required.
// Either choose another referential action, or make the referenced fields optional.
// --> schema.prisma:11
//  | 
// 10 |  id Int @id
// 11 |  user SomeUser? @relation(fields: [user_id, user_ref], references: [id, ref], onUpdate: SetNull, onDelete: SetNull)
// 12 |  user_id Int?
//  | 
// error: Error parsing attribute "@relation": The `onUpdate` referential action of a relation must not be set to `SetNull` when a referenced field is required.
// Either choose another referential action, or make the referenced fields optional.
// --> schema.prisma:11
//  | 
// 10 |  id Int @id
// 11 |  user SomeUser? @relation(fields: [user_id, user_ref], references: [id, ref], onUpdate: SetNull, onDelete: SetNull)
// 12 |  user_id Int?
//  | 
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
model SomeUser {
id Int @id
profile Profile?
}

model Profile {
id Int @id
user SomeUser? @relation(fields: [user_id], references: [id], onUpdate: SetNull, onDelete: SetNull)
user_id Int @unique
}
// error: Error parsing attribute "@relation": The `onDelete` referential action of a relation must not be set to `SetNull` when a referenced field is required.
// Either choose another referential action, or make the referenced fields optional.
// --> schema.prisma:8
//  | 
//  7 |  id Int @id
//  8 |  user SomeUser? @relation(fields: [user_id], references: [id], onUpdate: SetNull, onDelete: SetNull)
//  9 |  user_id Int @unique
//  | 
// error: Error parsing attribute "@relation": The `onUpdate` referential action of a relation must not be set to `SetNull` when a referenced field is required.
// Either choose another referential action, or make the referenced fields optional.
// --> schema.prisma:8
//  | 
//  7 |  id Int @id
//  8 |  user SomeUser? @relation(fields: [user_id], references: [id], onUpdate: SetNull, onDelete: SetNull)
//  9 |  user_id Int @unique
//  | 
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
model SomeUser {
id Int @id
profile Profile?
}

model Profile {
id Int @id
user SomeUser? @relation(fields: [user_id], references: [id], onUpdate: SetNull, onDelete: SetNull)
user_id Int? @unique
}

0 comments on commit abd71fc

Please sign in to comment.