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::UnresolvedReference,
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 fk_dependency_related: false,
52 });
53 }
54 Mutation::DropTable(d) => {
55 if !pre_state.relations.contains_key(&d.id) {
56 violations.push(Violation { source_range: None,
57 rule_id: self.id(),
58 operation_kind: OperationKind::DropTable,
59 object_kind: ObjectKind::Table,
60 object_name: d.id.to_string(),
61 tier: self.default_tier(),
62 reason: format!(
63 "Migration DROPs table \"{}\" which does not exist in the production baseline",
64 d.id
65 ),
66 recipe: self.recipe(),
67 dedup_key: None,
68 sql: None,
69 fk_dependency_related: false,
70 });
71 }
72 }
73 Mutation::AlterTable(a) => {
74 if !pre_state.relations.contains_key(&a.id) {
75 violations.push(Violation { source_range: None,
76 rule_id: self.id(),
77 operation_kind: OperationKind::Other("alter_table".to_string()),
78 object_kind: ObjectKind::Table,
79 object_name: a.id.to_string(),
80 tier: self.default_tier(),
81 reason: format!(
82 "Migration ALTERs table \"{}\" which does not exist in the production baseline",
83 a.id
84 ),
85 recipe: self.recipe(),
86 dedup_key: None,
87 sql: None,
88 fk_dependency_related: false,
89 });
90 }
91 }
92 Mutation::DropView(d) => {
93 for id in &d.ids {
94 if !pre_state.relations.contains_key(id) {
95 violations.push(Violation { source_range: None,
96 rule_id: self.id(),
97 operation_kind: OperationKind::DropView,
98 object_kind: ObjectKind::View,
99 object_name: id.to_string(),
100 tier: self.default_tier(),
101 reason: format!(
102 "Migration DROPs view \"{}\" which does not exist in the production baseline",
103 id
104 ),
105 recipe: self.recipe(),
106 dedup_key: None,
107 sql: None,
108 fk_dependency_related: false,
109 });
110 }
111 }
112 }
113 Mutation::DropMaterializedView(d) => {
114 for id in &d.ids {
115 if !pre_state.relations.contains_key(id) {
116 violations.push(Violation { source_range: None,
117 rule_id: self.id(),
118 operation_kind: OperationKind::DropMaterializedView,
119 object_kind: ObjectKind::MaterializedView,
120 object_name: id.to_string(),
121 tier: self.default_tier(),
122 reason: format!(
123 "Migration DROPs materialized view \"{}\" which does not exist in the production baseline",
124 id
125 ),
126 recipe: self.recipe(),
127 dedup_key: None,
128 sql: None,
129 fk_dependency_related: false,
130 });
131 }
132 }
133 }
134 Mutation::DropSequence(d) => {
135 for id in &d.ids {
136 if !pre_state.sequences.contains_key(id) {
137 violations.push(Violation { source_range: None,
138 rule_id: self.id(),
139 operation_kind: OperationKind::DropSequence,
140 object_kind: ObjectKind::Sequence,
141 object_name: id.to_string(),
142 tier: self.default_tier(),
143 reason: format!(
144 "Migration DROPs sequence \"{}\" which does not exist in the production baseline",
145 id
146 ),
147 recipe: self.recipe(),
148 dedup_key: None,
149 sql: None,
150 fk_dependency_related: false,
151 });
152 }
153 }
154 }
155 Mutation::DropFunction(d) => {
156 for sig in &d.signatures {
157 let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
158 let schema = state.resolve_function_schema(&sig.name, &sig_str);
159 let id = ObjectId::new(schema, sig_str);
160 if !pre_state.functions.contains_key(&id) {
161 violations.push(Violation { source_range: None,
162 rule_id: self.id(),
163 operation_kind: OperationKind::DropFunction,
164 object_kind: ObjectKind::Function,
165 object_name: id.to_string(),
166 tier: self.default_tier(),
167 reason: format!(
168 "Migration DROPs function \"{}\" which does not exist in the production baseline",
169 id
170 ),
171 recipe: self.recipe(),
172 dedup_key: None,
173 sql: None,
174 fk_dependency_related: false,
175 });
176 }
177 }
178 }
179 Mutation::DropProcedure(d) => {
180 for sig in &d.signatures {
181 let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
182 let schema = state.resolve_function_schema(&sig.name, &sig_str);
183 let id = ObjectId::new(schema, sig_str);
184 if !pre_state.functions.contains_key(&id) {
185 violations.push(Violation { source_range: None,
186 rule_id: self.id(),
187 operation_kind: OperationKind::DropProcedure,
188 object_kind: ObjectKind::Procedure,
189 object_name: id.to_string(),
190 tier: self.default_tier(),
191 reason: format!(
192 "Migration DROPs procedure \"{}\" which does not exist in the production baseline",
193 id
194 ),
195 recipe: self.recipe(),
196 dedup_key: None,
197 sql: None,
198 fk_dependency_related: false,
199 });
200 }
201 }
202 }
203 Mutation::DropIndex(d) => {
204 if !pre_state.indexes.iter().any(|idx| idx.dependent == d.id) {
205 violations.push(Violation { source_range: None,
206 rule_id: self.id(),
207 operation_kind: OperationKind::DropIndex,
208 object_kind: ObjectKind::Index,
209 object_name: d.id.to_string(),
210 tier: self.default_tier(),
211 reason: format!(
212 "Migration DROPs index \"{}\" which does not exist in the production baseline",
213 d.id
214 ),
215 recipe: self.recipe(),
216 dedup_key: None,
217 sql: None,
218 fk_dependency_related: false,
219 });
220 }
221 }
222 Mutation::DropDomain(d) => {
223 for id in &d.ids {
224 if !pre_state.types.contains_key(id) {
225 violations.push(Violation { source_range: None,
226 rule_id: self.id(),
227 operation_kind: OperationKind::DropDomain,
228 object_kind: ObjectKind::Domain,
229 object_name: id.to_string(),
230 tier: self.default_tier(),
231 reason: format!(
232 "Migration DROPs domain \"{}\" which does not exist in the production baseline",
233 id
234 ),
235 recipe: self.recipe(),
236 dedup_key: None,
237 sql: None,
238 fk_dependency_related: false,
239 });
240 }
241 }
242 }
243 Mutation::DropType(d) => {
244 for id in &d.ids {
245 if !pre_state.types.contains_key(id) {
246 violations.push(Violation { source_range: None,
247 rule_id: self.id(),
248 operation_kind: OperationKind::DropType,
249 object_kind: ObjectKind::Type,
250 object_name: id.to_string(),
251 tier: self.default_tier(),
252 reason: format!(
253 "Migration DROPs type \"{}\" which does not exist in the production baseline",
254 id
255 ),
256 recipe: self.recipe(),
257 dedup_key: None,
258 sql: None,
259 fk_dependency_related: false,
260 });
261 }
262 }
263 }
264 Mutation::Rename(r) => {
265 if !pre_state.relations.contains_key(&r.old_id)
266 && !pre_state.types.contains_key(&r.old_id)
267 && !pre_state.sequences.contains_key(&r.old_id)
268 && !pre_state
269 .indexes
270 .iter()
271 .any(|idx| idx.dependent == r.old_id)
272 {
273 violations.push(Violation { source_range: None,
274 rule_id: self.id(),
275 operation_kind: OperationKind::Rename,
276 object_kind: ObjectKind::Table, object_name: r.old_id.to_string(),
278 tier: self.default_tier(),
279 reason: format!(
280 "Migration RENAMEs object \"{}\" which does not exist in the production baseline",
281 r.old_id
282 ),
283 recipe: self.recipe(),
284 dedup_key: None,
285 sql: None,
286 fk_dependency_related: false,
287 });
288 }
289 }
290 Mutation::AlterType(a) if !pre_state.types.contains_key(&a.id) => {
291 violations.push(Violation { source_range: None,
292 rule_id: self.id(),
293 operation_kind: OperationKind::AlterType,
294 object_kind: ObjectKind::Type,
295 object_name: a.id.to_string(),
296 tier: self.default_tier(),
297 reason: format!(
298 "Migration ALTERs type \"{}\" which does not exist in the production baseline",
299 a.id
300 ),
301 recipe: self.recipe(),
302 dedup_key: None,
303 sql: None,
304 fk_dependency_related: false,
305 });
306 }
307 Mutation::AlterFunction(f) if !pre_state.functions.contains_key(&f.id) => {
308 violations.push(Violation { source_range: None,
309 rule_id: self.id(),
310 operation_kind: OperationKind::AlterFunction,
311 object_kind: ObjectKind::Function,
312 object_name: f.id.to_string(),
313 tier: self.default_tier(),
314 reason: format!(
315 "Migration ALTERs function \"{}\" which does not exist in the production baseline",
316 f.id
317 ),
318 recipe: self.recipe(),
319 dedup_key: None,
320 sql: None,
321 fk_dependency_related: false,
322 });
323 }
324 Mutation::AlterProcedure(p) if !pre_state.functions.contains_key(&p.id) => {
325 violations.push(Violation { source_range: None,
326 rule_id: self.id(),
327 operation_kind: OperationKind::AlterProcedure,
328 object_kind: ObjectKind::Procedure,
329 object_name: p.id.to_string(),
330 tier: self.default_tier(),
331 reason: format!(
332 "Migration ALTERs procedure \"{}\" which does not exist in the production baseline",
333 p.id
334 ),
335 recipe: self.recipe(),
336 dedup_key: None,
337 sql: None,
338 fk_dependency_related: false,
339 });
340 }
341 Mutation::CreateTable(c) => {
342 if let Some(parent_id) = &c.partition_of
344 && !pre_state.relations.contains_key(parent_id)
345 {
346 violations.push(Violation { source_range: None,
347 rule_id: self.id(),
348 operation_kind: OperationKind::CreateTable,
349 object_kind: ObjectKind::Table,
350 object_name: c.id.to_string(),
351 tier: self.default_tier(),
352 reason: format!(
353 "Migration creates {} as a partition of parent \"{}\" which does not exist in the production baseline. Parent must be created first.",
354 c.id, parent_id
355 ),
356 recipe: self.recipe(),
357 dedup_key: None,
358 sql: None,
359 fk_dependency_related: false,
360 });
361 }
362 }
363 _ => {}
364 }
365
366 violations
367 }
368}