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