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
27#[allow(clippy::too_many_lines)]
29pub fn diff_sequences(
30 target: &Catalog,
31 source: &Catalog,
32 out: &mut ChangeSet,
33 managed_roles: &BTreeSet<Identifier>,
34) {
35 let target_map: BTreeMap<&QualifiedName, &Sequence> = target
37 .sequences
38 .iter()
39 .filter(|s| s.owned_by.is_none())
40 .map(|s| (&s.qname, s))
41 .collect();
42 let source_map: BTreeMap<&QualifiedName, &Sequence> = source
43 .sequences
44 .iter()
45 .filter(|s| s.owned_by.is_none())
46 .map(|s| (&s.qname, s))
47 .collect();
48
49 for (qname, source_seq) in &source_map {
50 if !target_map.contains_key(qname) {
51 out.push(
52 Change::CreateSequence((*source_seq).clone()),
53 Destructiveness::Safe,
54 );
55 }
56 }
57
58 for (qname, target_seq) in &target_map {
59 match source_map.get(qname) {
60 None => {
61 out.push(
62 Change::DropSequence((*qname).clone()),
63 Destructiveness::RequiresApproval {
64 reason: format!("drops sequence {qname} (current value lost)"),
65 },
66 );
67 }
68 Some(source_seq) => {
69 if target_seq.start != source_seq.start {
70 out.push(
72 Change::DropSequence((*qname).clone()),
73 Destructiveness::RequiresApproval {
74 reason: format!(
75 "drops sequence {qname} to change START (current value lost)"
76 ),
77 },
78 );
79 out.push(
80 Change::CreateSequence((*source_seq).clone()),
81 Destructiveness::Safe,
82 );
83 continue;
84 }
85
86 let ops = diff_sequence_fields(target_seq, source_seq);
87 if !ops.is_empty() {
88 out.push(
89 Change::AlterSequence {
90 qname: (*qname).clone(),
91 ops,
92 },
93 Destructiveness::Safe,
94 );
95 }
96
97 if let Some(source_owner) = &source_seq.owner
99 && target_seq.owner.as_ref() != Some(source_owner)
100 {
101 let from = target_seq.owner.clone().unwrap_or_else(|| {
102 Identifier::from_unquoted("__unknown_owner__").expect("literal is valid")
103 });
104 out.push(
105 Change::AlterObjectOwner(AlterObjectOwner {
106 kind: OwnerObjectKind::Sequence,
107 qname: (*qname).clone(),
108 signature: String::new(),
109 from,
110 to: source_owner.clone(),
111 }),
112 Destructiveness::Safe,
113 );
114 }
115
116 {
118 let object_label = format!("sequence {qname}");
119 let (to_add, to_revoke, unmanaged) =
120 diff_grants(&target_seq.grants, &source_seq.grants, managed_roles);
121 for g in to_add {
122 out.push(
123 Change::GrantObjectPrivilege {
124 qname: (*qname).clone(),
125 kind: OwnerObjectKind::Sequence,
126 signature: String::new(),
127 grant: g,
128 },
129 Destructiveness::Safe,
130 );
131 }
132 for g in to_revoke {
133 if let Some(source_owner) = &source_seq.owner {
134 out.revokes_with_owner.push(RevokeWithOwnerObservation {
135 object_label: object_label.clone(),
136 privilege_label: g.privilege.sql_keyword().into(),
137 grantee: g.grantee.clone(),
138 owner: source_owner.clone(),
139 });
140 }
141 out.push(
142 Change::RevokeObjectPrivilege {
143 qname: (*qname).clone(),
144 kind: OwnerObjectKind::Sequence,
145 signature: String::new(),
146 grant: g,
147 },
148 Destructiveness::Safe,
149 );
150 }
151 for g in unmanaged {
152 if let GrantTarget::Role(role_name) = &g.grantee {
153 out.unmanaged_grants.push(UnmanagedGrantObservation {
154 object_label: object_label.clone(),
155 privilege_label: g.privilege.sql_keyword().into(),
156 role_name: role_name.clone(),
157 });
158 }
159 }
160 }
161 }
162 }
163 }
164}
165
166fn diff_sequence_fields(target: &Sequence, source: &Sequence) -> Vec<SequenceOpEntry> {
167 let mut ops = Vec::new();
168
169 if target.increment != source.increment {
170 ops.push(SequenceOpEntry {
171 op: SequenceOp::SetIncrement(source.increment),
172 destructiveness: Destructiveness::Safe,
173 });
174 }
175 if target.min_value != source.min_value {
176 ops.push(SequenceOpEntry {
177 op: SequenceOp::SetMinValue(source.min_value),
178 destructiveness: Destructiveness::Safe,
179 });
180 }
181 if target.max_value != source.max_value {
182 ops.push(SequenceOpEntry {
183 op: SequenceOp::SetMaxValue(source.max_value),
184 destructiveness: Destructiveness::Safe,
185 });
186 }
187 if target.cache != source.cache {
188 ops.push(SequenceOpEntry {
189 op: SequenceOp::SetCache(source.cache),
190 destructiveness: Destructiveness::Safe,
191 });
192 }
193 if target.cycle != source.cycle {
194 ops.push(SequenceOpEntry {
195 op: SequenceOp::SetCycle(source.cycle),
196 destructiveness: Destructiveness::Safe,
197 });
198 }
199 if target.data_type != source.data_type {
200 ops.push(SequenceOpEntry {
201 op: SequenceOp::SetDataType(source.data_type.clone()),
202 destructiveness: Destructiveness::RequiresApproval {
203 reason: format!(
204 "changing data type may overflow current value of sequence {}",
205 target.qname
206 ),
207 },
208 });
209 }
210 if target.owned_by != source.owned_by {
211 ops.push(SequenceOpEntry {
212 op: SequenceOp::SetOwnedBy(source.owned_by.clone()),
213 destructiveness: Destructiveness::Safe,
214 });
215 }
216
217 ops
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use std::collections::BTreeSet;
224
225 use crate::identifier::Identifier;
226 use crate::ir::column_type::ColumnType;
227 use crate::ir::sequence::SequenceOwner;
228
229 fn id(s: &str) -> Identifier {
230 Identifier::from_unquoted(s).unwrap()
231 }
232
233 fn qn(name: &str) -> QualifiedName {
234 QualifiedName::new(id("app"), id(name))
235 }
236
237 fn seq(name: &str) -> Sequence {
238 Sequence {
239 qname: qn(name),
240 data_type: ColumnType::BigInt,
241 start: 1,
242 increment: 1,
243 min_value: None,
244 max_value: None,
245 cache: 1,
246 cycle: false,
247 owned_by: None,
248 comment: None,
249 owner: None,
250 grants: vec![],
251 }
252 }
253
254 #[test]
255 fn add_sequence_is_safe() {
256 let target = Catalog::empty();
257 let mut source = Catalog::empty();
258 source.sequences.push(seq("seq1"));
259 let mut cs = ChangeSet::new();
260 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
261 assert_eq!(cs.len(), 1);
262 assert!(matches!(cs.entries[0].change, Change::CreateSequence(_)));
263 assert_eq!(cs.entries[0].destructiveness, Destructiveness::Safe);
264 }
265
266 #[test]
267 fn drop_sequence_requires_approval() {
268 let mut target = Catalog::empty();
269 target.sequences.push(seq("seq1"));
270 let source = Catalog::empty();
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 assert!(matches!(entry.change, Change::DropSequence(_)));
276 assert!(entry.destructiveness.requires_approval());
277 }
278
279 #[test]
280 fn increment_change_emits_alter_safe() {
281 let mut target = Catalog::empty();
282 target.sequences.push(seq("seq1"));
283 let mut source = Catalog::empty();
284 source.sequences.push(Sequence {
285 increment: 2,
286 ..seq("seq1")
287 });
288 let mut cs = ChangeSet::new();
289 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
290 assert_eq!(cs.len(), 1);
291 let entry = &cs.entries[0];
292 match &entry.change {
293 Change::AlterSequence { qname, ops } => {
294 assert_eq!(qname, &qn("seq1"));
295 assert_eq!(ops.len(), 1);
296 assert!(matches!(ops[0].op, SequenceOp::SetIncrement(2)));
297 assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
298 }
299 other => panic!("got {other:?}"),
300 }
301 }
302
303 #[test]
304 fn data_type_change_requires_approval_inside_alter() {
305 let mut target = Catalog::empty();
306 target.sequences.push(seq("seq1"));
307 let mut source = Catalog::empty();
308 source.sequences.push(Sequence {
309 data_type: ColumnType::Integer,
310 ..seq("seq1")
311 });
312 let mut cs = ChangeSet::new();
313 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
314 let entry = &cs.entries[0];
315 match &entry.change {
316 Change::AlterSequence { ops, .. } => {
317 assert_eq!(ops.len(), 1);
318 assert!(ops[0].destructiveness.requires_approval());
319 assert!(!ops[0].destructiveness.data_loss_risk());
320 }
321 other => panic!("got {other:?}"),
322 }
323 }
324
325 #[test]
326 fn cycle_min_max_cache_each_emit_safe_ops() {
327 let mut target = Catalog::empty();
328 target.sequences.push(seq("seq1"));
329 let mut source = Catalog::empty();
330 source.sequences.push(Sequence {
331 cycle: true,
332 min_value: Some(0),
333 max_value: Some(1_000_000),
334 cache: 32,
335 ..seq("seq1")
336 });
337 let mut cs = ChangeSet::new();
338 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
339 match &cs.entries[0].change {
340 Change::AlterSequence { ops, .. } => {
341 assert_eq!(ops.len(), 4);
342 assert!(
343 ops.iter()
344 .all(|o| o.destructiveness == Destructiveness::Safe)
345 );
346 }
347 other => panic!("got {other:?}"),
348 }
349 }
350
351 #[test]
352 fn start_change_emits_drop_then_create() {
353 let mut target = Catalog::empty();
354 target.sequences.push(seq("seq1"));
355 let mut source = Catalog::empty();
356 source.sequences.push(Sequence {
357 start: 100,
358 ..seq("seq1")
359 });
360 let mut cs = ChangeSet::new();
361 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
362 assert_eq!(cs.len(), 2);
363 assert!(matches!(cs.entries[0].change, Change::DropSequence(_)));
364 assert!(matches!(cs.entries[1].change, Change::CreateSequence(_)));
365 }
366
367 #[test]
368 fn owned_sequences_are_skipped() {
369 let owned = Sequence {
370 owned_by: Some(SequenceOwner {
371 table: qn("users"),
372 column: id("id"),
373 }),
374 ..seq("seq1")
375 };
376 let mut target = Catalog::empty();
377 target.sequences.push(owned);
378 let source = Catalog::empty(); let mut cs = ChangeSet::new();
380 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
381 assert!(
382 cs.is_empty(),
383 "owned sequences are driven by their owner column"
384 );
385 }
386
387 #[test]
388 fn equal_sequences_emit_nothing() {
389 let mut target = Catalog::empty();
390 target.sequences.push(seq("seq1"));
391 let mut source = Catalog::empty();
392 source.sequences.push(seq("seq1"));
393 let mut cs = ChangeSet::new();
394 diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
395 assert!(cs.is_empty());
396 }
397}