pg-blast-radius 0.3.0

Workload-aware blast radius forecaster for PostgreSQL migrations
Documentation
use std::collections::HashMap;
use std::path::Path;

use anyhow::Result;
use serde::Deserialize;

use crate::types::{TableSize, human_size};
use crate::workload::WorkloadProfile;
use super::CatalogInfo;

#[derive(Deserialize)]
struct StatsEntry {
    table_name: String,
    total_bytes: i64,
    row_estimate: i64,
}

#[derive(Deserialize)]
struct StatsFileV2 {
    tables: Vec<StatsEntry>,
    workload: Option<WorkloadProfile>,
}

pub fn load_stats_file(path: &Path) -> Result<CatalogInfo> {
    let content = std::fs::read_to_string(path)
        .map_err(|e| anyhow::anyhow!("Failed to read stats file: {e}"))?;

    if let Ok(v2) = serde_json::from_str::<StatsFileV2>(&content) {
        return Ok(CatalogInfo {
            tables: entries_to_map(v2.tables),
            workload: v2.workload,
        });
    }

    let entries: Vec<StatsEntry> = serde_json::from_str(&content)
        .map_err(|e| anyhow::anyhow!("Failed to parse stats file: {e}"))?;

    Ok(CatalogInfo {
        tables: entries_to_map(entries),
        workload: None,
    })
}

fn entries_to_map(entries: Vec<StatsEntry>) -> HashMap<String, TableSize> {
    let mut tables = HashMap::new();
    for entry in entries {
        let size = TableSize {
            total_bytes: entry.total_bytes,
            row_estimate: entry.row_estimate,
            human_size: human_size(entry.total_bytes),
        };
        tables.insert(entry.table_name, size);
    }
    tables
}