Skip to content

Commit

Permalink
feat: dup relation validator
Browse files Browse the repository at this point in the history
  • Loading branch information
apskhem committed Mar 12, 2024
1 parent 9955c2d commit dfc5f9b
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 57 deletions.
6 changes: 3 additions & 3 deletions src/analyzer/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ pub enum Err {
DuplicatedTableName,
#[display(fmt = "Duplicated column name")]
DuplicatedColumnName,
#[display(fmt = "Duplicated relation")]
DuplicatedRelation,
#[display(fmt = "Conflict relation")]
ConflictRelation,
#[display(fmt = "Duplicated enum name")]
DuplicatedEnumName,
#[display(fmt = "Duplicated enum value")]
Expand Down Expand Up @@ -98,7 +98,7 @@ pub enum InvalidForeignKeyErr {
One2One,
#[display(fmt = "either side of the composite one-to-one relation must be a composite primary key or a composite unique key")]
One2OneComposite,
#[display(fmt = "Invalid foreign key: both sides of the many-to-many relation must be a primary key or a unique key")]
#[display(fmt = "both sides of the many-to-many relation must be a primary key or a unique key")]
Many2Many,
#[display(fmt = "both sides of the composite many-to-many relation must be either a composite primary key or a composite unique key")]
Many2ManyComposite
Expand Down
4 changes: 4 additions & 0 deletions src/analyzer/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ pub(super) fn check_prop_duplicate_keys(attrs: &Vec<Property>, input: &str) -> A
}

Ok(())
}

pub(super) fn eq_elements<T: Eq + Ord>(lhs: impl Iterator<Item = T>, rhs: impl Iterator<Item = T>) -> bool {
BTreeSet::from_iter(lhs) == BTreeSet::from_iter(rhs)
}
110 changes: 62 additions & 48 deletions src/analyzer/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,17 +379,17 @@ impl IndexedRef {
let is_valid_composite = |compositions: &Vec<Ident>, table_indexes: &Option<IndexesBlock>| -> bool {
table_indexes.as_ref().map(|indexes| {
indexes.defs.iter().any(|def_item| {
let composition_cols = BTreeSet::from_iter(compositions.iter().map(|s| s.to_string.clone()));
let indexes_cols = BTreeSet::from_iter(def_item.cols.iter().filter_map(|s| {
match s {
IndexesColumnType::String(s) => Some(s.to_string.clone()),
_ => None
}
}));

compositions.len() == def_item.cols.len()
&& def_item.settings.as_ref().is_some_and(|s| s.is_pk || s.is_unique)
&& composition_cols == indexes_cols
&& eq_elements(
compositions.iter().map(|s| &s.to_string),
def_item.cols.iter().filter_map(|s| {
match s {
IndexesColumnType::String(s) => Some(&s.to_string),
_ => None
}
})
)
})
}).unwrap_or_default()
};
Expand Down Expand Up @@ -421,18 +421,18 @@ impl IndexedRef {

if composition_len == 1 {
let err = match (
self.rel.clone(),
l_col.settings.clone().is_some_and(|s| s.is_pk || s.is_unique),
r_col.settings.clone().is_some_and(|s| s.is_pk || s.is_unique)) {
(Relation::One2One, false, false) => Some(InvalidForeignKeyErr::One2One),
(Relation::Many2Many, false, false) => Some(InvalidForeignKeyErr::Many2Many),
(Relation::One2Many, false, _) => Some(InvalidForeignKeyErr::NitherUniqueKeyNorPrimaryKey),
(Relation::Many2One, _, false) => Some(InvalidForeignKeyErr::NitherUniqueKeyNorPrimaryKey),
&self.rel,
l_col.settings.as_ref().is_some_and(|s| s.is_pk || s.is_unique),
r_col.settings.as_ref().is_some_and(|s| s.is_pk || s.is_unique)) {
(Relation::One2One, false, false) => Some((InvalidForeignKeyErr::One2One, &self.span_range)),
(Relation::Many2Many, false, false) => Some((InvalidForeignKeyErr::Many2Many, &self.span_range)),
(Relation::One2Many, false, _) => Some((InvalidForeignKeyErr::NitherUniqueKeyNorPrimaryKey, &self.lhs.span_range)),
(Relation::Many2One, _, false) => Some((InvalidForeignKeyErr::NitherUniqueKeyNorPrimaryKey, &self.rhs.span_range)),
_ => None
};

if let Some(err) = err {
throw_err(Err::InvalidForeignKey { err }, &self.lhs.span_range, input)?;
if let Some((err, span_range)) = err {
throw_err(Err::InvalidForeignKey { err }, &span_range, input)?;
}
}
}
Expand All @@ -444,18 +444,18 @@ impl IndexedRef {

let err = match self.rel {
Relation::One2One
if !is_valid_lhs && !is_valid_rhs => Some(InvalidForeignKeyErr::One2OneComposite),
if !is_valid_lhs && !is_valid_rhs => Some((InvalidForeignKeyErr::One2OneComposite, &self.span_range)),
Relation::Many2Many
if !is_valid_lhs || !is_valid_rhs => Some(InvalidForeignKeyErr::Many2ManyComposite),
if !is_valid_lhs || !is_valid_rhs => Some((InvalidForeignKeyErr::Many2ManyComposite, &self.span_range)),
Relation::One2Many
if !is_valid_lhs => Some(InvalidForeignKeyErr::NitherUniqueKeyNorPrimaryKeyComposite),
if !is_valid_lhs => Some((InvalidForeignKeyErr::NitherUniqueKeyNorPrimaryKeyComposite, &self.lhs.span_range)),
Relation::Many2One
if !is_valid_rhs => Some(InvalidForeignKeyErr::NitherUniqueKeyNorPrimaryKeyComposite),
if !is_valid_rhs => Some((InvalidForeignKeyErr::NitherUniqueKeyNorPrimaryKeyComposite, &self.rhs.span_range)),
_ => None
};

if let Some(err) = err {
throw_err(Err::InvalidForeignKey { err }, &self.lhs.span_range, input)?;
if let Some((err, span_range)) = err {
throw_err(Err::InvalidForeignKey { err }, &span_range, input)?;
}
}
_ => ()
Expand All @@ -464,36 +464,50 @@ impl IndexedRef {
Ok(())
}

fn normalize(self) -> Self {
match self.rel {
Relation::One2Many => {
Self {
span_range: self.span_range,
rel: Relation::Many2One,
lhs: self.rhs,
rhs: self.lhs,
settings: self.settings
}
}
_ => self
}
}

/// Check if both relations point to the same column.
/// Should use after calling `validate_ref_type`.
pub fn occupy_same_column(&self, other: &Self, indexer: &Indexer) -> bool {
let self_normalized = self.clone().normalize();
let other_normalized = other.clone().normalize();
let eq_ident = |lhs: &RefIdent, rhs: &RefIdent| -> bool {
lhs.schema.as_ref().map(|s| &s.to_string) == rhs.schema.as_ref().map(|s| &s.to_string)
&& lhs.table.to_string == rhs.table.to_string
&& eq_elements(lhs.compositions.iter().map(|s| &s.to_string), rhs.compositions.iter().map(|s| &s.to_string))
};

let self_ident = indexer.resolve_ref_alias(&self.lhs);
let other_ident = indexer.resolve_ref_alias(&other.lhs);
match (&self.rel, &other.rel) {
(Relation::Many2Many, Relation::Many2Many) => {
[&self.lhs, &self.rhs].iter().all(|self_side| {
[&other.lhs, &other.rhs].iter().any(|other_side| {
let self_ident = indexer.resolve_ref_alias(self_side);
let other_ident = indexer.resolve_ref_alias(other_side);

let self_compositions = self_ident.compositions.iter().map(|s| s.to_string.clone()).collect::<Vec<_>>();
let other_compositions = other_ident.compositions.iter().map(|s| s.to_string.clone()).collect::<Vec<_>>();
eq_ident(&self_ident, &other_ident)
})
})
}
_ => {
// TODO: add support for one-to-one relation validation
let self_occupied_ident = match &self.rel {
Relation::Many2One => Some(&self.lhs),
Relation::One2Many => Some(&self.rhs),
_ => None
};
let other_occupied_ident = match &other.rel {
Relation::Many2One => Some(&other.lhs),
Relation::One2Many => Some(&other.rhs),
_ => None
};

self_compositions == other_compositions
&& self_ident.schema.map(|s| s.to_string) == other_ident.schema.map(|s| s.to_string)
&& self_ident.table.to_string == other_ident.table.to_string
match (self_occupied_ident, other_occupied_ident) {
(Some(self_ident), Some(other_ident)) => {
let self_ident = indexer.resolve_ref_alias(self_ident);
let other_ident = indexer.resolve_ref_alias(other_ident);

eq_ident(&self_ident, &other_ident)
}
_ => false
}
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ pub fn analyze(schema_block: &SchemaBlock) -> AnalyzerResult<AnalyzedIndexer> {
.count();

if count != 1 {
throw_err(Err::DuplicatedRelation, &indexed_ref.span_range, input)?;
throw_err(Err::ConflictRelation, &indexed_ref.span_range, input)?;
}
}

Expand Down
10 changes: 5 additions & 5 deletions tests/dbml/checked/sample_tmp.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ Table users {
}

// Ref: users.(role, age) <> orders.(status, number)
// Ref: users.role > orders.status
// Ref: users.role < orders.tmp
Ref: users.role > orders.status
// Ref: orders.tmp < users.role

Table posts as P {
id integer [pk]
title varchar
body text [note: 'Content of the post']
user_id integer [ref: > users.id]
status post_status [default: draft]
tmp post_status
status post_status [default: draft, unique]
tmp post_status [unique]
created_at timestamp
}

Expand All @@ -45,7 +45,7 @@ Table orders {
✔️ 2 = shipped,
❌ 3 = cancelled,
😔 4 = refunded
']
', unique]

indexes {
(status, number) [unique]
Expand Down

0 comments on commit dfc5f9b

Please sign in to comment.