safe_migrate/rules/
transactions.rs1use 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}