safe_migrate/rules/
transactions.rs1use crate::analysis::mutations::Mutation;
3use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
4use crate::ast::identifiers::ObjectId;
5use crate::engine::config::Config;
6use crate::model::relation::RelationState;
7use crate::report::violations::{Violation, ViolationTier};
8use crate::rules::Rule;
9use std::collections::HashMap;
10
11pub struct ConcurrentInsideTransactionRule;
12
13impl Rule for ConcurrentInsideTransactionRule {
14 fn id(&self) -> &'static str {
15 "concurrent-in-transaction"
16 }
17 fn default_tier(&self) -> ViolationTier {
18 ViolationTier::Tier1
19 }
20 fn recipe(&self) -> &'static str {
21 "PostgreSQL does not allow CREATE/DROP INDEX CONCURRENTLY inside a transaction block (BEGIN/COMMIT)."
22 }
23
24 fn evaluate(
25 &self,
26 mutation: &Mutation,
27 _result: &MutationResult,
28 _pre_relations: &HashMap<ObjectId, RelationState>,
29 state: &AnalysisState,
30 _config: &Config,
31 _cascade: Option<&CascadeResult>,
32 ) -> Vec<Violation> {
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 {
39 rule_id: self.id(),
40 title: format!("CREATE INDEX CONCURRENTLY on {} inside a transaction block", c.table),
41 tier: self.default_tier(),
42 recipe: "Move CONCURRENTLY index creation outside of explicit transaction blocks.",
43 dedup_key: Some(format!("{}_{}", self.id(), c.id)),
44 });
45 }
46 Mutation::DropIndex(d) if d.concurrently => {
47 violations.push(Violation {
48 rule_id: self.id(),
49 title: format!(
50 "DROP INDEX CONCURRENTLY on {} inside a transaction block",
51 d.id
52 ),
53 tier: self.default_tier(),
54 recipe: self.recipe(),
55 dedup_key: None,
56 });
57 }
58 _ => {}
59 }
60 }
61
62 violations
63 }
64}
65
66pub struct VacuumFullRule;
67
68impl Rule for VacuumFullRule {
69 fn id(&self) -> &'static str {
70 "vacuum-full"
71 }
72 fn default_tier(&self) -> ViolationTier {
73 ViolationTier::Tier1
74 }
75 fn recipe(&self) -> &'static str {
76 "VACUUM FULL rewrites the entire table and requires an ACCESS EXCLUSIVE lock. Run this manually outside of migration pipelines."
77 }
78
79 fn evaluate(
80 &self,
81 mutation: &Mutation,
82 _result: &MutationResult,
83 _pre_relations: &HashMap<ObjectId, RelationState>,
84 _state: &AnalysisState,
85 _config: &Config,
86 _cascade: Option<&CascadeResult>,
87 ) -> Vec<Violation> {
88 if let Mutation::Vacuum { is_full: true } = mutation {
89 return vec![Violation {
90 rule_id: self.id(),
91 title: "VACUUM FULL requires an ACCESS EXCLUSIVE lock".to_string(),
92 tier: self.default_tier(),
93 recipe: self.recipe(),
94 dedup_key: None,
95 }];
96 }
97 vec![]
98 }
99}