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::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                // Warn if parent table doesn't exist for partitioned tables
283                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}