Skip to main content

reddb_server/storage/wal/
reader.rs

1use super::record::WalRecord;
2use std::fs::File;
3use std::io::{self, BufReader, Read};
4use std::path::Path;
5
6/// Reader for the Write-Ahead Log
7pub struct WalReader {
8    reader: BufReader<File>,
9    position: u64,
10    format_version: u8,
11}
12
13impl WalReader {
14    /// Open a WAL file for reading
15    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
16        let file = File::open(path)?;
17        let mut reader = BufReader::new(file);
18
19        // Check header
20        let mut header = [0u8; reddb_file::WAL_FILE_HEADER_BYTES];
21        reader.read_exact(&mut header)?;
22        let decoded = reddb_file::decode_wal_file_header(&header)?;
23
24        Ok(Self {
25            reader,
26            position: reddb_file::WAL_FILE_HEADER_BYTES as u64,
27            format_version: decoded.version,
28        })
29    }
30
31    /// Iterate over records
32    /// Returns iterator that yields (LSn, WalRecord)
33    pub fn iter(self) -> WalIterator {
34        WalIterator {
35            reader: self.reader,
36            position: self.position,
37            format_version: self.format_version,
38        }
39    }
40
41    /// Scan a WAL file and collect the most recent full-page image for
42    /// each page id observed. Returned map is `page_id → (lsn, data)`.
43    /// Recovery applies these images before redo so torn writes are
44    /// healed without the legacy `-dwb` sidecar (gh-478).
45    pub fn collect_full_page_images<P: AsRef<Path>>(
46        path: P,
47    ) -> io::Result<std::collections::BTreeMap<u32, (u64, Vec<u8>)>> {
48        use std::collections::BTreeMap;
49        let mut out: BTreeMap<u32, (u64, Vec<u8>)> = BTreeMap::new();
50        if !path.as_ref().exists() {
51            return Ok(out);
52        }
53        let reader = Self::open(path)?;
54        for item in reader.iter() {
55            let (lsn, record) = item?;
56            if let WalRecord::FullPageImage { page_id, data, .. } = record {
57                match out.get(&page_id) {
58                    Some((existing_lsn, _)) if *existing_lsn > lsn => {}
59                    _ => {
60                        out.insert(page_id, (lsn, data));
61                    }
62                }
63            }
64        }
65        Ok(out)
66    }
67}
68
69pub struct WalIterator {
70    reader: BufReader<File>,
71    position: u64,
72    format_version: u8,
73}
74
75impl WalIterator {
76    pub(crate) fn next_record_lsn(&self) -> u64 {
77        self.position
78    }
79}
80
81impl Iterator for WalIterator {
82    type Item = io::Result<(u64, WalRecord)>;
83
84    fn next(&mut self) -> Option<Self::Item> {
85        // Need to record start position for LSN
86        // Since BufReader buffers, we can't trust file.seek(Current) directly without accounting for buffer.
87        // But `WalRecord::read` reads sequentially.
88        // The simple way: track position manually based on bytes read.
89        // WalRecord::read consumes bytes.
90
91        // Wait, WalRecord::read takes &mut R. We can wrap the reader to count bytes?
92        // Or just rely on the fact that we read sequentially.
93        // But we need to know the *start* LSN of the record to return it.
94
95        let start_pos = self.position;
96
97        // We need a way to track how many bytes were read by `WalRecord::read`.
98        // Let's create a counting wrapper.
99        struct CountingReader<'a, R> {
100            inner: &'a mut R,
101            count: u64,
102        }
103
104        impl<'a, R: Read> Read for CountingReader<'a, R> {
105            fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
106                let n = self.inner.read(buf)?;
107                self.count += n as u64;
108                Ok(n)
109            }
110        }
111
112        let mut counter = CountingReader {
113            inner: &mut self.reader,
114            count: 0,
115        };
116
117        match WalRecord::read_with_format_version(&mut counter, self.format_version) {
118            Ok(Some((_, record))) => {
119                self.position += counter.count;
120                Some(Ok((start_pos, record)))
121            }
122            Ok(None) => None, // EOF
123            Err(e) => Some(Err(e)),
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::super::writer::WalWriter;
131    use super::*;
132    use std::path::PathBuf;
133
134    struct FileGuard {
135        path: PathBuf,
136    }
137
138    impl Drop for FileGuard {
139        fn drop(&mut self) {
140            let _ = std::fs::remove_file(&self.path);
141        }
142    }
143
144    fn temp_wal(name: &str) -> (FileGuard, PathBuf) {
145        let path = reddb_file::layout::wal_component_temp_path(
146            &std::env::temp_dir(),
147            "reader",
148            name,
149            std::process::id(),
150        );
151        let guard = FileGuard { path: path.clone() };
152        let _ = std::fs::remove_file(&path);
153        (guard, path)
154    }
155
156    #[test]
157    fn test_read_empty_wal() {
158        let (_guard, path) = temp_wal("empty");
159
160        // Create empty WAL
161        {
162            let _writer = WalWriter::open(&path).unwrap();
163        }
164
165        // Read it
166        let reader = WalReader::open(&path).unwrap();
167        let records: Vec<_> = reader.iter().collect();
168        assert!(records.is_empty());
169    }
170
171    #[test]
172    fn test_read_single_record() {
173        let (_guard, path) = temp_wal("single");
174
175        // Write one record
176        {
177            let mut writer = WalWriter::open(&path).unwrap();
178            writer.append(&WalRecord::Begin { tx_id: 42 }).unwrap();
179        }
180
181        // Read it back
182        let reader = WalReader::open(&path).unwrap();
183        let records: Vec<_> = reader.iter().collect();
184
185        assert_eq!(records.len(), 1);
186        let (lsn, record) = records[0].as_ref().unwrap();
187        assert_eq!(*lsn, 8);
188        assert_eq!(*record, WalRecord::Begin { tx_id: 42 });
189    }
190
191    #[test]
192    fn test_read_multiple_records() {
193        let (_guard, path) = temp_wal("multi");
194
195        // Write multiple records
196        {
197            let mut writer = WalWriter::open(&path).unwrap();
198            writer.append(&WalRecord::Begin { tx_id: 1 }).unwrap();
199            writer
200                .append(&WalRecord::PageWrite {
201                    tx_id: 1,
202                    page_id: 10,
203                    data: vec![1, 2, 3],
204                })
205                .unwrap();
206            writer.append(&WalRecord::Commit { tx_id: 1 }).unwrap();
207        }
208
209        // Read back
210        let reader = WalReader::open(&path).unwrap();
211        let records: Vec<_> = reader.iter().collect();
212
213        assert_eq!(records.len(), 3);
214
215        // Check each record
216        match &records[0].as_ref().unwrap().1 {
217            WalRecord::Begin { tx_id } => assert_eq!(*tx_id, 1),
218            _ => panic!("Expected Begin"),
219        }
220        match &records[1].as_ref().unwrap().1 {
221            WalRecord::PageWrite {
222                tx_id,
223                page_id,
224                data,
225            } => {
226                assert_eq!(*tx_id, 1);
227                assert_eq!(*page_id, 10);
228                assert_eq!(data, &vec![1, 2, 3]);
229            }
230            _ => panic!("Expected PageWrite"),
231        }
232        match &records[2].as_ref().unwrap().1 {
233            WalRecord::Commit { tx_id } => assert_eq!(*tx_id, 1),
234            _ => panic!("Expected Commit"),
235        }
236    }
237
238    #[test]
239    fn test_lsn_tracking() {
240        let (_guard, path) = temp_wal("lsn");
241
242        // Write records
243        let (lsn1, lsn2, lsn3);
244        {
245            let mut writer = WalWriter::open(&path).unwrap();
246            lsn1 = writer.append(&WalRecord::Begin { tx_id: 1 }).unwrap();
247            lsn2 = writer.append(&WalRecord::Checkpoint { lsn: 100 }).unwrap();
248            lsn3 = writer.append(&WalRecord::Rollback { tx_id: 1 }).unwrap();
249        }
250
251        // Read and verify LSNs
252        let reader = WalReader::open(&path).unwrap();
253        let records: Vec<_> = reader.iter().collect();
254
255        assert_eq!(records.len(), 3);
256        assert_eq!(records[0].as_ref().unwrap().0, lsn1);
257        assert_eq!(records[1].as_ref().unwrap().0, lsn2);
258        assert_eq!(records[2].as_ref().unwrap().0, lsn3);
259    }
260
261    #[test]
262    fn test_invalid_magic() {
263        let (_guard, path) = temp_wal("badmagic");
264
265        // Write invalid file
266        std::fs::write(&path, b"BAAD0000").unwrap();
267
268        let result = WalReader::open(&path);
269        assert!(result.is_err());
270    }
271
272    #[test]
273    fn test_invalid_version() {
274        let (_guard, path) = temp_wal("badver");
275
276        // Write header with wrong version
277        let mut header = reddb_file::encode_wal_file_header();
278        header[4] = 99; // Wrong version
279        std::fs::write(&path, header).unwrap();
280
281        let result = WalReader::open(&path);
282        assert!(result.is_err());
283    }
284
285    #[test]
286    fn test_read_large_page_write() {
287        let (_guard, path) = temp_wal("large");
288
289        let large_data: Vec<u8> = (0..4096).map(|i| (i % 256) as u8).collect();
290
291        // Write large record
292        {
293            let mut writer = WalWriter::open(&path).unwrap();
294            writer
295                .append(&WalRecord::PageWrite {
296                    tx_id: 1,
297                    page_id: 0,
298                    data: large_data.clone(),
299                })
300                .unwrap();
301        }
302
303        // Read back
304        let reader = WalReader::open(&path).unwrap();
305        let records: Vec<_> = reader.iter().collect();
306
307        assert_eq!(records.len(), 1);
308        match &records[0].as_ref().unwrap().1 {
309            WalRecord::PageWrite { data, .. } => {
310                assert_eq!(*data, large_data);
311            }
312            _ => panic!("Expected PageWrite"),
313        }
314    }
315}