mod affinity;
mod cell;
mod ddl;
mod error;
mod header;
mod page;
mod record;
mod schema;
mod source;
mod survey;
mod tree;
mod varint;
mod wal;
pub use affinity::{Affinity, StorageClass};
pub use cell::Cell;
pub use ddl::{parse_create_table, ColumnDef, Constraint, Generated, KeyColumn, TableDef};
pub use error::{Result, SqliteError};
pub use header::{Header, TextEncoding};
pub use page::{BtreePage, PageKind};
pub use record::{decode as decode_record, SqliteValue};
pub use schema::{ObjectKind, Schema, SchemaObject};
pub use source::{FileSource, SliceSource, Source};
pub use survey::{Cell as MappedCell, ColumnSurvey, RowLayout, TableSurvey};
pub use tree::{IndexScan, Row, Rows, TableRow, TableScan};
pub use wal::{Wal, WalSource};
pub struct Reader<S: Source> {
source: S,
header: Header,
page_count: u32,
}
impl<S: Source> Reader<S> {
pub fn open(source: S) -> Result<Reader<S>> {
let file_len = source.len()?;
if file_len < header::HEADER_LEN as u64 {
return Err(SqliteError::not_sqlite(format!(
"file is {file_len} bytes, shorter than the {} byte header",
header::HEADER_LEN
)));
}
let mut buf = [0u8; header::HEADER_LEN];
source.read_at(0, &mut buf)?;
let header = Header::parse(&buf)?;
if header.wal_mode && !source.accounts_for_wal() {
return Err(SqliteError::unsupported(
"the database is in wal mode and nothing here accounts for its log, so the \
main file may be missing committed data; open it with open_file, put a \
WalSource around it, or check the log in first, for example with: \
sqlite3 db.sqlite 'pragma wal_checkpoint(truncate)'",
));
}
let pages_in_file = file_len / header.page_size as u64;
let page_count = match header.header_page_count {
Some(claimed) => {
if claimed as u64 > pages_in_file {
return Err(SqliteError::malformed(
None,
format!(
"the header says {claimed} pages of {} bytes, but the file only \
holds {pages_in_file}",
header.page_size
),
));
}
claimed
}
None => u32::try_from(pages_in_file).unwrap_or(u32::MAX),
};
if page_count == 0 {
return Err(SqliteError::malformed(
None,
"the file holds no complete page",
));
}
Ok(Reader {
source,
header,
page_count,
})
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn page_count(&self) -> u32 {
self.page_count
}
pub fn read_page(&self, number: u32) -> Result<Vec<u8>> {
if number == 0 || number > self.page_count {
return Err(SqliteError::malformed(
number,
format!(
"page {number} is outside the file's 1..={}",
self.page_count
),
));
}
let size = self.header.page_size as usize;
let offset = (number as u64 - 1) * self.header.page_size as u64;
let mut bytes = vec![0u8; size];
self.source.read_at(offset, &mut bytes)?;
Ok(bytes)
}
pub fn btree_page(&self, number: u32) -> Result<BtreePage> {
let bytes = self.read_page(number)?;
let body_offset = if number == 1 { header::HEADER_LEN } else { 0 };
BtreePage::parse(
number,
bytes,
self.header.usable_size() as usize,
body_offset,
)
}
pub fn cell(&self, page: &BtreePage, index: usize) -> Result<Cell> {
let usable = self.header.usable_size() as usize;
let layout = cell::parse(page, index, usable, self.payload_limit())?;
Ok(match layout {
cell::CellLayout::TableInterior { child, rowid } => {
Cell::TableInterior { child, rowid }
}
cell::CellLayout::TableLeaf { rowid, payload } => Cell::TableLeaf {
rowid,
payload: self.assemble(payload)?,
},
cell::CellLayout::IndexInterior { child, payload } => Cell::IndexInterior {
child,
payload: self.assemble(payload)?,
},
cell::CellLayout::IndexLeaf { payload } => Cell::IndexLeaf {
payload: self.assemble(payload)?,
},
})
}
pub fn schema(&self) -> Result<Schema> {
let mut rows = Vec::new();
for row in self.table_scan(1)? {
rows.push(row?.values);
}
Schema::from_rows(rows, self.page_count)
}
pub fn table_scan(&self, root: u32) -> Result<TableScan<'_, S>> {
TableScan::new(self, root)
}
pub fn index_scan(&self, root: u32) -> Result<IndexScan<'_, S>> {
IndexScan::new(self, root)
}
pub fn decode_record(&self, payload: &[u8]) -> Result<Vec<SqliteValue>> {
record::decode(payload, self.header.text_encoding)
}
pub fn rows(&self, root: u32) -> Result<Rows<'_, S>> {
let kind = self.btree_page(root)?.kind;
if kind.is_table() {
Ok(Rows::Rowid(self.table_scan(root)?))
} else {
Ok(Rows::Keyed(self.index_scan(root)?))
}
}
fn payload_limit(&self) -> u64 {
self.page_count as u64 * self.header.page_size as u64
}
fn assemble(&self, payload: cell::PayloadRef<'_>) -> Result<Vec<u8>> {
if payload.overflow.is_none() {
return Ok(payload.local.to_vec());
}
let usable = self.header.usable_size() as usize;
let per_page = usable - 4;
let mut out = Vec::with_capacity(payload.total);
out.extend_from_slice(payload.local);
let mut next = payload.overflow.unwrap_or(0);
while out.len() < payload.total {
if next == 0 {
return Err(SqliteError::malformed(
None,
format!(
"an overflow chain ended after {} of {} payload bytes",
out.len(),
payload.total
),
));
}
let page = self.read_page(next)?;
next = u32::from_be_bytes([page[0], page[1], page[2], page[3]]);
let take = per_page.min(payload.total - out.len());
out.extend_from_slice(&page[4..4 + take]);
}
if next != 0 {
return Err(SqliteError::malformed(
None,
"an overflow chain continues past the end of its payload",
));
}
Ok(out)
}
}
pub fn open_file(
path: impl AsRef<std::path::Path>,
) -> Result<Reader<WalSource<FileSource, FileSource>>> {
let path = path.as_ref();
let main = FileSource::open(path)?;
let mut log_path = path.as_os_str().to_os_string();
log_path.push("-wal");
let log = match FileSource::open(std::path::Path::new(&log_path)) {
Ok(source) => Some(source),
Err(SqliteError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => return Err(e),
};
Reader::open(WalSource::new(main, log)?)
}