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