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