Skip to main content

pg_blast_radius/
workload.rs

1use serde::{Deserialize, Serialize};
2
3use crate::locks::DmlKind;
4use crate::types::LockMode;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct QueryFamily {
8    pub queryid: i64,
9    pub normalised_sql: String,
10    pub label: String,
11    pub tables: Vec<String>,
12    pub dml_kind: DmlKind,
13    pub lock_mode: LockMode,
14    pub calls_per_sec: f64,
15    pub mean_exec_ms: f64,
16    pub p95_exec_ms: Option<f64>,
17}
18
19impl QueryFamily {
20    pub fn calls_per_min(&self) -> f64 {
21        self.calls_per_sec * 60.0
22    }
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct TransactionBaseline {
27    pub active_sessions: i64,
28    pub idle_in_transaction: i64,
29    pub median_age_ms: f64,
30    pub p95_age_ms: f64,
31    pub max_age_ms: f64,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct WorkloadProfile {
36    pub query_families: Vec<QueryFamily>,
37    pub transaction_baseline: TransactionBaseline,
38    pub collected_at: String,
39    pub stats_reset: Option<String>,
40    #[serde(default)]
41    pub stats_window_seconds: Option<f64>,
42    pub unparseable_queries: usize,
43}
44
45impl WorkloadProfile {
46    pub fn families_for_table(&self, table: &str) -> Vec<&QueryFamily> {
47        self.query_families
48            .iter()
49            .filter(|qf| qf.tables.iter().any(|t| t == table || t.ends_with(&format!(".{table}"))))
50            .collect()
51    }
52
53    pub fn table_qps(&self, table: &str) -> f64 {
54        self.families_for_table(table)
55            .iter()
56            .map(|qf| qf.calls_per_sec)
57            .sum()
58    }
59}
60
61pub fn make_label(sql: &str) -> String {
62    let trimmed = sql.trim();
63    if trimmed.len() <= 60 {
64        trimmed.to_string()
65    } else {
66        format!("{}...", &trimmed[..57])
67    }
68}