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