1use std::collections::BTreeMap;
13
14use crate::diff::change::{Change, CollationChange};
15use crate::diff::changeset::ChangeSet;
16use crate::diff::destructiveness::Destructiveness;
17use crate::diff::owner_op::{AlterObjectOwner, OwnedObjectId, OwnerObjectKind};
18use crate::identifier::QualifiedName;
19use crate::ir::catalog::Catalog;
20use crate::ir::collation::Collation;
21
22pub fn diff_collations(target: &Catalog, source: &Catalog, out: &mut ChangeSet) {
25 let target_map: BTreeMap<&QualifiedName, &Collation> =
26 target.collations.iter().map(|c| (&c.qname, c)).collect();
27 let source_map: BTreeMap<&QualifiedName, &Collation> =
28 source.collations.iter().map(|c| (&c.qname, c)).collect();
29
30 for (qname, src) in &source_map {
32 if !target_map.contains_key(qname) {
33 out.push(
34 Change::Collation(CollationChange::Create((*src).clone())),
35 Destructiveness::Safe,
36 );
37 }
38 }
39
40 for (qname, src) in &source_map {
44 let Some(tgt) = target_map.get(qname) else {
45 continue;
46 };
47 diff_one(tgt, src, out);
48 }
49}
50
51fn diff_one(target: &Collation, source: &Collation, out: &mut ChangeSet) {
52 if (
56 target.provider,
57 &target.lc_collate,
58 &target.lc_ctype,
59 target.deterministic,
60 ) != (
61 source.provider,
62 &source.lc_collate,
63 &source.lc_ctype,
64 source.deterministic,
65 ) {
66 out.push(
67 Change::Collation(CollationChange::Replace {
68 from: target.clone(),
69 to: source.clone(),
70 }),
71 Destructiveness::RequiresApproval {
72 reason: format!(
73 "structural change to collation {} requires DROP + CREATE (PG has no in-place ALTER for provider/locale/deterministic)",
74 source.qname
75 ),
76 },
77 );
78 return;
79 }
80
81 if let Some(s_owner) = &source.owner
83 && target.owner.as_ref() != Some(s_owner)
84 {
85 out.push(
86 Change::AlterObjectOwner(AlterObjectOwner {
87 kind: OwnerObjectKind::Collation,
88 id: OwnedObjectId::Qualified(source.qname.clone()),
89 signature: String::new(),
90 from: target.owner.clone(),
91 to: s_owner.clone(),
92 }),
93 Destructiveness::Safe,
94 );
95 }
96
97 if target.comment != source.comment {
99 out.push(
100 Change::Collation(CollationChange::CommentOn {
101 qname: source.qname.clone(),
102 comment: source.comment.clone(),
103 }),
104 Destructiveness::Safe,
105 );
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::identifier::Identifier;
113 use crate::ir::collation::CollationProvider;
114
115 fn id(s: &str) -> Identifier {
116 Identifier::from_unquoted(s).unwrap()
117 }
118
119 fn qn(schema: &str, name: &str) -> QualifiedName {
120 QualifiedName::new(id(schema), id(name))
121 }
122
123 fn basic_collation(name: &str) -> Collation {
124 Collation {
125 qname: qn("app", name),
126 provider: CollationProvider::Libc,
127 lc_collate: "en_US.utf8".into(),
128 lc_ctype: "en_US.utf8".into(),
129 deterministic: true,
130 version: None,
131 owner: None,
132 comment: None,
133 }
134 }
135
136 fn catalog_with(collations: Vec<Collation>) -> Catalog {
137 let mut c = Catalog::empty();
138 c.collations = collations;
139 c
140 }
141
142 fn run_diff(target: &Catalog, source: &Catalog) -> ChangeSet {
143 let mut out = ChangeSet::new();
144 diff_collations(target, source, &mut out);
145 out
146 }
147
148 #[test]
151 fn create_collation_when_source_has_it_and_target_doesnt() {
152 let target = Catalog::empty();
153 let source = catalog_with(vec![basic_collation("c")]);
154 let changes = run_diff(&target, &source);
155 assert_eq!(changes.len(), 1);
156 assert!(matches!(
157 changes.iter().next().unwrap().change,
158 Change::Collation(CollationChange::Create(_))
159 ));
160 }
161
162 #[test]
163 fn create_collation_is_safe() {
164 let target = Catalog::empty();
165 let source = catalog_with(vec![basic_collation("c")]);
166 let changes = run_diff(&target, &source);
167 let entry = changes.iter().next().unwrap();
168 assert!(!entry.destructiveness.requires_approval());
169 }
170
171 #[test]
174 fn no_drop_when_target_has_collation_but_source_doesnt() {
175 let target = catalog_with(vec![basic_collation("c")]);
176 let source = Catalog::empty();
177 let changes = run_diff(&target, &source);
178 assert!(
179 changes.is_empty(),
180 "expected no changes (lenient), got {changes:?}"
181 );
182 }
183
184 #[test]
187 fn identical_collations_produce_no_changes() {
188 let c = catalog_with(vec![basic_collation("c")]);
189 let changes = run_diff(&c, &c);
190 assert!(changes.is_empty());
191 }
192
193 #[test]
196 fn provider_differs_emits_replace() {
197 let mut src = basic_collation("c");
198 src.provider = CollationProvider::Icu;
199 let target = catalog_with(vec![basic_collation("c")]);
200 let source = catalog_with(vec![src]);
201 let changes = run_diff(&target, &source);
202 assert_eq!(changes.len(), 1);
203 let entry = changes.iter().next().unwrap();
204 assert!(matches!(
205 entry.change,
206 Change::Collation(CollationChange::Replace { .. })
207 ));
208 assert!(entry.destructiveness.requires_approval());
209 }
210
211 #[test]
212 fn lc_collate_differs_emits_replace() {
213 let mut src = basic_collation("c");
214 src.lc_collate = "de_DE.utf8".into();
215 let target = catalog_with(vec![basic_collation("c")]);
216 let source = catalog_with(vec![src]);
217 let changes = run_diff(&target, &source);
218 assert_eq!(changes.len(), 1);
219 assert!(matches!(
220 changes.iter().next().unwrap().change,
221 Change::Collation(CollationChange::Replace { .. })
222 ));
223 }
224
225 #[test]
226 fn lc_ctype_differs_emits_replace() {
227 let mut src = basic_collation("c");
228 src.lc_ctype = "de_DE.utf8".into();
229 let target = catalog_with(vec![basic_collation("c")]);
230 let source = catalog_with(vec![src]);
231 let changes = run_diff(&target, &source);
232 assert_eq!(changes.len(), 1);
233 assert!(matches!(
234 changes.iter().next().unwrap().change,
235 Change::Collation(CollationChange::Replace { .. })
236 ));
237 }
238
239 #[test]
240 fn deterministic_differs_emits_replace() {
241 let mut tgt = basic_collation("c");
242 tgt.provider = CollationProvider::Icu;
243 tgt.lc_collate = "und".into();
244 tgt.lc_ctype = "und".into();
245 tgt.deterministic = true;
246 let mut src = tgt.clone();
247 src.deterministic = false;
248 let target = catalog_with(vec![tgt]);
249 let source = catalog_with(vec![src]);
250 let changes = run_diff(&target, &source);
251 assert_eq!(changes.len(), 1);
252 assert!(matches!(
253 changes.iter().next().unwrap().change,
254 Change::Collation(CollationChange::Replace { .. })
255 ));
256 }
257
258 #[test]
259 fn version_differs_does_not_emit_change() {
260 let mut tgt = basic_collation("c");
262 tgt.version = Some("153.97".into());
263 let mut src = basic_collation("c");
264 src.version = Some("153.128".into());
265 let target = catalog_with(vec![tgt]);
266 let source = catalog_with(vec![src]);
267 let changes = run_diff(&target, &source);
268 assert!(
269 changes.is_empty(),
270 "version field must be ignored by differ, got {changes:?}"
271 );
272 }
273
274 #[test]
275 fn structural_change_skips_downstream_per_field_checks() {
276 let mut tgt = basic_collation("c");
278 tgt.owner = Some(id("alice"));
279 tgt.comment = Some("old".into());
280
281 let mut src = basic_collation("c");
282 src.provider = CollationProvider::Icu; src.owner = Some(id("bob"));
284 src.comment = Some("new".into());
285
286 let target = catalog_with(vec![tgt]);
287 let source = catalog_with(vec![src]);
288 let changes = run_diff(&target, &source);
289 assert_eq!(changes.len(), 1, "only Replace, no downstream diffs");
290 assert!(matches!(
291 changes.iter().next().unwrap().change,
292 Change::Collation(CollationChange::Replace { .. })
293 ));
294 }
295
296 #[test]
299 fn owner_change_emits_alter_object_owner() {
300 let mut tgt = basic_collation("c");
301 tgt.owner = Some(id("alice"));
302 let mut src = basic_collation("c");
303 src.owner = Some(id("bob"));
304 let target = catalog_with(vec![tgt]);
305 let source = catalog_with(vec![src]);
306 let changes = run_diff(&target, &source);
307 assert_eq!(changes.len(), 1);
308 let entry = changes.iter().next().unwrap();
309 if let Change::AlterObjectOwner(op) = &entry.change {
310 assert_eq!(op.kind, OwnerObjectKind::Collation);
311 assert_eq!(op.to, id("bob"));
312 } else {
313 panic!("expected AlterObjectOwner, got {:?}", entry.change);
314 }
315 }
316
317 #[test]
318 fn no_owner_change_when_source_owner_is_none() {
319 let mut tgt = basic_collation("c");
321 tgt.owner = Some(id("alice"));
322 let src = basic_collation("c"); let target = catalog_with(vec![tgt]);
324 let source = catalog_with(vec![src]);
325 let changes = run_diff(&target, &source);
326 assert!(
327 changes.is_empty(),
328 "source owner None = unmanaged, no change expected"
329 );
330 }
331
332 #[test]
335 fn comment_change_emits_comment_on() {
336 let src = {
337 let mut s = basic_collation("c");
338 s.comment = Some("pinned for sorting".into());
339 s
340 };
341 let target = catalog_with(vec![basic_collation("c")]);
342 let source = catalog_with(vec![src]);
343 let changes = run_diff(&target, &source);
344 assert_eq!(changes.len(), 1);
345 assert!(matches!(
346 changes.iter().next().unwrap().change,
347 Change::Collation(CollationChange::CommentOn { .. })
348 ));
349 }
350
351 #[test]
352 fn clear_comment_emits_comment_on_with_none() {
353 let mut tgt = basic_collation("c");
354 tgt.comment = Some("old comment".into());
355 let src = basic_collation("c"); let target = catalog_with(vec![tgt]);
357 let source = catalog_with(vec![src]);
358 let changes = run_diff(&target, &source);
359 assert_eq!(changes.len(), 1);
360 let entry = changes.iter().next().unwrap();
361 if let Change::Collation(CollationChange::CommentOn { comment, .. }) = &entry.change {
362 assert!(comment.is_none());
363 } else {
364 panic!(
365 "expected CollationChange::CommentOn, got {:?}",
366 entry.change
367 );
368 }
369 }
370
371 #[test]
374 fn owner_and_comment_both_changed_emit_two_changes() {
375 let mut tgt = basic_collation("c");
376 tgt.owner = Some(id("alice"));
377 let mut src = basic_collation("c");
378 src.owner = Some(id("bob"));
379 src.comment = Some("new comment".into());
380 let target = catalog_with(vec![tgt]);
381 let source = catalog_with(vec![src]);
382 let changes = run_diff(&target, &source);
383 assert_eq!(changes.len(), 2);
384 assert!(
385 changes
386 .iter()
387 .any(|e| matches!(&e.change, Change::AlterObjectOwner(_)))
388 );
389 assert!(changes.iter().any(|e| matches!(
390 &e.change,
391 Change::Collation(CollationChange::CommentOn { .. })
392 )));
393 }
394}