1use std::collections::BTreeMap;
20
21use crate::diff::change::{AggregateChange, Change};
22use crate::diff::changeset::ChangeSet;
23use crate::diff::destructiveness::Destructiveness;
24use crate::identifier::QualifiedName;
25use crate::ir::aggregate::Aggregate;
26use crate::ir::catalog::Catalog;
27
28type AggKey = (QualifiedName, Vec<String>);
30
31fn agg_key(a: &Aggregate) -> AggKey {
32 (
33 a.qname.clone(),
34 a.arg_types
35 .iter()
36 .map(crate::ir::column_type::ColumnType::render_sql)
37 .collect(),
38 )
39}
40
41pub fn diff_aggregates(target: &Catalog, source: &Catalog, out: &mut ChangeSet) {
46 let target_map: BTreeMap<AggKey, &Aggregate> =
47 target.aggregates.iter().map(|a| (agg_key(a), a)).collect();
48 let source_map: BTreeMap<AggKey, &Aggregate> =
49 source.aggregates.iter().map(|a| (agg_key(a), a)).collect();
50
51 for (key, src) in &source_map {
53 if !target_map.contains_key(key) {
54 out.push(
55 Change::Aggregate(AggregateChange::Create((*src).clone())),
56 Destructiveness::Safe,
57 );
58 }
59 }
60
61 for (key, tgt) in &target_map {
63 if !source_map.contains_key(key) {
64 out.push(
65 Change::Aggregate(AggregateChange::Drop {
66 qname: tgt.qname.clone(),
67 arg_types: tgt.arg_types.clone(),
68 }),
69 Destructiveness::Safe,
70 );
71 }
72 }
73
74 for (key, src) in &source_map {
76 let Some(tgt) = target_map.get(key) else {
77 continue;
78 };
79 emit_modify(tgt, src, out);
80 }
81}
82
83fn structural_differs(t: &Aggregate, s: &Aggregate) -> bool {
84 t.state_type != s.state_type
85 || t.sfunc != s.sfunc
86 || t.finalfunc != s.finalfunc
87 || t.initcond != s.initcond
88}
89
90fn emit_modify(t: &Aggregate, s: &Aggregate, out: &mut ChangeSet) {
91 if structural_differs(t, s) {
92 out.push(
94 Change::Aggregate(AggregateChange::Replace {
95 from: t.clone(),
96 to: s.clone(),
97 }),
98 Destructiveness::Safe,
99 );
100 return;
101 }
102
103 if let Some(src_owner) = &s.owner
105 && t.owner.as_ref() != Some(src_owner)
106 {
107 out.push(
108 Change::Aggregate(AggregateChange::AlterOwner {
109 qname: s.qname.clone(),
110 arg_types: s.arg_types.clone(),
111 owner: src_owner.clone(),
112 }),
113 Destructiveness::Safe,
114 );
115 }
116
117 if t.comment != s.comment {
119 out.push(
120 Change::Aggregate(AggregateChange::CommentOn {
121 qname: s.qname.clone(),
122 arg_types: s.arg_types.clone(),
123 comment: s.comment.clone(),
124 }),
125 Destructiveness::Safe,
126 );
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::diff::change::{AggregateChange, Change};
134 use crate::identifier::{Identifier, QualifiedName};
135 use crate::ir::catalog::Catalog;
136 use crate::ir::column_type::ColumnType;
137
138 fn id(s: &str) -> Identifier {
139 Identifier::from_unquoted(s).unwrap()
140 }
141
142 fn qn(schema: &str, name: &str) -> QualifiedName {
143 QualifiedName::new(id(schema), id(name))
144 }
145
146 fn basic_agg(name: &str) -> Aggregate {
148 Aggregate {
149 qname: qn("app", name),
150 arg_types: vec![ColumnType::Integer],
151 state_type: ColumnType::BigInt,
152 sfunc: qn("app", "my_sfunc"),
153 finalfunc: None,
154 initcond: None,
155 owner: None,
156 comment: None,
157 }
158 }
159
160 fn cat(aggs: Vec<Aggregate>) -> Catalog {
161 let mut c = Catalog::empty();
162 c.aggregates = aggs;
163 c
164 }
165
166 fn run(target: &Catalog, source: &Catalog) -> ChangeSet {
167 let mut out = ChangeSet::new();
168 diff_aggregates(target, source, &mut out);
169 out
170 }
171
172 #[test]
175 fn source_only_creates() {
176 let changes = run(&cat(vec![]), &cat(vec![basic_agg("my_sum")]));
177 assert_eq!(changes.len(), 1);
178 assert!(matches!(
179 changes.iter().next().unwrap().change,
180 Change::Aggregate(AggregateChange::Create(_))
181 ));
182 }
183
184 #[test]
187 fn target_only_drops() {
188 let changes = run(&cat(vec![basic_agg("my_sum")]), &cat(vec![]));
189 assert_eq!(
190 changes.len(),
191 1,
192 "managed aggregate must emit Drop when absent from source"
193 );
194 assert!(
195 matches!(
196 changes.iter().next().unwrap().change,
197 Change::Aggregate(AggregateChange::Drop { .. })
198 ),
199 "expected Drop, got {:?}",
200 changes.iter().next().unwrap().change
201 );
202 }
203
204 #[test]
207 fn different_sfunc_replaces() {
208 let t = basic_agg("my_sum");
209 let mut s = basic_agg("my_sum");
210 s.sfunc = qn("app", "other_sfunc");
211 let changes = run(&cat(vec![t]), &cat(vec![s]));
212 assert_eq!(changes.len(), 1);
213 assert!(matches!(
214 changes.iter().next().unwrap().change,
215 Change::Aggregate(AggregateChange::Replace { .. })
216 ));
217 }
218
219 #[test]
220 fn different_state_type_replaces() {
221 let t = basic_agg("my_sum");
222 let mut s = basic_agg("my_sum");
223 s.state_type = ColumnType::Numeric {
224 precision: None,
225 scale: None,
226 };
227 let changes = run(&cat(vec![t]), &cat(vec![s]));
228 assert_eq!(changes.len(), 1);
229 assert!(matches!(
230 changes.iter().next().unwrap().change,
231 Change::Aggregate(AggregateChange::Replace { .. })
232 ));
233 }
234
235 #[test]
236 fn different_finalfunc_replaces() {
237 let t = basic_agg("my_sum");
238 let mut s = basic_agg("my_sum");
239 s.finalfunc = Some(qn("app", "my_final"));
240 let changes = run(&cat(vec![t]), &cat(vec![s]));
241 assert_eq!(changes.len(), 1);
242 assert!(matches!(
243 changes.iter().next().unwrap().change,
244 Change::Aggregate(AggregateChange::Replace { .. })
245 ));
246 }
247
248 #[test]
249 fn different_initcond_replaces() {
250 let t = basic_agg("my_sum");
251 let mut s = basic_agg("my_sum");
252 s.initcond = Some("0".into());
253 let changes = run(&cat(vec![t]), &cat(vec![s]));
254 assert_eq!(changes.len(), 1);
255 assert!(matches!(
256 changes.iter().next().unwrap().change,
257 Change::Aggregate(AggregateChange::Replace { .. })
258 ));
259 }
260
261 #[test]
264 fn replace_subsumes_owner_and_comment() {
265 let t = basic_agg("my_sum");
266 let mut s = basic_agg("my_sum");
267 s.sfunc = qn("app", "other_sfunc"); s.owner = Some(id("alice"));
269 s.comment = Some("new".into());
270 let changes = run(&cat(vec![t]), &cat(vec![s]));
271 assert_eq!(changes.len(), 1, "Replace must subsume owner + comment");
272 assert!(matches!(
273 changes.iter().next().unwrap().change,
274 Change::Aggregate(AggregateChange::Replace { .. })
275 ));
276 }
277
278 #[test]
281 fn owner_change_emits_alter_owner() {
282 let mut t = basic_agg("my_sum");
283 t.owner = Some(id("alice"));
284 let mut s = basic_agg("my_sum");
285 s.owner = Some(id("bob"));
286 let changes = run(&cat(vec![t]), &cat(vec![s]));
287 assert_eq!(changes.len(), 1);
288 assert!(matches!(
289 changes.iter().next().unwrap().change,
290 Change::Aggregate(AggregateChange::AlterOwner { .. })
291 ));
292 }
293
294 #[test]
295 fn source_owner_none_no_alter_owner() {
296 let mut t = basic_agg("my_sum");
297 t.owner = Some(id("alice"));
298 let s = basic_agg("my_sum"); let changes = run(&cat(vec![t]), &cat(vec![s]));
300 assert!(
301 changes.is_empty(),
302 "source owner None = unmanaged, no change expected"
303 );
304 }
305
306 #[test]
309 fn comment_change_emits_comment_on() {
310 let t = basic_agg("my_sum");
311 let mut s = basic_agg("my_sum");
312 s.comment = Some("aggregates integers".into());
313 let changes = run(&cat(vec![t]), &cat(vec![s]));
314 assert_eq!(changes.len(), 1);
315 assert!(matches!(
316 changes.iter().next().unwrap().change,
317 Change::Aggregate(AggregateChange::CommentOn { .. })
318 ));
319 }
320
321 #[test]
324 fn different_arg_types_are_distinct_identities() {
325 let mut t = basic_agg("my_sum"); t.arg_types = vec![ColumnType::Integer];
328 let mut s = basic_agg("my_sum");
329 s.arg_types = vec![ColumnType::BigInt];
330 let changes = run(&cat(vec![t]), &cat(vec![s]));
331 assert_eq!(
332 changes.len(),
333 2,
334 "different arg_types are distinct: Drop + Create"
335 );
336 assert!(
337 changes
338 .iter()
339 .any(|e| matches!(&e.change, Change::Aggregate(AggregateChange::Create(_)))),
340 "expected Create"
341 );
342 assert!(
343 changes
344 .iter()
345 .any(|e| matches!(&e.change, Change::Aggregate(AggregateChange::Drop { .. }))),
346 "expected Drop"
347 );
348 }
349
350 #[test]
353 fn identical_aggregates_produce_no_changes() {
354 let agg = basic_agg("my_sum");
355 let c = cat(vec![agg]);
356 let changes = run(&c, &c);
357 assert!(changes.is_empty());
358 }
359}