1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
5#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
6pub enum LockMode {
7 AccessShare,
8 RowShare,
9 RowExclusive,
10 ShareUpdateExclusive,
11 Share,
12 ShareRowExclusive,
13 Exclusive,
14 AccessExclusive,
15}
16
17impl LockMode {
18 pub fn blocks_reads(self) -> bool {
19 self == LockMode::AccessExclusive
20 }
21
22 pub fn blocks_writes(self) -> bool {
23 self >= LockMode::Share
24 }
25}
26
27impl fmt::Display for LockMode {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::AccessShare => write!(f, "ACCESS SHARE"),
31 Self::RowShare => write!(f, "ROW SHARE"),
32 Self::RowExclusive => write!(f, "ROW EXCLUSIVE"),
33 Self::ShareUpdateExclusive => write!(f, "SHARE UPDATE EXCLUSIVE"),
34 Self::Share => write!(f, "SHARE"),
35 Self::ShareRowExclusive => write!(f, "SHARE ROW EXCLUSIVE"),
36 Self::Exclusive => write!(f, "EXCLUSIVE"),
37 Self::AccessExclusive => write!(f, "ACCESS EXCLUSIVE"),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, clap::ValueEnum)]
43#[serde(rename_all = "lowercase")]
44pub enum RiskLevel {
45 Low,
46 Medium,
47 High,
48 Extreme,
49}
50
51impl fmt::Display for RiskLevel {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::Low => write!(f, "LOW"),
55 Self::Medium => write!(f, "MEDIUM"),
56 Self::High => write!(f, "HIGH"),
57 Self::Extreme => write!(f, "EXTREME"),
58 }
59 }
60}
61
62
63#[derive(Debug, Clone, Serialize)]
64#[serde(rename_all = "snake_case", tag = "type")]
65pub enum RewriteRisk {
66 None,
67 Required,
68 Conditional { reason: String },
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
72#[serde(rename_all = "snake_case")]
73pub enum RolloutPhase {
74 Expand,
75 Backfill,
76 Validate,
77 Switch,
78 Contract,
79}
80
81impl fmt::Display for RolloutPhase {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::Expand => write!(f, "expand"),
85 Self::Backfill => write!(f, "backfill"),
86 Self::Validate => write!(f, "validate"),
87 Self::Switch => write!(f, "switch"),
88 Self::Contract => write!(f, "contract"),
89 }
90 }
91}
92
93
94#[derive(Debug, Clone, Serialize)]
95pub struct RecipeStep {
96 pub phase: RolloutPhase,
97 pub description: String,
98 pub sql: String,
99 pub separate_transaction: bool,
100}
101
102#[derive(Debug, Clone, Serialize)]
103pub struct RolloutRecipe {
104 pub title: String,
105 pub steps: Vec<RecipeStep>,
106}
107
108#[derive(Debug, Clone, Serialize)]
109pub struct Finding {
110 pub rule_id: String,
111 pub risk_level: RiskLevel,
112 pub confidence: ConfidenceLedger,
113 pub lock_mode: LockMode,
114 pub rewrite: RewriteRisk,
115 pub affected_table: Option<String>,
116 pub summary: String,
117 pub explanation: String,
118 pub recipe: Option<RolloutRecipe>,
119 pub pg_version_note: Option<String>,
120 pub statement_sql: String,
121 pub duration_forecast: Option<DurationForecast>,
122}
123
124#[derive(Debug, Clone, Serialize)]
125pub struct TableSize {
126 pub total_bytes: i64,
127 pub row_estimate: i64,
128 pub human_size: String,
129}
130
131#[derive(Debug, Clone, Serialize)]
132pub struct TableBlastRadius {
133 pub table_name: String,
134 pub strongest_lock: LockMode,
135 pub blocks_reads: bool,
136 pub blocks_writes: bool,
137 pub statement_count: usize,
138 pub table_size: Option<TableSize>,
139 pub duration_forecast: Option<DurationForecast>,
140 pub blocked_queries: Vec<BlockedQueryForecast>,
141 pub total_blocked_qps: f64,
142 pub confidence: ConfidenceLedger,
143 pub recommendation: Option<String>,
144}
145
146#[derive(Debug, Clone, Serialize)]
147pub struct BlastRadius {
148 pub per_table: Vec<TableBlastRadius>,
149}
150
151#[derive(Debug, Clone, Serialize)]
152pub struct WorkloadMeta {
153 pub stats_reset: Option<String>,
154 pub collected_at: String,
155 pub stats_window_seconds: Option<f64>,
156 pub unparseable_queries: usize,
157}
158
159#[derive(Debug, Clone, Serialize)]
160pub struct AnalysisResult {
161 pub file: String,
162 pub findings: Vec<Finding>,
163 pub blast_radius: BlastRadius,
164 pub overall_risk: RiskLevel,
165 pub overall_confidence: ConfidenceGrade,
166 pub workload_meta: Option<WorkloadMeta>,
167}
168
169pub fn human_size(bytes: i64) -> String {
170 let bytes = bytes as f64;
171 if bytes < 1024.0 {
172 format!("{bytes:.0} B")
173 } else if bytes < 1024.0 * 1024.0 {
174 format!("{:.1} kB", bytes / 1024.0)
175 } else if bytes < 1024.0 * 1024.0 * 1024.0 {
176 format!("{:.1} MB", bytes / (1024.0 * 1024.0))
177 } else {
178 format!("{:.1} GB", bytes / (1024.0 * 1024.0 * 1024.0))
179 }
180}
181
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
184#[serde(rename_all = "snake_case")]
185pub enum ConfidenceGrade {
186 Static,
187 Estimated,
188 Measured,
189}
190
191impl fmt::Display for ConfidenceGrade {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 match self {
194 Self::Static => write!(f, "STATIC"),
195 Self::Estimated => write!(f, "ESTIMATED"),
196 Self::Measured => write!(f, "MEASURED"),
197 }
198 }
199}
200
201#[derive(Debug, Clone, Serialize)]
202pub struct ConfidenceLedger {
203 pub from_docs: Vec<String>,
204 pub from_catalog: Vec<String>,
205 pub from_stats: Vec<String>,
206 pub unknowns: Vec<String>,
207 pub grade: ConfidenceGrade,
208}
209
210impl ConfidenceLedger {
211 pub fn static_only(doc_facts: Vec<String>) -> Self {
212 Self {
213 from_docs: doc_facts,
214 from_catalog: vec![],
215 from_stats: vec![],
216 unknowns: vec![
217 "table size unknown".into(),
218 "query load unknown".into(),
219 "cache state unknown".into(),
220 ],
221 grade: ConfidenceGrade::Static,
222 }
223 }
224
225 pub fn with_catalog(doc_facts: Vec<String>, catalog_facts: Vec<String>) -> Self {
226 Self {
227 from_docs: doc_facts,
228 from_catalog: catalog_facts,
229 from_stats: vec![],
230 unknowns: vec![
231 "query load unknown".into(),
232 "cache state unknown".into(),
233 ],
234 grade: ConfidenceGrade::Estimated,
235 }
236 }
237
238 pub fn with_workload(
239 doc_facts: Vec<String>,
240 catalog_facts: Vec<String>,
241 stats_facts: Vec<String>,
242 ) -> Self {
243 Self {
244 from_docs: doc_facts,
245 from_catalog: catalog_facts,
246 from_stats: stats_facts,
247 unknowns: vec!["cache state assumed".into()],
248 grade: ConfidenceGrade::Measured,
249 }
250 }
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
254#[serde(rename_all = "snake_case")]
255pub enum AssumptionSource {
256 Documentation,
257 Catalog,
258 Workload,
259 Assumed,
260}
261
262#[derive(Debug, Clone, Serialize)]
263pub struct ForecastAssumption {
264 pub factor: String,
265 pub assumed: String,
266 pub source: AssumptionSource,
267}
268
269#[derive(Debug, Clone, Serialize)]
270pub struct DurationForecast {
271 pub p50_seconds: f64,
272 pub p90_seconds: f64,
273 pub worst_seconds: f64,
274 pub assumptions: Vec<ForecastAssumption>,
275}
276
277impl fmt::Display for DurationForecast {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 write!(
280 f,
281 "{} (p50) {} (p90) {} (worst)",
282 format_seconds(self.p50_seconds),
283 format_seconds(self.p90_seconds),
284 format_seconds(self.worst_seconds),
285 )
286 }
287}
288
289fn format_seconds(secs: f64) -> String {
290 if secs < 1.0 {
291 format!("{:.0}ms", secs * 1000.0)
292 } else if secs < 60.0 {
293 format!("{:.1}s", secs)
294 } else if secs < 3600.0 {
295 format!("{:.0}m", secs / 60.0)
296 } else {
297 format!("{:.1}h", secs / 3600.0)
298 }
299}
300
301#[derive(Debug, Clone, Serialize)]
302pub struct BlockedQueryForecast {
303 pub query_label: String,
304 pub normalised_sql: String,
305 pub calls_per_sec: f64,
306 pub queued_at_p50: u64,
307 pub queued_at_p90: u64,
308}
309
310pub fn adjust_risk_for_size(base: RiskLevel, total_bytes: Option<i64>) -> RiskLevel {
311 match total_bytes {
312 Some(b) if b < 10_000_000 => {
313 match base {
314 RiskLevel::Extreme => RiskLevel::High,
315 RiskLevel::High => RiskLevel::Medium,
316 RiskLevel::Medium => RiskLevel::Low,
317 RiskLevel::Low => RiskLevel::Low,
318 }
319 }
320 Some(b) if b > 10_000_000_000 => {
321 match base {
322 RiskLevel::Low => RiskLevel::Medium,
323 RiskLevel::Medium => RiskLevel::High,
324 RiskLevel::High => RiskLevel::Extreme,
325 RiskLevel::Extreme => RiskLevel::Extreme,
326 }
327 }
328 _ => base,
329 }
330}