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                });
52            };
53
54        match mutation {
55            // Creation Guards
56            Mutation::CreateTable(c) if !c.if_not_exists => {
57                add_violation(
58                    OperationKind::CreateTable,
59                    ObjectKind::Table,
60                    c.id.to_string(),
61                    format!("CREATE TABLE {} without IF NOT EXISTS", c.id),
62                );
63            }
64            Mutation::CreateIndex(c) if !c.if_not_exists => {
65                add_violation(
66                    OperationKind::CreateIndex,
67                    ObjectKind::Index,
68                    c.id.to_string(),
69                    format!("CREATE INDEX {} without IF NOT EXISTS", c.id),
70                );
71            }
72            Mutation::CreateSequence(c) if !c.if_not_exists => {
73                add_violation(
74                    OperationKind::Other("create_sequence".to_string()),
75                    ObjectKind::Sequence,
76                    c.id.to_string(),
77                    format!("CREATE SEQUENCE {} without IF NOT EXISTS", c.id),
78                );
79            }
80
81            // Drop Guards (Singular targets)
82            Mutation::DropTable(d) if !d.if_exists => {
83                add_violation(
84                    OperationKind::DropTable,
85                    ObjectKind::Table,
86                    d.id.to_string(),
87                    format!("DROP TABLE {} without IF EXISTS", d.id),
88                );
89            }
90            Mutation::DropIndex(d) if !d.if_exists => {
91                add_violation(
92                    OperationKind::DropIndex,
93                    ObjectKind::Index,
94                    d.id.to_string(),
95                    format!("DROP INDEX {} without IF EXISTS", d.id),
96                );
97            }
98            Mutation::DropPolicy(d) if !d.if_exists => {
99                add_violation(
100                    OperationKind::DropPolicy,
101                    ObjectKind::Policy,
102                    format!("{} on {}", d.name, d.table),
103                    format!("DROP POLICY {} on {} without IF EXISTS", d.name, d.table),
104                );
105            }
106            Mutation::DropTrigger(d) if !d.if_exists => {
107                add_violation(
108                    OperationKind::DropTrigger,
109                    ObjectKind::Trigger,
110                    format!("{} on {}", d.name, d.table),
111                    format!("DROP TRIGGER {} on {} without IF EXISTS", d.name, d.table),
112                );
113            }
114
115            // Drop Guards (Vector targets)
116            Mutation::DropSequence(d) if !d.if_exists => {
117                for id in &d.ids {
118                    add_violation(
119                        OperationKind::DropSequence,
120                        ObjectKind::Sequence,
121                        id.to_string(),
122                        format!("DROP SEQUENCE {} without IF EXISTS", id),
123                    );
124                }
125            }
126            Mutation::DropView(d) if !d.if_exists => {
127                for id in &d.ids {
128                    add_violation(
129                        OperationKind::DropView,
130                        ObjectKind::View,
131                        id.to_string(),
132                        format!("DROP VIEW {} without IF EXISTS", id),
133                    );
134                }
135            }
136            Mutation::DropMaterializedView(d) if !d.if_exists => {
137                for id in &d.ids {
138                    add_violation(
139                        OperationKind::DropMaterializedView,
140                        ObjectKind::MaterializedView,
141                        id.to_string(),
142                        format!("DROP MATERIALIZED VIEW {} without IF EXISTS", id),
143                    );
144                }
145            }
146            Mutation::DropDomain(d) if !d.if_exists => {
147                for id in &d.ids {
148                    add_violation(
149                        OperationKind::DropDomain,
150                        ObjectKind::Domain,
151                        id.to_string(),
152                        format!("DROP DOMAIN {} without IF EXISTS", id),
153                    );
154                }
155            }
156
157            // Alter Table Action Guards
158            Mutation::AlterTable(a) => match &a.action {
159                AlterTableActionMutation::AddColumn {
160                    name,
161                    if_not_exists,
162                    ..
163                } if !*if_not_exists => {
164                    add_violation(
165                        OperationKind::AddColumn,
166                        ObjectKind::Table,
167                        format!("{}.{}", a.id, name),
168                        format!(
169                            "ALTER TABLE {} ADD COLUMN {} without IF NOT EXISTS",
170                            a.id, name
171                        ),
172                    );
173                }
174                AlterTableActionMutation::DropColumn {
175                    name, if_exists, ..
176                } if !*if_exists => {
177                    add_violation(
178                        OperationKind::DropColumn,
179                        ObjectKind::Table,
180                        format!("{}.{}", a.id, name),
181                        format!(
182                            "ALTER TABLE {} DROP COLUMN {} without IF EXISTS",
183                            a.id, name
184                        ),
185                    );
186                }
187                _ => {}
188            },
189            _ => {}
190        }
191
192        violations
193    }
194}