pg_blast_radius/rules/
mod.rs1pub 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}