safe_migrate/engine/
config.rs1use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct RuleConfig {
9 pub disabled: Option<bool>,
10 pub tier1_threshold_rows: Option<u64>,
11 pub tier2_threshold_rows: Option<u64>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(default)] pub struct Config {
17 pub tier1_threshold_rows: u64,
18 pub tier2_threshold_rows: u64,
19 pub stale_stats_days: u64,
20 pub toast_width_threshold_bytes: i32,
21 pub default_rows: u64, pub rules: HashMap<String, RuleConfig>, pub assume_pg_version: u32,
24 pub disabled_rules: Vec<String>,
25 pub schemas: Option<Vec<String>>,
26}
27
28impl Default for Config {
29 fn default() -> Self {
30 Self {
31 tier1_threshold_rows: 100_000,
32 tier2_threshold_rows: 10_000,
33 stale_stats_days: 7,
34 toast_width_threshold_bytes: 2048,
35 default_rows: 10_000,
36 assume_pg_version: 100000,
37 disabled_rules: Vec::new(),
38 rules: HashMap::new(),
39 schemas: None,
40 }
41 }
42}
43
44impl Config {
45 pub fn load_from_file(path: &Path) -> Result<Self, anyhow::Error> {
46 if path.exists() {
47 let contents = fs::read_to_string(path)?;
48 match toml::from_str(&contents) {
49 Ok(config) => return Ok(config),
50 Err(e) => {
51 return Err(anyhow::anyhow!(
52 "Failed to parse config at {}: {}",
53 path.display(),
54 e
55 ));
56 }
57 }
58 }
59 Ok(Self::default())
60 }
61
62 pub fn is_rule_disabled(&self, rule_id: &str) -> bool {
64 if self.disabled_rules.contains(&rule_id.to_string()) {
65 return true;
66 }
67 self.rules
68 .get(rule_id)
69 .and_then(|r| r.disabled)
70 .unwrap_or(false)
71 }
72
73 pub fn rule_tier1_threshold(&self, rule_id: &str) -> u64 {
75 self.rules
76 .get(rule_id)
77 .and_then(|r| r.tier1_threshold_rows)
78 .unwrap_or(self.tier1_threshold_rows)
79 }
80
81 pub fn rule_tier2_threshold(&self, rule_id: &str) -> u64 {
83 self.rules
84 .get(rule_id)
85 .and_then(|r| r.tier2_threshold_rows)
86 .unwrap_or(self.tier2_threshold_rows)
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::io::Write;
94 use tempfile::NamedTempFile;
95
96 #[test]
97 fn test_granular_rule_config() {
98 let mut file = NamedTempFile::new().expect("Failed to create temp file");
99 writeln!(
100 file,
101 r#"
102 tier1_threshold_rows = 500000
103
104 [rules.blocking-constraint]
105 tier1_threshold_rows = 5000
106
107 [rules.missing-idempotency]
108 disabled = true
109 "#
110 )
111 .expect("Failed to write temp config");
112
113 let config = Config::load_from_file(file.path()).expect("Failed to load valid config");
114
115 assert_eq!(config.tier1_threshold_rows, 500_000);
117
118 assert_eq!(config.rule_tier1_threshold("blocking-constraint"), 5000);
120 assert_eq!(config.rule_tier1_threshold("unspecified-rule"), 500_000);
121
122 assert!(config.is_rule_disabled("missing-idempotency"));
124 assert!(!config.is_rule_disabled("blocking-constraint"));
125 }
126}