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