use compact_str::CompactString;
use smallvec::SmallVec;
use crate::types::QualifiedIdentifier;
#[derive(Debug, Clone)]
pub struct Relationship {
pub table: QualifiedIdentifier,
pub foreign_table: QualifiedIdentifier,
pub is_self: bool,
pub cardinality: Cardinality,
pub table_is_view: bool,
pub foreign_table_is_view: bool,
}
impl Relationship {
pub fn is_to_one(&self) -> bool {
matches!(
self.cardinality,
Cardinality::M2O { .. } | Cardinality::O2O { .. }
)
}
pub fn is_to_many(&self) -> bool {
matches!(
self.cardinality,
Cardinality::O2M { .. } | Cardinality::M2M(_)
)
}
pub fn constraint_name(&self) -> &str {
match &self.cardinality {
Cardinality::M2O { constraint, .. } => constraint,
Cardinality::O2M { constraint, .. } => constraint,
Cardinality::O2O { constraint, .. } => constraint,
Cardinality::M2M(j) => &j.constraint1,
}
}
pub fn columns(&self) -> &[(CompactString, CompactString)] {
match &self.cardinality {
Cardinality::M2O { columns, .. } => columns,
Cardinality::O2M { columns, .. } => columns,
Cardinality::O2O { columns, .. } => columns,
Cardinality::M2M(j) => &j.cols_source,
}
}
pub fn source_columns(&self) -> impl Iterator<Item = &str> {
self.columns().iter().map(|(src, _)| src.as_str())
}
pub fn target_columns(&self) -> impl Iterator<Item = &str> {
self.columns().iter().map(|(_, tgt)| tgt.as_str())
}
pub fn uses_column(&self, col_name: &str) -> bool {
self.columns()
.iter()
.any(|(src, tgt)| src.as_str() == col_name || tgt.as_str() == col_name)
}
pub fn is_m2m(&self) -> bool {
matches!(self.cardinality, Cardinality::M2M(_))
}
pub fn junction(&self) -> Option<&Junction> {
match &self.cardinality {
Cardinality::M2M(j) => Some(j),
_ => None,
}
}
pub fn reverse(&self) -> Self {
let rev_cardinality = match &self.cardinality {
Cardinality::M2O {
constraint,
columns,
} => Cardinality::O2M {
constraint: constraint.clone(),
columns: columns
.iter()
.map(|(a, b)| (b.clone(), a.clone()))
.collect(),
},
Cardinality::O2M {
constraint,
columns,
} => Cardinality::M2O {
constraint: constraint.clone(),
columns: columns
.iter()
.map(|(a, b)| (b.clone(), a.clone()))
.collect(),
},
Cardinality::O2O {
constraint,
columns,
is_parent,
} => Cardinality::O2O {
constraint: constraint.clone(),
columns: columns
.iter()
.map(|(a, b)| (b.clone(), a.clone()))
.collect(),
is_parent: !is_parent,
},
Cardinality::M2M(j) => Cardinality::M2M(j.clone()), };
Relationship {
table: self.foreign_table.clone(),
foreign_table: self.table.clone(),
is_self: self.is_self,
cardinality: rev_cardinality,
table_is_view: self.foreign_table_is_view,
foreign_table_is_view: self.table_is_view,
}
}
pub fn is_o2o_parent(&self) -> bool {
matches!(
&self.cardinality,
Cardinality::O2O {
is_parent: true,
..
}
)
}
pub fn is_o2o_child(&self) -> bool {
matches!(
&self.cardinality,
Cardinality::O2O {
is_parent: false,
..
}
)
}
}
#[derive(Debug, Clone)]
pub enum Cardinality {
M2O {
constraint: CompactString,
columns: SmallVec<[(CompactString, CompactString); 2]>,
},
O2M {
constraint: CompactString,
columns: SmallVec<[(CompactString, CompactString); 2]>,
},
O2O {
constraint: CompactString,
columns: SmallVec<[(CompactString, CompactString); 2]>,
is_parent: bool,
},
M2M(Junction),
}
impl Cardinality {
pub fn as_str(&self) -> &'static str {
match self {
Cardinality::M2O { .. } => "M2O",
Cardinality::O2M { .. } => "O2M",
Cardinality::O2O { .. } => "O2O",
Cardinality::M2M(_) => "M2M",
}
}
}
#[derive(Debug, Clone)]
pub struct Junction {
pub table: QualifiedIdentifier,
pub constraint1: CompactString,
pub constraint2: CompactString,
pub cols_source: SmallVec<[(CompactString, CompactString); 2]>,
pub cols_target: SmallVec<[(CompactString, CompactString); 2]>,
}
impl Junction {
pub fn junction_columns(&self) -> impl Iterator<Item = &str> {
self.cols_source
.iter()
.chain(self.cols_target.iter())
.map(|(junc_col, _)| junc_col.as_str())
}
pub fn source_columns(&self) -> impl Iterator<Item = &str> {
self.cols_source.iter().map(|(_, src_col)| src_col.as_str())
}
pub fn target_columns(&self) -> impl Iterator<Item = &str> {
self.cols_target.iter().map(|(_, tgt_col)| tgt_col.as_str())
}
}
#[derive(Debug, Clone)]
pub struct ComputedRelationship {
pub table: QualifiedIdentifier,
pub function: QualifiedIdentifier,
pub foreign_table: QualifiedIdentifier,
pub table_alias: QualifiedIdentifier,
pub is_self: bool,
pub single_row: bool,
}
impl ComputedRelationship {
pub fn returns_set(&self) -> bool {
!self.single_row
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)] pub enum AnyRelationship {
ForeignKey(Relationship),
Computed(ComputedRelationship),
}
impl AnyRelationship {
pub fn table(&self) -> &QualifiedIdentifier {
match self {
AnyRelationship::ForeignKey(r) => &r.table,
AnyRelationship::Computed(r) => &r.table,
}
}
pub fn foreign_table(&self) -> &QualifiedIdentifier {
match self {
AnyRelationship::ForeignKey(r) => &r.foreign_table,
AnyRelationship::Computed(r) => &r.foreign_table,
}
}
pub fn is_self(&self) -> bool {
match self {
AnyRelationship::ForeignKey(r) => r.is_self,
AnyRelationship::Computed(r) => r.is_self,
}
}
pub fn is_to_one(&self) -> bool {
match self {
AnyRelationship::ForeignKey(r) => r.is_to_one(),
AnyRelationship::Computed(r) => r.single_row,
}
}
pub fn is_fk(&self) -> bool {
matches!(self, AnyRelationship::ForeignKey(_))
}
pub fn is_computed(&self) -> bool {
matches!(self, AnyRelationship::Computed(_))
}
pub fn as_fk(&self) -> Option<&Relationship> {
match self {
AnyRelationship::ForeignKey(r) => Some(r),
AnyRelationship::Computed(_) => None,
}
}
pub fn as_computed(&self) -> Option<&ComputedRelationship> {
match self {
AnyRelationship::ForeignKey(_) => None,
AnyRelationship::Computed(r) => Some(r),
}
}
}
impl From<Relationship> for AnyRelationship {
fn from(r: Relationship) -> Self {
AnyRelationship::ForeignKey(r)
}
}
impl From<ComputedRelationship> for AnyRelationship {
fn from(r: ComputedRelationship) -> Self {
AnyRelationship::Computed(r)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
#[test]
fn test_relationship_is_to_one_m2o() {
let rel = test_relationship()
.m2o("fk_user", &[("user_id", "id")])
.build();
assert!(rel.is_to_one());
assert!(!rel.is_to_many());
}
#[test]
fn test_relationship_is_to_one_o2o() {
let rel = test_relationship()
.o2o("fk_profile", &[("user_id", "id")], false)
.build();
assert!(rel.is_to_one());
assert!(!rel.is_to_many());
}
#[test]
fn test_relationship_is_to_many_o2m() {
let rel = test_relationship()
.o2m("fk_posts", &[("id", "user_id")])
.build();
assert!(!rel.is_to_one());
assert!(rel.is_to_many());
}
#[test]
fn test_relationship_is_to_many_m2m() {
let junction = test_junction()
.table("public", "user_roles")
.cols_source(&[("user_id", "id")])
.cols_target(&[("role_id", "id")])
.build();
let rel = test_relationship().m2m(junction).build();
assert!(!rel.is_to_one());
assert!(rel.is_to_many());
}
#[test]
fn test_relationship_constraint_name() {
let rel = test_relationship()
.m2o("my_constraint", &[("fk_col", "pk_col")])
.build();
assert_eq!(rel.constraint_name(), "my_constraint");
}
#[test]
fn test_relationship_columns() {
let rel = test_relationship()
.m2o("fk_test", &[("col_a", "col_b"), ("col_c", "col_d")])
.build();
let cols = rel.columns();
assert_eq!(cols.len(), 2);
assert_eq!(cols[0].0.as_str(), "col_a");
assert_eq!(cols[0].1.as_str(), "col_b");
}
#[test]
fn test_relationship_source_columns() {
let rel = test_relationship()
.m2o("fk", &[("src1", "tgt1"), ("src2", "tgt2")])
.build();
let sources: Vec<_> = rel.source_columns().collect();
assert_eq!(sources, vec!["src1", "src2"]);
}
#[test]
fn test_relationship_target_columns() {
let rel = test_relationship()
.m2o("fk", &[("src1", "tgt1"), ("src2", "tgt2")])
.build();
let targets: Vec<_> = rel.target_columns().collect();
assert_eq!(targets, vec!["tgt1", "tgt2"]);
}
#[test]
fn test_relationship_uses_column() {
let rel = test_relationship().m2o("fk", &[("user_id", "id")]).build();
assert!(rel.uses_column("user_id"));
assert!(rel.uses_column("id"));
assert!(!rel.uses_column("name"));
}
#[test]
fn test_relationship_is_m2m() {
let junction = test_junction().build();
let m2m_rel = test_relationship().m2m(junction).build();
assert!(m2m_rel.is_m2m());
let m2o_rel = test_relationship().m2o("fk", &[("a", "b")]).build();
assert!(!m2o_rel.is_m2m());
}
#[test]
fn test_relationship_junction() {
let junction = test_junction().table("public", "user_roles").build();
let rel = test_relationship().m2m(junction).build();
let j = rel.junction().unwrap();
assert_eq!(j.table.name.as_str(), "user_roles");
}
#[test]
fn test_relationship_o2o_parent_child() {
let parent_rel = test_relationship()
.o2o("fk", &[("id", "user_id")], true)
.build();
assert!(parent_rel.is_o2o_parent());
assert!(!parent_rel.is_o2o_child());
let child_rel = test_relationship()
.o2o("fk", &[("user_id", "id")], false)
.build();
assert!(!child_rel.is_o2o_parent());
assert!(child_rel.is_o2o_child());
}
#[test]
fn test_relationship_is_self() {
let self_rel = test_relationship()
.table("public", "employees")
.foreign_table("public", "employees")
.is_self(true)
.build();
assert!(self_rel.is_self);
let normal_rel = test_relationship()
.table("public", "posts")
.foreign_table("public", "users")
.is_self(false)
.build();
assert!(!normal_rel.is_self);
}
#[test]
fn test_cardinality_as_str() {
assert_eq!(
Cardinality::M2O {
constraint: "fk".into(),
columns: smallvec::smallvec![]
}
.as_str(),
"M2O"
);
assert_eq!(
Cardinality::O2M {
constraint: "fk".into(),
columns: smallvec::smallvec![]
}
.as_str(),
"O2M"
);
assert_eq!(
Cardinality::O2O {
constraint: "fk".into(),
columns: smallvec::smallvec![],
is_parent: false
}
.as_str(),
"O2O"
);
assert_eq!(Cardinality::M2M(test_junction().build()).as_str(), "M2M");
}
#[test]
fn test_junction_columns() {
let junction = test_junction()
.cols_source(&[("user_id", "id")])
.cols_target(&[("role_id", "id")])
.build();
let junc_cols: Vec<_> = junction.junction_columns().collect();
assert_eq!(junc_cols, vec!["user_id", "role_id"]);
}
#[test]
fn test_junction_source_columns() {
let junction = test_junction().cols_source(&[("user_id", "id")]).build();
let cols: Vec<_> = junction.source_columns().collect();
assert_eq!(cols, vec!["id"]);
}
#[test]
fn test_junction_target_columns() {
let junction = test_junction().cols_target(&[("role_id", "id")]).build();
let cols: Vec<_> = junction.target_columns().collect();
assert_eq!(cols, vec!["id"]);
}
#[test]
fn test_computed_rel_returns_set() {
let single_row = test_computed_rel().single_row(true).build();
assert!(!single_row.returns_set());
let multi_row = test_computed_rel().single_row(false).build();
assert!(multi_row.returns_set());
}
#[test]
fn test_any_relationship_table() {
let fk_rel: AnyRelationship = test_relationship().table("api", "posts").build().into();
assert_eq!(fk_rel.table().schema.as_str(), "api");
assert_eq!(fk_rel.table().name.as_str(), "posts");
let computed_rel: AnyRelationship =
test_computed_rel().table("api", "users").build().into();
assert_eq!(computed_rel.table().schema.as_str(), "api");
assert_eq!(computed_rel.table().name.as_str(), "users");
}
#[test]
fn test_any_relationship_foreign_table() {
let fk_rel: AnyRelationship = test_relationship()
.foreign_table("api", "users")
.build()
.into();
assert_eq!(fk_rel.foreign_table().name.as_str(), "users");
}
#[test]
fn test_any_relationship_is_self() {
let self_rel: AnyRelationship = test_relationship().is_self(true).build().into();
assert!(self_rel.is_self());
let computed_self: AnyRelationship = test_computed_rel().is_self(true).build().into();
assert!(computed_self.is_self());
}
#[test]
fn test_any_relationship_is_to_one() {
let m2o: AnyRelationship = test_relationship().m2o("fk", &[("a", "b")]).build().into();
assert!(m2o.is_to_one());
let o2m: AnyRelationship = test_relationship().o2m("fk", &[("a", "b")]).build().into();
assert!(!o2m.is_to_one());
let computed_single: AnyRelationship = test_computed_rel().single_row(true).build().into();
assert!(computed_single.is_to_one());
let computed_multi: AnyRelationship = test_computed_rel().single_row(false).build().into();
assert!(!computed_multi.is_to_one());
}
#[test]
fn test_any_relationship_is_fk_computed() {
let fk_rel: AnyRelationship = test_relationship().build().into();
assert!(fk_rel.is_fk());
assert!(!fk_rel.is_computed());
let computed_rel: AnyRelationship = test_computed_rel().build().into();
assert!(!computed_rel.is_fk());
assert!(computed_rel.is_computed());
}
#[test]
fn test_any_relationship_as_fk_computed() {
let fk_rel: AnyRelationship = test_relationship().build().into();
assert!(fk_rel.as_fk().is_some());
assert!(fk_rel.as_computed().is_none());
let computed_rel: AnyRelationship = test_computed_rel().build().into();
assert!(computed_rel.as_fk().is_none());
assert!(computed_rel.as_computed().is_some());
}
}