Skip to main content

safe_migrate/rules/
expressions.rs

1// FILE: src/rules/expressions.rs
2
3use crate::analysis::mutations::Mutation;
4use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
5use crate::engine::config::Config;
6use crate::report::violations::{ObjectKind, OperationKind, Violation, ViolationTier};
7use crate::rules::Rule;
8
9pub struct VolatileDefaultRule;
10
11impl Rule for VolatileDefaultRule {
12    fn id(&self) -> &'static str {
13        "volatile-default"
14    }
15    fn default_tier(&self) -> ViolationTier {
16        ViolationTier::Tier3
17    }
18    fn recipe(&self) -> &'static str {
19        "Using volatile functions (like random() or now()) as defaults can cause unexpected behavior in logical replication or caching."
20    }
21
22    fn evaluate(
23        &self,
24        mutation: &Mutation,
25        result: &MutationResult,
26        _pre_state: &crate::analysis::state::PreState,
27        _state: &AnalysisState,
28        _config: &Config,
29        _cascade: Option<&CascadeResult>,
30    ) -> Vec<Violation> {
31        if *result == MutationResult::Skipped {
32            return vec![];
33        }
34
35        let mut violations = Vec::new();
36
37        if let Mutation::CreateTable(c) = mutation {
38            for col in &c.columns {
39                if let Some(def) = &col.default
40                    && def.is_volatile()
41                {
42                    violations.push(Violation {
43                        source_range: None,
44                        rule_id: self.id(),
45                        operation_kind: OperationKind::CreateTable,
46                        object_kind: ObjectKind::Table,
47                        object_name: c.id.to_string(),
48                        tier: self.default_tier(),
49                        reason: format!("Volatile default expression on {}.{}", c.id, col.name),
50                        recipe: self.recipe(),
51                        dedup_key: None,
52                        sql: None,
53                    });
54                }
55            }
56        }
57
58        if let Mutation::AlterTable(a) = mutation {
59            match &a.action {
60                crate::analysis::mutations::AlterTableActionMutation::AddColumn {
61                    name,
62                    default: Some(def),
63                    ..
64                } if def.is_volatile() => {
65                    violations.push(Violation {
66                        source_range: None,
67                        rule_id: self.id(),
68                        operation_kind: OperationKind::AddColumn,
69                        object_kind: ObjectKind::Table,
70                        object_name: a.id.to_string(),
71                        tier: self.default_tier(),
72                        reason: format!("Volatile default expression on {}.{}", a.id, name),
73                        recipe: self.recipe(),
74                        dedup_key: None,
75                        sql: None,
76                    });
77                }
78                crate::analysis::mutations::AlterTableActionMutation::SetDefault {
79                    column,
80                    default: Some(def),
81                } if def.is_volatile() => {
82                    violations.push(Violation {
83                        source_range: None,
84                        rule_id: self.id(),
85                        operation_kind: OperationKind::Other("set_default".to_string()),
86                        object_kind: ObjectKind::Table,
87                        object_name: a.id.to_string(),
88                        tier: self.default_tier(),
89                        reason: format!("Volatile default expression on {}.{}", a.id, column),
90                        recipe: self.recipe(),
91                        dedup_key: None,
92                        sql: None,
93                    });
94                }
95                _ => {}
96            }
97        }
98
99        violations
100    }
101}