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 });
52 };
53
54 match mutation {
55 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 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 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 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}