Skip to main content

safe_migrate/rules/
timeouts.rs

1use crate::analysis::mutations::Mutation;
2use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
3use crate::engine::config::Config;
4use crate::report::violations::{ObjectKind, OperationKind, Violation, ViolationTier};
5use crate::rules::Rule;
6
7pub struct RequireLockTimeoutRule;
8
9impl Rule for RequireLockTimeoutRule {
10    fn id(&self) -> &'static str {
11        "require-lock-timeout"
12    }
13
14    fn default_tier(&self) -> ViolationTier {
15        ViolationTier::Tier2
16    }
17
18    fn recipe(&self) -> &'static str {
19        "Set a positive lock_timeout before this operation, or configure it for the intended migration role and run safe-migrate sync again."
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 !matches!(mutation, Mutation::CheckTimeouts) || result != &MutationResult::Applied {
32            return Vec::new();
33        }
34
35        let reason = match state.local.lock_timeout.effective {
36            None => "No lock_timeout is known from SQL or a synchronized cache.".to_string(),
37            Some(0) => "lock_timeout is disabled (0).".to_string(),
38            Some(lock_timeout) => match state.local.statement_timeout.effective {
39                Some(statement_timeout)
40                    if statement_timeout > 0 && lock_timeout >= statement_timeout =>
41                {
42                    format!(
43                        "lock_timeout ({lock_timeout} ms) is not shorter than statement_timeout ({statement_timeout} ms), so PostgreSQL reaches statement_timeout first."
44                    )
45                }
46                _ => return Vec::new(),
47            },
48        };
49
50        vec![Violation {
51            source_range: None,
52            rule_id: self.id(),
53            operation_kind: OperationKind::Other("timeout_check".to_string()),
54            object_kind: ObjectKind::Unknown,
55            object_name: "<statement>".to_string(),
56            tier: self.default_tier(),
57            reason,
58            recipe: self.recipe(),
59            dedup_key: Some(self.id().to_string()),
60            sql: None,
61            fk_dependency_related: false,
62        }]
63    }
64}
65
66pub struct RequireStatementTimeoutRule;
67
68impl Rule for RequireStatementTimeoutRule {
69    fn id(&self) -> &'static str {
70        "require-statement-timeout"
71    }
72
73    fn default_tier(&self) -> ViolationTier {
74        ViolationTier::Tier2
75    }
76
77    fn recipe(&self) -> &'static str {
78        "Set a positive statement_timeout before this operation, or configure it for the intended migration role and run safe-migrate sync again."
79    }
80
81    fn evaluate(
82        &self,
83        mutation: &Mutation,
84        result: &MutationResult,
85        _pre_state: &crate::analysis::state::PreState,
86        state: &AnalysisState,
87        _config: &Config,
88        _cascade: Option<&CascadeResult>,
89    ) -> Vec<Violation> {
90        if !matches!(mutation, Mutation::CheckTimeouts) || result != &MutationResult::Applied {
91            return Vec::new();
92        }
93
94        let reason = match state.local.statement_timeout.effective {
95            None => "No statement_timeout is known from SQL or a synchronized cache.".to_string(),
96            Some(0) => "statement_timeout is disabled (0).".to_string(),
97            Some(_) => return Vec::new(),
98        };
99
100        vec![Violation {
101            source_range: None,
102            rule_id: self.id(),
103            operation_kind: OperationKind::Other("timeout_check".to_string()),
104            object_kind: ObjectKind::Unknown,
105            object_name: "<statement>".to_string(),
106            tier: self.default_tier(),
107            reason,
108            recipe: self.recipe(),
109            dedup_key: Some(self.id().to_string()),
110            sql: None,
111            fk_dependency_related: false,
112        }]
113    }
114}