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.index_id == 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 println!("Evaluate DropType in drift rule: ids={:?}", d.ids);
245 for id in &d.ids {
246 if !pre_state.types.contains_key(id) {
247 violations.push(Violation { source_range: None,
248 rule_id: self.id(),
249 operation_kind: OperationKind::DropType,
250 object_kind: ObjectKind::Type,
251 object_name: id.to_string(),
252 tier: self.default_tier(),
253 reason: format!(
254 "Migration DROPs type \"{}\" which does not exist in the production baseline",
255 id
256 ),
257 recipe: self.recipe(),
258 dedup_key: None,
259 sql: None,
260 fk_dependency_related: false,
261 });
262 }
263 }
264 }
265 Mutation::Rename(r) => {
266 if !pre_state.relations.contains_key(&r.old_id)
267 && !pre_state.types.contains_key(&r.old_id)
268 && !pre_state.sequences.contains_key(&r.old_id)
269 && !pre_state.indexes.iter().any(|idx| idx.index_id == r.old_id)
270 {
271 violations.push(Violation { source_range: None,
272 rule_id: self.id(),
273 operation_kind: OperationKind::Rename,
274 object_kind: ObjectKind::Table, object_name: r.old_id.to_string(),
276 tier: self.default_tier(),
277 reason: format!(
278 "Migration RENAMEs object \"{}\" which does not exist in the production baseline",
279 r.old_id
280 ),
281 recipe: self.recipe(),
282 dedup_key: None,
283 sql: None,
284 fk_dependency_related: false,
285 });
286 }
287 }
288 Mutation::AlterType(a) if !pre_state.types.contains_key(&a.id) => {
289 violations.push(Violation { source_range: None,
290 rule_id: self.id(),
291 operation_kind: OperationKind::AlterType,
292 object_kind: ObjectKind::Type,
293 object_name: a.id.to_string(),
294 tier: self.default_tier(),
295 reason: format!(
296 "Migration ALTERs type \"{}\" which does not exist in the production baseline",
297 a.id
298 ),
299 recipe: self.recipe(),
300 dedup_key: None,
301 sql: None,
302 fk_dependency_related: false,
303 });
304 }
305 Mutation::AlterFunction(f) if !pre_state.functions.contains_key(&f.id) => {
306 violations.push(Violation { source_range: None,
307 rule_id: self.id(),
308 operation_kind: OperationKind::AlterFunction,
309 object_kind: ObjectKind::Function,
310 object_name: f.id.to_string(),
311 tier: self.default_tier(),
312 reason: format!(
313 "Migration ALTERs function \"{}\" which does not exist in the production baseline",
314 f.id
315 ),
316 recipe: self.recipe(),
317 dedup_key: None,
318 sql: None,
319 fk_dependency_related: false,
320 });
321 }
322 Mutation::AlterProcedure(p) if !pre_state.functions.contains_key(&p.id) => {
323 violations.push(Violation { source_range: None,
324 rule_id: self.id(),
325 operation_kind: OperationKind::AlterProcedure,
326 object_kind: ObjectKind::Procedure,
327 object_name: p.id.to_string(),
328 tier: self.default_tier(),
329 reason: format!(
330 "Migration ALTERs procedure \"{}\" which does not exist in the production baseline",
331 p.id
332 ),
333 recipe: self.recipe(),
334 dedup_key: None,
335 sql: None,
336 fk_dependency_related: false,
337 });
338 }
339 Mutation::CreateTable(c) => {
340 if let Some(parent_id) = &c.partition_of
342 && !pre_state.relations.contains_key(parent_id)
343 {
344 violations.push(Violation { source_range: None,
345 rule_id: self.id(),
346 operation_kind: OperationKind::CreateTable,
347 object_kind: ObjectKind::Table,
348 object_name: c.id.to_string(),
349 tier: self.default_tier(),
350 reason: format!(
351 "Migration creates {} as a partition of parent \"{}\" which does not exist in the production baseline. Parent must be created first.",
352 c.id, parent_id
353 ),
354 recipe: self.recipe(),
355 dedup_key: None,
356 sql: None,
357 fk_dependency_related: false,
358 });
359 }
360 }
361 _ => {}
362 }
363
364 violations
365 }
366}