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::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
26/// Diff sequences in `target` against `source`, appending entries to `out`.
27pub fn diff_sequences(
28    target: &Catalog,
29    source: &Catalog,
30    out: &mut ChangeSet,
31    managed_roles: &BTreeSet<Identifier>,
32) {
33    // Skip column-owned sequences — they are driven by their owner column's diff.
34    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            // Synthesize an empty target so the attribute helper can diff
54            // source attributes against "nothing" and emit the appropriate
55            // follow-up Changes (owner, grants).
56            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                    // `START` cannot be altered in place; drop+create.
87                    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                // ---- owner / grants diffs ----
114                emit_sequence_attribute_changes(target_seq, source_seq, managed_roles, out);
115            }
116        }
117    }
118}
119
120/// Emit per-attribute diff changes (owner, grants) for one sequence pair.
121///
122/// Called from two sites:
123/// - "both catalogs have the sequence" branch — with the real target sequence.
124/// - "new sequence" branch — with a synthesized empty target so the diff
125///   against "nothing" produces one `Change` per non-default attribute the
126///   source has.
127///
128/// Intentionally excludes structural fields (`start`, `increment`, `min_value`,
129/// `max_value`, `cache`, `cycle`, `data_type`, `owned_by`) — those are handled
130/// by `diff_sequence_fields` and the `AlterSequence` / drop+create paths above.
131fn 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(); // sequence missing in source — would normally drop
362        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    /// New sequence with owner + grant must emit `CreateSequence`, `AlterObjectOwner`,
382    /// and `GrantObjectPrivilege` — not just `CreateSequence`.
383    #[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}