1use 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 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 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 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 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 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}