1use std::{
5 fs::{File, OpenOptions},
6 io::{BufRead, BufReader, Write},
7 path::{Path, PathBuf},
8};
9
10pub struct Wal {
15 file: File,
16 path: PathBuf,
17}
18
19impl Wal {
20 pub fn open_append(path: &Path) -> std::io::Result<Self> {
25 let file = OpenOptions::new().create(true).append(true).open(path)?;
26 Ok(Self {
27 file,
28 path: path.to_path_buf(),
29 })
30 }
31
32 pub fn append(&mut self, cmd: &str) -> std::io::Result<()> {
37 writeln!(self.file, "{cmd}")?;
38 self.file.flush()?;
39 self.file.sync_data()
40 }
41
42 pub fn truncate(&mut self) -> std::io::Result<()> {
47 self.file = File::create(&self.path)?;
48 Ok(())
49 }
50
51 #[must_use]
53 pub fn path(&self) -> &Path {
54 &self.path
55 }
56
57 pub fn size(&self) -> std::io::Result<u64> {
62 std::fs::metadata(&self.path).map(|m| m.len())
63 }
64
65 pub fn read_commands(path: &Path) -> std::io::Result<Vec<String>> {
70 let file = File::open(path)?;
71 let reader = BufReader::new(file);
72 let mut commands = Vec::new();
73
74 for line in reader.lines() {
75 let cmd = line?;
76 let trimmed = cmd.trim();
77 if !trimmed.is_empty() {
78 commands.push(trimmed.to_string());
79 }
80 }
81
82 Ok(commands)
83 }
84}
85
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
88pub enum WalRecoveryMode {
89 #[default]
91 Strict,
92 Recover,
94}
95
96#[derive(Debug, Clone)]
98pub struct WalReplayResult {
99 pub replayed: usize,
101 pub errors: Vec<WalReplayError>,
103}
104
105#[derive(Debug, Clone)]
107pub struct WalReplayError {
108 pub line: usize,
110 pub command: String,
112 pub error: String,
114}
115
116impl WalReplayError {
117 #[must_use]
119 pub fn new(line: usize, command: &str, error: String) -> Self {
120 let command = if command.len() > 80 {
121 format!("{}...", &command[..77])
122 } else {
123 command.to_string()
124 };
125 Self {
126 line,
127 command,
128 error,
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use std::io::Read;
137
138 #[test]
139 fn test_wal_append_and_read() {
140 let dir = std::env::temp_dir();
141 let path = dir.join("test_wal_append.wal");
142
143 let _ = std::fs::remove_file(&path);
145
146 {
148 let mut wal = Wal::open_append(&path).unwrap();
149 wal.append("INSERT INTO users VALUES (1, 'Alice')").unwrap();
150 wal.append("INSERT INTO users VALUES (2, 'Bob')").unwrap();
151 }
152
153 let commands = Wal::read_commands(&path).unwrap();
155 assert_eq!(commands.len(), 2);
156 assert!(commands[0].contains("Alice"));
157 assert!(commands[1].contains("Bob"));
158
159 let _ = std::fs::remove_file(&path);
161 }
162
163 #[test]
164 fn test_wal_truncate() {
165 let dir = std::env::temp_dir();
166 let path = dir.join("test_wal_truncate.wal");
167
168 let _ = std::fs::remove_file(&path);
170
171 {
173 let mut wal = Wal::open_append(&path).unwrap();
174 wal.append("INSERT INTO users VALUES (1, 'Alice')").unwrap();
175 wal.truncate().unwrap();
176 }
177
178 let mut content = String::new();
180 File::open(&path)
181 .unwrap()
182 .read_to_string(&mut content)
183 .unwrap();
184 assert!(content.is_empty());
185
186 let _ = std::fs::remove_file(&path);
188 }
189
190 #[test]
191 fn test_wal_size() {
192 let dir = std::env::temp_dir();
193 let path = dir.join("test_wal_size.wal");
194
195 let _ = std::fs::remove_file(&path);
197
198 let mut wal = Wal::open_append(&path).unwrap();
199 wal.append("test").unwrap();
200 let size = wal.size().unwrap();
201 assert!(size > 0);
202
203 let _ = std::fs::remove_file(&path);
205 }
206
207 #[test]
208 fn test_wal_path() {
209 let dir = std::env::temp_dir();
210 let path = dir.join("test_wal_path.wal");
211
212 let _ = std::fs::remove_file(&path);
214
215 let wal = Wal::open_append(&path).unwrap();
216 assert_eq!(wal.path(), path);
217
218 let _ = std::fs::remove_file(&path);
220 }
221
222 #[test]
223 fn test_wal_replay_error() {
224 let error = WalReplayError::new(1, "short command", "error msg".to_string());
225 assert_eq!(error.line, 1);
226 assert_eq!(error.command, "short command");
227 assert_eq!(error.error, "error msg");
228 }
229
230 #[test]
231 fn test_wal_replay_error_truncates_long_command() {
232 let long_command = "x".repeat(100);
233 let error = WalReplayError::new(1, &long_command, "error".to_string());
234 assert!(error.command.len() < 100);
235 assert!(error.command.ends_with("..."));
236 }
237
238 #[test]
239 fn test_recovery_mode_default() {
240 let mode = WalRecoveryMode::default();
241 assert_eq!(mode, WalRecoveryMode::Strict);
242 }
243
244 #[test]
245 fn test_wal_replay_result_debug() {
246 let result = WalReplayResult {
247 replayed: 5,
248 errors: vec![],
249 };
250 let debug_str = format!("{result:?}");
251 assert!(debug_str.contains("WalReplayResult"));
252 }
253
254 #[test]
255 fn test_wal_replay_result_clone() {
256 let result = WalReplayResult {
257 replayed: 10,
258 errors: vec![WalReplayError::new(1, "cmd", "err".to_string())],
259 };
260 let cloned = result;
261 assert_eq!(cloned.replayed, 10);
262 assert_eq!(cloned.errors.len(), 1);
263 }
264
265 #[test]
266 fn test_wal_replay_error_debug() {
267 let error = WalReplayError::new(3, "test", "msg".to_string());
268 let debug_str = format!("{error:?}");
269 assert!(debug_str.contains("WalReplayError"));
270 }
271
272 #[test]
273 fn test_wal_replay_error_clone() {
274 let error = WalReplayError::new(7, "cmd", "error".to_string());
275 let cloned = error;
276 assert_eq!(cloned.line, 7);
277 assert_eq!(cloned.command, "cmd");
278 }
279
280 #[test]
281 fn test_wal_recovery_mode_eq() {
282 assert_eq!(WalRecoveryMode::Strict, WalRecoveryMode::Strict);
283 assert_eq!(WalRecoveryMode::Recover, WalRecoveryMode::Recover);
284 assert_ne!(WalRecoveryMode::Strict, WalRecoveryMode::Recover);
285 }
286
287 #[test]
288 fn test_wal_recovery_mode_debug() {
289 let strict = format!("{:?}", WalRecoveryMode::Strict);
290 assert!(strict.contains("Strict"));
291 let recover = format!("{:?}", WalRecoveryMode::Recover);
292 assert!(recover.contains("Recover"));
293 }
294
295 #[test]
296 fn test_wal_recovery_mode_copy() {
297 let mode = WalRecoveryMode::Recover;
298 let copied: WalRecoveryMode = mode;
299 assert_eq!(copied, WalRecoveryMode::Recover);
300 }
301
302 #[test]
303 fn test_read_commands_empty_lines() {
304 let dir = std::env::temp_dir();
305 let path = dir.join("test_wal_empty_lines.wal");
306
307 let _ = std::fs::remove_file(&path);
309
310 {
312 let mut file = File::create(&path).unwrap();
313 writeln!(file, "cmd1").unwrap();
314 writeln!(file).unwrap(); writeln!(file, " ").unwrap(); writeln!(file, "cmd2").unwrap();
317 }
318
319 let commands = Wal::read_commands(&path).unwrap();
321 assert_eq!(commands.len(), 2);
322 assert_eq!(commands[0], "cmd1");
323 assert_eq!(commands[1], "cmd2");
324
325 let _ = std::fs::remove_file(&path);
327 }
328
329 #[test]
330 fn test_wal_replay_error_exact_80_chars() {
331 let cmd = "x".repeat(80);
333 let error = WalReplayError::new(1, &cmd, "err".to_string());
334 assert_eq!(error.command.len(), 80);
335 assert!(!error.command.ends_with("..."));
336 }
337
338 #[test]
339 fn test_wal_replay_error_81_chars() {
340 let cmd = "x".repeat(81);
342 let error = WalReplayError::new(1, &cmd, "err".to_string());
343 assert!(error.command.ends_with("..."));
344 assert_eq!(error.command.len(), 80); }
346}