bestool_postgres/pgtune/
conf_block.rs1use super::Setting;
9
10pub const BEGIN: &str =
12 "# BEGIN PGTUNE — generated by `bestool tamanu pg-tune`, do not edit by hand";
13pub const END: &str = "# END PGTUNE";
15
16const LEGACY_HEADER: &str = "# PGTUNE";
18
19pub fn render(settings: &[Setting]) -> String {
21 let mut out = String::new();
22 out.push_str(BEGIN);
23 out.push('\n');
24 for setting in settings {
25 out.push_str(setting.key);
26 out.push_str(" = ");
27 out.push_str(&setting.value);
28 out.push('\n');
29 }
30 out.push_str(END);
31 out.push('\n');
32 out
33}
34
35pub fn splice(existing: &str, block: &str) -> String {
42 let block_lines: Vec<&str> = block.trim_end_matches('\n').split('\n').collect();
43 let lines: Vec<&str> = if existing.is_empty() {
44 Vec::new()
45 } else {
46 existing.split('\n').collect()
47 };
48
49 let begin = lines.iter().position(|l| l.trim_end() == BEGIN);
50 let end = lines.iter().position(|l| l.trim_end() == END);
51
52 let mut result: Vec<&str> = Vec::new();
53 match (begin, end) {
54 (Some(b), Some(e)) if b <= e => {
55 result.extend_from_slice(&lines[..b]);
56 result.extend_from_slice(&block_lines);
57 result.extend_from_slice(&lines[e + 1..]);
58 }
59 _ => {
60 let cut = lines
61 .iter()
62 .position(|l| l.trim_start().starts_with(LEGACY_HEADER))
63 .unwrap_or(lines.len());
64 result.extend_from_slice(&lines[..cut]);
65 trim_trailing_empty(&mut result);
66 if !result.is_empty() {
67 result.push("");
68 }
69 result.extend_from_slice(&block_lines);
70 }
71 }
72
73 trim_trailing_empty(&mut result);
74 let mut out = result.join("\n");
75 out.push('\n');
76 out
77}
78
79fn trim_trailing_empty(lines: &mut Vec<&str>) {
80 while lines.last().is_some_and(|l| l.trim().is_empty()) {
81 lines.pop();
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 fn sample_block() -> String {
90 render(&[
91 Setting::new("shared_buffers", "1GB"),
92 Setting::new("work_mem", "16MB"),
93 ])
94 }
95
96 #[test]
97 fn appends_to_a_file_without_markers() {
98 let existing = "listen_addresses = '*'\nport = 5432\n";
99 let out = splice(existing, &sample_block());
100 assert!(out.starts_with("listen_addresses = '*'\nport = 5432\n\n# BEGIN PGTUNE"));
101 assert!(out.ends_with("# END PGTUNE\n"));
102 assert!(out.contains("shared_buffers = 1GB"));
103 }
104
105 #[test]
106 fn replaces_between_existing_markers() {
107 let first = splice("port = 5432\n", &sample_block());
108 let updated = render(&[Setting::new("shared_buffers", "2GB")]);
109 let second = splice(&first, &updated);
110 assert!(second.contains("shared_buffers = 2GB"));
111 assert!(!second.contains("shared_buffers = 1GB"));
112 assert!(!second.contains("work_mem"));
113 assert!(second.starts_with("port = 5432\n"));
115 assert_eq!(second.matches(BEGIN).count(), 1);
116 assert_eq!(second.matches(END).count(), 1);
117 }
118
119 #[test]
120 fn migrates_a_legacy_header_to_eof() {
121 let existing = "port = 5432\n\n# PGTUNE https://pgtune.leopard.in.ua/\nshared_buffers = 512MB\nwork_mem = 8MB\n";
122 let out = splice(existing, &sample_block());
123 assert!(out.starts_with("port = 5432\n"));
124 assert!(!out.contains("# PGTUNE https"));
125 assert!(!out.contains("512MB"));
126 assert!(out.contains("# BEGIN PGTUNE"));
127 assert!(out.contains("shared_buffers = 1GB"));
128 assert!(out.ends_with("# END PGTUNE\n"));
129 }
130
131 #[test]
132 fn is_idempotent() {
133 let block = sample_block();
134 let once = splice("port = 5432\n", &block);
135 let twice = splice(&once, &block);
136 assert_eq!(once, twice);
137 }
138
139 #[test]
140 fn empty_file_gets_only_the_block() {
141 let out = splice("", &sample_block());
142 assert!(out.starts_with("# BEGIN PGTUNE"));
143 assert!(out.ends_with("# END PGTUNE\n"));
144 }
145}