1use crate::analysis::mutations::Mutation;
2use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
3use crate::ast::identifiers::ObjectId;
4use crate::engine::config::Config;
5use crate::report::violations::{ObjectKind, OperationKind, Violation, ViolationTier};
6use crate::rules::Rule;
7
8pub struct DriftDetectionRule;
9
10impl Rule for DriftDetectionRule {
11 fn id(&self) -> &'static str {
12 "schema-drift"
13 }
14 fn default_tier(&self) -> ViolationTier {
15 ViolationTier::Tier1
16 }
17 fn recipe(&self) -> &'static str {
18 "This migration references a database object that does not exist in the production baseline. If this object exists in production, sync the cache with `safe-migrate sync`. If it does not, this migration may fail."
19 }
20
21 fn evaluate(
22 &self,
23 mutation: &Mutation,
24 _result: &MutationResult,
25 pre_state: &crate::analysis::state::PreState,
26 state: &AnalysisState,
27 _config: &Config,
28 _cascade_closure: Option<&CascadeResult>,
29 ) -> Vec<Violation> {
30 let mut violations = Vec::new();
31
32 match mutation {
33 Mutation::Opaque(crate::analysis::mutations::OpaqueMutation::UnresolvedReference {
34 object_kind,
35 object_name,
36 }) => {
37 violations.push(Violation { source_range: None,
38 rule_id: self.id(),
39 operation_kind: OperationKind::Other("unresolved_reference".to_string()),
40 object_kind: object_kind.clone(),
41 object_name: object_name.clone(),
42 tier: self.default_tier(),
43 reason: format!(
44 "Migration references {} \"{}\" which does not exist in the production baseline",
45 object_kind,
46 object_name
47 ),
48 recipe: self.recipe(),
49 dedup_key: None,
50 sql: None,
51 });
52 }
53 Mutation::DropTable(d) => {
54 if !pre_state.relations.contains_key(&d.id) {
55 violations.push(Violation { source_range: None,
56 rule_id: self.id(),
57 operation_kind: OperationKind::DropTable,
58 object_kind: ObjectKind::Table,
59 object_name: d.id.to_string(),
60 tier: self.default_tier(),
61 reason: format!(
62 "Migration DROPs table \"{}\" which does not exist in the production baseline",
63 d.id
64 ),
65 recipe: self.recipe(),
66 dedup_key: None,
67 sql: None,
68 });
69 }
70 }
71 Mutation::AlterTable(a) => {
72 if !pre_state.relations.contains_key(&a.id) {
73 violations.push(Violation { source_range: None,
74 rule_id: self.id(),
75 operation_kind: OperationKind::Other("alter_table".to_string()),
76 object_kind: ObjectKind::Table,
77 object_name: a.id.to_string(),
78 tier: self.default_tier(),
79 reason: format!(
80 "Migration ALTERs table \"{}\" which does not exist in the production baseline",
81 a.id
82 ),
83 recipe: self.recipe(),
84 dedup_key: None,
85 sql: None,
86 });
87 }
88 }
89 Mutation::DropView(d) => {
90 for id in &d.ids {
91 if !pre_state.relations.contains_key(id) {
92 violations.push(Violation { source_range: None,
93 rule_id: self.id(),
94 operation_kind: OperationKind::DropView,
95 object_kind: ObjectKind::View,
96 object_name: id.to_string(),
97 tier: self.default_tier(),
98 reason: format!(
99 "Migration DROPs view \"{}\" which does not exist in the production baseline",
100 id
101 ),
102 recipe: self.recipe(),
103 dedup_key: None,
104 sql: None,
105 });
106 }
107 }
108 }
109 Mutation::DropMaterializedView(d) => {
110 for id in &d.ids {
111 if !pre_state.relations.contains_key(id) {
112 violations.push(Violation { source_range: None,
113 rule_id: self.id(),
114 operation_kind: OperationKind::DropMaterializedView,
115 object_kind: ObjectKind::MaterializedView,
116 object_name: id.to_string(),
117 tier: self.default_tier(),
118 reason: format!(
119 "Migration DROPs materialized view \"{}\" which does not exist in the production baseline",
120 id
121 ),
122 recipe: self.recipe(),
123 dedup_key: None,
124 sql: None,
125 });
126 }
127 }
128 }
129 Mutation::DropSequence(d) => {
130 for id in &d.ids {
131 if !pre_state.sequences.contains_key(id) {
132 violations.push(Violation { source_range: None,
133 rule_id: self.id(),
134 operation_kind: OperationKind::DropSequence,
135 object_kind: ObjectKind::Sequence,
136 object_name: id.to_string(),
137 tier: self.default_tier(),
138 reason: format!(
139 "Migration DROPs sequence \"{}\" which does not exist in the production baseline",
140 id
141 ),
142 recipe: self.recipe(),
143 dedup_key: None,
144 sql: None,
145 });
146 }
147 }
148 }
149 Mutation::DropFunction(d) => {
150 for sig in &d.signatures {
151 let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
152 let schema = state.resolve_function_schema(&sig.name, &sig_str);
153 let id = ObjectId::new(schema, sig_str);
154 if !pre_state.functions.contains_key(&id) {
155 violations.push(Violation { source_range: None,
156 rule_id: self.id(),
157 operation_kind: OperationKind::DropFunction,
158 object_kind: ObjectKind::Function,
159 object_name: id.to_string(),
160 tier: self.default_tier(),
161 reason: format!(
162 "Migration DROPs function \"{}\" which does not exist in the production baseline",
163 id
164 ),
165 recipe: self.recipe(),
166 dedup_key: None,
167 sql: None,
168 });
169 }
170 }
171 }
172 Mutation::DropProcedure(d) => {
173 for sig in &d.signatures {
174 let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
175 let schema = state.resolve_function_schema(&sig.name, &sig_str);
176 let id = ObjectId::new(schema, sig_str);
177 if !pre_state.functions.contains_key(&id) {
178 violations.push(Violation { source_range: None,
179 rule_id: self.id(),
180 operation_kind: OperationKind::DropProcedure,
181 object_kind: ObjectKind::Procedure,
182 object_name: id.to_string(),
183 tier: self.default_tier(),
184 reason: format!(
185 "Migration DROPs procedure \"{}\" which does not exist in the production baseline",
186 id
187 ),
188 recipe: self.recipe(),
189 dedup_key: None,
190 sql: None,
191 });
192 }
193 }
194 }
195 Mutation::DropIndex(d) => {
196 if !pre_state.indexes.iter().any(|idx| idx.index_id == d.id) {
197 violations.push(Violation { source_range: None,
198 rule_id: self.id(),
199 operation_kind: OperationKind::DropIndex,
200 object_kind: ObjectKind::Index,
201 object_name: d.id.to_string(),
202 tier: self.default_tier(),
203 reason: format!(
204 "Migration DROPs index \"{}\" which does not exist in the production baseline",
205 d.id
206 ),
207 recipe: self.recipe(),
208 dedup_key: None,
209 sql: None,
210 });
211 }
212 }
213 Mutation::DropDomain(d) => {
214 for id in &d.ids {
215 if !pre_state.types.contains_key(id) {
216 violations.push(Violation { source_range: None,
217 rule_id: self.id(),
218 operation_kind: OperationKind::DropDomain,
219 object_kind: ObjectKind::Domain,
220 object_name: id.to_string(),
221 tier: self.default_tier(),
222 reason: format!(
223 "Migration DROPs domain \"{}\" which does not exist in the production baseline",
224 id
225 ),
226 recipe: self.recipe(),
227 dedup_key: None,
228 sql: None,
229 });
230 }
231 }
232 }
233 Mutation::AlterType(a) if !pre_state.types.contains_key(&a.id) => {
234 violations.push(Violation { source_range: None,
235 rule_id: self.id(),
236 operation_kind: OperationKind::AlterType,
237 object_kind: ObjectKind::Type,
238 object_name: a.id.to_string(),
239 tier: self.default_tier(),
240 reason: format!(
241 "Migration ALTERs type \"{}\" which does not exist in the production baseline",
242 a.id
243 ),
244 recipe: self.recipe(),
245 dedup_key: None,
246 sql: None,
247 });
248 }
249 Mutation::AlterFunction(f) if !pre_state.functions.contains_key(&f.id) => {
250 violations.push(Violation { source_range: None,
251 rule_id: self.id(),
252 operation_kind: OperationKind::AlterFunction,
253 object_kind: ObjectKind::Function,
254 object_name: f.id.to_string(),
255 tier: self.default_tier(),
256 reason: format!(
257 "Migration ALTERs function \"{}\" which does not exist in the production baseline",
258 f.id
259 ),
260 recipe: self.recipe(),
261 dedup_key: None,
262 sql: None,
263 });
264 }
265 Mutation::AlterProcedure(p) if !pre_state.functions.contains_key(&p.id) => {
266 violations.push(Violation { source_range: None,
267 rule_id: self.id(),
268 operation_kind: OperationKind::AlterProcedure,
269 object_kind: ObjectKind::Procedure,
270 object_name: p.id.to_string(),
271 tier: self.default_tier(),
272 reason: format!(
273 "Migration ALTERs procedure \"{}\" which does not exist in the production baseline",
274 p.id
275 ),
276 recipe: self.recipe(),
277 dedup_key: None,
278 sql: None,
279 });
280 }
281 Mutation::CreateTable(c) => {
282 if let Some(parent_id) = &c.partition_of
284 && !pre_state.relations.contains_key(parent_id)
285 {
286 violations.push(Violation { source_range: None,
287 rule_id: self.id(),
288 operation_kind: OperationKind::CreateTable,
289 object_kind: ObjectKind::Table,
290 object_name: c.id.to_string(),
291 tier: self.default_tier(),
292 reason: format!(
293 "Migration creates {} as a partition of parent \"{}\" which does not exist in the production baseline. Parent must be created first.",
294 c.id, parent_id
295 ),
296 recipe: self.recipe(),
297 dedup_key: None,
298 sql: None,
299 });
300 }
301 }
302 _ => {}
303 }
304
305 violations
306 }
307}