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}