use serde::{Deserialize, Serialize};
use crate::identifier::{Identifier, QualifiedName};
use crate::ir::default_expr::NormalizedExpr;
use crate::ir::difference::Difference;
use crate::ir::eq::{Equiv, field_difference, prefix_differences};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Constraint {
pub qname: QualifiedName,
pub kind: ConstraintKind,
pub deferrable: Deferrable,
pub comment: Option<String>,
}
impl Equiv for Constraint {
fn differences(&self, other: &Self) -> Vec<Difference> {
let Self {
qname: _,
kind: _,
deferrable: _,
comment: _,
} = self;
let mut out = Vec::new();
out.extend(field_difference("qname", &self.qname, &other.qname));
out.extend(prefix_differences(
"kind",
Equiv::differences(&self.kind, &other.kind),
));
out.extend(field_difference(
"deferrable",
&format!("{:?}", self.deferrable),
&format!("{:?}", other.deferrable),
));
out.extend(field_difference(
"comment",
&format!("{:?}", self.comment),
&format!("{:?}", other.comment),
));
out
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ConstraintKind {
PrimaryKey {
columns: Vec<Identifier>,
include: Vec<Identifier>,
},
Unique {
columns: Vec<Identifier>,
include: Vec<Identifier>,
nulls_distinct: bool,
},
ForeignKey(ForeignKey),
Check {
expression: NormalizedExpr,
no_inherit: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ForeignKey {
pub columns: Vec<Identifier>,
pub referenced_table: QualifiedName,
pub referenced_columns: Vec<Identifier>,
pub on_update: ReferentialAction,
pub on_delete: ReferentialAction,
pub match_type: FkMatchType,
}
impl Equiv for ForeignKey {
fn differences(&self, other: &Self) -> Vec<Difference> {
let Self {
columns: _,
referenced_table: _,
referenced_columns: _,
on_update: _,
on_delete: _,
match_type: _,
} = self;
let mut out = Vec::new();
out.extend(field_difference(
"columns",
&format!("{:?}", self.columns),
&format!("{:?}", other.columns),
));
out.extend(field_difference(
"referenced_table",
&self.referenced_table,
&other.referenced_table,
));
out.extend(field_difference(
"referenced_columns",
&format!("{:?}", self.referenced_columns),
&format!("{:?}", other.referenced_columns),
));
out.extend(field_difference(
"on_update",
&format!("{:?}", self.on_update),
&format!("{:?}", other.on_update),
));
out.extend(field_difference(
"on_delete",
&format!("{:?}", self.on_delete),
&format!("{:?}", other.on_delete),
));
out.extend(field_difference(
"match_type",
&format!("{:?}", self.match_type),
&format!("{:?}", other.match_type),
));
out
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReferentialAction {
NoAction,
Restrict,
Cascade,
SetNull(Vec<Identifier>),
SetDefault(Vec<Identifier>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FkMatchType {
Simple,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Deferrable {
NotDeferrable,
Deferrable {
initially_deferred: bool,
},
}
fn render_idents(v: &[Identifier]) -> String {
let mut s = String::from("[");
for (i, id) in v.iter().enumerate() {
if i > 0 {
s.push(',');
}
s.push_str(id.as_str());
}
s.push(']');
s
}
impl Equiv for ConstraintKind {
fn differences(&self, other: &Self) -> Vec<Difference> {
let mut out = Vec::new();
match (self, other) {
(
Self::PrimaryKey {
columns: c1,
include: i1,
},
Self::PrimaryKey {
columns: c2,
include: i2,
},
) => {
out.extend(field_difference(
"columns",
&render_idents(c1),
&render_idents(c2),
));
out.extend(field_difference(
"include",
&render_idents(i1),
&render_idents(i2),
));
}
(
Self::Unique {
columns: c1,
include: i1,
nulls_distinct: n1,
},
Self::Unique {
columns: c2,
include: i2,
nulls_distinct: n2,
},
) => {
out.extend(field_difference(
"columns",
&render_idents(c1),
&render_idents(c2),
));
out.extend(field_difference(
"include",
&render_idents(i1),
&render_idents(i2),
));
out.extend(field_difference("nulls_distinct", n1, n2));
}
(Self::ForeignKey(a), Self::ForeignKey(b)) => {
out.extend(prefix_differences("fk", a.differences(b)));
}
(
Self::Check {
expression: e1,
no_inherit: n1,
},
Self::Check {
expression: e2,
no_inherit: n2,
},
) => {
out.extend(field_difference(
"expression",
&e1.canonical_text,
&e2.canonical_text,
));
out.extend(field_difference("no_inherit", n1, n2));
}
_ => {
out.push(Difference::new(
"",
format!("{self:?}"),
format!("{other:?}"),
));
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn qn(schema: &str, name: &str) -> QualifiedName {
QualifiedName::new(id(schema), id(name))
}
fn pk_constraint(cols: &[&str]) -> Constraint {
Constraint {
qname: qn("app", "users_pkey"),
kind: ConstraintKind::PrimaryKey {
columns: cols.iter().map(|c| id(c)).collect(),
include: vec![],
},
deferrable: Deferrable::NotDeferrable,
comment: None,
}
}
#[test]
fn equal_pks_have_no_diff() {
assert!(pk_constraint(&["id"]).canonical_eq(&pk_constraint(&["id"])));
}
#[test]
fn pk_column_list_change_diffs() {
let a = pk_constraint(&["id"]);
let b = pk_constraint(&["id", "tenant_id"]);
let d = a.differences(&b);
assert!(d.iter().any(|x| x.path == "kind.columns"));
}
#[test]
fn pk_column_order_matters() {
let a = pk_constraint(&["a", "b"]);
let b = pk_constraint(&["b", "a"]);
assert!(!a.canonical_eq(&b));
}
#[test]
fn fk_on_delete_change_diffs() {
let mk = |on_delete| Constraint {
qname: qn("app", "users_org_fkey"),
kind: ConstraintKind::ForeignKey(ForeignKey {
columns: vec![id("org_id")],
referenced_table: qn("app", "orgs"),
referenced_columns: vec![id("id")],
on_update: ReferentialAction::NoAction,
on_delete,
match_type: FkMatchType::Simple,
}),
deferrable: Deferrable::NotDeferrable,
comment: None,
};
let a = mk(ReferentialAction::NoAction);
let b = mk(ReferentialAction::Cascade);
let d = a.differences(&b);
assert!(d.iter().any(|x| x.path == "kind.fk.on_delete"));
}
#[test]
fn pk_vs_unique_kind_change() {
let a = pk_constraint(&["id"]);
let b = Constraint {
kind: ConstraintKind::Unique {
columns: vec![id("id")],
include: vec![],
nulls_distinct: true,
},
..pk_constraint(&["id"])
};
let d = a.differences(&b);
assert!(d.iter().any(|x| x.path == "kind"));
}
#[test]
fn deferrable_change_diffs() {
let mut b = pk_constraint(&["id"]);
b.deferrable = Deferrable::Deferrable {
initially_deferred: true,
};
let d = pk_constraint(&["id"]).differences(&b);
assert!(d.iter().any(|x| x.path == "deferrable"));
}
}