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`.
28pub fn diff_sequences(
29    target: &Catalog,
30    source: &Catalog,
31    out: &mut ChangeSet,
32    managed_roles: &BTreeSet<Identifier>,
33) {
34    // Skip column-owned sequences — they are driven by their owner column's diff.
35    let target_map: BTreeMap<&QualifiedName, &Sequence> = target
36        .sequences
37        .iter()
38        .filter(|s| s.owned_by.is_none())
39        .map(|s| (&s.qname, s))
40        .collect();
41    let source_map: BTreeMap<&QualifiedName, &Sequence> = source
42        .sequences
43        .iter()
44        .filter(|s| s.owned_by.is_none())
45        .map(|s| (&s.qname, s))
46        .collect();
47
48    for (qname, source_seq) in &source_map {
49        if !target_map.contains_key(qname) {
50            out.push(
51                Change::CreateSequence((*source_seq).clone()),
52                Destructiveness::Safe,
53            );
54            // Synthesize an empty target so the attribute helper can diff
55            // source attributes against "nothing" and emit the appropriate
56            // follow-up Changes (owner, grants).
57            let empty_target = Sequence {
58                qname: source_seq.qname.clone(),
59                data_type: source_seq.data_type.clone(),
60                start: source_seq.start,
61                increment: source_seq.increment,
62                min_value: source_seq.min_value,
63                max_value: source_seq.max_value,
64                cache: source_seq.cache,
65                cycle: source_seq.cycle,
66                owned_by: source_seq.owned_by.clone(),
67                comment: source_seq.comment.clone(),
68                owner: None,
69                grants: vec![],
70            };
71            emit_sequence_attribute_changes(&empty_target, source_seq, managed_roles, out);
72        }
73    }
74
75    for (qname, target_seq) in &target_map {
76        match source_map.get(qname) {
77            None => {
78                out.push(
79                    Change::DropSequence((*qname).clone()),
80                    Destructiveness::RequiresApproval {
81                        reason: format!("drops sequence {qname} (current value lost)"),
82                    },
83                );
84            }
85            Some(source_seq) => {
86                if target_seq.start != source_seq.start {
87                    // `START` cannot be altered in place; drop+create.
88                    out.push(
89                        Change::DropSequence((*qname).clone()),
90                        Destructiveness::RequiresApproval {
91                            reason: format!(
92                                "drops sequence {qname} to change START (current value lost)"
93                            ),
94                        },
95                    );
96                    out.push(
97                        Change::CreateSequence((*source_seq).clone()),
98                        Destructiveness::Safe,
99                    );
100                    continue;
101                }
102
103                let ops = diff_sequence_fields(target_seq, source_seq);
104                if !ops.is_empty() {
105                    out.push(
106                        Change::AlterSequence {
107                            qname: (*qname).clone(),
108                            ops,
109                        },
110                        Destructiveness::Safe,
111                    );
112                }
113
114                // ---- owner / grants diffs ----
115                emit_sequence_attribute_changes(target_seq, source_seq, managed_roles, out);
116            }
117        }
118    }
119}
120
121/// Emit per-attribute diff changes (owner, grants) for one sequence pair.
122///
123/// Called from two sites:
124/// - "both catalogs have the sequence" branch — with the real target sequence.
125/// - "new sequence" branch — with a synthesized empty target so the diff
126///   against "nothing" produces one `Change` per non-default attribute the
127///   source has.
128///
129/// Intentionally excludes structural fields (`start`, `increment`, `min_value`,
130/// `max_value`, `cache`, `cycle`, `data_type`, `owned_by`) — those are handled
131/// by `diff_sequence_fields` and the `AlterSequence` / drop+create paths above.
132fn emit_sequence_attribute_changes(
133    target_seq: &Sequence,
134    source_seq: &Sequence,
135    managed_roles: &BTreeSet<Identifier>,
136    out: &mut ChangeSet,
137) {
138    let qname = &source_seq.qname;
139
140    // ---- owner diff ----
141    if let Some(source_owner) = &source_seq.owner
142        && target_seq.owner.as_ref() != Some(source_owner)
143    {
144        out.push(
145            Change::AlterObjectOwner(AlterObjectOwner {
146                kind: OwnerObjectKind::Sequence,
147                id: crate::diff::owner_op::OwnedObjectId::Qualified(qname.clone()),
148                signature: String::new(),
149                from: target_seq.owner.clone(),
150                to: source_owner.clone(),
151            }),
152            Destructiveness::Safe,
153        );
154    }
155
156    // ---- grant diff ----
157    {
158        let object_label = format!("sequence {qname}");
159        let (to_add, to_revoke, unmanaged) =
160            diff_grants(&target_seq.grants, &source_seq.grants, managed_roles);
161        // Emit REVOKEs before GRANTs (issue #33): if a grant's with_grant_option
162        // flag changes, the REVOKE must run first so the subsequent GRANT is not
163        // immediately undone. Both changes land in the modifies bucket with the
164        // same NodeId, so insertion order controls execution order.
165        for g in to_revoke {
166            if let Some(source_owner) = &source_seq.owner {
167                out.revokes_with_owner.push(RevokeWithOwnerObservation {
168                    object_label: object_label.clone(),
169                    privilege_label: g.privilege.sql_keyword().into(),
170                    grantee: g.grantee.clone(),
171                    owner: source_owner.clone(),
172                });
173            }
174            out.push(
175                Change::RevokeObjectPrivilege {
176                    qname: qname.clone(),
177                    kind: OwnerObjectKind::Sequence,
178                    signature: String::new(),
179                    grant: g,
180                },
181                Destructiveness::Safe,
182            );
183        }
184        for g in to_add {
185            out.push(
186                Change::GrantObjectPrivilege {
187                    qname: qname.clone(),
188                    kind: OwnerObjectKind::Sequence,
189                    signature: String::new(),
190                    grant: g,
191                },
192                Destructiveness::Safe,
193            );
194        }
195        for g in unmanaged {
196            if let GrantTarget::Role(role_name) = &g.grantee {
197                out.unmanaged_grants.push(UnmanagedGrantObservation {
198                    object_label: object_label.clone(),
199                    privilege_label: g.privilege.sql_keyword().into(),
200                    role_name: role_name.clone(),
201                });
202            }
203        }
204    }
205}
206
207fn diff_sequence_fields(target: &Sequence, source: &Sequence) -> Vec<SequenceOpEntry> {
208    let mut ops = Vec::new();
209
210    if target.increment != source.increment {
211        ops.push(SequenceOpEntry {
212            op: SequenceOp::SetIncrement(source.increment),
213            destructiveness: Destructiveness::Safe,
214        });
215    }
216    if target.min_value != source.min_value {
217        ops.push(SequenceOpEntry {
218            op: SequenceOp::SetMinValue(source.min_value),
219            destructiveness: Destructiveness::Safe,
220        });
221    }
222    if target.max_value != source.max_value {
223        ops.push(SequenceOpEntry {
224            op: SequenceOp::SetMaxValue(source.max_value),
225            destructiveness: Destructiveness::Safe,
226        });
227    }
228    if target.cache != source.cache {
229        ops.push(SequenceOpEntry {
230            op: SequenceOp::SetCache(source.cache),
231            destructiveness: Destructiveness::Safe,
232        });
233    }
234    if target.cycle != source.cycle {
235        ops.push(SequenceOpEntry {
236            op: SequenceOp::SetCycle(source.cycle),
237            destructiveness: Destructiveness::Safe,
238        });
239    }
240    if target.data_type != source.data_type {
241        ops.push(SequenceOpEntry {
242            op: SequenceOp::SetDataType(source.data_type.clone()),
243            destructiveness: Destructiveness::RequiresApproval {
244                reason: format!(
245                    "changing data type may overflow current value of sequence {}",
246                    target.qname
247                ),
248            },
249        });
250    }
251    if target.owned_by != source.owned_by {
252        ops.push(SequenceOpEntry {
253            op: SequenceOp::SetOwnedBy(source.owned_by.clone()),
254            destructiveness: Destructiveness::Safe,
255        });
256    }
257
258    ops
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use std::collections::BTreeSet;
265
266    use crate::identifier::Identifier;
267    use crate::ir::column_type::ColumnType;
268    use crate::ir::sequence::SequenceOwner;
269
270    fn id(s: &str) -> Identifier {
271        Identifier::from_unquoted(s).unwrap()
272    }
273
274    fn qn(name: &str) -> QualifiedName {
275        QualifiedName::new(id("app"), id(name))
276    }
277
278    fn seq(name: &str) -> Sequence {
279        Sequence {
280            qname: qn(name),
281            data_type: ColumnType::BigInt,
282            start: 1,
283            increment: 1,
284            min_value: None,
285            max_value: None,
286            cache: 1,
287            cycle: false,
288            owned_by: None,
289            comment: None,
290            owner: None,
291            grants: vec![],
292        }
293    }
294
295    #[test]
296    fn add_sequence_is_safe() {
297        let target = Catalog::empty();
298        let mut source = Catalog::empty();
299        source.sequences.push(seq("seq1"));
300        let mut cs = ChangeSet::new();
301        diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
302        assert_eq!(cs.len(), 1);
303        assert!(matches!(cs.entries[0].change, Change::CreateSequence(_)));
304        assert_eq!(cs.entries[0].destructiveness, Destructiveness::Safe);
305    }
306
307    #[test]
308    fn drop_sequence_requires_approval() {
309        let mut target = Catalog::empty();
310        target.sequences.push(seq("seq1"));
311        let source = Catalog::empty();
312        let mut cs = ChangeSet::new();
313        diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
314        assert_eq!(cs.len(), 1);
315        let entry = &cs.entries[0];
316        assert!(matches!(entry.change, Change::DropSequence(_)));
317        assert!(entry.destructiveness.requires_approval());
318    }
319
320    #[test]
321    fn increment_change_emits_alter_safe() {
322        let mut target = Catalog::empty();
323        target.sequences.push(seq("seq1"));
324        let mut source = Catalog::empty();
325        source.sequences.push(Sequence {
326            increment: 2,
327            ..seq("seq1")
328        });
329        let mut cs = ChangeSet::new();
330        diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
331        assert_eq!(cs.len(), 1);
332        let entry = &cs.entries[0];
333        match &entry.change {
334            Change::AlterSequence { qname, ops } => {
335                assert_eq!(qname, &qn("seq1"));
336                assert_eq!(ops.len(), 1);
337                assert!(matches!(ops[0].op, SequenceOp::SetIncrement(2)));
338                assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
339            }
340            other => panic!("got {other:?}"),
341        }
342    }
343
344    #[test]
345    fn data_type_change_requires_approval_inside_alter() {
346        let mut target = Catalog::empty();
347        target.sequences.push(seq("seq1"));
348        let mut source = Catalog::empty();
349        source.sequences.push(Sequence {
350            data_type: ColumnType::Integer,
351            ..seq("seq1")
352        });
353        let mut cs = ChangeSet::new();
354        diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
355        let entry = &cs.entries[0];
356        match &entry.change {
357            Change::AlterSequence { ops, .. } => {
358                assert_eq!(ops.len(), 1);
359                assert!(ops[0].destructiveness.requires_approval());
360                assert!(!ops[0].destructiveness.data_loss_risk());
361            }
362            other => panic!("got {other:?}"),
363        }
364    }
365
366    #[test]
367    fn cycle_min_max_cache_each_emit_safe_ops() {
368        let mut target = Catalog::empty();
369        target.sequences.push(seq("seq1"));
370        let mut source = Catalog::empty();
371        source.sequences.push(Sequence {
372            cycle: true,
373            min_value: Some(0),
374            max_value: Some(1_000_000),
375            cache: 32,
376            ..seq("seq1")
377        });
378        let mut cs = ChangeSet::new();
379        diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
380        match &cs.entries[0].change {
381            Change::AlterSequence { ops, .. } => {
382                assert_eq!(ops.len(), 4);
383                assert!(
384                    ops.iter()
385                        .all(|o| o.destructiveness == Destructiveness::Safe)
386                );
387            }
388            other => panic!("got {other:?}"),
389        }
390    }
391
392    #[test]
393    fn start_change_emits_drop_then_create() {
394        let mut target = Catalog::empty();
395        target.sequences.push(seq("seq1"));
396        let mut source = Catalog::empty();
397        source.sequences.push(Sequence {
398            start: 100,
399            ..seq("seq1")
400        });
401        let mut cs = ChangeSet::new();
402        diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
403        assert_eq!(cs.len(), 2);
404        assert!(matches!(cs.entries[0].change, Change::DropSequence(_)));
405        assert!(matches!(cs.entries[1].change, Change::CreateSequence(_)));
406    }
407
408    #[test]
409    fn owned_sequences_are_skipped() {
410        let owned = Sequence {
411            owned_by: Some(SequenceOwner {
412                table: qn("users"),
413                column: id("id"),
414            }),
415            ..seq("seq1")
416        };
417        let mut target = Catalog::empty();
418        target.sequences.push(owned);
419        let source = Catalog::empty(); // sequence missing in source — would normally drop
420        let mut cs = ChangeSet::new();
421        diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
422        assert!(
423            cs.is_empty(),
424            "owned sequences are driven by their owner column"
425        );
426    }
427
428    #[test]
429    fn equal_sequences_emit_nothing() {
430        let mut target = Catalog::empty();
431        target.sequences.push(seq("seq1"));
432        let mut source = Catalog::empty();
433        source.sequences.push(seq("seq1"));
434        let mut cs = ChangeSet::new();
435        diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
436        assert!(cs.is_empty());
437    }
438
439    /// New sequence with owner + grant must emit `CreateSequence`, `AlterObjectOwner`,
440    /// and `GrantObjectPrivilege` — not just `CreateSequence`.
441    #[test]
442    fn new_sequence_emits_owner_and_grant() {
443        use crate::ir::grant::{Grant, GrantTarget, Privilege};
444
445        let target = Catalog::empty();
446        let mut source = Catalog::empty();
447        let app_role = id("app");
448        source.sequences.push(Sequence {
449            owner: Some(app_role.clone()),
450            grants: vec![Grant {
451                grantee: GrantTarget::Role(app_role.clone()),
452                privilege: Privilege::Usage,
453                with_grant_option: false,
454                columns: None,
455            }],
456            ..seq("seq1")
457        });
458
459        let managed = {
460            let mut s = BTreeSet::new();
461            s.insert(app_role);
462            s
463        };
464
465        let mut cs = ChangeSet::new();
466        diff_sequences(&target, &source, &mut cs, &managed);
467
468        let has_create = cs
469            .entries
470            .iter()
471            .any(|e| matches!(&e.change, Change::CreateSequence(_)));
472        let has_owner = cs
473            .entries
474            .iter()
475            .any(|e| matches!(&e.change, Change::AlterObjectOwner(_)));
476        let has_grant = cs
477            .entries
478            .iter()
479            .any(|e| matches!(&e.change, Change::GrantObjectPrivilege { .. }));
480
481        assert!(has_create, "expected CreateSequence");
482        assert!(has_owner, "expected AlterObjectOwner");
483        assert!(has_grant, "expected GrantObjectPrivilege");
484    }
485}