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::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        // ARCHITECTURAL NOTE:
35        // We INTENTIONALLY ignore `MutationResult::Skipped` here.
36        // This rule is a syntactic policy enforcer. It flags missing IF EXISTS / IF NOT EXISTS
37        // clauses regardless of whether the object actually existed during this specific simulator run.
38
39        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            // Creation Guards
53            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            // Drop Guards (Singular targets)
64            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            // Drop Guards (Vector targets)
84            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            // Alter Table Action Guards
106            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}