diff --git a/introspection-engine/introspection-engine-tests/tests/named_constraints/mod.rs b/introspection-engine/introspection-engine-tests/tests/named_constraints/mod.rs index 283139a189d7..6f9f6b6fc7e4 100644 --- a/introspection-engine/introspection-engine-tests/tests/named_constraints/mod.rs +++ b/introspection-engine/introspection-engine-tests/tests/named_constraints/mod.rs @@ -5,6 +5,7 @@ use introspection_engine_tests::test_api::TestApi; use introspection_engine_tests::test_api::*; use introspection_engine_tests::TestResult; +use quaint::prelude::Queryable; use test_macros::test_connector; #[test_connector(preview_features("NamedConstraints"), tags(Mssql, Postgres))] @@ -359,3 +360,45 @@ async fn introspecting_custom_fk_names_does_not_return_them_on_sqlite(api: &Test Ok(()) } + +#[test_connector(preview_features("NamedConstraints"), tags(Mssql))] +async fn introspecting_custom_default_names_should_output_to_dml(api: &TestApi) -> TestResult { + let create_table = format!( + "CREATE TABLE [{}].[custom_defaults_test] (id INT CONSTRAINT pk_meow PRIMARY KEY, data NVARCHAR(255) CONSTRAINT meow DEFAULT 'foo')", + api.schema_name() + ); + + api.database().raw_cmd(&create_table).await?; + + let dm = r#" + model custom_defaults_test { + id Int @id(map: "pk_meow") + data String? @default("foo", map: "meow") @db.NVarChar(255) + } + "#; + + api.assert_eq_datamodels(&dm, &api.introspect().await?); + + Ok(()) +} + +#[test_connector(preview_features("NamedConstraints"), tags(Mssql))] +async fn introspecting_default_default_names_should_not_output_to_dml(api: &TestApi) -> TestResult { + let create_table = format!( + "CREATE TABLE [{}].[custom_defaults_test] (id INT CONSTRAINT pk_meow PRIMARY KEY, data NVARCHAR(255) CONSTRAINT custom_defaults_test_data_df DEFAULT 'foo')", + api.schema_name() + ); + + api.database().raw_cmd(&create_table).await?; + + let dm = indoc! {r#" + model custom_defaults_test { + id Int @id(map: "pk_meow") + data String? @default("foo") @db.NVarChar(255) + } + "#}; + + api.assert_eq_datamodels(&dm, &api.introspect().await?); + + Ok(()) +} diff --git a/libs/datamodel/connectors/dml/src/default_value.rs b/libs/datamodel/connectors/dml/src/default_value.rs index 7cf64b4649e8..b1b0a02efe8f 100644 --- a/libs/datamodel/connectors/dml/src/default_value.rs +++ b/libs/datamodel/connectors/dml/src/default_value.rs @@ -117,6 +117,11 @@ impl DefaultValue { _ => None, } } + + /// The default value constraint name. + pub fn db_name(&self) -> Option<&str> { + self.db_name.as_ref().map(|s| s.as_str()) + } } #[derive(Clone)] diff --git a/libs/datamodel/connectors/dml/src/field.rs b/libs/datamodel/connectors/dml/src/field.rs index aff48f4a9727..2a599fc8eefd 100644 --- a/libs/datamodel/connectors/dml/src/field.rs +++ b/libs/datamodel/connectors/dml/src/field.rs @@ -431,6 +431,7 @@ impl ScalarField { is_ignored: false, } } + /// Creates a new field with the given name and type, marked as generated and optional. pub fn new_generated(name: &str, field_type: FieldType) -> ScalarField { let mut field = Self::new(name, FieldArity::Optional, field_type); @@ -461,9 +462,13 @@ impl ScalarField { } pub fn is_auto_increment(&self) -> bool { - let kind = self.default_value.as_ref().map(|val| &val.kind); + let kind = self.default_value().map(|val| &val.kind); matches!(kind, Some(DefaultKind::Expression(ref expr)) if expr == &ValueGenerator::new_autoincrement()) } + + pub fn default_value(&self) -> Option<&DefaultValue> { + self.default_value.as_ref() + } } impl WithName for ScalarField { diff --git a/libs/datamodel/core/src/ast/attribute.rs b/libs/datamodel/core/src/ast/attribute.rs index bee520c5f777..bc9b34f421d2 100644 --- a/libs/datamodel/core/src/ast/attribute.rs +++ b/libs/datamodel/core/src/ast/attribute.rs @@ -23,6 +23,10 @@ impl Attribute { pub fn is_index(&self) -> bool { matches!(self.name.name.as_str(), "index" | "unique") } + + pub fn is_id(&self) -> bool { + matches!(self.name.name.as_str(), "id") + } } impl WithIdentifier for Attribute { diff --git a/libs/datamodel/core/src/ast/model.rs b/libs/datamodel/core/src/ast/model.rs index 01e41f1974b1..cffac0f9d9c0 100644 --- a/libs/datamodel/core/src/ast/model.rs +++ b/libs/datamodel/core/src/ast/model.rs @@ -52,6 +52,16 @@ impl Model { pub(crate) fn find_field_bang(&self, name: &str) -> &Field { self.find_field(name).unwrap() } + + pub(crate) fn id_attribute(&self) -> &Attribute { + let from_model = self.attributes().iter().find(|attr| attr.is_id()); + + let mut from_field = self + .iter_fields() + .flat_map(|(_, field)| field.attributes().iter().find(|attr| attr.is_id())); + + from_model.or_else(|| from_field.next()).unwrap() + } } impl WithIdentifier for Model { 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 9a9026223fa0..d976fecc3d94 100644 --- a/libs/datamodel/core/src/transform/ast_to_dml/validate.rs +++ b/libs/datamodel/core/src/transform/ast_to_dml/validate.rs @@ -1,16 +1,19 @@ #![allow(clippy::suspicious_operation_groupings)] // clippy is wrong there -use crate::common::constraint_names::ConstraintNames; -use crate::PreviewFeature::NamedConstraints; + +mod names; + use crate::{ ast::{self, Span}, - common::preview_features::PreviewFeature, + common::{constraint_names::ConstraintNames, preview_features::PreviewFeature}, configuration, diagnostics::{DatamodelError, Diagnostics}, dml, + PreviewFeature::NamedConstraints, }; use ::dml::{datamodel::Datamodel, field::RelationField, model::Model, traits::WithName}; use datamodel_connector::ConnectorCapability; use enumflags2::BitFlags; +use names::NamesValidator; use std::collections::HashSet; /// Helper for validating a datamodel. @@ -39,7 +42,7 @@ impl<'a> Validator<'a> { } } - pub(crate) fn validate(&self, ast: &ast::SchemaAst, schema: &mut dml::Datamodel, diagnostics: &mut Diagnostics) { + pub(crate) fn validate(&self, ast: &ast::SchemaAst, schema: &dml::Datamodel, diagnostics: &mut Diagnostics) { for model in schema.models() { let ast_model = ast.find_model(&model.name).expect(STATE_ERROR); @@ -107,17 +110,51 @@ impl<'a> Validator<'a> { } } - pub fn post_standardisation_validate( + pub(crate) fn post_standardisation_validate( &self, ast_schema: &ast::SchemaAst, - schema: &mut dml::Datamodel, + schema: &dml::Datamodel, diagnostics: &mut Diagnostics, ) { + let constraint_names = NamesValidator::new(&schema, self.preview_features, self.source); + for model in schema.models() { + let ast_model = ast_schema.find_model(&model.name).expect(STATE_ERROR); + + if let Some(pk) = &model.primary_key { + if let Some(name) = &pk.db_name { + // Only for SQL Server for now... + if constraint_names.is_duplicate(name) { + let span = ast_model.id_attribute().span; + let message = "Given constraint name is already in use in the data model."; + let error = DatamodelError::new_attribute_validation_error(message, "default", span); + + diagnostics.push_error(error); + } + } + } + + // TODO: Extend this check for other constraints. Now only used + // for SQL Server default constraint names. + for field in model.fields().filter(|f| f.is_scalar_field()) { + if let Some(name) = field.default_value().and_then(|d| d.db_name()) { + let ast_field = ast_model.find_field_bang(field.name()); + + if constraint_names.is_duplicate(name) { + let message = "Given constraint name is already in use in the data model."; + let span = ast_field.span_for_argument("default", "map"); + let error = DatamodelError::new_attribute_validation_error(message, "default", span); + + diagnostics.push_error(error); + } + } + } + let mut new_errors = self.validate_relation_arguments_bla( schema, ast_schema.find_model(&model.name).expect(STATE_ERROR), model, + &constraint_names, ); diagnostics.append(&mut new_errors); @@ -544,11 +581,12 @@ impl<'a> Validator<'a> { } } - fn validate_relation_arguments_bla( + fn validate_relation_arguments_bla<'dml>( &self, - datamodel: &dml::Datamodel, + datamodel: &'dml dml::Datamodel, ast_model: &ast::Model, model: &dml::Model, + constraint_names: &NamesValidator<'dml>, ) -> Diagnostics { let mut errors = Diagnostics::new(); @@ -563,6 +601,20 @@ impl<'a> Validator<'a> { let rel_info = &field.relation_info; let related_model = datamodel.find_model(&rel_info.to).expect(STATE_ERROR); + if let Some(name) = field.relation_info.fk_name.as_ref() { + // Only for SQL Server for now... + if constraint_names.is_duplicate(name) { + let span = ast_field + .map(|f| f.span_for_argument("relation", "map")) + .unwrap_or_else(ast::Span::empty); + + let message = "Given constraint name is already in use in the data model."; + let error = DatamodelError::new_attribute_validation_error(message, RELATION_ATTRIBUTE_NAME, span); + + errors.push_error(error); + } + } + if let Some((_rel_field_idx, related_field)) = datamodel.find_related_field(field) { let related_field_rel_info = &related_field.relation_info; 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 edb9794cb4c3..1a961d9acaad 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 @@ -80,7 +80,7 @@ impl<'a, 'b> ValidationPipeline<'a> { // Phase 6: Post Standardisation Validation self.validator - .post_standardisation_validate(ast_schema, &mut schema, &mut diagnostics); + .post_standardisation_validate(ast_schema, &schema, &mut diagnostics); diagnostics.to_result()?; diff --git a/libs/datamodel/core/src/transform/dml_to_ast/lower_field.rs b/libs/datamodel/core/src/transform/dml_to_ast/lower_field.rs index cb08211ca007..d8643ceb2f58 100644 --- a/libs/datamodel/core/src/transform/dml_to_ast/lower_field.rs +++ b/libs/datamodel/core/src/transform/dml_to_ast/lower_field.rs @@ -7,6 +7,8 @@ use crate::{ ast::{self, Attribute, Span}, dml, Datasource, Field, Ignorable, }; +use ::dml::traits::WithName; +use datamodel_connector::{Connector, EmptyDatamodelConnector}; use prisma_value::PrismaValue; impl<'a> LowerDmlToAst<'a> { @@ -109,13 +111,27 @@ impl<'a> LowerDmlToAst<'a> { // @default if let Some(default_value) = field.default_value() { - attributes.push(ast::Attribute::new( - "default", - vec![ast::Argument::new( - "", - LowerDmlToAst::<'a>::lower_default_value(default_value.clone()), - )], - )); + let mut args = vec![ast::Argument::new( + "", + LowerDmlToAst::<'a>::lower_default_value(default_value.clone()), + )]; + + if self.preview_features.contains(PreviewFeature::NamedConstraints) { + let connector = self + .datasource + .map(|source| source.active_connector.as_ref()) + .unwrap_or(&EmptyDatamodelConnector as &dyn Connector); + + let prisma_default = ConstraintNames::default_name(model.name(), field.name(), connector); + + if let Some(name) = default_value.db_name() { + if name != &prisma_default { + args.push(ast::Argument::new("map", Self::lower_string(name))) + } + } + } + + attributes.push(ast::Attribute::new("default", args)); } // @updatedAt @@ -224,19 +240,23 @@ impl<'a> LowerDmlToAst<'a> { } } + pub fn lower_string(s: impl ToString) -> ast::Expression { + ast::Expression::StringValue(s.to_string(), ast::Span::empty()) + } + pub fn lower_prisma_value(pv: &PrismaValue) -> ast::Expression { match pv { PrismaValue::Boolean(true) => ast::Expression::BooleanValue(String::from("true"), ast::Span::empty()), PrismaValue::Boolean(false) => ast::Expression::BooleanValue(String::from("false"), ast::Span::empty()), - PrismaValue::String(value) => ast::Expression::StringValue(value.clone(), ast::Span::empty()), + PrismaValue::String(value) => Self::lower_string(value), PrismaValue::Enum(value) => ast::Expression::ConstantValue(value.clone(), ast::Span::empty()), - PrismaValue::DateTime(value) => ast::Expression::StringValue(value.to_rfc3339(), ast::Span::empty()), + PrismaValue::DateTime(value) => Self::lower_string(value), PrismaValue::Float(value) => ast::Expression::NumericValue(value.to_string(), ast::Span::empty()), PrismaValue::Int(value) => ast::Expression::NumericValue(value.to_string(), ast::Span::empty()), PrismaValue::BigInt(value) => ast::Expression::NumericValue(value.to_string(), ast::Span::empty()), PrismaValue::Null => ast::Expression::ConstantValue("null".to_string(), ast::Span::empty()), - PrismaValue::Uuid(val) => ast::Expression::StringValue(val.to_string(), ast::Span::empty()), - PrismaValue::Json(val) => ast::Expression::StringValue(val.to_string(), ast::Span::empty()), + PrismaValue::Uuid(val) => Self::lower_string(val), + PrismaValue::Json(val) => Self::lower_string(val), PrismaValue::List(vec) => ast::Expression::Array( vec.iter() .map(|pv| LowerDmlToAst::<'a>::lower_prisma_value(pv)) diff --git a/libs/datamodel/core/tests/attributes/default_negative.rs b/libs/datamodel/core/tests/attributes/default_negative.rs index 4fda8ce11830..e9ffab44c7cc 100644 --- a/libs/datamodel/core/tests/attributes/default_negative.rs +++ b/libs/datamodel/core/tests/attributes/default_negative.rs @@ -427,7 +427,18 @@ fn named_default_constraints_cannot_have_duplicate_names() { let error = datamodel::parse_schema(dml).map(drop).unwrap_err(); let expectation = expect![[r#" - TODO: Talk with Matthias on Monday! + error: Error parsing attribute "@default": Given constraint name is already in use in the data model. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  a String @default("asdf", map: "reserved") +  |  + error: Error parsing attribute "@default": Given constraint name is already in use in the data model. + --> schema.prisma:18 +  |  + 17 |  id Int @id @default(autoincrement()) + 18 |  b String @default("asdf", map: "reserved") +  |  "#]]; expectation.assert_eq(&error) @@ -459,7 +470,18 @@ fn named_default_constraints_cannot_clash_with_pk_names() { let error = datamodel::parse_schema(dml).map(drop).unwrap_err(); let expectation = expect![[r#" - TODO: Talk with Matthias on Monday! + error: Error parsing attribute "@default": Given constraint name is already in use in the data model. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  a String @default("asdf", map: "reserved") +  |  + error: Error parsing attribute "@default": Given constraint name is already in use in the data model. + --> schema.prisma:17 +  |  + 16 | model B { + 17 |  id Int @id(map: "reserved") @default(autoincrement()) +  |  "#]]; expectation.assert_eq(&error) @@ -480,13 +502,13 @@ fn named_default_constraints_cannot_clash_with_fk_names() { model A { id Int @id @default(autoincrement()) - a String @default("asdf", map: "reserved") - b B @relation(fields: [bId], references: [id], map: "name") + a String @default("asdf", map: "reserved") + b B @relation(fields: [bId], references: [id], map: "reserved") bId Int } model B { - id Int @id(map: "reserved") @default(autoincrement()) + id Int @id @default(autoincrement()) as A[] } "#}; @@ -494,7 +516,18 @@ fn named_default_constraints_cannot_clash_with_fk_names() { let error = datamodel::parse_schema(dml).map(drop).unwrap_err(); let expectation = expect![[r#" - TODO: Talk with Matthias on Monday! + error: Error parsing attribute "@default": Given constraint name is already in use in the data model. + --> schema.prisma:13 +  |  + 12 |  id Int @id @default(autoincrement()) + 13 |  a String @default("asdf", map: "reserved") +  |  + error: Error parsing attribute "@relation": Given constraint name is already in use in the data model. + --> schema.prisma:14 +  |  + 13 |  a String @default("asdf", map: "reserved") + 14 |  b B @relation(fields: [bId], references: [id], map: "reserved") +  |  "#]]; expectation.assert_eq(&error) diff --git a/libs/datamodel/core/tests/attributes/default_positive.rs b/libs/datamodel/core/tests/attributes/default_positive.rs index 4abed0785a2d..e60b92aded9b 100644 --- a/libs/datamodel/core/tests/attributes/default_positive.rs +++ b/libs/datamodel/core/tests/attributes/default_positive.rs @@ -149,3 +149,94 @@ fn named_default_constraints_should_work_on_sql_server() { .assert_has_scalar_field("data") .assert_default_value(expected_default); } + +// TODO: Change me when we do validate +#[test] +fn named_default_constraints_should_not_validate_name_clashes_on_pk() { + let dml = indoc! { r#" + datasource test { + provider = "postgres" + url = "postgres://" + } + + generator js { + provider = "prisma-client-js" + previewFeatures = ["namedConstraints"] + } + + model A { + id Int @id(map: "meow") @default(autoincrement()) + } + + model B { + id Int @id(map: "meow") @default(autoincrement()) + } + "#}; + + assert!(datamodel::parse_schema(dml).is_ok()); +} + +// TODO: Change me when we do validate +#[test] +fn named_default_constraints_should_not_validate_name_clashes_on_pk_fk() { + let dml = indoc! { r#" + datasource test { + provider = "postgres" + url = "postgres://" + } + + generator js { + provider = "prisma-client-js" + previewFeatures = ["namedConstraints"] + } + + model A { + id Int @id(map: "meow") @default(autoincrement()) + b B @relation(fields: [bId], references: [id], map: "meow") + bId Int + } + + model B { + id Int @id(map: "meow") @default(autoincrement()) + as A[] + } + "#}; + + assert!(datamodel::parse_schema(dml).is_ok()); +} + +// TODO: Change me when we do validate +#[test] +fn named_default_constraints_should_not_validate_name_clashes_on_fk() { + let dml = indoc! { r#" + datasource test { + provider = "postgres" + url = "postgres://" + } + + generator js { + provider = "prisma-client-js" + previewFeatures = ["namedConstraints"] + } + + model A { + id Int @id(map: "meow") @default(autoincrement()) + b B @relation(fields: [bId], references: [id], map: "meow") + c C @relation(fields: [cId], references: [id], map: "meow") + bId Int + cId Int + } + + model B { + id Int @id @default(autoincrement()) + as A[] + } + + model C { + id Int @id @default(autoincrement()) + as A[] + } + "#}; + + assert!(datamodel::parse_schema(dml).is_ok()); +} diff --git a/migration-engine/migration-engine-tests/tests/migrations/defaults.rs b/migration-engine/migration-engine-tests/tests/migrations/defaults.rs index 748f6ce84653..8a57b9720a7c 100644 --- a/migration-engine/migration-engine-tests/tests/migrations/defaults.rs +++ b/migration-engine/migration-engine-tests/tests/migrations/defaults.rs @@ -215,6 +215,32 @@ fn column_defaults_must_be_migrated(api: TestApi) { }); } +#[test_connector(tags(Mssql))] +fn default_constraint_names_should_work(api: TestApi) { + let dm = r#" + generator js { + provider = "prisma-client-js" + previewFeatures = ["microsoftSqlServer", "namedConstraints"] + } + + model A { + id Int @id @default(autoincrement()) + data String @default("beeb buub", map: "meow") + } + "#; + + api.schema_push_w_datasource(dm).send().assert_green_bang(); + + api.assert_schema().assert_table("A", |table| { + table.assert_column("data", |col| { + let mut expected = DefaultValue::value("beeb buub"); + expected.set_constraint_name("meow"); + + col.assert_default(Some(expected)) + }) + }); +} + #[test_connector] fn escaped_string_defaults_are_not_arbitrarily_migrated(api: TestApi) { use quaint::ast::Insert;