Skip to main content

quantydb_sqlite/
wal.rs

1//! The write-ahead log.
2//!
3//! In wal mode a writer does not touch the main database file. It appends
4//! whole pages to a `-wal` file next to it, and only a checkpoint folds
5//! them back in. So the main file on its own is a snapshot of the database
6//! as of the last checkpoint, and every change since then lives here. A
7//! reader that ignores this file does not fail; it quietly returns an older
8//! database, which is the worst kind of wrong.
9//!
10//! The format is a 32 byte header followed by frames, each of which is a 24
11//! byte header and one page of data. Reading it correctly is three rules:
12//!
13//! Frames belong to the log only if their salts match the header's. A
14//! checkpoint bumps the salts and then writes new frames from the start of
15//! the file, so a `-wal` file routinely contains stale frames from an
16//! earlier generation, sitting after the new ones and looking perfectly
17//! well formed.
18//!
19//! Frames are only valid up to the first checksum that does not match. The
20//! checksum is cumulative: each frame's covers the previous frame's result,
21//! its own header and its page, so a torn write anywhere invalidates
22//! everything after it rather than just itself. That is what makes the log
23//! safe to read after a crash without a recovery pass.
24//!
25//! Only frames up to the last commit frame count. A commit frame is one
26//! whose header carries the database size, and frames after the last one
27//! belong to a transaction that was still open. Those rows were never
28//! committed, and returning them would be inventing data.
29
30use std::collections::HashMap;
31
32use crate::error::{Result, SqliteError};
33use crate::source::Source;
34
35pub const WAL_HEADER_LEN: usize = 32;
36pub const FRAME_HEADER_LEN: usize = 24;
37
38const MAGIC_LITTLE: u32 = 0x377f_0682;
39const MAGIC_BIG: u32 = 0x377f_0683;
40
41/// A `-wal` file, read up to its last commit.
42pub struct Wal<S: Source> {
43    source: S,
44    page_size: u32,
45    /// Page number to the byte offset of that page's newest committed
46    /// frame data. Later frames overwrite earlier ones, which is how a page
47    /// written three times in a row ends up at its third version.
48    pages: HashMap<u32, u64>,
49    /// Database size in pages as of the last commit. A transaction can
50    /// shrink the database, so this is not simply the largest page seen.
51    page_count: u32,
52    committed_frames: u32,
53    total_frames: u32,
54}
55
56impl<S: Source> Wal<S> {
57    /// Read and validate a `-wal` file.
58    ///
59    /// An empty file is not an error: sqlite leaves a zero length `-wal`
60    /// behind after a checkpoint, and so does a database that has been in
61    /// wal mode but never written to.
62    pub fn open(source: S) -> Result<Wal<S>> {
63        let len = source.len()?;
64        if len < WAL_HEADER_LEN as u64 {
65            return Ok(Wal {
66                source,
67                page_size: 0,
68                pages: HashMap::new(),
69                page_count: 0,
70                committed_frames: 0,
71                total_frames: 0,
72            });
73        }
74
75        let mut header = [0u8; WAL_HEADER_LEN];
76        source.read_at(0, &mut header)?;
77
78        let magic = be32(&header, 0);
79        let big_endian = match magic {
80            MAGIC_LITTLE => false,
81            MAGIC_BIG => true,
82            other => {
83                return Err(SqliteError::malformed(
84                    None,
85                    format!("the wal file starts with {other:#010x}, not a wal magic number"),
86                ))
87            }
88        };
89
90        // the checksum comes before any of the fields are believed. it
91        // covers the header's first 24 bytes starting from zero, and a
92        // header that fails it cannot be trusted to state its own page
93        // size, let alone the salts every frame is judged against.
94        let running = checksum(big_endian, (0, 0), &header[..24]);
95        let expected = (be32(&header, 24), be32(&header, 28));
96        if running != expected {
97            return Err(SqliteError::malformed(
98                None,
99                "the wal header checksum does not match its contents",
100            ));
101        }
102
103        let format = be32(&header, 4);
104        if format != 3_007_000 {
105            return Err(SqliteError::unsupported(format!(
106                "wal format version {format} is not the one this reader knows (3007000)"
107            )));
108        }
109
110        let page_size = be32(&header, 8);
111        if page_size < 512 || !page_size.is_power_of_two() {
112            return Err(SqliteError::malformed(
113                None,
114                format!("the wal header gives a page size of {page_size}"),
115            ));
116        }
117
118        let salt = (be32(&header, 16), be32(&header, 20));
119
120        let frame_len = FRAME_HEADER_LEN as u64 + page_size as u64;
121        let total_frames =
122            u32::try_from((len - WAL_HEADER_LEN as u64) / frame_len).unwrap_or(u32::MAX);
123
124        let mut pages: HashMap<u32, u64> = HashMap::new();
125        let mut running = running;
126        let mut committed_frames = 0u32;
127        let mut page_count = 0u32;
128        // frames seen since the last commit: they only count once a commit
129        // frame arrives, so they are held here rather than published
130        let mut pending: Vec<(u32, u64)> = Vec::new();
131        let mut frame_header = [0u8; FRAME_HEADER_LEN];
132        let mut page = vec![0u8; page_size as usize];
133
134        for index in 0..total_frames {
135            let at = WAL_HEADER_LEN as u64 + index as u64 * frame_len;
136            source.read_at(at, &mut frame_header)?;
137            let number = be32(&frame_header, 0);
138            let after_commit = be32(&frame_header, 4);
139            let frame_salt = (be32(&frame_header, 8), be32(&frame_header, 12));
140            if frame_salt != salt {
141                // a frame from an earlier generation of the log
142                break;
143            }
144
145            source.read_at(at + FRAME_HEADER_LEN as u64, &mut page)?;
146            let after_header = checksum(big_endian, running, &frame_header[..8]);
147            let computed = checksum(big_endian, after_header, &page);
148            if computed != (be32(&frame_header, 16), be32(&frame_header, 20)) {
149                // a torn or interrupted write: nothing from here on is
150                // trustworthy, including frames that look intact
151                break;
152            }
153            running = computed;
154
155            if number == 0 {
156                return Err(SqliteError::malformed(
157                    None,
158                    format!("wal frame {index} claims to hold page 0"),
159                ));
160            }
161            pending.push((number, at + FRAME_HEADER_LEN as u64));
162
163            if after_commit != 0 {
164                for (number, offset) in pending.drain(..) {
165                    pages.insert(number, offset);
166                }
167                committed_frames = index + 1;
168                page_count = after_commit;
169            }
170        }
171
172        // pages written past the committed size belong to a transaction
173        // that grew the database and then did not commit
174        pages.retain(|number, _| *number <= page_count);
175
176        Ok(Wal {
177            source,
178            page_size,
179            pages,
180            page_count,
181            committed_frames,
182            total_frames,
183        })
184    }
185
186    /// Whether the log contributes anything at all.
187    pub fn is_empty(&self) -> bool {
188        self.pages.is_empty()
189    }
190
191    /// Page size the log was written with. Zero for an empty log.
192    pub fn page_size(&self) -> u32 {
193        self.page_size
194    }
195
196    /// Database size in pages as of the last commit, zero for an empty log.
197    pub fn page_count(&self) -> u32 {
198        self.page_count
199    }
200
201    /// Frames the log holds, and how many of them survived validation and
202    /// ended at a commit.
203    pub fn frame_counts(&self) -> (u32, u32) {
204        (self.total_frames, self.committed_frames)
205    }
206
207    /// Read `buf.len()` bytes of page `number` starting `offset` bytes into
208    /// it, if the log holds a committed version of that page.
209    pub fn read_page_part(&self, number: u32, offset: u32, buf: &mut [u8]) -> Result<bool> {
210        let Some(at) = self.pages.get(&number) else {
211            return Ok(false);
212        };
213        let end = offset as u64 + buf.len() as u64;
214        if end > self.page_size as u64 {
215            return Err(SqliteError::malformed(
216                number,
217                format!(
218                    "a read of {} bytes at {offset} runs past the {} byte wal page",
219                    buf.len(),
220                    self.page_size
221                ),
222            ));
223        }
224        self.source.read_at(at + offset as u64, buf)?;
225        Ok(true)
226    }
227}
228
229/// SQLite's wal checksum: two running 32 bit words over the data read as
230/// pairs of 32 bit integers, in the byte order the header's magic selects.
231///
232/// The data length is always a multiple of eight in the places this is
233/// used, and anything left over is ignored rather than padded, which
234/// matches what sqlite does.
235fn checksum(big_endian: bool, start: (u32, u32), data: &[u8]) -> (u32, u32) {
236    let (mut s0, mut s1) = start;
237    for chunk in data.as_chunks::<8>().0 {
238        let (x, y) = if big_endian {
239            (
240                u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]),
241                u32::from_be_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]),
242            )
243        } else {
244            (
245                u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]),
246                u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]),
247            )
248        };
249        s0 = s0.wrapping_add(x).wrapping_add(s1);
250        s1 = s1.wrapping_add(y).wrapping_add(s0);
251    }
252    (s0, s1)
253}
254
255fn be32(bytes: &[u8], at: usize) -> u32 {
256    u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
257}
258
259/// A database file with its log read on top of it.
260///
261/// This is a `Source`, not a special case inside the reader: a page that
262/// the log holds a committed version of is served from there, everything
263/// else from the main file, and nothing above this line has to know that a
264/// log exists at all. The reader that walks b-trees is the same code
265/// either way.
266///
267/// Reads are split at page boundaries before being served, so a caller
268/// that asks for a range spanning two pages gets each half from wherever
269/// that page currently lives.
270pub struct WalSource<S: Source, W: Source> {
271    main: S,
272    log: Option<Wal<W>>,
273    page_size: u32,
274    /// Database size in pages, which the log's last commit can change: a
275    /// transaction may grow the database, and may shrink it.
276    page_count: u32,
277}
278
279impl<S: Source, W: Source> WalSource<S, W> {
280    /// Put `log` on top of `main`. Pass `None` when there is no log file,
281    /// which is the state sqlite leaves behind after a checkpoint.
282    pub fn new(main: S, log: Option<W>) -> Result<WalSource<S, W>> {
283        let mut header = [0u8; crate::header::HEADER_LEN];
284        if main.len()? < header.len() as u64 {
285            return Err(SqliteError::not_sqlite(
286                "the file is shorter than a database header",
287            ));
288        }
289        main.read_at(0, &mut header)?;
290        let page_size = crate::header::Header::parse(&header)?.page_size;
291
292        let log = match log {
293            Some(source) => {
294                let wal = Wal::open(source)?;
295                // this check comes before the empty case on purpose. a log
296                // whose page size disagrees with the database is one we
297                // have misread, and misreading it makes it look empty:
298                // frames land at the wrong offsets and fail their salt
299                // check. treating that as "no log" would drop real commits
300                // and then report the database as complete, which is the
301                // exact outcome this whole path exists to prevent. a page
302                // size of zero is the one honest empty case, a file with no
303                // header in it at all.
304                if wal.page_size() != 0 && wal.page_size() != page_size {
305                    return Err(SqliteError::malformed(
306                        None,
307                        format!(
308                            "the log is written in {} byte pages and the database in {page_size}",
309                            wal.page_size()
310                        ),
311                    ));
312                }
313                if wal.is_empty() {
314                    None
315                } else {
316                    Some(wal)
317                }
318            }
319            None => None,
320        };
321
322        let page_count = match &log {
323            Some(wal) => wal.page_count(),
324            None => u32::try_from(main.len()? / page_size as u64).unwrap_or(u32::MAX),
325        };
326
327        Ok(WalSource {
328            main,
329            log,
330            page_size,
331            page_count,
332        })
333    }
334
335    /// Whether any page is actually being served from the log.
336    pub fn has_log(&self) -> bool {
337        self.log.is_some()
338    }
339}
340
341impl<S: Source, W: Source> Source for WalSource<S, W> {
342    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
343        let Some(log) = &self.log else {
344            return self.main.read_at(offset, buf);
345        };
346
347        let page_size = self.page_size as u64;
348        let mut done = 0usize;
349        while done < buf.len() {
350            let at = offset + done as u64;
351            let page = (at / page_size) + 1;
352            let within = (at % page_size) as u32;
353            let take = ((page_size - within as u64) as usize).min(buf.len() - done);
354            let slice = &mut buf[done..done + take];
355
356            // a page number past u32 cannot be in the log, so such a read
357            // goes to the main file and fails there if it is out of range
358            let served = match u32::try_from(page) {
359                Ok(number) => log.read_page_part(number, within, slice)?,
360                Err(_) => false,
361            };
362            if !served {
363                self.main.read_at(at, slice)?;
364            }
365            done += take;
366        }
367        Ok(())
368    }
369
370    fn len(&self) -> Result<u64> {
371        match &self.log {
372            // the log's last commit is what says how large the database is
373            // now; the main file may be shorter, and may be longer
374            Some(_) => Ok(self.page_count as u64 * self.page_size as u64),
375            None => self.main.len(),
376        }
377    }
378
379    /// This source reads the log, so a wal mode database read through it is
380    /// complete by construction.
381    fn accounts_for_wal(&self) -> bool {
382        true
383    }
384}