Skip to main content

pgevolve_core/diff/
sequences.rs

1//! Sequence-level diffing.
2//!
3//! Pairs sequences by [`QualifiedName`]. Sequences that are owned by a column
4//! (`OWNED BY table.column`) are *skipped* — they are managed implicitly via
5//! the owning column's identity / generated specification, and emitting
6//! independent sequence ops for them would conflict with the column-driven
7//! changes.
8//!
9//! `START` is intentionally absent from [`SequenceOp`] (Postgres requires
10//! `RESTART`, which has different semantics). When `start` differs we emit a
11//! drop+create pair so the new starting point is honored.
12
13use 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/// Diff sequences in `target` against `source`, appending entries to `out`.
28#[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    // Skip column-owned sequences — they are driven by their owner column's diff.
36    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                    // `START` cannot be altered in place; drop+create.
71                    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                // ---- owner diff ----
98                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                // ---- grant diff ----
117                {
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(); // sequence missing in source — would normally drop
379        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}