use crate::introspection::{datamodel_calculator::DatamodelCalculatorContext, introspection_pair::ModelPair};
use psl::{
datamodel_connector::constraint_names::ConstraintNames,
parser_database::walkers::{self, RelationName},
};
use sql_schema_describer as sql;
use std::borrow::Cow;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum RelationFieldDirection {
Forward,
Back,
}
impl RelationFieldDirection {
fn is_forward(self) -> bool {
matches!(self, Self::Forward)
}
}
#[derive(Clone, Copy)]
struct InlineRelationField<'a> {
previous: Option<walkers::RelationFieldWalker<'a>>,
next: sql::ForeignKeyWalker<'a>,
direction: RelationFieldDirection,
}
impl<'a> InlineRelationField<'a> {
fn any_field_required(self) -> bool {
self.next.constrained_columns().any(|col| col.arity().is_required())
}
fn any_field_optional(self) -> bool {
self.next.constrained_columns().any(|col| !col.arity().is_required())
}
fn model(self, context: &'a DatamodelCalculatorContext<'a>) -> ModelPair<'a> {
let previous = self.previous.map(|prev| prev.model());
let next = self.next.table();
ModelPair::new(context, previous, next)
}
fn referenced_model(self, context: &'a DatamodelCalculatorContext<'a>) -> ModelPair<'a> {
let previous = self.previous.map(|prev| prev.related_model());
let next = self.next.referenced_table();
ModelPair::new(context, previous, next)
}
fn default_constraint_name(self, context: &DatamodelCalculatorContext<'a>) -> String {
let connector = context.active_connector();
let cols: Vec<_> = self.next.constrained_columns().map(|c| c.name()).collect();
ConstraintNames::foreign_key_constraint_name(self.next.table().name(), &cols, connector)
}
}
#[derive(Clone, Copy)]
struct Many2ManyRelationField<'a> {
next: sql::ForeignKeyWalker<'a>,
direction: RelationFieldDirection,
}
#[derive(Clone, Copy)]
struct EmulatedRelationField<'a> {
previous: walkers::RelationFieldWalker<'a>,
}
#[derive(Clone, Copy)]
enum RelationType<'a> {
Inline(InlineRelationField<'a>),
Many2Many(Many2ManyRelationField<'a>),
Emulated(EmulatedRelationField<'a>),
}
#[derive(Clone, Copy)]
pub(crate) struct RelationFieldPair<'a> {
relation_type: RelationType<'a>,
context: &'a DatamodelCalculatorContext<'a>,
}
impl<'a> RelationFieldPair<'a> {
pub(crate) fn inline(
context: &'a DatamodelCalculatorContext<'a>,
previous: Option<walkers::RelationFieldWalker<'a>>,
next: sql::ForeignKeyWalker<'a>,
direction: RelationFieldDirection,
) -> Self {
let relation_type = InlineRelationField {
previous,
next,
direction,
};
Self {
relation_type: RelationType::Inline(relation_type),
context,
}
}
pub(crate) fn m2m(
context: &'a DatamodelCalculatorContext<'a>,
next: sql::ForeignKeyWalker<'a>,
direction: RelationFieldDirection,
) -> Self {
let relation_type = Many2ManyRelationField { next, direction };
Self {
relation_type: RelationType::Many2Many(relation_type),
context,
}
}
pub(crate) fn emulated(
context: &'a DatamodelCalculatorContext<'a>,
previous: walkers::RelationFieldWalker<'a>,
) -> Self {
let relation_type = EmulatedRelationField { previous };
Self {
relation_type: RelationType::Emulated(relation_type),
context,
}
}
pub(crate) fn field_name(self) -> &'a str {
use RelationType::*;
match self.relation_type {
Inline(field) if field.direction.is_forward() => {
self.context.forward_inline_relation_field_prisma_name(field.next.id)
}
Inline(field) => self.context.back_inline_relation_field_prisma_name(field.next.id),
Many2Many(field) if field.direction.is_forward() => self
.context
.forward_m2m_relation_field_prisma_name(field.next.table().id),
Many2Many(field) => self.context.back_m2m_relation_field_prisma_name(field.next.table().id),
Emulated(field) => field.previous.name(),
}
}
pub(crate) fn prisma_type(self) -> Cow<'a, str> {
use RelationType::*;
match self.relation_type {
Inline(field) if field.direction.is_forward() => {
let id = field.next.referenced_table().id;
self.context.table_prisma_name(id).prisma_name()
}
Inline(field) => {
let id = field.next.table().id;
self.context.table_prisma_name(id).prisma_name()
}
Many2Many(field) => {
let id = field.next.referenced_table().id;
self.context.table_prisma_name(id).prisma_name()
}
Emulated(field) => {
let name = field.previous.related_model().name();
Cow::Borrowed(name)
}
}
}
pub(crate) fn constraint_name(self) -> Option<&'a str> {
match self.relation_type {
RelationType::Inline(field) if field.direction.is_forward() => {
if let Some(name) = field.previous.and_then(|prev| prev.mapped_name()) {
return Some(name);
}
let default_name = field.default_constraint_name(self.context);
field.next.constraint_name().filter(|name| name != &default_name)
}
RelationType::Emulated(field) => field.previous.mapped_name(),
_ => None,
}
}
pub(crate) fn relation_name(self) -> Option<Cow<'a, str>> {
let name = match self.relation_type {
RelationType::Inline(field) => self.context.inline_relation_prisma_name(field.next.id),
RelationType::Many2Many(field) => self.context.m2m_relation_prisma_name(field.next.table().id),
RelationType::Emulated(field) => match field.previous.relation_name() {
RelationName::Explicit(name) => Cow::Borrowed(name),
RelationName::Generated(_) => Cow::Borrowed(""),
},
};
if name.is_empty() {
None
} else {
Some(name)
}
}
pub(crate) fn fields(self) -> Option<Box<dyn Iterator<Item = Cow<'a, str>> + 'a>> {
match self.relation_type {
RelationType::Inline(field) if field.direction.is_forward() => {
let iter = field
.next
.constrained_columns()
.map(move |c| self.context.table_column_prisma_name(c.id).prisma_name());
let iter: Box<dyn Iterator<Item = Cow<'a, str>>> = Box::new(iter);
Some(iter)
}
RelationType::Emulated(field) => field.previous.referencing_fields().map(|f| {
let iter = Box::new(f.map(|f| Cow::Borrowed(f.name())));
iter as Box<dyn Iterator<Item = Cow<'a, str>>>
}),
_ => None,
}
}
pub(crate) fn references(self) -> Option<Box<dyn Iterator<Item = Cow<'a, str>> + 'a>> {
match self.relation_type {
RelationType::Inline(field) if field.direction.is_forward() => {
let iter = field
.next
.referenced_columns()
.map(move |c| self.context.table_column_prisma_name(c.id).prisma_name());
let iter: Box<dyn Iterator<Item = Cow<'a, str>>> = Box::new(iter);
Some(iter)
}
RelationType::Emulated(field) => field.previous.referenced_fields().map(|f| {
let iter = Box::new(f.map(|f| Cow::Borrowed(f.name())));
iter as Box<dyn Iterator<Item = Cow<'a, str>>>
}),
_ => None,
}
}
pub(crate) fn on_delete(self) -> Option<&'a str> {
match self.relation_type {
RelationType::Inline(field) if field.direction.is_forward() => {
use sql::ForeignKeyAction::*;
match (field.any_field_required(), field.next.on_delete_action()) {
(false, SetNull) => None,
(true, Restrict) => None,
(true, NoAction) if self.context.sql_family.is_mssql() => None,
(_, Cascade) => Some("Cascade"),
(_, SetDefault) => Some("SetDefault"),
(true, SetNull) => Some("SetNull"),
(_, NoAction) => Some("NoAction"),
(false, Restrict) => Some("Restrict"),
}
}
RelationType::Emulated(field) => field.previous.explicit_on_delete().map(|act| act.as_str()),
_ => None,
}
}
pub(crate) fn on_update(self) -> Option<&'a str> {
match self.relation_type {
RelationType::Inline(field) if field.direction.is_forward() => {
use sql::ForeignKeyAction::*;
match field.next.on_update_action() {
Cascade => None,
NoAction => Some("NoAction"),
Restrict => Some("Restrict"),
SetNull => Some("SetNull"),
SetDefault => Some("SetDefault"),
}
}
RelationType::Emulated(field) => field.previous.explicit_on_update().map(|act| act.as_str()),
_ => None,
}
}
pub(crate) fn ignore(self) -> bool {
use RelationFieldDirection::*;
match self.relation_type {
RelationType::Inline(field) => {
let missing_identifiers = !table_has_usable_identifier(field.next.table())
|| !table_has_usable_identifier(field.next.referenced_table());
let model_ignored = match field.direction {
Forward => field.model(self.context).ignored(),
Back => field.referenced_model(self.context).ignored(),
};
missing_identifiers && !model_ignored
}
RelationType::Many2Many(_) => false,
RelationType::Emulated(field) => field.previous.is_ignored(),
}
}
pub(crate) fn renders_attribute(self) -> bool {
match self.relation_type {
RelationType::Inline(field) if field.direction.is_forward() => true,
RelationType::Emulated(field) => field.previous.relation_attribute().is_some(),
_ => self.relation_name().is_some(),
}
}
pub(crate) fn is_optional(self) -> bool {
match self.relation_type {
RelationType::Inline(field) if field.direction.is_forward() => field.any_field_optional(),
RelationType::Inline(field) => forward_relation_field_is_unique(field.next),
RelationType::Emulated(field) => field.previous.ast_field().arity.is_optional(),
RelationType::Many2Many(_) => false,
}
}
pub(crate) fn is_array(self) -> bool {
match self.relation_type {
RelationType::Inline(field) if field.direction.is_forward() => false,
RelationType::Inline(field) => !forward_relation_field_is_unique(field.next),
RelationType::Emulated(field) => field.previous.ast_field().arity.is_list(),
RelationType::Many2Many(_) => true,
}
}
pub(crate) fn adds_non_default_deferring(self) -> bool {
match self.relation_type {
RelationType::Inline(field) => {
field.previous.is_none()
&& self
.context
.flavour
.uses_non_default_foreign_key_deferring(self.context, field.next)
}
RelationType::Many2Many(_) => false,
RelationType::Emulated(_) => false,
}
}
pub(crate) fn reintrospected_relation(self) -> bool {
matches!(self.relation_type, RelationType::Emulated(_))
}
}
fn forward_relation_field_is_unique(fk: sql::ForeignKeyWalker<'_>) -> bool {
fk.table()
.indexes()
.filter(|idx| idx.is_primary_key() || idx.is_unique())
.any(|idx| {
idx.columns().all(|idx_col| {
fk.constrained_columns()
.any(|fk_col| fk_col.id == idx_col.as_column().id)
})
})
}
fn table_has_usable_identifier(table: sql::TableWalker<'_>) -> bool {
table
.indexes()
.filter(|idx| idx.is_primary_key() || idx.is_unique())
.any(|idx| {
idx.columns().all(|c| {
!matches!(
c.as_column().column_type().family,
sql::ColumnTypeFamily::Unsupported(_)
) && c.as_column().arity().is_required()
})
})
}