Skip to main content

safe_migrate/rules/
idempotency.rs

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