Skip to main content

pgevolve_core/ir/
constraint.rs

1//! `Constraint` and related types.
2//!
3//! The IR represents fully-validated constraints. The `NOT VALID` intermediate
4//! state is a planner concern; it does not appear here.
5
6use serde::{Deserialize, Serialize};
7
8use crate::identifier::{Identifier, QualifiedName};
9use crate::ir::default_expr::NormalizedExpr;
10use crate::ir::difference::Difference;
11use crate::ir::eq::{Diff, DiffMacro, diff_field, prefix_diffs};
12
13/// A table constraint.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DiffMacro)]
15pub struct Constraint {
16    /// Schema-qualified constraint name (constraints carry their own names).
17    pub qname: QualifiedName,
18    /// What the constraint enforces.
19    #[diff(nested)]
20    pub kind: ConstraintKind,
21    /// Deferrability.
22    #[diff(via_debug)]
23    pub deferrable: Deferrable,
24    /// Optional comment.
25    #[diff(via_debug)]
26    pub comment: Option<String>,
27}
28
29/// What a constraint enforces.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(tag = "kind", rename_all = "snake_case")]
32pub enum ConstraintKind {
33    /// `PRIMARY KEY (cols) [INCLUDE (cols)]`. Column order is significant.
34    PrimaryKey {
35        /// Key columns; order matters.
36        columns: Vec<Identifier>,
37        /// `INCLUDE` (covering) columns.
38        include: Vec<Identifier>,
39    },
40    /// `UNIQUE (cols) [INCLUDE (cols)] [NULLS [NOT] DISTINCT]`.
41    Unique {
42        /// Unique columns; order matters.
43        columns: Vec<Identifier>,
44        /// `INCLUDE` columns.
45        include: Vec<Identifier>,
46        /// Default Postgres semantics: nulls are distinct (i.e., multiple NULLs allowed).
47        /// PG 15+ supports `NULLS NOT DISTINCT` to disallow duplicate NULLs.
48        nulls_distinct: bool,
49    },
50    /// `FOREIGN KEY ...`.
51    ForeignKey(ForeignKey),
52    /// `CHECK (expr)`.
53    Check {
54        /// Boolean predicate expression.
55        expression: NormalizedExpr,
56        /// `NO INHERIT` flag.
57        no_inherit: bool,
58    },
59}
60
61/// `FOREIGN KEY ... REFERENCES ...` definition.
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DiffMacro)]
63pub struct ForeignKey {
64    /// Local columns; order matches `referenced_columns`.
65    #[diff(via_debug)]
66    pub columns: Vec<Identifier>,
67    /// Referenced table.
68    pub referenced_table: QualifiedName,
69    /// Referenced columns; order matches `columns`.
70    #[diff(via_debug)]
71    pub referenced_columns: Vec<Identifier>,
72    /// Action on update.
73    #[diff(via_debug)]
74    pub on_update: ReferentialAction,
75    /// Action on delete.
76    #[diff(via_debug)]
77    pub on_delete: ReferentialAction,
78    /// Match type.
79    #[diff(via_debug)]
80    pub match_type: FkMatchType,
81}
82
83/// Referential action for `ON UPDATE` / `ON DELETE`.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum ReferentialAction {
87    /// `NO ACTION` (default).
88    NoAction,
89    /// `RESTRICT`.
90    Restrict,
91    /// `CASCADE`.
92    Cascade,
93    /// `SET NULL [ (cols) ]`.
94    SetNull(Vec<Identifier>),
95    /// `SET DEFAULT [ (cols) ]`.
96    SetDefault(Vec<Identifier>),
97}
98
99/// Foreign-key match type.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum FkMatchType {
103    /// `MATCH SIMPLE` (default).
104    Simple,
105    /// `MATCH FULL`.
106    Full,
107}
108
109/// Deferrability of a constraint.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
111#[serde(tag = "kind", rename_all = "snake_case")]
112pub enum Deferrable {
113    /// `NOT DEFERRABLE` (default).
114    NotDeferrable,
115    /// `DEFERRABLE [INITIALLY DEFERRED|IMMEDIATE]`.
116    Deferrable {
117        /// `INITIALLY DEFERRED` if true; `INITIALLY IMMEDIATE` otherwise.
118        initially_deferred: bool,
119    },
120}
121
122fn render_idents(v: &[Identifier]) -> String {
123    let mut s = String::from("[");
124    for (i, id) in v.iter().enumerate() {
125        if i > 0 {
126            s.push(',');
127        }
128        s.push_str(id.as_str());
129    }
130    s.push(']');
131    s
132}
133
134impl Diff for ConstraintKind {
135    fn diff(&self, other: &Self) -> Vec<Difference> {
136        let mut out = Vec::new();
137        match (self, other) {
138            (
139                Self::PrimaryKey {
140                    columns: c1,
141                    include: i1,
142                },
143                Self::PrimaryKey {
144                    columns: c2,
145                    include: i2,
146                },
147            ) => {
148                out.extend(diff_field(
149                    "columns",
150                    &render_idents(c1),
151                    &render_idents(c2),
152                ));
153                out.extend(diff_field(
154                    "include",
155                    &render_idents(i1),
156                    &render_idents(i2),
157                ));
158            }
159            (
160                Self::Unique {
161                    columns: c1,
162                    include: i1,
163                    nulls_distinct: n1,
164                },
165                Self::Unique {
166                    columns: c2,
167                    include: i2,
168                    nulls_distinct: n2,
169                },
170            ) => {
171                out.extend(diff_field(
172                    "columns",
173                    &render_idents(c1),
174                    &render_idents(c2),
175                ));
176                out.extend(diff_field(
177                    "include",
178                    &render_idents(i1),
179                    &render_idents(i2),
180                ));
181                out.extend(diff_field("nulls_distinct", n1, n2));
182            }
183            (Self::ForeignKey(a), Self::ForeignKey(b)) => {
184                out.extend(prefix_diffs("fk", a.diff(b)));
185            }
186            (
187                Self::Check {
188                    expression: e1,
189                    no_inherit: n1,
190                },
191                Self::Check {
192                    expression: e2,
193                    no_inherit: n2,
194                },
195            ) => {
196                out.extend(diff_field(
197                    "expression",
198                    &e1.canonical_text,
199                    &e2.canonical_text,
200                ));
201                out.extend(diff_field("no_inherit", n1, n2));
202            }
203            _ => {
204                out.push(Difference::new(
205                    "",
206                    format!("{self:?}"),
207                    format!("{other:?}"),
208                ));
209            }
210        }
211        out
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    fn id(s: &str) -> Identifier {
220        Identifier::from_unquoted(s).unwrap()
221    }
222
223    fn qn(schema: &str, name: &str) -> QualifiedName {
224        QualifiedName::new(id(schema), id(name))
225    }
226
227    fn pk_constraint(cols: &[&str]) -> Constraint {
228        Constraint {
229            qname: qn("app", "users_pkey"),
230            kind: ConstraintKind::PrimaryKey {
231                columns: cols.iter().map(|c| id(c)).collect(),
232                include: vec![],
233            },
234            deferrable: Deferrable::NotDeferrable,
235            comment: None,
236        }
237    }
238
239    #[test]
240    fn equal_pks_have_no_diff() {
241        assert!(pk_constraint(&["id"]).canonical_eq(&pk_constraint(&["id"])));
242    }
243
244    #[test]
245    fn pk_column_list_change_diffs() {
246        let a = pk_constraint(&["id"]);
247        let b = pk_constraint(&["id", "tenant_id"]);
248        let d = a.diff(&b);
249        assert!(d.iter().any(|x| x.path == "kind.columns"));
250    }
251
252    #[test]
253    fn pk_column_order_matters() {
254        let a = pk_constraint(&["a", "b"]);
255        let b = pk_constraint(&["b", "a"]);
256        assert!(!a.canonical_eq(&b));
257    }
258
259    #[test]
260    fn fk_on_delete_change_diffs() {
261        let mk = |on_delete| Constraint {
262            qname: qn("app", "users_org_fkey"),
263            kind: ConstraintKind::ForeignKey(ForeignKey {
264                columns: vec![id("org_id")],
265                referenced_table: qn("app", "orgs"),
266                referenced_columns: vec![id("id")],
267                on_update: ReferentialAction::NoAction,
268                on_delete,
269                match_type: FkMatchType::Simple,
270            }),
271            deferrable: Deferrable::NotDeferrable,
272            comment: None,
273        };
274        let a = mk(ReferentialAction::NoAction);
275        let b = mk(ReferentialAction::Cascade);
276        let d = a.diff(&b);
277        assert!(d.iter().any(|x| x.path == "kind.fk.on_delete"));
278    }
279
280    #[test]
281    fn pk_vs_unique_kind_change() {
282        let a = pk_constraint(&["id"]);
283        let b = Constraint {
284            kind: ConstraintKind::Unique {
285                columns: vec![id("id")],
286                include: vec![],
287                nulls_distinct: true,
288            },
289            ..pk_constraint(&["id"])
290        };
291        let d = a.diff(&b);
292        assert!(d.iter().any(|x| x.path == "kind"));
293    }
294
295    #[test]
296    fn deferrable_change_diffs() {
297        let mut b = pk_constraint(&["id"]);
298        b.deferrable = Deferrable::Deferrable {
299            initially_deferred: true,
300        };
301        let d = pk_constraint(&["id"]).diff(&b);
302        assert!(d.iter().any(|x| x.path == "deferrable"));
303    }
304}