Skip to content

Commit

Permalink
feat: warn about broken m2m relations (#5213)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacek-prisma authored Mar 5, 2025
1 parent 0fa8af8 commit ebe9274
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 2 deletions.
20 changes: 19 additions & 1 deletion schema-engine/connectors/schema-connector/src/warnings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ pub struct Warnings {
pub json_schema_defined: Vec<Model>,
/// Warning about JSONSchema on a model.
pub capped_collection: Vec<Model>,
/// Warning about broken m2m relations.
pub broken_m2m_relations: BTreeSet<(Model, Model)>,
}

impl Warnings {
Expand Down Expand Up @@ -411,17 +413,33 @@ impl fmt::Display for Warnings {
f
)?;

if !self.broken_m2m_relations.is_empty() {
for (model_a, model_b) in self.broken_m2m_relations.iter() {
write!(
f,
"The many-to-many relation between {model_a} and {model_b} is broken due to the naming of the models. Prisma creates many-to-many relations based on the alphabetical ordering of the names of the models and these two models now produce the reverse of the expected ordering.",
)?;
}
}

Ok(())
}
}

/// A model that triggered a warning.
#[derive(PartialEq, Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Model {
/// The name of the model
pub model: String,
}

impl Model {
/// Creates a new model with the given name.
pub fn new(model: impl Into<String>) -> Self {
Self { model: model.into() }
}
}

impl fmt::Display for Model {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, r#""{}""#, self.model)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,4 +411,20 @@ impl<'a> DatamodelCalculatorContext<'a> {
(*direction, next)
})
}

pub(crate) fn m2m_relations(
&'a self,
) -> impl Iterator<Item = (RelationFieldDirection, sql::ForeignKeyWalker<'a>)> + 'a {
self.introspection_map
.m2m_relation_positions
.iter()
.map(|(_, fk_id, direction)| {
let next = sql::Walker {
id: *fk_id,
schema: self.sql_schema,
};

(*direction, next)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ mod view;

use crate::introspection::datamodel_calculator::DatamodelCalculatorContext;
use psl::PreviewFeature;
use schema_connector::Warnings;
use schema_connector::{warnings::Model, Warnings};

use super::introspection_pair::RelationFieldDirection;

/// Analyzes the described database schema, triggering
/// warnings to the user if necessary.
Expand All @@ -22,6 +24,27 @@ pub(crate) fn generate(ctx: &DatamodelCalculatorContext<'_>) -> Warnings {
model::generate_warnings(model, &mut warnings);
}

for (dir, fk) in ctx.m2m_relations() {
let Some(model) = ctx.existing_model(fk.referenced_table().id) else {
continue;
};
let Some(rel) = ctx.existing_m2m_relation(fk.table().id) else {
continue;
};
let expected_model = match dir {
RelationFieldDirection::Back => rel.model_a(),
RelationFieldDirection::Forward => rel.model_b(),
};
if model != expected_model {
let pair = if rel.model_a().id < rel.model_b().id {
(Model::new(rel.model_a().name()), Model::new(rel.model_b().name()))
} else {
(Model::new(rel.model_b().name()), Model::new(rel.model_a().name()))
};
warnings.broken_m2m_relations.insert(pair);
}
}

if ctx.config.preview_features().contains(PreviewFeature::Views) {
for view in ctx.view_pairs() {
view::generate_warnings(view, &mut warnings);
Expand Down
52 changes: 52 additions & 0 deletions schema-engine/sql-introspection-tests/tests/relations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,58 @@ async fn a_prisma_many_to_many_relation(api: &mut TestApi) -> TestResult {
Ok(())
}

#[test_connector(tags(Mysql), exclude(Vitess))]
async fn a_broken_prisma_many_to_many_relation(api: &mut TestApi) -> TestResult {
api.barrel()
.execute(|migration| {
migration.create_table("User", |t| {
t.add_column("id", types::integer().increments(true));
t.add_constraint("User_pkey", types::primary_constraint(["id"]));
});

migration.create_table("Post", |t| {
t.add_column("id", types::integer().increments(true));
t.add_constraint("Post_pkey", types::primary_constraint(["id"]));
});

migration.create_table("_PostToUser", |t| {
t.add_column("A", types::integer().nullable(false).unique(false));
t.add_column("B", types::integer().nullable(false).unique(false));

t.add_foreign_key(&["A"], "Post", &["id"]);
t.add_foreign_key(&["B"], "User", &["id"]);

t.add_index("test", types::index(vec!["A", "B"]).unique(true));
});
})
.await?;

api.expect_re_introspect_warnings(
indoc! {r##"
model Post {
id Int @id @default(autoincrement())
User Author[] @relation("PostToUser")
}
model Author {
id Int @id @default(autoincrement())
Post Post[] @relation("PostToUser")
@@map("User")
}
"##},
expect![[r#"
*** WARNING ***
These models were enriched with `@@map` information taken from the previous Prisma schema:
- "Author"
The many-to-many relation between "Post" and "Author" is broken due to the naming of the models. Prisma creates many-to-many relations based on the alphabetical ordering of the names of the models and these two models now produce the reverse of the expected ordering."#]],
)
.await;

Ok(())
}

#[test_connector(exclude(Mysql, Mssql, CockroachDb, Sqlite))]
async fn a_many_to_many_relation_with_an_id(api: &mut TestApi) -> TestResult {
api.barrel()
Expand Down

0 comments on commit ebe9274

Please sign in to comment.