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
94            Mutation::DropTable(d) if !d.if_exists => {
95                for id in &d.ids {
96                    add_violation(
97                        OperationKind::DropTable,
98                        ObjectKind::Table,
99                        id.to_string(),
100                        format!("DROP TABLE {} without IF EXISTS", id),
101                    );
102                }
103            }
104            Mutation::DropSchema(d) if !d.if_exists => {
105                for name in &d.names {
106                    add_violation(
107                        OperationKind::DropSchema,
108                        ObjectKind::Schema,
109                        name.clone(),
110                        format!("DROP SCHEMA {} without IF EXISTS", name),
111                    );
112                }
113            }
114            Mutation::DropIndex(d) if !d.if_exists => {
115                for id in &d.ids {
116                    add_violation(
117                        OperationKind::DropIndex,
118                        ObjectKind::Index,
119                        id.to_string(),
120                        format!("DROP INDEX {} without IF EXISTS", id),
121                    );
122                }
123            }
124            Mutation::DropPolicy(d) if !d.if_exists => {
125                add_violation(
126                    OperationKind::DropPolicy,
127                    ObjectKind::Policy,
128                    format!("{} on {}", d.name, d.table),
129                    format!("DROP POLICY {} on {} without IF EXISTS", d.name, d.table),
130                );
131            }
132            Mutation::DropTrigger(d) if !d.if_exists => {
133                add_violation(
134                    OperationKind::DropTrigger,
135                    ObjectKind::Trigger,
136                    format!("{} on {}", d.name, d.table),
137                    format!("DROP TRIGGER {} on {} without IF EXISTS", d.name, d.table),
138                );
139            }
140
141            // Drop Guards (Vector targets)
142            Mutation::DropSequence(d) if !d.if_exists => {
143                for id in &d.ids {
144                    add_violation(
145                        OperationKind::DropSequence,
146                        ObjectKind::Sequence,
147                        id.to_string(),
148                        format!("DROP SEQUENCE {} without IF EXISTS", id),
149                    );
150                }
151            }
152            Mutation::DropView(d) if !d.if_exists => {
153                for id in &d.ids {
154                    add_violation(
155                        OperationKind::DropView,
156                        ObjectKind::View,
157                        id.to_string(),
158                        format!("DROP VIEW {} without IF EXISTS", id),
159                    );
160                }
161            }
162            Mutation::DropMaterializedView(d) if !d.if_exists => {
163                for id in &d.ids {
164                    add_violation(
165                        OperationKind::DropMaterializedView,
166                        ObjectKind::MaterializedView,
167                        id.to_string(),
168                        format!("DROP MATERIALIZED VIEW {} without IF EXISTS", id),
169                    );
170                }
171            }
172            Mutation::DropDomain(d) if !d.if_exists => {
173                for id in &d.ids {
174                    add_violation(
175                        OperationKind::DropDomain,
176                        ObjectKind::Domain,
177                        id.to_string(),
178                        format!("DROP DOMAIN {} without IF EXISTS", id),
179                    );
180                }
181            }
182
183            // Alter Table Action Guards
184            Mutation::AlterTable(a) => match &a.action {
185                AlterTableActionMutation::AddColumn {
186                    name,
187                    if_not_exists,
188                    ..
189                } if !*if_not_exists => {
190                    add_violation(
191                        OperationKind::AddColumn,
192                        ObjectKind::Table,
193                        format!("{}.{}", a.id, name),
194                        format!(
195                            "ALTER TABLE {} ADD COLUMN {} without IF NOT EXISTS",
196                            a.id, name
197                        ),
198                    );
199                }
200                AlterTableActionMutation::DropColumn {
201                    name, if_exists, ..
202                } if !*if_exists => {
203                    add_violation(
204                        OperationKind::DropColumn,
205                        ObjectKind::Table,
206                        format!("{}.{}", a.id, name),
207                        format!(
208                            "ALTER TABLE {} DROP COLUMN {} without IF EXISTS",
209                            a.id, name
210                        ),
211                    );
212                }
213                _ => {}
214            },
215            _ => {}
216        }
217
218        violations
219    }
220}