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 });
49 }
50 Mutation::DropIndex(d) if d.concurrently => {
51 violations.push(Violation {
52 source_range: None,
53 rule_id: self.id(),
54 operation_kind: OperationKind::DropIndex,
55 object_kind: ObjectKind::Index,
56 object_name: d.id.to_string(),
57 tier: self.default_tier(),
58 reason: format!(
59 "DROP INDEX CONCURRENTLY on {} inside a transaction block",
60 d.id
61 ),
62 recipe: self.recipe(),
63 dedup_key: None,
64 sql: None,
65 });
66 }
67 _ => {}
68 }
69 }
70
71 violations
72 }
73}
74
75pub struct AlterTypeAddValueRule;
76
77impl Rule for AlterTypeAddValueRule {
78 fn id(&self) -> &'static str {
79 "alter-type-add-value-txn"
80 }
81 fn default_tier(&self) -> ViolationTier {
82 ViolationTier::Tier1
83 }
84 fn recipe(&self) -> &'static str {
85 "ALTER TYPE ... ADD VALUE cannot be executed inside a transaction block in PostgreSQL."
86 }
87
88 fn evaluate(
89 &self,
90 mutation: &Mutation,
91 _result: &MutationResult,
92 _pre_state: &crate::analysis::state::PreState,
93 state: &AnalysisState,
94 _config: &Config,
95 _cascade: Option<&CascadeResult>,
96 ) -> Vec<Violation> {
97 if !state.local.transactions.is_empty()
98 && let Mutation::AlterType(alter) = mutation
99 {
100 return vec![Violation {
101 source_range: None,
102 rule_id: self.id(),
103 operation_kind: OperationKind::AlterType,
104 object_kind: ObjectKind::Type,
105 object_name: alter.id.to_string(),
106 tier: self.default_tier(),
107 reason: format!("ALTER TYPE {} ADD VALUE inside transaction", alter.id),
108 recipe: self.recipe(),
109 dedup_key: None,
110 sql: None,
111 }];
112 }
113 vec![]
114 }
115}
116
117pub struct VacuumFullRule;
118
119impl Rule for VacuumFullRule {
120 fn id(&self) -> &'static str {
121 "vacuum-full"
122 }
123 fn default_tier(&self) -> ViolationTier {
124 ViolationTier::Tier1
125 }
126 fn recipe(&self) -> &'static str {
127 "VACUUM FULL rewrites the entire table and requires an ACCESS EXCLUSIVE lock. Run this manually outside of migration pipelines."
128 }
129
130 fn evaluate(
131 &self,
132 mutation: &Mutation,
133 _result: &MutationResult,
134 _pre_state: &crate::analysis::state::PreState,
135 _state: &AnalysisState,
136 _config: &Config,
137 _cascade: Option<&CascadeResult>,
138 ) -> Vec<Violation> {
139 if let Mutation::Vacuum {
140 is_full: true,
141 table_id,
142 } = mutation
143 {
144 let object_name = table_id
145 .as_ref()
146 .map(|id| id.to_string())
147 .unwrap_or_else(|| "<all tables>".to_string());
148 return vec![Violation {
149 source_range: None,
150 rule_id: self.id(),
151 operation_kind: OperationKind::VacuumFull,
152 object_kind: ObjectKind::Table,
153 object_name,
154 tier: self.default_tier(),
155 reason: "VACUUM FULL requires an ACCESS EXCLUSIVE lock".to_string(),
156 recipe: self.recipe(),
157 dedup_key: None,
158 sql: None,
159 }];
160 }
161 vec![]
162 }
163}