Skip to content

Commit

Permalink
psl: fix database name clash validation with multiSchema
Browse files Browse the repository at this point in the history
The validation we have validates that no two models in the whole schema
have the same database name. Database name is defined as the contents of
the model's `@@map` attribute, or the model name if the model name does
not have an `@@map` attribute. It is the name we give to the
table/collection corresponding to the model in the database.

When the `multiSchema` preview feature is turned on, the tables in the
database can be in different schemas, so we have to adapt the
validation to allow multiple models to have the same database name, as
long as they are in a different schema. This is what this commit does.

closes prisma/prisma#15009
  • Loading branch information
tomhoule committed Sep 30, 2022
1 parent 05756aa commit 9c6abca
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 26 deletions.
2 changes: 1 addition & 1 deletion psl/parser-database/src/attributes.rs
Expand Up @@ -155,7 +155,7 @@ fn resolve_model_attributes<'db>(model_id: ast::ModelId, ast_model: &'db ast::Mo

// @@map
if ctx.visit_optional_single_attr("map") {
map::model(&mut model_attributes, model_id, ctx);
map::model(&mut model_attributes, ctx);
ctx.validate_visited_arguments();
}

Expand Down
20 changes: 1 addition & 19 deletions psl/parser-database/src/attributes/map.rs
Expand Up @@ -6,31 +6,13 @@ use crate::{
DatamodelError, StringId,
};

pub(super) fn model(model_attributes: &mut ModelAttributes, model_id: ast::ModelId, ctx: &mut Context<'_>) {
pub(super) fn model(model_attributes: &mut ModelAttributes, ctx: &mut Context<'_>) {
let mapped_name = match visit_map_attribute(ctx) {
Some(name) => name,
None => return,
};

model_attributes.mapped_name = Some(mapped_name);

if let Some(existing_model_id) = ctx.mapped_model_names.insert(mapped_name, model_id) {
let existing_model_name = ctx.ast[existing_model_id].name();
ctx.push_error(DatamodelError::new_duplicate_model_database_name_error(
&ctx[mapped_name],
existing_model_name,
ctx.ast[model_id].span(),
));
}

if let Some(existing_model_id) = ctx.names.tops.get(&mapped_name).and_then(|id| id.as_model_id()) {
let existing_model_name = ctx.ast[existing_model_id].name();
ctx.push_error(DatamodelError::new_duplicate_model_database_name_error(
&ctx[mapped_name],
existing_model_name,
ctx.current_attribute().span,
));
}
}

pub(super) fn scalar_field(
Expand Down
2 changes: 0 additions & 2 deletions psl/parser-database/src/context.rs
Expand Up @@ -30,7 +30,6 @@ pub(crate) struct Context<'db> {
attributes: AttributesValidationState, // state machine for attribute validation

// @map'ed names indexes. These are not in the db because they are only used for validation.
pub(super) mapped_model_names: HashMap<StringId, ast::ModelId>,
pub(super) mapped_model_scalar_field_names: HashMap<(ast::ModelId, StringId), ast::FieldId>,
pub(super) mapped_composite_type_names: HashMap<(ast::CompositeTypeId, StringId), ast::FieldId>,
pub(super) mapped_enum_names: HashMap<StringId, ast::EnumId>,
Expand All @@ -55,7 +54,6 @@ impl<'db> Context<'db> {
diagnostics,
attributes: AttributesValidationState::default(),

mapped_model_names: Default::default(),
mapped_model_scalar_field_names: Default::default(),
mapped_enum_names: Default::default(),
mapped_enum_value_names: Default::default(),
Expand Down
2 changes: 1 addition & 1 deletion psl/parser-database/src/lib.rs
Expand Up @@ -140,7 +140,7 @@ impl ParserDatabase {
&self.ast
}

/// The total number of models.
/// The total number of models. This is O(1).
pub fn models_count(&self) -> usize {
self.types.model_attributes.len()
}
Expand Down
4 changes: 3 additions & 1 deletion psl/psl-core/src/validate/validation_pipeline/validations.rs
Expand Up @@ -36,10 +36,12 @@ pub(super) fn validate(ctx: &mut Context<'_>) {
}
}

// Model validations
ctx.connector
.validate_scalar_field_unknown_default_functions(ctx.db, ctx.diagnostics);

// Model validations
models::database_name_clashes(ctx);

for model in db.walk_models() {
models::has_a_strict_unique_criteria(model, ctx);
models::has_a_unique_primary_key_name(model, &names, ctx);
Expand Down
@@ -1,13 +1,14 @@
use super::database_name::validate_db_name;
use crate::{
ast,
common::preview_features::PreviewFeature,
datamodel_connector::{walker_ext_traits::*, ConnectorCapability},
diagnostics::DatamodelError,
parser_database::ast::WithSpan,
parser_database::ast::{WithName, WithSpan},
validate::validation_pipeline::context::Context,
};
use parser_database::walkers::{ModelWalker, PrimaryKeyWalker};
use std::borrow::Cow;
use std::{borrow::Cow, collections::HashMap};

/// A model must have either a primary key, or a unique criteria
/// with no optional, commented-out or unsupported fields.
Expand Down Expand Up @@ -298,3 +299,46 @@ pub(super) fn schema_attribute(model: ModelWalker<'_>, ctx: &mut Context<'_>) {
_ => (),
}
}

pub(super) fn database_name_clashes(ctx: &mut Context<'_>) {
// (schema_name, model_database_name) -> ModelId
let mut database_names: HashMap<(Option<&str>, &str), ast::ModelId> = HashMap::with_capacity(ctx.db.models_count());

for model in ctx.db.walk_models() {
let key = (model.schema().map(|(name, _)| name), model.database_name());
match database_names.insert(key, model.model_id()) {
// Two branches because we want to put the error on the @@map attribute, and it can be
// on either model.
Some(existing) if model.mapped_name().is_some() => {
let existing_model_name = &ctx.db.ast()[existing].name();
let attribute = model
.ast_model()
.attributes
.iter()
.find(|attr| attr.name() == "map")
.unwrap();

ctx.push_error(DatamodelError::new_duplicate_model_database_name_error(
model.database_name(),
existing_model_name,
attribute.span(),
));
}
Some(existing) => {
let existing_model = &ctx.db.ast()[existing];
let attribute = existing_model
.attributes
.iter()
.find(|attr| attr.name() == "map")
.unwrap();

ctx.push_error(DatamodelError::new_duplicate_model_database_name_error(
model.database_name(),
model.name(),
attribute.span(),
));
}
None => (),
}
}
}
@@ -0,0 +1,23 @@
datasource mydb {
provider = "sqlite"
url = env("TEST_DB_URL")
}

model Dog {
id Int @id
@@map("pets")
}

model Cat {
id Int @id
@@map("pets")
}

// error: The model with database name "pets" could not be defined because another model with this name exists: "Dog"
// --> schema.prisma:15
//  | 
// 14 | 
// 15 |  @@map("pets")
//  | 
@@ -0,0 +1,30 @@
// issue: https://github.com/prisma/prisma/issues/15009

generator client {
provider = "prisma-client-js"
previewFeatures = ["multiSchema"]
}

datasource db {
provider = "postgresql"
url = env("TEST_DATABASE_URL")
schemas = ["base", "transactional"]
}

model User {
id String @id
email String
posts Post[]
@@map("some_table")
@@schema("base")
}

model Post {
title String
authorId String @unique
author User? @relation(fields: [authorId], references: [id])
@@map("some_table")
@@schema("transactional")
}
@@ -0,0 +1,31 @@
datasource mydb {
provider = "sqlserver"
url = env("TEST_DB_URL")
schemas = ["base", "transactional"]
}

generator client {
provider = "prisma-client-js"
previewFeatures = ["multiSchema"]
}


model Dog {
id Int @id
@@map("pets")
}

model Cat {
id Int @id
@@map("pets")
}

// error: The model with database name "pets" could not be defined because another model with this name exists: "Dog"
// --> schema.prisma:15
//  | 
// 14 | 
// 15 |  @@map("pets")
//  | 

0 comments on commit 9c6abca

Please sign in to comment.