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