Skip to main content

safe_migrate/rules/
drift.rs

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, // Or general
275                        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                // Warn if parent table doesn't exist for partitioned tables
341                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}