Skip to main content

pgevolve_core/diff/
destructiveness.rs

1//! `Destructiveness` — risk classification attached to every diff change.
2//!
3//! Each `ChangeEntry`, `TableOpEntry`, and `SequenceOpEntry` carries one of these
4//! tags so the planner and CLI can decide whether a plan needs explicit approval
5//! or carries data-loss risk.
6
7use serde::{Deserialize, Serialize};
8
9/// Risk classification for a single diff change.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(tag = "level", rename_all = "snake_case")]
12pub enum Destructiveness {
13    /// No approval required; the change cannot lose data on its own.
14    Safe,
15    /// Requires explicit approval but is not a data-loss risk on success.
16    RequiresApproval {
17        /// Human-readable reason shown to the user / written to the plan manifest.
18        reason: String,
19    },
20    /// Requires explicit approval *and* warns of data loss.
21    RequiresApprovalAndDataLossWarning {
22        /// Human-readable reason shown to the user / written to the plan manifest.
23        reason: String,
24    },
25}
26
27impl Destructiveness {
28    /// True when the change is anything other than [`Self::Safe`].
29    pub const fn requires_approval(&self) -> bool {
30        !matches!(self, Self::Safe)
31    }
32
33    /// True when the change is flagged as a data-loss risk.
34    pub const fn data_loss_risk(&self) -> bool {
35        matches!(self, Self::RequiresApprovalAndDataLossWarning { .. })
36    }
37
38    /// Returns the attached human-readable reason, if any.
39    pub const fn reason(&self) -> Option<&str> {
40        match self {
41            Self::Safe => None,
42            Self::RequiresApproval { reason }
43            | Self::RequiresApprovalAndDataLossWarning { reason } => Some(reason.as_str()),
44        }
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn safe_does_not_require_approval() {
54        let d = Destructiveness::Safe;
55        assert!(!d.requires_approval());
56        assert!(!d.data_loss_risk());
57        assert_eq!(d.reason(), None);
58    }
59
60    #[test]
61    fn requires_approval_flags_approval_only() {
62        let d = Destructiveness::RequiresApproval {
63            reason: "drops index".into(),
64        };
65        assert!(d.requires_approval());
66        assert!(!d.data_loss_risk());
67        assert_eq!(d.reason(), Some("drops index"));
68    }
69
70    #[test]
71    fn data_loss_warning_flags_both() {
72        let d = Destructiveness::RequiresApprovalAndDataLossWarning {
73            reason: "drops column email".into(),
74        };
75        assert!(d.requires_approval());
76        assert!(d.data_loss_risk());
77        assert_eq!(d.reason(), Some("drops column email"));
78    }
79
80    #[test]
81    fn serde_round_trip_safe() {
82        let d = Destructiveness::Safe;
83        let json = serde_json::to_string(&d).unwrap();
84        assert_eq!(json, r#"{"level":"safe"}"#);
85        let back: Destructiveness = serde_json::from_str(&json).unwrap();
86        assert_eq!(d, back);
87    }
88
89    #[test]
90    fn serde_round_trip_requires_approval() {
91        let d = Destructiveness::RequiresApproval { reason: "x".into() };
92        let json = serde_json::to_string(&d).unwrap();
93        let back: Destructiveness = serde_json::from_str(&json).unwrap();
94        assert_eq!(d, back);
95    }
96
97    #[test]
98    fn serde_round_trip_data_loss() {
99        let d = Destructiveness::RequiresApprovalAndDataLossWarning { reason: "x".into() };
100        let json = serde_json::to_string(&d).unwrap();
101        let back: Destructiveness = serde_json::from_str(&json).unwrap();
102        assert_eq!(d, back);
103    }
104}