Skip to main content

pg_blast_radius/rules/
mod.rs

1pub mod alter_table;
2pub mod constraints;
3pub mod create_index;
4pub mod drop;
5pub mod maintenance;
6pub mod rename;
7
8use crate::catalog::CatalogInfo;
9use crate::parse;
10use crate::types::Finding;
11use crate::workload::TransactionBaseline;
12use anyhow::Result;
13use pg_query::protobuf::node;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16pub struct PgVersion {
17    pub major: u32,
18}
19
20impl PgVersion {
21    pub fn new(major: u32) -> Self {
22        Self { major }
23    }
24
25    pub fn at_least(self, major: u32) -> bool {
26        self.major >= major
27    }
28}
29
30impl Default for PgVersion {
31    fn default() -> Self {
32        Self { major: 16 }
33    }
34}
35
36pub struct RuleContext<'a> {
37    pub pg_version: PgVersion,
38    pub catalog: Option<&'a CatalogInfo>,
39    pub transaction_baseline: Option<&'a TransactionBaseline>,
40}
41
42pub fn analyse(source: &str, ctx: &RuleContext) -> Result<Vec<Finding>> {
43    let parsed = parse::parse(source)?;
44    let mut findings = Vec::new();
45
46    for stmt in &parsed.protobuf.stmts {
47        let Some(ref wrapper) = stmt.stmt else { continue };
48        let Some(ref n) = wrapper.node else { continue };
49        let stmt_sql = parse::extract_statement_sql(source, stmt);
50
51        match n {
52            node::Node::AlterTableStmt(alter) => {
53                findings.extend(alter_table::analyse_alter_table(alter, &stmt_sql, ctx));
54            }
55            node::Node::IndexStmt(index) => {
56                findings.extend(create_index::analyse_index_stmt(index, &stmt_sql, ctx));
57            }
58            node::Node::DropStmt(drop_stmt) => {
59                findings.extend(drop::analyse_drop(drop_stmt, &stmt_sql, ctx));
60            }
61            node::Node::RenameStmt(rename) => {
62                findings.extend(rename::analyse_rename(rename, &stmt_sql, ctx));
63            }
64            node::Node::TruncateStmt(truncate) => {
65                findings.extend(maintenance::analyse_truncate(truncate, &stmt_sql, ctx));
66            }
67            node::Node::VacuumStmt(vacuum) => {
68                findings.extend(maintenance::analyse_vacuum(vacuum, &stmt_sql, ctx));
69            }
70            node::Node::ReindexStmt(reindex) => {
71                findings.extend(maintenance::analyse_reindex(reindex, &stmt_sql, ctx));
72            }
73            node::Node::RefreshMatViewStmt(refresh) => {
74                findings.extend(maintenance::analyse_refresh_matview(refresh, &stmt_sql, ctx));
75            }
76            node::Node::SelectStmt(_)
77            | node::Node::InsertStmt(_)
78            | node::Node::UpdateStmt(_)
79            | node::Node::DeleteStmt(_) => {
80                eprintln!(
81                    "warning: DML statement ignored (pg-blast-radius analyses DDL only): {}",
82                    stmt_sql.lines().next().unwrap_or("").trim()
83                );
84            }
85            _ => {}
86        }
87    }
88
89    Ok(findings)
90}
91
92pub fn extract_string_value(n: &pg_query::protobuf::Node) -> Option<&str> {
93    match n.node.as_ref()? {
94        node::Node::String(s) => Some(&s.sval),
95        _ => None,
96    }
97}
98
99pub fn type_name_to_string(tn: &pg_query::protobuf::TypeName) -> String {
100    tn.names
101        .iter()
102        .filter_map(extract_string_value)
103        .filter(|s| *s != "pg_catalog")
104        .collect::<Vec<_>>()
105        .join(".")
106}