Skip to main content

schema_risk/
types.rs

1//! All shared types used across the tool.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6// ─────────────────────────────────────────────
7// Risk levels
8// ─────────────────────────────────────────────
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
11#[serde(rename_all = "UPPERCASE")]
12pub enum RiskLevel {
13    Low,
14    Medium,
15    High,
16    Critical,
17}
18
19impl fmt::Display for RiskLevel {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            RiskLevel::Low => write!(f, "LOW"),
23            RiskLevel::Medium => write!(f, "MEDIUM"),
24            RiskLevel::High => write!(f, "HIGH"),
25            RiskLevel::Critical => write!(f, "CRITICAL"),
26        }
27    }
28}
29
30impl RiskLevel {
31    /// Convert a numeric score to a risk level.
32    pub fn from_score(score: u32) -> Self {
33        match score {
34            0..=20 => RiskLevel::Low,
35            21..=50 => RiskLevel::Medium,
36            51..=100 => RiskLevel::High,
37            _ => RiskLevel::Critical,
38        }
39    }
40
41    /// Returns an exit code suitable for CI: non-zero when risk >= threshold.
42    pub fn exit_code(self, fail_on: RiskLevel) -> i32 {
43        if self >= fail_on {
44            1
45        } else {
46            0
47        }
48    }
49}
50
51// ─────────────────────────────────────────────
52// Detected schema operations
53// ─────────────────────────────────────────────
54
55/// A single high-level action the migration performs.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct DetectedOperation {
58    /// Human-readable summary of what the SQL does.
59    pub description: String,
60    /// Which table(s) are touched (may be empty for DROP INDEX etc.).
61    pub tables: Vec<String>,
62    /// Risk contribution of this single operation.
63    pub risk_level: RiskLevel,
64    /// Score contribution (additive).
65    pub score: u32,
66    /// One-line warning emitted for this operation.
67    pub warning: Option<String>,
68    /// Whether this op acquires a full table lock.
69    pub acquires_lock: bool,
70    /// Whether this op triggers an index rebuild.
71    pub index_rebuild: bool,
72}
73
74// ─────────────────────────────────────────────
75// Foreign-key impact
76// ─────────────────────────────────────────────
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct FkImpact {
80    pub constraint_name: String,
81    pub from_table: String,
82    pub to_table: String,
83    pub cascade: bool,
84}
85
86// ─────────────────────────────────────────────
87// The final report produced for one SQL file
88// ─────────────────────────────────────────────
89
90// ─────────────────────────────────────────────
91// Guard decision — recorded outcome of a guard confirmation
92// ─────────────────────────────────────────────
93
94/// The actor type detected when running `guard`.
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "lowercase")]
97pub enum ActorKind {
98    Human,
99    Ci,
100    Agent,
101}
102
103impl std::fmt::Display for ActorKind {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            ActorKind::Human => write!(f, "human (interactive terminal)"),
107            ActorKind::Ci => write!(f, "ci"),
108            ActorKind::Agent => write!(f, "agent"),
109        }
110    }
111}
112
113/// Recorded outcome of a single guard confirmation prompt.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct GuardDecision {
116    /// Human-readable operation description.
117    pub operation: String,
118    /// Risk level of this operation.
119    pub risk_level: RiskLevel,
120    /// Numeric risk score.
121    pub score: u32,
122    /// One-sentence human-readable impact summary.
123    pub impact_summary: String,
124    /// `true` = user confirmed; `false` = user declined / blocked.
125    pub confirmed: bool,
126    /// The phrase the user typed (or None for non-interactive runs).
127    pub typed_phrase: Option<String>,
128    /// ISO-8601 timestamp of the decision.
129    pub timestamp: String,
130    /// Which actor made the decision.
131    pub actor: ActorKind,
132}
133
134// ─────────────────────────────────────────────
135// Audit log — written by `guard` on confirmed runs
136// ─────────────────────────────────────────────
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct GuardAuditLog {
140    pub schemarisk_version: String,
141    pub file: String,
142    pub timestamp: String,
143    pub actor: ActorKind,
144    pub decisions: Vec<GuardDecision>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct MigrationReport {
149    pub file: String,
150    pub overall_risk: RiskLevel,
151    pub score: u32,
152    pub affected_tables: Vec<String>,
153    pub operations: Vec<DetectedOperation>,
154    pub warnings: Vec<String>,
155    pub recommendations: Vec<String>,
156    pub fk_impacts: Vec<FkImpact>,
157    pub estimated_lock_seconds: Option<u64>,
158    pub index_rebuild_required: bool,
159    pub requires_maintenance_window: bool,
160    pub analyzed_at: String,
161    /// Target PostgreSQL major version used for scoring (e.g. 14).
162    pub pg_version: u32,
163    /// Whether any operation in this report requires guard confirmation.
164    pub guard_required: bool,
165    /// Guard decisions recorded during a `guard` run.
166    pub guard_decisions: Vec<GuardDecision>,
167}