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)] // exhaustive per-sequence-attribute diff; extraction would fragment a single conceptual pass.
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                    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                // ---- grant diff ----
114                {
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(); // sequence missing in source — would normally drop
376        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}