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 add_violation(
96 OperationKind::DropTable,
97 ObjectKind::Table,
98 d.id.to_string(),
99 format!("DROP TABLE {} without IF EXISTS", d.id),
100 );
101 }
102 Mutation::DropSchema(d) if !d.if_exists => {
103 for name in &d.names {
104 add_violation(
105 OperationKind::DropSchema,
106 ObjectKind::Schema,
107 name.clone(),
108 format!("DROP SCHEMA {} without IF EXISTS", name),
109 );
110 }
111 }
112 Mutation::DropIndex(d) if !d.if_exists => {
113 add_violation(
114 OperationKind::DropIndex,
115 ObjectKind::Index,
116 d.id.to_string(),
117 format!("DROP INDEX {} without IF EXISTS", d.id),
118 );
119 }
120 Mutation::DropPolicy(d) if !d.if_exists => {
121 add_violation(
122 OperationKind::DropPolicy,
123 ObjectKind::Policy,
124 format!("{} on {}", d.name, d.table),
125 format!("DROP POLICY {} on {} without IF EXISTS", d.name, d.table),
126 );
127 }
128 Mutation::DropTrigger(d) if !d.if_exists => {
129 add_violation(
130 OperationKind::DropTrigger,
131 ObjectKind::Trigger,
132 format!("{} on {}", d.name, d.table),
133 format!("DROP TRIGGER {} on {} without IF EXISTS", d.name, d.table),
134 );
135 }
136
137 Mutation::DropSequence(d) if !d.if_exists => {
139 for id in &d.ids {
140 add_violation(
141 OperationKind::DropSequence,
142 ObjectKind::Sequence,
143 id.to_string(),
144 format!("DROP SEQUENCE {} without IF EXISTS", id),
145 );
146 }
147 }
148 Mutation::DropView(d) if !d.if_exists => {
149 for id in &d.ids {
150 add_violation(
151 OperationKind::DropView,
152 ObjectKind::View,
153 id.to_string(),
154 format!("DROP VIEW {} without IF EXISTS", id),
155 );
156 }
157 }
158 Mutation::DropMaterializedView(d) if !d.if_exists => {
159 for id in &d.ids {
160 add_violation(
161 OperationKind::DropMaterializedView,
162 ObjectKind::MaterializedView,
163 id.to_string(),
164 format!("DROP MATERIALIZED VIEW {} without IF EXISTS", id),
165 );
166 }
167 }
168 Mutation::DropDomain(d) if !d.if_exists => {
169 for id in &d.ids {
170 add_violation(
171 OperationKind::DropDomain,
172 ObjectKind::Domain,
173 id.to_string(),
174 format!("DROP DOMAIN {} without IF EXISTS", id),
175 );
176 }
177 }
178
179 Mutation::AlterTable(a) => match &a.action {
181 AlterTableActionMutation::AddColumn {
182 name,
183 if_not_exists,
184 ..
185 } if !*if_not_exists => {
186 add_violation(
187 OperationKind::AddColumn,
188 ObjectKind::Table,
189 format!("{}.{}", a.id, name),
190 format!(
191 "ALTER TABLE {} ADD COLUMN {} without IF NOT EXISTS",
192 a.id, name
193 ),
194 );
195 }
196 AlterTableActionMutation::DropColumn {
197 name, if_exists, ..
198 } if !*if_exists => {
199 add_violation(
200 OperationKind::DropColumn,
201 ObjectKind::Table,
202 format!("{}.{}", a.id, name),
203 format!(
204 "ALTER TABLE {} DROP COLUMN {} without IF EXISTS",
205 a.id, name
206 ),
207 );
208 }
209 _ => {}
210 },
211 _ => {}
212 }
213
214 violations
215 }
216}