Skip to content

Commit

Permalink
Do not render default referential actions
Browse files Browse the repository at this point in the history
  • Loading branch information
Julius de Bruijn committed Jun 2, 2021
1 parent 619733a commit 2def366
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 20 deletions.
Expand Up @@ -41,7 +41,10 @@ pub fn introspect(
for foreign_key in &foreign_keys_copy {
version_check.has_inline_relations(table);
version_check.uses_on_delete(foreign_key, table);
let relation_field = calculate_relation_field(schema, table, foreign_key)?;

let mut relation_field = calculate_relation_field(schema, table, foreign_key)?;
relation_field.supports_restrict_action(!sql_family.is_mssql());

model.add_field(Field::RelationField(relation_field));
}

Expand Down
Expand Up @@ -1752,3 +1752,144 @@ async fn do_not_try_to_keep_custom_many_to_many_self_relation_names(api: &TestAp

Ok(())
}

#[test_connector(tags(Postgres, Mysql, Sqlite))]
async fn default_required_actions_with_restrict(api: &TestApi) -> TestResult {
api.barrel()
.execute(|migration| {
migration.create_table("a", |t| {
t.add_column("id", types::primary());
});

migration.create_table("b", |t| {
t.add_column("id", types::primary());
t.add_column("a_id", types::integer().nullable(false));
t.inject_custom(
"CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE RESTRICT ON UPDATE CASCADE",
);
});
})
.await?;

let extra_index = if api.sql_family().is_mysql() {
r#"@@index([a_id], name: "asdf")"#
} else {
""
};

let input_dm = formatdoc! {r#"
model a {{
id Int @id @default(autoincrement())
bs b[]
}}
model b {{
id Int @id @default(autoincrement())
a_id Int
a a @relation(fields: [a_id], references: [id])
{}
}}
"#, extra_index};

api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?);

Ok(())
}

#[test_connector(tags(Mssql))]
async fn default_required_actions_without_restrict(api: &TestApi) -> TestResult {
api.barrel()
.execute(|migration| {
migration.create_table("a", |t| {
t.add_column("id", types::primary());
});

migration.create_table("b", |t| {
t.add_column("id", types::primary());
t.add_column("a_id", types::integer().nullable(false));
t.inject_custom(
"CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_required_actions_without_restrict.a(id) ON DELETE NO ACTION ON UPDATE CASCADE",
);
});
})
.await?;

let extra_index = if api.sql_family().is_mysql() {
r#"@@index([a_id], name: "asdf")"#
} else {
""
};

let input_dm = formatdoc! {r#"
model a {{
id Int @id @default(autoincrement())
bs b[]
}}
model b {{
id Int @id @default(autoincrement())
a_id Int
a a @relation(fields: [a_id], references: [id])
{}
}}
"#, extra_index};

api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?);

Ok(())
}

#[test_connector]
async fn default_optional_actions(api: &TestApi) -> TestResult {
let family = api.sql_family();

api.barrel()
.execute(move |migration| {
migration.create_table("a", |t| {
t.add_column("id", types::primary());
});

migration.create_table("b", move |t| {
t.add_column("id", types::primary());
t.add_column("a_id", types::integer().nullable(true));

match family {
SqlFamily::Mssql => {
t.inject_custom(
"CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES default_optional_actions.a(id) ON DELETE SET NULL ON UPDATE SET NULL",
);
}
_ => {
t.inject_custom(
"CONSTRAINT asdf FOREIGN KEY (a_id) REFERENCES a(id) ON DELETE SET NULL ON UPDATE SET NULL",
);
}
}
});
})
.await?;

let extra_index = if api.sql_family().is_mysql() {
r#"@@index([a_id], name: "asdf")"#
} else {
""
};

let input_dm = formatdoc! {r#"
model a {{
id Int @id @default(autoincrement())
bs b[]
}}
model b {{
id Int @id @default(autoincrement())
a_id Int?
a a? @relation(fields: [a_id], references: [id])
{}
}}
"#, extra_index};

api.assert_eq_datamodels(&input_dm, &api.re_introspect(&input_dm).await?);

Ok(())
}
4 changes: 2 additions & 2 deletions libs/datamodel/connectors/dml/src/datamodel.rs
@@ -1,4 +1,4 @@
use crate::field::{Field, FieldType, RelationField, ScalarField};
use crate::field::{Field, RelationField, ScalarField};
use crate::model::Model;
use crate::r#enum::Enum;
use crate::relation_info::RelationInfo;
Expand Down Expand Up @@ -117,7 +117,7 @@ impl Datamodel {
let mut fields = vec![];
for model in self.models() {
for field in model.scalar_fields() {
if FieldType::Enum(enum_name.to_owned()) == field.field_type {
if field.field_type.is_enum(enum_name) {
fields.push((model.name.clone(), field.name.clone()))
}
}
Expand Down
72 changes: 70 additions & 2 deletions libs/datamodel/connectors/dml/src/field.rs
@@ -1,8 +1,11 @@
use super::*;
use crate::default_value::{DefaultValue, ValueGenerator};
use crate::native_type_instance::NativeTypeInstance;
use crate::scalars::ScalarType;
use crate::traits::{Ignorable, WithDatabaseName, WithName};
use crate::{
default_value::{DefaultValue, ValueGenerator},
relation_info::ReferentialAction,
};
use std::hash::Hash;

/// Arity of a Field in a Model.
Expand Down Expand Up @@ -74,6 +77,10 @@ impl FieldType {
self.scalar_type().map(|st| st.is_string()).unwrap_or(false)
}

pub fn is_enum(&self, name: &str) -> bool {
matches!(self, Self::Enum(this) if this == name)
}

pub fn scalar_type(&self) -> Option<ScalarType> {
match self {
FieldType::NativeType(st, _) => Some(*st),
Expand Down Expand Up @@ -230,7 +237,7 @@ impl WithDatabaseName for Field {
}

/// Represents a relation field in a model.
#[derive(Debug, PartialEq, Clone)]
#[derive(Debug, Clone)]
pub struct RelationField {
/// Name of the field.
pub name: String,
Expand All @@ -252,6 +259,45 @@ pub struct RelationField {

/// Indicates if this field has to be ignored by the Client.
pub is_ignored: bool,

/// Is `ON DELETE/UPDATE RESTRICT` allowed.
pub supports_restrict_action: Option<bool>,
}

impl PartialEq for RelationField {
//ignores the relation name for reintrospection
fn eq(&self, other: &Self) -> bool {
let this_matches = self.name == other.name
&& self.arity == other.arity
&& self.documentation == other.documentation
&& self.is_generated == other.is_generated
&& self.is_commented_out == other.is_commented_out
&& self.is_ignored == other.is_ignored
&& self.relation_info == other.relation_info;

let this_on_delete = self.relation_info.on_delete.or_else(|| self.default_on_delete_action());

let other_on_delete = other
.relation_info
.on_delete
.or_else(|| other.default_on_delete_action());

let on_delete_matches = this_on_delete == other_on_delete;

let this_on_update = self
.relation_info
.on_update
.unwrap_or_else(|| self.default_on_update_action());

let other_on_update = other
.relation_info
.on_update
.unwrap_or_else(|| other.default_on_update_action());

let on_update_matches = this_on_update == other_on_update;

this_matches && on_delete_matches && on_update_matches
}
}

impl RelationField {
Expand All @@ -265,8 +311,15 @@ impl RelationField {
is_generated: false,
is_commented_out: false,
is_ignored: false,
supports_restrict_action: None,
}
}

/// The default `onDelete` can be `Restrict`.
pub fn supports_restrict_action(&mut self, value: bool) {
self.supports_restrict_action = Some(value);
}

/// Creates a new field with the given name and type, marked as generated and optional.
pub fn new_generated(name: &str, info: RelationInfo, required: bool) -> Self {
let arity = if required {
Expand Down Expand Up @@ -300,6 +353,21 @@ impl RelationField {
pub fn is_optional(&self) -> bool {
self.arity.is_optional()
}

pub fn default_on_delete_action(&self) -> Option<ReferentialAction> {
self.supports_restrict_action.map(|restrict_ok| match self.arity {
FieldArity::Required if restrict_ok => ReferentialAction::Restrict,
FieldArity::Required => ReferentialAction::NoAction,
_ => ReferentialAction::SetNull,
})
}

pub fn default_on_update_action(&self) -> ReferentialAction {
match self.arity {
FieldArity::Required => ReferentialAction::Cascade,
_ => ReferentialAction::SetNull,
}
}
}

/// Represents a scalar field in a model.
Expand Down
8 changes: 2 additions & 6 deletions libs/datamodel/connectors/dml/src/relation_info.rs
Expand Up @@ -21,13 +21,9 @@ pub struct RelationInfo {
}

impl PartialEq for RelationInfo {
//ignores the relation name for reintrospection
//ignores the relation name for reintrospection, ignores referential actions that are compared in the relation field.
fn eq(&self, other: &Self) -> bool {
self.to == other.to
&& self.fields == other.fields
&& self.references == other.references
&& self.on_delete == other.on_delete
&& self.on_update == other.on_update
self.to == other.to && self.fields == other.fields && self.references == other.references
}
}

Expand Down
10 changes: 10 additions & 0 deletions libs/datamodel/core/src/transform/ast_to_dml/lift.rs
Expand Up @@ -8,6 +8,7 @@ use crate::{
diagnostics::{DatamodelError, Diagnostics},
};
use crate::{dml::ScalarType, Datasource};
use ::dml::relation_info::ReferentialAction;
use datamodel_connector::connector_error::{ConnectorError, ErrorKind};
use itertools::Itertools;
use once_cell::sync::Lazy;
Expand Down Expand Up @@ -157,6 +158,15 @@ impl<'a> LiftAstToDml<'a> {
FieldType::Relation(info) => {
let arity = self.lift_field_arity(&ast_field.arity);
let mut field = dml::RelationField::new(&ast_field.name.name, arity, info);

if let Some(ref source) = self.source {
field.supports_restrict_action(
source
.active_connector
.supports_referential_action(ReferentialAction::Restrict),
);
}

field.documentation = ast_field.documentation.clone().map(|comment| comment.text);
Field::RelationField(field)
}
Expand Down
21 changes: 13 additions & 8 deletions libs/datamodel/core/src/transform/attributes/relation.rs
Expand Up @@ -49,9 +49,7 @@ impl AttributeValidator<dml::Field> for RelationAttributeValidator {
fn serialize(&self, field: &dml::Field, datamodel: &dml::Datamodel) -> Vec<ast::Attribute> {
if let dml::Field::RelationField(rf) = field {
let mut args = Vec::new();

let relation_info = &rf.relation_info;

let parent_model = datamodel.find_model_by_relation_field_ref(rf).unwrap();

let related_model = datamodel
Expand Down Expand Up @@ -104,15 +102,22 @@ impl AttributeValidator<dml::Field> for RelationAttributeValidator {
}

if let Some(ref_action) = relation_info.on_delete {
let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty());

args.push(ast::Argument::new("onDelete", expression));
let is_default = rf
.default_on_delete_action()
.map(|default| default == ref_action)
.unwrap_or(false);

if !is_default {
let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty());
args.push(ast::Argument::new("onDelete", expression));
}
}

if let Some(ref_action) = relation_info.on_update {
let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty());

args.push(ast::Argument::new("onUpdate", expression));
if rf.default_on_update_action() != ref_action {
let expression = ast::Expression::ConstantValue(ref_action.to_string(), ast::Span::empty());
args.push(ast::Argument::new("onUpdate", expression));
}
}

if !args.is_empty() {
Expand Down
2 changes: 1 addition & 1 deletion libs/sql-schema-describer/src/sqlite.rs
Expand Up @@ -338,7 +338,7 @@ impl SqlSchemaDescriber {
if let Some(column) = referenced_column {
referenced_columns.insert(seq, column);
};
let on_delete_action = match dbg!(&row)
let on_delete_action = match row
.get("on_delete")
.and_then(|x| x.to_string())
.expect("on_delete")
Expand Down
Expand Up @@ -638,3 +638,19 @@ fn on_update_restrict_should_work(api: TestApi) {
})
});
}

#[test_connector]
fn on_delete_default_values(api: TestApi) {
let dm = r#"
model A {
id Int @id
b B[]
}
model B {
id Int @id
aId Int
a A @relation(fields: [aId], references: [id], onUpdate: Restrict)
}
"#;
}

0 comments on commit 2def366

Please sign in to comment.