1use std::collections::{BTreeMap, BTreeSet};
14
15use crate::identifier::{Identifier, QualifiedName};
16use crate::ir::catalog::Catalog;
17use crate::ir::grant::GrantTarget;
18use crate::ir::sequence::Sequence;
19
20use super::change::Change;
21use super::changeset::{ChangeSet, RevokeWithOwnerObservation, UnmanagedGrantObservation};
22use super::destructiveness::Destructiveness;
23use super::grants::diff_grants;
24use super::owner_op::{AlterObjectOwner, OwnerObjectKind};
25use super::sequence_op::{SequenceOp, SequenceOpEntry};
26
27pub fn diff_sequences(
29 target: &Catalog,
30 source: &Catalog,
31 out: &mut ChangeSet,
32 managed_roles: &BTreeSet<Identifier>,
33) {
34 let target_map: BTreeMap<&QualifiedName, &Sequence> = target
36 .sequences
37 .iter()
38 .filter(|s| s.owned_by.is_none())
39 .map(|s| (&s.qname, s))
40 .collect();
41 let source_map: BTreeMap<&QualifiedName, &Sequence> = source
42 .sequences
43 .iter()
44 .filter(|s| s.owned_by.is_none())
45 .map(|s| (&s.qname, s))
46 .collect();
47
48 for (qname, source_seq) in &source_map {
49 if !target_map.contains_key(qname) {
50 out.push(
51 Change::CreateSequence((*source_seq).clone()),
52 Destructiveness::Safe,
53 );
54 let empty_target = Sequence {
58 qname: source_seq.qname.clone(),
59 data_type: source_seq.data_type.clone(),
60 start: source_seq.start,
61 increment: source_seq.increment,
62 min_value: source_seq.min_value,
63 max_value: source_seq.max_value,
64 cache: source_seq.cache,
65 cycle: source_seq.cycle,
66 owned_by: source_seq.owned_by.clone(),
67 comment: source_seq.comment.clone(),
68 owner: None,
69 grants: vec![],
70 };
71 emit_sequence_attribute_changes(&empty_target, source_seq, managed_roles, out);
72 }
73 }
74
75 for (qname, target_seq) in &target_map {
76 match source_map.get(qname) {
77 None => {
78 out.push(
79 Change::DropSequence((*qname).clone()),
80 Destructiveness::RequiresApproval {
81 reason: format!("drops sequence {qname} (current value lost)"),
82 },
83 );
84 }
85 Some(source_seq) => {
86 if target_seq.start != source_seq.start {
87 out.push(
89 Change::DropSequence((*qname).clone()),
90 Destructiveness::RequiresApproval {
91 reason: format!(
92 "drops sequence {qname} to change START (current value lost)"
93 ),
94 },
95 );
96 out.push(
97 Change::CreateSequence((*source_seq).clone()),
98 Destructiveness::Safe,
99 );
100 continue;
101 }
102
103 let ops = diff_sequence_fields(target_seq, source_seq);
104 if !ops.is_empty() {
105 out.push(
106 Change::AlterSequence {
107 qname: (*qname).clone(),
108 ops,
109 },
110 Destructiveness::Safe,
111 );
112 }
113
114 emit_sequence_attribute_changes(target_seq, source_seq, managed_roles, out);
116 }
117 }
118 }
119}
120
121fn emit_sequence_attribute_changes(
133 target_seq: &Sequence,
134 source_seq: &Sequence,
135 managed_roles: &BTreeSet<Identifier>,
136 out: &mut ChangeSet,
137) {
138 let qname = &source_seq.qname;
139
140 if let Some(source_owner) = &source_seq.owner
142 && target_seq.owner.as_ref() != Some(source_owner)
143 {
144 out.push(
145 Change::AlterObjectOwner(AlterObjectOwner {
146 kind: OwnerObjectKind::Sequence,
147 id: crate::diff::owner_op::OwnedObjectId::Qualified(qname.clone()),
148 signature: String::new(),
149 from: target_seq.owner.clone(),
150 to: source_owner.clone(),
151 }),
152 Destructiveness::Safe,
153 );
154 }
155
156 {
158 let object_label = format!("sequence {qname}");
159 let (to_add, to_revoke, unmanaged) =
160 diff_grants(&target_seq.grants, &source_seq.grants, managed_roles);
161 for g in to_revoke {
166 if let Some(source_owner) = &source_seq.owner {
167 out.revokes_with_owner.push(RevokeWithOwnerObservation {
168 object_label: object_label.clone(),
169 privilege_label: g.privilege.sql_keyword().into(),
170 grantee: g.grantee.clone(),
171 owner: source_owner.clone(),
172 });
173 }
174 out.push(
175 Change::RevokeObjectPrivilege {
176 qname: qname.clone(),
177 kind: OwnerObjectKind::Sequence,
178 signature: String::new(),
179 grant: g,
180 },
181 Destructiveness::Safe,
182 );
183 }
184 for g in to_add {
185 out.push(
186 Change::GrantObjectPrivilege {
187 qname: qname.clone(),
188 kind: OwnerObjectKind::Sequence,
189 signature: String::new(),
190 grant: g,
191 },
192 Destructiveness::Safe,
193 );
194 }
195 for g in unmanaged {
196 if let GrantTarget::Role(role_name) = &g.grantee {
197 out.unmanaged_grants.push(UnmanagedGrantObservation {
198 object_label: object_label.clone(),
199 privilege_label: g.privilege.sql_keyword().into(),
200 role_name: role_name.clone(),
201 });
202 }
203 }
204 }
205}
206
207fn diff_sequence_fields(target: &Sequence, source: &Sequence) -> Vec<SequenceOpEntry> {
208 let mut ops = Vec::new();
209
210 if target.increment != source.increment {
211 ops.push(SequenceOpEntry {
212 op: SequenceOp::SetIncrement(source.increment),
213 destructiveness: Destructiveness::Safe,
214 });
215 }
216 if target.min_value != source.min_value {
217 ops.push(SequenceOpEntry {
218 op: SequenceOp::SetMinValue(source.min_value),
219 destructiveness: Destructiveness::Safe,
220 });
221 }
222 if target.max_value != source.max_value {
223 ops.push(SequenceOpEntry {
224 op: SequenceOp::SetMaxValue(source.max_value),
225 destructiveness: Destructiveness::Safe,
226 });
227 }
228 if target.cache != source.cache {
229 ops.push(SequenceOpEntry {
230 op: SequenceOp::SetCache(source.cache),
231 destructiveness: Destructiveness::Safe,
232 });
233 }
234 if target.cycle != source.cycle {
235 ops.push(SequenceOpEntry {
236 op: SequenceOp::SetCycle(source.cycle),
237 destructiveness: Destructiveness::Safe,
238 });
239 }
240 if target.data_type != source.data_type {
241 ops.push(SequenceOpEntry {
242 op: SequenceOp::SetDataType(source.data_type.clone()),
243 destructiveness: Destructiveness::RequiresApproval {
244 reason: format!(
245 "changing data type may overflow current value of sequence {}",
246 target.qname
247 ),
248 },
249 });
250 }
251 if target.owned_by != source.owned_by {
252 ops.push(SequenceOpEntry {
253 op: SequenceOp::SetOwnedBy(source.owned_by.clone()),
254 destructiveness: Destructiveness::Safe,
255 });
256 }
257
258 ops
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use std::collections::BTreeSet;
265
266 use crate::identifier::Identifier;
267 use crate::ir::column_type::ColumnType;
268 use crate::ir::sequence::SequenceOwner;
269
270 fn id(s: &str) -> Identifier {
271 Identifier::from_unquoted(s).unwrap()
272 }
273
274 fn qn(name: &str) -> QualifiedName {
275 QualifiedName::new(id("app"), id(name))
276 }
277
278 fn seq(name: &str) -> Sequence {
279 Sequence {
280 qname: qn(name),
281 data_type: ColumnType::BigInt,
282 start: 1,
283 increment: 1,
284 min_value: None,
285 max_value: None,
286 cache: 1,
287 cycle: false,
288 owned_by: None,
289 comment: None,
290 owner: None,
291 grants: vec![],
292 }
293 }
294
295 #[test]
296 fn add_sequence_is_safe() {
297 let target = Catalog::empty();
298 let mut source = Catalog::empty();
299 source.sequences.push(seq("seq1"));
300 let mut cs = ChangeSet::new();
301 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
302 assert_eq!(cs.len(), 1);
303 assert!(matches!(cs.entries[0].change, Change::CreateSequence(_)));
304 assert_eq!(cs.entries[0].destructiveness, Destructiveness::Safe);
305 }
306
307 #[test]
308 fn drop_sequence_requires_approval() {
309 let mut target = Catalog::empty();
310 target.sequences.push(seq("seq1"));
311 let source = Catalog::empty();
312 let mut cs = ChangeSet::new();
313 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
314 assert_eq!(cs.len(), 1);
315 let entry = &cs.entries[0];
316 assert!(matches!(entry.change, Change::DropSequence(_)));
317 assert!(entry.destructiveness.requires_approval());
318 }
319
320 #[test]
321 fn increment_change_emits_alter_safe() {
322 let mut target = Catalog::empty();
323 target.sequences.push(seq("seq1"));
324 let mut source = Catalog::empty();
325 source.sequences.push(Sequence {
326 increment: 2,
327 ..seq("seq1")
328 });
329 let mut cs = ChangeSet::new();
330 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
331 assert_eq!(cs.len(), 1);
332 let entry = &cs.entries[0];
333 match &entry.change {
334 Change::AlterSequence { qname, ops } => {
335 assert_eq!(qname, &qn("seq1"));
336 assert_eq!(ops.len(), 1);
337 assert!(matches!(ops[0].op, SequenceOp::SetIncrement(2)));
338 assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
339 }
340 other => panic!("got {other:?}"),
341 }
342 }
343
344 #[test]
345 fn data_type_change_requires_approval_inside_alter() {
346 let mut target = Catalog::empty();
347 target.sequences.push(seq("seq1"));
348 let mut source = Catalog::empty();
349 source.sequences.push(Sequence {
350 data_type: ColumnType::Integer,
351 ..seq("seq1")
352 });
353 let mut cs = ChangeSet::new();
354 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
355 let entry = &cs.entries[0];
356 match &entry.change {
357 Change::AlterSequence { ops, .. } => {
358 assert_eq!(ops.len(), 1);
359 assert!(ops[0].destructiveness.requires_approval());
360 assert!(!ops[0].destructiveness.data_loss_risk());
361 }
362 other => panic!("got {other:?}"),
363 }
364 }
365
366 #[test]
367 fn cycle_min_max_cache_each_emit_safe_ops() {
368 let mut target = Catalog::empty();
369 target.sequences.push(seq("seq1"));
370 let mut source = Catalog::empty();
371 source.sequences.push(Sequence {
372 cycle: true,
373 min_value: Some(0),
374 max_value: Some(1_000_000),
375 cache: 32,
376 ..seq("seq1")
377 });
378 let mut cs = ChangeSet::new();
379 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
380 match &cs.entries[0].change {
381 Change::AlterSequence { ops, .. } => {
382 assert_eq!(ops.len(), 4);
383 assert!(
384 ops.iter()
385 .all(|o| o.destructiveness == Destructiveness::Safe)
386 );
387 }
388 other => panic!("got {other:?}"),
389 }
390 }
391
392 #[test]
393 fn start_change_emits_drop_then_create() {
394 let mut target = Catalog::empty();
395 target.sequences.push(seq("seq1"));
396 let mut source = Catalog::empty();
397 source.sequences.push(Sequence {
398 start: 100,
399 ..seq("seq1")
400 });
401 let mut cs = ChangeSet::new();
402 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
403 assert_eq!(cs.len(), 2);
404 assert!(matches!(cs.entries[0].change, Change::DropSequence(_)));
405 assert!(matches!(cs.entries[1].change, Change::CreateSequence(_)));
406 }
407
408 #[test]
409 fn owned_sequences_are_skipped() {
410 let owned = Sequence {
411 owned_by: Some(SequenceOwner {
412 table: qn("users"),
413 column: id("id"),
414 }),
415 ..seq("seq1")
416 };
417 let mut target = Catalog::empty();
418 target.sequences.push(owned);
419 let source = Catalog::empty(); let mut cs = ChangeSet::new();
421 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
422 assert!(
423 cs.is_empty(),
424 "owned sequences are driven by their owner column"
425 );
426 }
427
428 #[test]
429 fn equal_sequences_emit_nothing() {
430 let mut target = Catalog::empty();
431 target.sequences.push(seq("seq1"));
432 let mut source = Catalog::empty();
433 source.sequences.push(seq("seq1"));
434 let mut cs = ChangeSet::new();
435 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
436 assert!(cs.is_empty());
437 }
438
439 #[test]
442 fn new_sequence_emits_owner_and_grant() {
443 use crate::ir::grant::{Grant, GrantTarget, Privilege};
444
445 let target = Catalog::empty();
446 let mut source = Catalog::empty();
447 let app_role = id("app");
448 source.sequences.push(Sequence {
449 owner: Some(app_role.clone()),
450 grants: vec![Grant {
451 grantee: GrantTarget::Role(app_role.clone()),
452 privilege: Privilege::Usage,
453 with_grant_option: false,
454 columns: None,
455 }],
456 ..seq("seq1")
457 });
458
459 let managed = {
460 let mut s = BTreeSet::new();
461 s.insert(app_role);
462 s
463 };
464
465 let mut cs = ChangeSet::new();
466 diff_sequences(&target, &source, &mut cs, &managed);
467
468 let has_create = cs
469 .entries
470 .iter()
471 .any(|e| matches!(&e.change, Change::CreateSequence(_)));
472 let has_owner = cs
473 .entries
474 .iter()
475 .any(|e| matches!(&e.change, Change::AlterObjectOwner(_)));
476 let has_grant = cs
477 .entries
478 .iter()
479 .any(|e| matches!(&e.change, Change::GrantObjectPrivilege { .. }));
480
481 assert!(has_create, "expected CreateSequence");
482 assert!(has_owner, "expected AlterObjectOwner");
483 assert!(has_grant, "expected GrantObjectPrivilege");
484 }
485}