Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

psl: add the relationMode 'prismaSkipIntegrity' for the MongoDB connector #4123

Closed
2 changes: 1 addition & 1 deletion psl/builtin-connectors/src/mongodb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ impl Connector for MongoDbDatamodelConnector {
}

fn allowed_relation_mode_settings(&self) -> enumflags2::BitFlags<RelationMode> {
RelationMode::Prisma.into()
RelationMode::Prisma | RelationMode::PrismaSkipIntegrity
}

/// Avoid checking whether the fields appearing in a `@relation` attribute are included in an index.
Expand Down
2 changes: 2 additions & 0 deletions psl/psl-core/src/datamodel_connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ pub trait Connector: Send + Sync {
match relation_mode {
RelationMode::ForeignKeys => self.referential_actions().contains(action),
RelationMode::Prisma => self.emulated_referential_actions().contains(action),
// PrismaSkipIntegrity does not support any referential action, cause it is used to skip any integrity check.
RelationMode::PrismaSkipIntegrity => false,
}
}

Expand Down
12 changes: 11 additions & 1 deletion psl/psl-core/src/datamodel_connector/relation_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ pub enum RelationMode {
/// Enforced in Prisma. Slower, but for databases that do not support
/// foreign keys.
Prisma,
/// Enforced in Prisma. Currently allowed only for MongoDB.
/// Skips any referential integrity checks (emulated relations) after an update or delete operation.
/// Using this mode, Prisma will STOP ensuring that every relation-field is valid (left intact), thus stricter checks
/// must be done at the PrismaClient side before executing a query or with a try-catch pattern.
PrismaSkipIntegrity,
}

impl RelationMode {
Expand All @@ -23,7 +28,11 @@ impl RelationMode {
}

pub fn is_prisma(&self) -> bool {
matches!(self, Self::Prisma)
matches!(self, Self::Prisma) || matches!(self, Self::PrismaSkipIntegrity)
}

pub fn should_skip_emulated_referential_integrity(&self) -> bool {
matches!(self, Self::ForeignKeys) || matches!(self, Self::PrismaSkipIntegrity)
}

/// True, if integrity is in database foreign keys
Expand All @@ -43,6 +52,7 @@ impl fmt::Display for RelationMode {
match self {
RelationMode::ForeignKeys => write!(f, "foreignKeys"),
RelationMode::Prisma => write!(f, "prisma"),
RelationMode::PrismaSkipIntegrity => write!(f, "prismaSkipIntegrity"),
}
}
}
3 changes: 2 additions & 1 deletion psl/psl-core/src/validate/datasource_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,10 @@ fn get_relation_mode(
let relation_mode = match coerce::string(rm, diagnostics)? {
"prisma" => RelationMode::Prisma,
"foreignKeys" => RelationMode::ForeignKeys,
"prismaSkipIntegrity" => RelationMode::PrismaSkipIntegrity,
other => {
let message = format!(
"Invalid relation mode setting: \"{other}\". Supported values: \"prisma\", \"foreignKeys\"",
"Invalid relation mode setting: \"{other}\". Supported values: \"prisma\", \"foreignKeys\", \"prismaSkipIntegrity\"",
);
let error = DatamodelError::new_source_validation_error(&message, "relationMode", source.span);
diagnostics.push_error(error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,17 @@ pub(super) fn referential_actions(field: RelationFieldWalker<'_>, ctx: &mut Cont
)
};

// validation template for relationMode = "prismaSkipIntegrity"
let msg_prisma_skip_integrity = |action: ReferentialAction| {
let additional_info = "When using `relationMode = \"prismaSkipIntegrity\"`, you cannot specify any referential action. Learn more at https://pris.ly/d/relation-mode";
format!("Invalid referential action: `{}`. {additional_info}", action.as_str())
};

let msg_template = |action: ReferentialAction| -> String {
match relation_mode {
RelationMode::ForeignKeys => msg_foreign_keys(action),
RelationMode::Prisma => msg_prisma(action),
RelationMode::PrismaSkipIntegrity => msg_prisma_skip_integrity(action),
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ pub(super) fn cycles(relation: CompleteInlineRelationWalker<'_>, ctx: &mut Conte
.has_capability(ConnectorCapability::ReferenceCycleDetection)
&& ctx
.datasource
.map(|ds| ds.relation_mode().uses_foreign_keys())
.map(|ds| ds.relation_mode().should_skip_emulated_referential_integrity())
.unwrap_or(true)
{
return;
Expand Down
35 changes: 34 additions & 1 deletion psl/psl/tests/attributes/relations/referential_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ fn foreign_keys_not_allowed_on_mongo() {
"#};

let expected = expect![[r#"
error: Error validating datasource `relationMode`: Invalid relation mode setting: "foreignKeys". Supported values: "prisma"
error: Error validating datasource `relationMode`: Invalid relation mode setting: "foreignKeys". Supported values: "prisma", "prismaSkipIntegrity"
--> schema.prisma:3
 | 
 2 |  provider = "mongodb"
Expand Down Expand Up @@ -912,3 +912,36 @@ fn on_update_cannot_be_defined_on_the_wrong_side_1_1() {

expect.assert_eq(&parse_unwrap_err(dml));
}

#[test]
fn mongo_prisma_skip_integrity_relation_mode_parses_correctly() {
let dml = indoc! {r#"
datasource db {
provider = "mongodb"
relationMode = "prismaSkipIntegrity"
url = "mongodb://"
}

generator client {
provider = "prisma-client-js"
}

model A {
id String @id @map("_id") @db.ObjectId
bs B[]
}

model B {
id String @id @map("_id") @db.ObjectId
aId String @db.ObjectId
a A @relation(fields: [aId], references: [id])
}
"#};

let schema = parse_schema(&dml);

schema.assert_has_model("A");
schema.assert_has_model("B").assert_has_relation_field("a");

assert_eq!(schema.relation_mode(), RelationMode::PrismaSkipIntegrity)
}
58 changes: 58 additions & 0 deletions psl/psl/tests/attributes/relations/relations_negative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1105,3 +1105,61 @@ fn multiple_relation_validation_errors_do_not_prevent_each_other_across_models()

expect_error(schema, &expected_error)
}

#[test]
fn mongo_prisma_skip_integrity_relation_mode_shouldnt_allow_any_referential_action() {
let dml = indoc! {r#"
datasource db {
provider = "mongodb"
relationMode = "prismaSkipIntegrity"
url = "mongodb://"
}

generator client {
provider = "prisma-client-js"
}

model A {
id String @id @map("_id") @db.ObjectId
bs B[]
}

model B {
id String @id @map("_id") @db.ObjectId
aId String @db.ObjectId
a A @relation(fields: [aId], references: [id], onUpdate: Restrict, onDelete: Restrict)
}
"#};

assert!(parse_unwrap_err(dml).contains("Invalid referential action: `Restrict`. When using `relationMode = \"prismaSkipIntegrity\"`, you cannot specify any referential action."));
}

#[test]
fn sql_should_fail_with_prisma_skip_integrity_relation_mode() {
let dml = indoc! {r#"
datasource db {
provider = "mysql"
relationMode = "prismaSkipIntegrity"
url = env("PLACEHOLDER")
}

generator client {
provider = "prisma-client-js"
}

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)
}
"#};

assert!(parse_unwrap_err(dml).contains(
"Error validating datasource `relationMode`: Invalid relation mode setting: \"prismaSkipIntegrity\"."
));
}
21 changes: 15 additions & 6 deletions query-engine/core/src/query_graph_builder/write/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,11 @@ pub(crate) fn insert_emulated_on_delete(
parent_node: &NodeRef,
child_node: &NodeRef,
) -> QueryGraphBuilderResult<()> {
// If the connector uses the `RelationMode::ForeignKeys` mode, we do not do any checks / emulation.
if query_schema.relation_mode().uses_foreign_keys() {
// If the connector uses the `RelationMode::ForeignKeys` or `RelationMode::PrismaSkipIntegrity` mode, we do not do any checks / emulation.
if query_schema
.relation_mode()
.should_skip_emulated_referential_integrity()
{
return Ok(());
}

Expand Down Expand Up @@ -934,8 +937,11 @@ pub fn insert_emulated_on_update_with_intermediary_node(
parent_node: &NodeRef,
child_node: &NodeRef,
) -> QueryGraphBuilderResult<Option<NodeRef>> {
// If the connector uses the `RelationMode::ForeignKeys` mode, we do not do any checks / emulation.
if query_schema.relation_mode().uses_foreign_keys() {
// If the connector uses the `RelationMode::ForeignKeys` mode or the `RelationMode::PrismaSkipIntegrity`, we do not do any checks / emulation.
if query_schema
.relation_mode()
.should_skip_emulated_referential_integrity()
{
return Ok(None);
}

Expand Down Expand Up @@ -981,8 +987,11 @@ pub fn insert_emulated_on_update(
parent_node: &NodeRef,
child_node: &NodeRef,
) -> QueryGraphBuilderResult<()> {
// If the connector uses the `RelationMode::ForeignKeys` mode, we do not do any checks / emulation.
if query_schema.relation_mode().uses_foreign_keys() {
// If the connector uses the `RelationMode::ForeignKeys` mode or the `RelationMode::PrismaSkipIntegrity` mode, we do not do any checks / emulation.
if query_schema
.relation_mode()
.should_skip_emulated_referential_integrity()
{
return Ok(());
}

Expand Down