Skip to main content

safe_migrate/rules/
idempotency.rs

1use crate::analysis::mutations::{AlterTableActionMutation, Mutation};
2use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
3use crate::engine::config::Config;
4use crate::report::violations::{ObjectKind, OperationKind, Violation, ViolationTier};
5use crate::rules::Rule;
6
7pub struct IdempotencyRule;
8
9impl Rule for IdempotencyRule {
10    fn id(&self) -> &'static str {
11        "missing-idempotency"
12    }
13    fn default_tier(&self) -> ViolationTier {
14        ViolationTier::Tier3
15    }
16    fn recipe(&self) -> &'static str {
17        "Use IF EXISTS or IF NOT EXISTS to prevent migration failures on partial re-runs."
18    }
19
20    fn evaluate(
21        &self,
22        mutation: &Mutation,
23        _result: &MutationResult,
24        _pre_state: &crate::analysis::state::PreState,
25        _state: &AnalysisState,
26        _config: &Config,
27        _cascade: Option<&CascadeResult>,
28    ) -> Vec<Violation> {
29        // Idempotency is syntactic, so a skipped mutation still needs an explicit guard.
30
31        let mut violations = Vec::new();
32
33        let mut add_violation =
34            |op: OperationKind, obj: ObjectKind, name: String, reason: String| {
35                violations.push(Violation {
36                    source_range: None,
37                    rule_id: self.id(),
38                    operation_kind: op,
39                    object_kind: obj,
40                    object_name: name,
41                    tier: self.default_tier(),
42                    reason,
43                    recipe: self.recipe(),
44                    dedup_key: None,
45                    sql: None,
46                    fk_dependency_related: false,
47                });
48            };
49
50        match mutation {
51            // Creation Guards
52            Mutation::CreateTable(c) if !c.if_not_exists => {
53                add_violation(
54                    OperationKind::CreateTable,
55                    ObjectKind::Table,
56                    c.id.to_string(),
57                    format!("CREATE TABLE {} without IF NOT EXISTS", c.id),
58                );
59            }
60            Mutation::CreateView(c) if !c.or_replace => {
61                add_violation(
62                    OperationKind::CreateView,
63                    ObjectKind::View,
64                    c.id.to_string(),
65                    format!("CREATE VIEW {} without OR REPLACE", c.id),
66                );
67            }
68            Mutation::CreateSchema(c) if !c.if_not_exists => {
69                add_violation(
70                    OperationKind::CreateSchema,
71                    ObjectKind::Schema,
72                    c.name.clone(),
73                    format!("CREATE SCHEMA {} without IF NOT EXISTS", c.name),
74                );
75            }
76            Mutation::CreateIndex(c) if !c.if_not_exists => {
77                add_violation(
78                    OperationKind::CreateIndex,
79                    ObjectKind::Index,
80                    c.id.to_string(),
81                    format!("CREATE INDEX {} without IF NOT EXISTS", c.id),
82                );
83            }
84            Mutation::CreateSequence(c) if !c.if_not_exists => {
85                add_violation(
86                    OperationKind::CreateSequence,
87                    ObjectKind::Sequence,
88                    c.id.to_string(),
89                    format!("CREATE SEQUENCE {} without IF NOT EXISTS", c.id),
90                );
91            }
92
93            // Drop Guards (Singular targets)
94            Mutation::DropTable(d) if !d.if_exists => {
95                add_violation(
96                    OperationKind::DropTable,
97                    ObjectKind::Table,
98                    d.id.to_string(),
99                    format!("DROP TABLE {} without IF EXISTS", d.id),
100                );
101            }
102            Mutation::DropSchema(d) if !d.if_exists => {
103                for name in &d.names {
104                    add_violation(
105                        OperationKind::DropSchema,
106                        ObjectKind::Schema,
107                        name.clone(),
108                        format!("DROP SCHEMA {} without IF EXISTS", name),
109                    );
110                }
111            }
112            Mutation::DropIndex(d) if !d.if_exists => {
113                add_violation(
114                    OperationKind::DropIndex,
115                    ObjectKind::Index,
116                    d.id.to_string(),
117                    format!("DROP INDEX {} without IF EXISTS", d.id),
118                );
119            }
120            Mutation::DropPolicy(d) if !d.if_exists => {
121                add_violation(
122                    OperationKind::DropPolicy,
123                    ObjectKind::Policy,
124                    format!("{} on {}", d.name, d.table),
125                    format!("DROP POLICY {} on {} without IF EXISTS", d.name, d.table),
126                );
127            }
128            Mutation::DropTrigger(d) if !d.if_exists => {
129                add_violation(
130                    OperationKind::DropTrigger,
131                    ObjectKind::Trigger,
132                    format!("{} on {}", d.name, d.table),
133                    format!("DROP TRIGGER {} on {} without IF EXISTS", d.name, d.table),
134                );
135            }
136
137            // Drop Guards (Vector targets)
138            Mutation::DropSequence(d) if !d.if_exists => {
139                for id in &d.ids {
140                    add_violation(
141                        OperationKind::DropSequence,
142                        ObjectKind::Sequence,
143                        id.to_string(),
144                        format!("DROP SEQUENCE {} without IF EXISTS", id),
145                    );
146                }
147            }
148            Mutation::DropView(d) if !d.if_exists => {
149                for id in &d.ids {
150                    add_violation(
151                        OperationKind::DropView,
152                        ObjectKind::View,
153                        id.to_string(),
154                        format!("DROP VIEW {} without IF EXISTS", id),
155                    );
156                }
157            }
158            Mutation::DropMaterializedView(d) if !d.if_exists => {
159                for id in &d.ids {
160                    add_violation(
161                        OperationKind::DropMaterializedView,
162                        ObjectKind::MaterializedView,
163                        id.to_string(),
164                        format!("DROP MATERIALIZED VIEW {} without IF EXISTS", id),
165                    );
166                }
167            }
168            Mutation::DropDomain(d) if !d.if_exists => {
169                for id in &d.ids {
170                    add_violation(
171                        OperationKind::DropDomain,
172                        ObjectKind::Domain,
173                        id.to_string(),
174                        format!("DROP DOMAIN {} without IF EXISTS", id),
175                    );
176                }
177            }
178
179            // Alter Table Action Guards
180            Mutation::AlterTable(a) => match &a.action {
181                AlterTableActionMutation::AddColumn {
182                    name,
183                    if_not_exists,
184                    ..
185                } if !*if_not_exists => {
186                    add_violation(
187                        OperationKind::AddColumn,
188                        ObjectKind::Table,
189                        format!("{}.{}", a.id, name),
190                        format!(
191                            "ALTER TABLE {} ADD COLUMN {} without IF NOT EXISTS",
192                            a.id, name
193                        ),
194                    );
195                }
196                AlterTableActionMutation::DropColumn {
197                    name, if_exists, ..
198                } if !*if_exists => {
199                    add_violation(
200                        OperationKind::DropColumn,
201                        ObjectKind::Table,
202                        format!("{}.{}", a.id, name),
203                        format!(
204                            "ALTER TABLE {} DROP COLUMN {} without IF EXISTS",
205                            a.id, name
206                        ),
207                    );
208                }
209                _ => {}
210            },
211            _ => {}
212        }
213
214        violations
215    }
216}