safe_migrate/rules/
timeouts.rs1use 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}