Skip to main content

quantydb_sqlite/
lib.rs

1//! quantydb-sqlite: a reader for the SQLite 3 file format.
2//!
3//! This crate exists so that importing a `.sqlite` file needs no SQLite
4//! library, no C toolchain and no ffi (ADR-005). It only ever reads, and
5//! the type it reads through cannot write, so the file being imported
6//! cannot be damaged by us.
7//!
8//! The bar it is held to is the same one our own format reader meets: any
9//! input at all, including one built specifically to break it, produces
10//! either correct data or an error. Never a panic, never a row that is not
11//! in the file.
12//!
13//! ```no_run
14//! use quantydb_sqlite::{FileSource, Reader};
15//!
16//! let reader = Reader::open(FileSource::open("chinook.sqlite")?)?;
17//! println!("{} pages of {} bytes", reader.page_count(), reader.header().page_size);
18//! # Ok::<(), quantydb_sqlite::SqliteError>(())
19//! ```
20
21mod affinity;
22mod cell;
23mod ddl;
24mod error;
25mod header;
26mod page;
27mod record;
28mod schema;
29mod source;
30mod survey;
31mod tree;
32mod varint;
33mod wal;
34
35pub use affinity::{Affinity, StorageClass};
36pub use cell::Cell;
37pub use ddl::{parse_create_table, ColumnDef, Constraint, Generated, KeyColumn, TableDef};
38pub use error::{Result, SqliteError};
39pub use header::{Header, TextEncoding};
40pub use page::{BtreePage, PageKind};
41pub use record::{decode as decode_record, SqliteValue};
42pub use schema::{ObjectKind, Schema, SchemaObject};
43pub use source::{FileSource, SliceSource, Source};
44pub use survey::{Cell as MappedCell, ColumnSurvey, RowLayout, TableSurvey};
45pub use tree::{IndexScan, Row, Rows, TableRow, TableScan};
46pub use wal::{Wal, WalSource};
47
48/// A SQLite database file, opened for reading.
49pub struct Reader<S: Source> {
50    source: S,
51    header: Header,
52    page_count: u32,
53}
54
55impl<S: Source> Reader<S> {
56    /// Read and validate the header, then work out how many pages the file
57    /// really has.
58    pub fn open(source: S) -> Result<Reader<S>> {
59        let file_len = source.len()?;
60        if file_len < header::HEADER_LEN as u64 {
61            return Err(SqliteError::not_sqlite(format!(
62                "file is {file_len} bytes, shorter than the {} byte header",
63                header::HEADER_LEN
64            )));
65        }
66        let mut buf = [0u8; header::HEADER_LEN];
67        source.read_at(0, &mut buf)?;
68        let header = Header::parse(&buf)?;
69
70        // a wal mode database is complete in its main file only once the
71        // log has been checkpointed into it. the flag alone cannot tell us
72        // whether that happened, so the source is asked: one that looked
73        // beside the file, or one that reads the log itself, says yes, and
74        // anything else gets refused rather than quietly handed a database
75        // as it stood at the last checkpoint.
76        if header.wal_mode && !source.accounts_for_wal() {
77            return Err(SqliteError::unsupported(
78                "the database is in wal mode and nothing here accounts for its log, so the \
79                 main file may be missing committed data; open it with open_file, put a \
80                 WalSource around it, or check the log in first, for example with: \
81                 sqlite3 db.sqlite 'pragma wal_checkpoint(truncate)'",
82            ));
83        }
84
85        // the header may state the page count, and the file itself implies
86        // one. where they disagree the file is truncated, and finding that
87        // out here gives a clear message instead of an unexpected eof in
88        // the middle of a scan later on.
89        let pages_in_file = file_len / header.page_size as u64;
90        let page_count = match header.header_page_count {
91            Some(claimed) => {
92                if claimed as u64 > pages_in_file {
93                    return Err(SqliteError::malformed(
94                        None,
95                        format!(
96                            "the header says {claimed} pages of {} bytes, but the file only \
97                             holds {pages_in_file}",
98                            header.page_size
99                        ),
100                    ));
101                }
102                claimed
103            }
104            None => u32::try_from(pages_in_file).unwrap_or(u32::MAX),
105        };
106        if page_count == 0 {
107            return Err(SqliteError::malformed(
108                None,
109                "the file holds no complete page",
110            ));
111        }
112
113        Ok(Reader {
114            source,
115            header,
116            page_count,
117        })
118    }
119
120    pub fn header(&self) -> &Header {
121        &self.header
122    }
123
124    /// Pages in the database. Page numbers run from 1 to this value.
125    pub fn page_count(&self) -> u32 {
126        self.page_count
127    }
128
129    /// Read one whole page, reserved trailer included.
130    pub fn read_page(&self, number: u32) -> Result<Vec<u8>> {
131        if number == 0 || number > self.page_count {
132            return Err(SqliteError::malformed(
133                number,
134                format!(
135                    "page {number} is outside the file's 1..={}",
136                    self.page_count
137                ),
138            ));
139        }
140        let size = self.header.page_size as usize;
141        let offset = (number as u64 - 1) * self.header.page_size as u64;
142        let mut bytes = vec![0u8; size];
143        self.source.read_at(offset, &mut bytes)?;
144        Ok(bytes)
145    }
146
147    /// Read a page and parse it as a b-tree page.
148    pub fn btree_page(&self, number: u32) -> Result<BtreePage> {
149        let bytes = self.read_page(number)?;
150        // page 1 carries the database header in front of its b-tree header
151        let body_offset = if number == 1 { header::HEADER_LEN } else { 0 };
152        BtreePage::parse(
153            number,
154            bytes,
155            self.header.usable_size() as usize,
156            body_offset,
157        )
158    }
159
160    /// Parse one cell of a b-tree page, following overflow pages so the
161    /// payload comes back whole.
162    pub fn cell(&self, page: &BtreePage, index: usize) -> Result<Cell> {
163        let usable = self.header.usable_size() as usize;
164        let layout = cell::parse(page, index, usable, self.payload_limit())?;
165        Ok(match layout {
166            cell::CellLayout::TableInterior { child, rowid } => {
167                Cell::TableInterior { child, rowid }
168            }
169            cell::CellLayout::TableLeaf { rowid, payload } => Cell::TableLeaf {
170                rowid,
171                payload: self.assemble(payload)?,
172            },
173            cell::CellLayout::IndexInterior { child, payload } => Cell::IndexInterior {
174                child,
175                payload: self.assemble(payload)?,
176            },
177            cell::CellLayout::IndexLeaf { payload } => Cell::IndexLeaf {
178                payload: self.assemble(payload)?,
179            },
180        })
181    }
182
183    /// Read the database's own description of itself.
184    ///
185    /// The schema lives in a table rooted at page 1, so this is an ordinary
186    /// scan; it is separate only because everything else in the file is
187    /// found through it.
188    pub fn schema(&self) -> Result<Schema> {
189        let mut rows = Vec::new();
190        for row in self.table_scan(1)? {
191            rows.push(row?.values);
192        }
193        Schema::from_rows(rows, self.page_count)
194    }
195
196    /// Walk the table b-tree rooted at `root`, yielding rows in rowid order.
197    ///
198    /// The walk is lazy: a table larger than memory is read a page at a
199    /// time, and a caller that only wants the first row only pays for the
200    /// pages down to it.
201    pub fn table_scan(&self, root: u32) -> Result<TableScan<'_, S>> {
202        TableScan::new(self, root)
203    }
204
205    /// Walk the index b-tree rooted at `root`, yielding entries in key
206    /// order.
207    ///
208    /// This reads real indexes and, because a without rowid table is an
209    /// index b-tree holding whole rows, those tables too.
210    pub fn index_scan(&self, root: u32) -> Result<IndexScan<'_, S>> {
211        IndexScan::new(self, root)
212    }
213
214    /// Decode a record with the encoding this database uses.
215    ///
216    /// The same as the free function, minus the chance of passing the
217    /// wrong encoding: this one reads it off the header in hand.
218    pub fn decode_record(&self, payload: &[u8]) -> Result<Vec<SqliteValue>> {
219        record::decode(payload, self.header.text_encoding)
220    }
221
222    /// Rows of a table, whichever kind of table it is.
223    ///
224    /// Which walk applies is not a matter of taste: a rowid table is a
225    /// table b-tree and a without rowid table is an index b-tree, and the
226    /// root page says which one it is. Reading that off the page rather
227    /// than off the create statement means this works before any sql has
228    /// been parsed, and cannot disagree with the file.
229    pub fn rows(&self, root: u32) -> Result<Rows<'_, S>> {
230        let kind = self.btree_page(root)?.kind;
231        if kind.is_table() {
232            Ok(Rows::Rowid(self.table_scan(root)?))
233        } else {
234            Ok(Rows::Keyed(self.index_scan(root)?))
235        }
236    }
237
238    /// The largest payload the file could hold, used to reject length
239    /// varints that ask for more memory than the file could ever supply.
240    fn payload_limit(&self) -> u64 {
241        self.page_count as u64 * self.header.page_size as u64
242    }
243
244    /// Stitch a payload back together across its overflow chain.
245    ///
246    /// Every overflow page spends its first four bytes on the next page
247    /// number and the rest on payload. The loop is bounded by the payload
248    /// length rather than by the chain, so a chain that loops back on
249    /// itself terminates instead of hanging; it then fails the check below
250    /// because the file is malformed either way.
251    fn assemble(&self, payload: cell::PayloadRef<'_>) -> Result<Vec<u8>> {
252        if payload.overflow.is_none() {
253            return Ok(payload.local.to_vec());
254        }
255        let usable = self.header.usable_size() as usize;
256        let per_page = usable - 4;
257
258        let mut out = Vec::with_capacity(payload.total);
259        out.extend_from_slice(payload.local);
260        let mut next = payload.overflow.unwrap_or(0);
261        while out.len() < payload.total {
262            if next == 0 {
263                return Err(SqliteError::malformed(
264                    None,
265                    format!(
266                        "an overflow chain ended after {} of {} payload bytes",
267                        out.len(),
268                        payload.total
269                    ),
270                ));
271            }
272            let page = self.read_page(next)?;
273            next = u32::from_be_bytes([page[0], page[1], page[2], page[3]]);
274            let take = per_page.min(payload.total - out.len());
275            out.extend_from_slice(&page[4..4 + take]);
276        }
277        if next != 0 {
278            return Err(SqliteError::malformed(
279                None,
280                "an overflow chain continues past the end of its payload",
281            ));
282        }
283        Ok(out)
284    }
285}
286
287/// Open a database file by path, log included if there is one.
288///
289/// This is the call that does the right thing without the caller having to
290/// know what wal mode is: if a `-wal` file sits next to the database and
291/// holds committed frames, its pages are read on top of the main file, and
292/// if it does not, nothing changes.
293pub fn open_file(
294    path: impl AsRef<std::path::Path>,
295) -> Result<Reader<WalSource<FileSource, FileSource>>> {
296    let path = path.as_ref();
297    let main = FileSource::open(path)?;
298
299    let mut log_path = path.as_os_str().to_os_string();
300    log_path.push("-wal");
301    let log = match FileSource::open(std::path::Path::new(&log_path)) {
302        Ok(source) => Some(source),
303        // no log next to the database is the ordinary case, not a failure
304        Err(SqliteError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => None,
305        Err(e) => return Err(e),
306    };
307
308    Reader::open(WalSource::new(main, log)?)
309}