Skip to main content

pgevolve_core/ir/
table.rs

1//! `Table` — a Postgres table.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::identifier::{Identifier, QualifiedName};
8use crate::ir::column::Column;
9use crate::ir::constraint::Constraint;
10use crate::ir::difference::Difference;
11use crate::ir::eq::{Diff, diff_field, prefix_diffs};
12
13/// A Postgres table.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct Table {
16    /// Schema-qualified table name.
17    pub qname: QualifiedName,
18    /// Columns in their logical order.
19    pub columns: Vec<Column>,
20    /// Constraints, paired by `qname` for diffing.
21    pub constraints: Vec<Constraint>,
22    /// `Some` → this table is a partitioned parent (`PARTITION BY …`).
23    pub partition_by: Option<crate::ir::partition::PartitionBy>,
24    /// `Some` → this table is itself a partition (`PARTITION OF … FOR VALUES …`).
25    pub partition_of: Option<crate::ir::partition::PartitionOf>,
26    /// Optional comment.
27    pub comment: Option<String>,
28    /// Object owner. `None` = unmanaged (the differ ignores ownership).
29    /// `Some(role)` = managed: diff emits `ALTER TABLE ... OWNER TO role`.
30    pub owner: Option<Identifier>,
31    /// Grants on this object. Empty = no grants. Canonicalized.
32    pub grants: Vec<crate::ir::grant::Grant>,
33    /// `ROW LEVEL SECURITY` enabled flag. PG default: false.
34    pub rls_enabled: bool,
35    /// `FORCE ROW LEVEL SECURITY` flag (applies even to owner). PG default: false.
36    pub rls_forced: bool,
37    /// Policies attached to this table. Canonicalized in `ir::canon::policies`.
38    pub policies: Vec<crate::ir::policy::Policy>,
39    /// Storage parameters (`WITH (fillfactor = …, autovacuum_* = …, …)`).
40    /// Default is the empty/no-overrides state.
41    pub storage: crate::ir::reloptions::TableStorageOptions,
42}
43
44impl Diff for Table {
45    fn diff(&self, other: &Self) -> Vec<Difference> {
46        let mut out = Vec::new();
47        out.extend(diff_field("qname", &self.qname, &other.qname));
48        out.extend(diff_field(
49            "partition_by",
50            &format!("{:?}", self.partition_by),
51            &format!("{:?}", other.partition_by),
52        ));
53        out.extend(diff_field(
54            "partition_of",
55            &format!("{:?}", self.partition_of),
56            &format!("{:?}", other.partition_of),
57        ));
58        out.extend(diff_field(
59            "comment",
60            &format!("{:?}", self.comment),
61            &format!("{:?}", other.comment),
62        ));
63        out.extend(diff_field(
64            "owner",
65            &format!("{:?}", self.owner),
66            &format!("{:?}", other.owner),
67        ));
68        out.extend(diff_field(
69            "grants",
70            &format!("{:?}", self.grants),
71            &format!("{:?}", other.grants),
72        ));
73        out.extend(diff_field(
74            "rls_enabled",
75            &format!("{:?}", self.rls_enabled),
76            &format!("{:?}", other.rls_enabled),
77        ));
78        out.extend(diff_field(
79            "rls_forced",
80            &format!("{:?}", self.rls_forced),
81            &format!("{:?}", other.rls_forced),
82        ));
83        out.extend(diff_field(
84            "policies",
85            &format!("{:?}", self.policies),
86            &format!("{:?}", other.policies),
87        ));
88        out.extend(diff_field(
89            "storage",
90            &format!("{:?}", self.storage),
91            &format!("{:?}", other.storage),
92        ));
93        out.extend(diff_columns(&self.columns, &other.columns));
94        out.extend(diff_constraints(&self.constraints, &other.constraints));
95        out
96    }
97}
98
99/// Diff two column slices: add/remove/change by name, then order drift.
100fn diff_columns(
101    lhs_cols: &[crate::ir::column::Column],
102    rhs_cols: &[crate::ir::column::Column],
103) -> Vec<Difference> {
104    let mut out = Vec::new();
105    let lhs: BTreeMap<_, _> = lhs_cols.iter().map(|c| (c.name.as_str(), c)).collect();
106    let rhs: BTreeMap<_, _> = rhs_cols.iter().map(|c| (c.name.as_str(), c)).collect();
107    for (name, l) in &lhs {
108        match rhs.get(name) {
109            None => out.push(Difference::new(
110                format!("columns.{name}"),
111                "present",
112                "removed",
113            )),
114            Some(r) => {
115                out.extend(prefix_diffs(&format!("columns.{name}"), l.diff(r)));
116            }
117        }
118    }
119    for name in rhs.keys() {
120        if !lhs.contains_key(name) {
121            out.push(Difference::new(
122                format!("columns.{name}"),
123                "missing",
124                "added",
125            ));
126        }
127    }
128    let lhs_order: Vec<&str> = lhs_cols.iter().map(|c| c.name.as_str()).collect();
129    let rhs_order: Vec<&str> = rhs_cols.iter().map(|c| c.name.as_str()).collect();
130    if lhs_order != rhs_order {
131        out.push(Difference::new(
132            "columns.<order>",
133            lhs_order.join(","),
134            rhs_order.join(","),
135        ));
136    }
137    out
138}
139
140/// Diff two constraint slices: add/remove/change by qname.
141fn diff_constraints(
142    lhs_cs_slice: &[crate::ir::constraint::Constraint],
143    rhs_cs_slice: &[crate::ir::constraint::Constraint],
144) -> Vec<Difference> {
145    let mut out = Vec::new();
146    let lhs_cs: BTreeMap<_, _> = lhs_cs_slice.iter().map(|c| (&c.qname, c)).collect();
147    let rhs_cs: BTreeMap<_, _> = rhs_cs_slice.iter().map(|c| (&c.qname, c)).collect();
148    for (qn, l) in &lhs_cs {
149        match rhs_cs.get(qn) {
150            None => out.push(Difference::new(
151                format!("constraints.{qn}"),
152                "present",
153                "removed",
154            )),
155            Some(r) => {
156                out.extend(prefix_diffs(&format!("constraints.{qn}"), l.diff(r)));
157            }
158        }
159    }
160    for qn in rhs_cs.keys() {
161        if !lhs_cs.contains_key(qn) {
162            out.push(Difference::new(
163                format!("constraints.{qn}"),
164                "missing",
165                "added",
166            ));
167        }
168    }
169    out
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::identifier::Identifier;
176    use crate::ir::column_type::ColumnType;
177    use crate::ir::constraint::{ConstraintKind, Deferrable};
178
179    fn id(s: &str) -> Identifier {
180        Identifier::from_unquoted(s).unwrap()
181    }
182
183    fn qn(name: &str) -> QualifiedName {
184        QualifiedName::new(id("app"), id(name))
185    }
186
187    fn col(name: &str, ty: ColumnType, nullable: bool) -> Column {
188        Column {
189            name: id(name),
190            ty,
191            nullable,
192            default: None,
193            identity: None,
194            generated: None,
195            collation: None,
196            storage: None,
197            compression: None,
198            comment: None,
199        }
200    }
201
202    fn pk(name: &str, cols: &[&str]) -> Constraint {
203        Constraint {
204            qname: qn(name),
205            kind: ConstraintKind::PrimaryKey {
206                columns: cols.iter().map(|c| id(c)).collect(),
207                include: vec![],
208            },
209            deferrable: Deferrable::NotDeferrable,
210            comment: None,
211        }
212    }
213
214    fn base() -> Table {
215        Table {
216            qname: qn("users"),
217            columns: vec![
218                col("id", ColumnType::BigInt, false),
219                col("email", ColumnType::Text, false),
220            ],
221            constraints: vec![pk("users_pkey", &["id"])],
222            partition_by: None,
223            partition_of: None,
224            comment: None,
225            owner: None,
226            grants: vec![],
227            rls_enabled: false,
228            rls_forced: false,
229            policies: vec![],
230            storage: crate::ir::reloptions::TableStorageOptions::default(),
231        }
232    }
233
234    #[test]
235    fn equal_tables_have_no_diff() {
236        assert!(base().canonical_eq(&base()));
237    }
238
239    #[test]
240    fn add_column_diffs() {
241        let mut b = base();
242        b.columns.push(col("name", ColumnType::Text, true));
243        let d = base().diff(&b);
244        assert!(d.iter().any(|x| x.path == "columns.name"));
245    }
246
247    #[test]
248    fn remove_column_diffs() {
249        let mut b = base();
250        b.columns.pop();
251        let d = base().diff(&b);
252        assert!(d.iter().any(|x| x.path == "columns.email"));
253    }
254
255    #[test]
256    fn reorder_columns_diffs_as_order() {
257        let mut b = base();
258        b.columns.reverse();
259        let d = base().diff(&b);
260        assert!(d.iter().any(|x| x.path == "columns.<order>"));
261    }
262
263    #[test]
264    fn add_constraint_diffs() {
265        let mut b = base();
266        b.constraints.push(pk("users_alt_pkey", &["email"]));
267        let d = base().diff(&b);
268        assert!(d.iter().any(|x| x.path == "constraints.app.users_alt_pkey"));
269    }
270
271    #[test]
272    fn changed_column_definition_diffs_under_path() {
273        let mut b = base();
274        b.columns[1].nullable = true;
275        let d = base().diff(&b);
276        assert!(d.iter().any(|x| x.path == "columns.email.nullable"));
277    }
278
279    #[test]
280    fn owner_change_diffs() {
281        let mut b = base();
282        b.owner = Some(id("new_owner"));
283        assert!(base().diff(&b).iter().any(|x| x.path == "owner"));
284    }
285
286    #[test]
287    fn grants_change_diffs() {
288        let mut b = base();
289        b.grants.push(crate::ir::grant::Grant {
290            grantee: crate::ir::grant::GrantTarget::Public,
291            privilege: crate::ir::grant::Privilege::Select,
292            with_grant_option: false,
293            columns: None,
294        });
295        assert!(base().diff(&b).iter().any(|x| x.path == "grants"));
296    }
297
298    #[test]
299    fn rls_enabled_change_diffs() {
300        let mut b = base();
301        b.rls_enabled = true;
302        assert!(base().diff(&b).iter().any(|x| x.path == "rls_enabled"));
303    }
304
305    #[test]
306    fn rls_forced_change_diffs() {
307        let mut b = base();
308        b.rls_forced = true;
309        assert!(base().diff(&b).iter().any(|x| x.path == "rls_forced"));
310    }
311
312    #[test]
313    fn policies_change_diffs() {
314        use crate::ir::grant::GrantTarget;
315        use crate::ir::policy::{Policy, PolicyCommand};
316        let mut b = base();
317        b.policies.push(Policy {
318            name: id("p1"),
319            permissive: true,
320            command: PolicyCommand::All,
321            roles: vec![GrantTarget::Public],
322            using: None,
323            with_check: None,
324        });
325        assert!(base().diff(&b).iter().any(|x| x.path == "policies"));
326    }
327
328    #[test]
329    fn storage_change_diffs() {
330        let mut b = base();
331        b.storage = crate::ir::reloptions::TableStorageOptions {
332            fillfactor: Some(80),
333            ..Default::default()
334        };
335        assert!(base().diff(&b).iter().any(|x| x.path == "storage"));
336    }
337}