Skip to main content

pgevolve_core/plan/
ordered.rs

1//! [`OrderedChangeSet`] — the planner's ordered output.
2//!
3//! Three buckets, each in dependency-correct order, plus a list of FK
4//! constraints that had to be deferred to a post-pass to break cycles.
5
6use crate::diff::change::ChangeEntry;
7use crate::identifier::QualifiedName;
8use crate::ir::constraint::Constraint;
9
10/// A `ChangeSet` partitioned and sorted by the planner.
11///
12/// Bucket semantics:
13/// - `creates_and_adds`: `CreateSchema/Table/Index/Sequence`, in dependency
14///   order (dependencies before dependents).
15/// - `modifies`: `AlterSchema/Table/Sequence`, `ReplaceIndex`, in dependency
16///   order over the source-side graph.
17/// - `drops`: `DropSchema/Table/Index/Sequence`, in **reverse** dependency
18///   order over the target-side graph.
19/// - `deferred_fks`: FK constraints removed from the create graph to break a
20///   cycle; emitted after `creates_and_adds` as `ALTER TABLE ... ADD CONSTRAINT`.
21// `ChangeEntry` is only `PartialEq` (it transitively contains `f64` literals),
22// so this struct cannot derive `Eq`. The clippy `derive_partial_eq_without_eq`
23// lint is suppressed accordingly.
24#[allow(clippy::derive_partial_eq_without_eq)]
25#[derive(Debug, Clone, Default, PartialEq)]
26pub struct OrderedChangeSet {
27    /// Creates and additive operations, in dependency order.
28    pub creates_and_adds: Vec<ChangeEntry>,
29    /// Modify-in-place operations, in dependency order.
30    pub modifies: Vec<ChangeEntry>,
31    /// Drops, in reverse dependency order.
32    pub drops: Vec<ChangeEntry>,
33    /// FK constraints deferred to break create-graph cycles.
34    pub deferred_fks: Vec<DeferredFkAdd>,
35}
36
37/// A single FK constraint extracted from the create graph for a post-pass
38/// `ALTER TABLE ... ADD CONSTRAINT`.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct DeferredFkAdd {
41    /// Table that owns the constraint.
42    pub table: QualifiedName,
43    /// The full constraint definition (still a `Constraint::ForeignKey(_)`).
44    pub constraint: Constraint,
45}
46
47impl OrderedChangeSet {
48    /// True iff every bucket is empty.
49    pub const fn is_empty(&self) -> bool {
50        self.creates_and_adds.is_empty()
51            && self.modifies.is_empty()
52            && self.drops.is_empty()
53            && self.deferred_fks.is_empty()
54    }
55
56    /// Total entries across all buckets, including deferred FKs.
57    pub const fn len(&self) -> usize {
58        self.creates_and_adds.len()
59            + self.modifies.len()
60            + self.drops.len()
61            + self.deferred_fks.len()
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::diff::change::Change;
69    use crate::diff::destructiveness::Destructiveness;
70    use crate::identifier::Identifier;
71    use crate::ir::constraint::{
72        ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
73    };
74    use crate::ir::schema::Schema;
75
76    fn id(s: &str) -> Identifier {
77        Identifier::from_unquoted(s).unwrap()
78    }
79
80    fn qn(schema: &str, name: &str) -> QualifiedName {
81        QualifiedName::new(id(schema), id(name))
82    }
83
84    fn fk_constraint() -> Constraint {
85        Constraint {
86            qname: qn("app", "fk1"),
87            kind: ConstraintKind::ForeignKey(ForeignKey {
88                columns: vec![id("ref_id")],
89                referenced_table: qn("app", "b"),
90                referenced_columns: vec![id("id")],
91                on_update: ReferentialAction::NoAction,
92                on_delete: ReferentialAction::NoAction,
93                match_type: FkMatchType::Simple,
94            }),
95            deferrable: Deferrable::NotDeferrable,
96            comment: None,
97        }
98    }
99
100    #[test]
101    fn default_is_empty() {
102        let o = OrderedChangeSet::default();
103        assert!(o.is_empty());
104        assert_eq!(o.len(), 0);
105    }
106
107    #[test]
108    fn len_sums_all_buckets() {
109        let o = OrderedChangeSet {
110            creates_and_adds: vec![ChangeEntry {
111                change: Change::CreateSchema(Schema::new(id("app"))),
112                destructiveness: Destructiveness::Safe,
113            }],
114            modifies: vec![],
115            drops: vec![ChangeEntry {
116                change: Change::DropSchema(id("old")),
117                destructiveness: Destructiveness::RequiresApproval {
118                    reason: "drop".into(),
119                },
120            }],
121            deferred_fks: vec![DeferredFkAdd {
122                table: qn("app", "a"),
123                constraint: fk_constraint(),
124            }],
125        };
126        assert!(!o.is_empty());
127        assert_eq!(o.len(), 3);
128    }
129
130    #[test]
131    fn is_empty_iff_all_buckets_empty() {
132        let mut o = OrderedChangeSet::default();
133        assert!(o.is_empty());
134        o.deferred_fks.push(DeferredFkAdd {
135            table: qn("app", "a"),
136            constraint: fk_constraint(),
137        });
138        assert!(!o.is_empty());
139    }
140}