safe_migrate/rules/
idempotency.rs1use crate::analysis::mutations::{AlterTableActionMutation, Mutation};
4use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
5use crate::ast::identifiers::ObjectId;
6use crate::engine::config::Config;
7use crate::model::relation::RelationState;
8use crate::report::violations::{Violation, ViolationTier};
9use crate::rules::Rule;
10use std::collections::HashMap;
11
12pub struct IdempotencyRule;
13
14impl Rule for IdempotencyRule {
15 fn id(&self) -> &'static str {
16 "missing-idempotency"
17 }
18 fn default_tier(&self) -> ViolationTier {
19 ViolationTier::Tier3
20 }
21 fn recipe(&self) -> &'static str {
22 "Use IF EXISTS or IF NOT EXISTS to prevent migration failures on partial re-runs."
23 }
24
25 fn evaluate(
26 &self,
27 mutation: &Mutation,
28 _result: &MutationResult,
29 _pre_relations: &HashMap<ObjectId, RelationState>,
30 _state: &AnalysisState,
31 _config: &Config,
32 _cascade: Option<&CascadeResult>,
33 ) -> Vec<Violation> {
34 let mut violations = Vec::new();
40
41 let mut add_violation = |title: String| {
42 violations.push(Violation {
43 rule_id: self.id(),
44 title,
45 tier: self.default_tier(),
46 recipe: self.recipe(),
47 dedup_key: None,
48 });
49 };
50
51 match mutation {
52 Mutation::CreateTable(c) if !c.if_not_exists => {
54 add_violation(format!("CREATE TABLE {} without IF NOT EXISTS", c.id));
55 }
56 Mutation::CreateIndex(c) if !c.if_not_exists => {
57 add_violation(format!("CREATE INDEX {} without IF NOT EXISTS", c.id));
58 }
59 Mutation::CreateSequence(c) if !c.if_not_exists => {
60 add_violation(format!("CREATE SEQUENCE {} without IF NOT EXISTS", c.id));
61 }
62
63 Mutation::DropTable(d) if !d.if_exists => {
65 add_violation(format!("DROP TABLE {} without IF EXISTS", d.id));
66 }
67 Mutation::DropIndex(d) if !d.if_exists => {
68 add_violation(format!("DROP INDEX {} without IF EXISTS", d.id));
69 }
70 Mutation::DropPolicy(d) if !d.if_exists => {
71 add_violation(format!(
72 "DROP POLICY {} on {} without IF EXISTS",
73 d.name, d.table
74 ));
75 }
76 Mutation::DropTrigger(d) if !d.if_exists => {
77 add_violation(format!(
78 "DROP TRIGGER {} on {} without IF EXISTS",
79 d.name, d.table
80 ));
81 }
82
83 Mutation::DropSequence(d) if !d.if_exists => {
85 for id in &d.ids {
86 add_violation(format!("DROP SEQUENCE {} without IF EXISTS", id));
87 }
88 }
89 Mutation::DropView(d) if !d.if_exists => {
90 for id in &d.ids {
91 add_violation(format!("DROP VIEW {} without IF EXISTS", id));
92 }
93 }
94 Mutation::DropMaterializedView(d) if !d.if_exists => {
95 for id in &d.ids {
96 add_violation(format!("DROP MATERIALIZED VIEW {} without IF EXISTS", id));
97 }
98 }
99 Mutation::DropDomain(d) if !d.if_exists => {
100 for id in &d.ids {
101 add_violation(format!("DROP DOMAIN {} without IF EXISTS", id));
102 }
103 }
104
105 Mutation::AlterTable(a) => match &a.action {
107 AlterTableActionMutation::AddColumn {
108 name,
109 if_not_exists,
110 ..
111 } if !*if_not_exists => {
112 add_violation(format!(
113 "ALTER TABLE {} ADD COLUMN {} without IF NOT EXISTS",
114 a.id, name
115 ));
116 }
117 AlterTableActionMutation::DropColumn {
118 name, if_exists, ..
119 } if !*if_exists => {
120 add_violation(format!(
121 "ALTER TABLE {} DROP COLUMN {} without IF EXISTS",
122 a.id, name
123 ));
124 }
125 _ => {}
126 },
127 _ => {}
128 }
129
130 violations
131 }
132}