pgevolve_core/diff/
extensions.rs1use std::collections::BTreeMap;
8
9use crate::diff::change::{Change, ExtensionChange};
10use crate::diff::changeset::ChangeSet;
11use crate::diff::destructiveness::Destructiveness;
12use crate::ir::extension::Extension;
13
14pub fn diff_extensions(target: &[Extension], source: &[Extension], out: &mut ChangeSet) {
19 let target_by_name: BTreeMap<_, _> = target.iter().map(|e| (e.name.clone(), e)).collect();
20 let source_by_name: BTreeMap<_, _> = source.iter().map(|e| (e.name.clone(), e)).collect();
21
22 for name in target_by_name.keys() {
24 if !source_by_name.contains_key(name) {
25 out.push(
26 Change::Extension(ExtensionChange::Drop(name.clone())),
27 Destructiveness::RequiresApprovalAndDataLossWarning {
28 reason: format!(
29 "DROP EXTENSION {name} CASCADE removes every object owned by the extension."
30 ),
31 },
32 );
33 }
34 }
35
36 for (name, s) in &source_by_name {
38 match target_by_name.get(name) {
39 None => out.push(
40 Change::Extension(ExtensionChange::Create((*s).clone())),
41 Destructiveness::Safe,
42 ),
43 Some(t) => {
44 if let Some(source_schema) = &s.schema
46 && t.schema.as_ref() != Some(source_schema)
47 {
48 out.push(
49 Change::Extension(ExtensionChange::ReplaceWithCascade((*s).clone())),
50 Destructiveness::RequiresApprovalAndDataLossWarning {
51 reason: format!(
52 "Changing the schema of extension {name} requires DROP CASCADE; \
53 every object owned by the extension is removed and re-created."
54 ),
55 },
56 );
57 continue;
58 }
59 if let Some(source_version) = &s.version
61 && t.version.as_ref() != Some(source_version)
62 {
63 out.push(
64 Change::Extension(ExtensionChange::AlterUpdate {
65 name: name.clone(),
66 to_version: source_version.clone(),
67 }),
68 Destructiveness::Safe,
69 );
70 }
71 if let Some(source_comment) = &s.comment
76 && t.comment.as_ref() != Some(source_comment)
77 {
78 out.push(
79 Change::Extension(ExtensionChange::CommentOn {
80 name: name.clone(),
81 comment: s.comment.clone(),
82 }),
83 Destructiveness::Safe,
84 );
85 }
86 }
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::identifier::Identifier;
95
96 fn id(s: &str) -> Identifier {
97 Identifier::from_unquoted(s).unwrap()
98 }
99 fn ext(name: &str) -> Extension {
100 Extension {
101 name: id(name),
102 schema: None,
103 version: None,
104 comment: None,
105 }
106 }
107
108 #[test]
109 fn create_when_only_in_source() {
110 let mut cs = ChangeSet::new();
111 diff_extensions(&[], &[ext("pgcrypto")], &mut cs);
112 assert!(matches!(
113 cs.iter().next().map(|e| &e.change),
114 Some(Change::Extension(ExtensionChange::Create(_)))
115 ));
116 }
117
118 #[test]
119 fn drop_when_only_in_target() {
120 let mut cs = ChangeSet::new();
121 diff_extensions(&[ext("pgcrypto")], &[], &mut cs);
122 let first = cs.iter().next().expect("one change");
123 assert!(matches!(
124 &first.change,
125 Change::Extension(ExtensionChange::Drop(_))
126 ));
127 assert!(matches!(
128 &first.destructiveness,
129 Destructiveness::RequiresApprovalAndDataLossWarning { .. }
130 ));
131 }
132
133 #[test]
134 fn version_unpinned_in_source_matches_any_catalog_version() {
135 let mut t = ext("pgcrypto");
136 t.version = Some("1.3".into());
137 let s = ext("pgcrypto"); let mut cs = ChangeSet::new();
139 diff_extensions(&[t], &[s], &mut cs);
140 assert!(cs.iter().next().is_none(), "unpinned source must not diff");
141 }
142
143 #[test]
144 fn version_pinned_in_source_triggers_alter_update() {
145 let mut t = ext("pgcrypto");
146 t.version = Some("1.3".into());
147 let mut s = ext("pgcrypto");
148 s.version = Some("1.4".into());
149 let mut cs = ChangeSet::new();
150 diff_extensions(&[t], &[s], &mut cs);
151 let first = cs.iter().next().expect("one change");
152 assert!(matches!(
153 &first.change,
154 Change::Extension(ExtensionChange::AlterUpdate { to_version, .. })
155 if to_version == "1.4"
156 ));
157 }
158
159 #[test]
160 fn schema_change_triggers_replace_with_cascade() {
161 let mut t = ext("pgcrypto");
162 t.schema = Some(id("public"));
163 let mut s = ext("pgcrypto");
164 s.schema = Some(id("app"));
165 let mut cs = ChangeSet::new();
166 diff_extensions(&[t], &[s], &mut cs);
167 let first = cs.iter().next().expect("one change");
168 assert!(matches!(
169 &first.change,
170 Change::Extension(ExtensionChange::ReplaceWithCascade(_))
171 ));
172 assert!(matches!(
173 &first.destructiveness,
174 Destructiveness::RequiresApprovalAndDataLossWarning { .. }
175 ));
176 }
177
178 #[test]
179 fn schema_unpinned_in_source_skips_schema_diff() {
180 let mut t = ext("pgcrypto");
181 t.schema = Some(id("public"));
182 let s = ext("pgcrypto"); let mut cs = ChangeSet::new();
184 diff_extensions(&[t], &[s], &mut cs);
185 assert!(cs.iter().next().is_none());
186 }
187}