pg-blast-radius 0.3.0

Workload-aware blast radius forecaster for PostgreSQL migrations
Documentation
use pg_query::protobuf;

use crate::forecast;
use crate::parse::format_relation;
use crate::recipe;
use crate::types::*;

use super::RuleContext;

pub fn analyse_index_stmt(
    index: &protobuf::IndexStmt,
    stmt_sql: &str,
    ctx: &RuleContext,
) -> Vec<Finding> {
    let table = index
        .relation
        .as_ref()
        .map(format_relation)
        .unwrap_or_else(|| "unknown".into());

    let idx_name = if index.idxname.is_empty() {
        "unnamed".into()
    } else {
        index.idxname.clone()
    };

    let table_bytes = ctx.catalog.and_then(|c| c.table_bytes(&table));

    if index.concurrent {
        let risk = adjust_risk_for_size(RiskLevel::Low, table_bytes);
        vec![Finding {
            rule_id: "create-index-concurrently".into(),
            risk_level: risk,
            confidence: match table_bytes {
                Some(b) => ConfidenceLedger::with_catalog(
                    vec!["SHARE UPDATE EXCLUSIVE lock (non-blocking) for CREATE INDEX CONCURRENTLY".into()],
                    vec![format!("table size is {}", human_size(b))],
                ),
                None => ConfidenceLedger::static_only(
                    vec!["SHARE UPDATE EXCLUSIVE lock (non-blocking) for CREATE INDEX CONCURRENTLY".into()],
                ),
            },
            lock_mode: LockMode::ShareUpdateExclusive,
            rewrite: RewriteRisk::None,
            affected_table: Some(table.clone()),
            summary: format!(
                "CREATE INDEX CONCURRENTLY \"{idx_name}\" on \"{table}\" (non-blocking)"
            ),
            explanation: "SHARE UPDATE EXCLUSIVE lock does not block reads or writes. \
                Index is built in the background. Cannot run inside a transaction block. \
                May leave an INVALID index if long-running transactions interfere."
                .into(),
            recipe: None,
            pg_version_note: None,
            statement_sql: stmt_sql.into(),
            duration_forecast: table_bytes.map(|b| forecast::forecast_index_build(b, ctx.transaction_baseline)),
        }]
    } else {
        let base_risk = RiskLevel::High;
        let risk = adjust_risk_for_size(base_risk, table_bytes);

        let columns = extract_index_columns(index);

        vec![Finding {
            rule_id: "create-index".into(),
            risk_level: risk,
            confidence: match table_bytes {
                Some(b) => ConfidenceLedger::with_catalog(
                    vec!["SHARE lock blocks writes for entire index build duration".into()],
                    vec![format!("table size is {}", human_size(b))],
                ),
                None => ConfidenceLedger::static_only(
                    vec!["SHARE lock blocks writes for entire index build duration".into()],
                ),
            },
            lock_mode: LockMode::Share,
            rewrite: RewriteRisk::None,
            affected_table: Some(table.clone()),
            summary: format!(
                "CREATE INDEX \"{idx_name}\" on \"{table}\" without CONCURRENTLY"
            ),
            explanation: format!(
                "Takes SHARE lock on \"{table}\", blocking all INSERT, UPDATE, and DELETE \
                 for the entire duration of the index build.{}",
                match table_bytes {
                    Some(b) => format!(" Table is {}.", human_size(b)),
                    None => " Table size unknown; use --dsn for duration estimate.".into(),
                }
            ),
            recipe: Some(recipe::create_index_concurrently(&table, &columns, &idx_name)),
            pg_version_note: None,
            statement_sql: stmt_sql.into(),
            duration_forecast: table_bytes.map(|b| forecast::forecast_index_build(b, ctx.transaction_baseline)),
        }]
    }
}

fn extract_index_columns(index: &protobuf::IndexStmt) -> String {
    index
        .index_params
        .iter()
        .filter_map(|n| {
            n.node.as_ref().and_then(|inner| match inner {
                pg_query::protobuf::node::Node::IndexElem(elem) => {
                    if elem.name.is_empty() {
                        None
                    } else {
                        Some(elem.name.clone())
                    }
                }
                _ => None,
            })
        })
        .collect::<Vec<_>>()
        .join(", ")
}