Skip to main content

safe_migrate/rules/
transactions.rs

1// FILE: src/rules/transactions.rs
2use crate::analysis::mutations::Mutation;
3use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
4use crate::engine::config::Config;
5use crate::report::violations::{ObjectKind, OperationKind, Violation, ViolationTier};
6use crate::rules::Rule;
7
8pub struct ConcurrentInsideTransactionRule;
9
10impl Rule for ConcurrentInsideTransactionRule {
11    fn id(&self) -> &'static str {
12        "concurrent-in-transaction"
13    }
14    fn default_tier(&self) -> ViolationTier {
15        ViolationTier::Tier1
16    }
17    fn recipe(&self) -> &'static str {
18        "PostgreSQL does not allow CREATE/DROP INDEX CONCURRENTLY inside a transaction block (BEGIN/COMMIT)."
19    }
20
21    fn evaluate(
22        &self,
23        mutation: &Mutation,
24        result: &MutationResult,
25        _pre_state: &crate::analysis::state::PreState,
26        state: &AnalysisState,
27        _config: &Config,
28        _cascade: Option<&CascadeResult>,
29    ) -> Vec<Violation> {
30        if *result == MutationResult::Skipped {
31            return vec![];
32        }
33        let mut violations = Vec::new();
34
35        if !state.local.transactions.is_empty() {
36            match mutation {
37                Mutation::CreateIndex(c) if c.concurrently => {
38                    violations.push(Violation { source_range: None,
39                        rule_id: self.id(),
40                        operation_kind: OperationKind::CreateIndex,
41                        object_kind: ObjectKind::Index,
42                        object_name: c.id.to_string(),
43                        tier: self.default_tier(),
44                        reason: format!("CREATE INDEX CONCURRENTLY on {} inside a transaction block", c.table),
45                        recipe: "Move CONCURRENTLY index creation outside of explicit transaction blocks.",
46                        dedup_key: Some(format!("{}_{}", self.id(), c.id)),
47                                    sql: None,
48                                    fk_dependency_related: false,
49                    });
50                }
51                Mutation::DropIndex(d) if d.concurrently => {
52                    violations.push(Violation {
53                        source_range: None,
54                        rule_id: self.id(),
55                        operation_kind: OperationKind::DropIndex,
56                        object_kind: ObjectKind::Index,
57                        object_name: d.id.to_string(),
58                        tier: self.default_tier(),
59                        reason: format!(
60                            "DROP INDEX CONCURRENTLY on {} inside a transaction block",
61                            d.id
62                        ),
63                        recipe: self.recipe(),
64                        dedup_key: None,
65                        sql: None,
66                        fk_dependency_related: false,
67                    });
68                }
69                _ => {}
70            }
71        }
72
73        violations
74    }
75}
76
77pub struct AlterTypeAddValueRule;
78
79impl Rule for AlterTypeAddValueRule {
80    fn id(&self) -> &'static str {
81        "alter-type-add-value-txn"
82    }
83    fn default_tier(&self) -> ViolationTier {
84        ViolationTier::Tier1
85    }
86    fn recipe(&self) -> &'static str {
87        "ALTER TYPE ... ADD VALUE cannot be executed inside a transaction block in PostgreSQL."
88    }
89
90    fn evaluate(
91        &self,
92        mutation: &Mutation,
93        _result: &MutationResult,
94        _pre_state: &crate::analysis::state::PreState,
95        state: &AnalysisState,
96        _config: &Config,
97        _cascade: Option<&CascadeResult>,
98    ) -> Vec<Violation> {
99        if !state.local.transactions.is_empty()
100            && let Mutation::AlterType(alter) = mutation
101        {
102            return vec![Violation {
103                source_range: None,
104                rule_id: self.id(),
105                operation_kind: OperationKind::AlterType,
106                object_kind: ObjectKind::Type,
107                object_name: alter.id.to_string(),
108                tier: self.default_tier(),
109                reason: format!("ALTER TYPE {} ADD VALUE inside transaction", alter.id),
110                recipe: self.recipe(),
111                dedup_key: None,
112                sql: None,
113                fk_dependency_related: false,
114            }];
115        }
116        vec![]
117    }
118}
119
120pub struct VacuumFullRule;
121
122impl Rule for VacuumFullRule {
123    fn id(&self) -> &'static str {
124        "vacuum-full"
125    }
126    fn default_tier(&self) -> ViolationTier {
127        ViolationTier::Tier1
128    }
129    fn recipe(&self) -> &'static str {
130        "VACUUM FULL rewrites the entire table and requires an ACCESS EXCLUSIVE lock. Run this manually outside of migration pipelines."
131    }
132
133    fn evaluate(
134        &self,
135        mutation: &Mutation,
136        _result: &MutationResult,
137        _pre_state: &crate::analysis::state::PreState,
138        _state: &AnalysisState,
139        _config: &Config,
140        _cascade: Option<&CascadeResult>,
141    ) -> Vec<Violation> {
142        if let Mutation::Vacuum {
143            is_full: true,
144            table_id,
145        } = mutation
146        {
147            let object_name = table_id
148                .as_ref()
149                .map(|id| id.to_string())
150                .unwrap_or_else(|| "<all tables>".to_string());
151            return vec![Violation {
152                source_range: None,
153                rule_id: self.id(),
154                operation_kind: OperationKind::VacuumFull,
155                object_kind: ObjectKind::Table,
156                object_name,
157                tier: self.default_tier(),
158                reason: "VACUUM FULL requires an ACCESS EXCLUSIVE lock".to_string(),
159                recipe: self.recipe(),
160                dedup_key: None,
161                sql: None,
162                fk_dependency_related: false,
163            }];
164        }
165        vec![]
166    }
167}