Skip to main content

sqlrite/sql/pager/
header.rs

1//! Database file header (page 0).
2//!
3//! The first 28 bytes of every `.sqlrite` file identify the format and point
4//! at the schema catalog. The rest of page 0 is reserved for future use.
5
6use crate::error::{Result, SQLRiteError};
7use crate::sql::pager::page::PAGE_SIZE;
8
9/// File magic. Distinct from SQLite's `"SQLite format 3\0"` so the formats
10/// can't be confused on inspection.
11pub const MAGIC: &[u8; 16] = b"SQLRiteFormat\0\0\0";
12
13/// On-disk format revision. Bump when the page layout changes incompatibly.
14///
15/// History:
16/// - Version 1 (Phases 2 / 3a / 3b): schema catalog and table data were
17///   opaque bincode blobs chained across typed payload pages.
18/// - Version 2 (Phases 3c / 3d): tables are stored as cell-based B-Trees;
19///   the schema catalog is itself a table called `sqlrite_master` with
20///   four columns `(name, sql, rootpage, last_rowid)`.
21/// - Version 3 (Phase 3e): `sqlrite_master` gains a `type` column
22///   (first), distinguishing `'table'` and `'index'` rows; secondary
23///   indexes persist as their own cell-based B-Trees whose cells use
24///   the new `KIND_INDEX` format.
25/// - Version 4 (Phase 7): cell encoding gains the `KIND_VECTOR` value
26///   tag (length-prefixed dense f32 array) for the new `VECTOR(N)`
27///   column type. Per the Phase 7 plan (`docs/phase-7-plan.md` Q8),
28///   later Phase 7 sub-phases (JSON, HNSW indexes) will add their own
29///   value/cell tags inside this same v4 envelope — no v5 mid-Phase-7.
30pub const FORMAT_VERSION: u16 = 4;
31
32/// Parsed header. `page_count` includes page 0 itself.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct DbHeader {
35    pub page_count: u32,
36    pub schema_root_page: u32,
37}
38
39/// Encodes the header into a `PAGE_SIZE`-sized buffer.
40pub fn encode_header(h: &DbHeader) -> [u8; PAGE_SIZE] {
41    let mut buf = [0u8; PAGE_SIZE];
42    buf[0..16].copy_from_slice(MAGIC);
43    buf[16..18].copy_from_slice(&FORMAT_VERSION.to_le_bytes());
44    buf[18..20].copy_from_slice(&(PAGE_SIZE as u16).to_le_bytes());
45    buf[20..24].copy_from_slice(&h.page_count.to_le_bytes());
46    buf[24..28].copy_from_slice(&h.schema_root_page.to_le_bytes());
47    buf
48}
49
50/// Decodes the header from a `PAGE_SIZE`-sized buffer. Returns an error if
51/// magic bytes, format version, or page size don't match what we wrote.
52pub fn decode_header(buf: &[u8]) -> Result<DbHeader> {
53    if buf.len() != PAGE_SIZE {
54        return Err(SQLRiteError::Internal(format!(
55            "header buffer length {} != PAGE_SIZE {PAGE_SIZE}",
56            buf.len()
57        )));
58    }
59    if &buf[0..16] != MAGIC {
60        return Err(SQLRiteError::General(
61            "file is not a SQLRite database (bad magic bytes)".to_string(),
62        ));
63    }
64    let version = u16::from_le_bytes(buf[16..18].try_into().unwrap());
65    if version != FORMAT_VERSION {
66        return Err(SQLRiteError::General(format!(
67            "unsupported SQLRite format version {version}; this build understands {FORMAT_VERSION}"
68        )));
69    }
70    let page_size = u16::from_le_bytes(buf[18..20].try_into().unwrap()) as usize;
71    if page_size != PAGE_SIZE {
72        return Err(SQLRiteError::General(format!(
73            "unsupported page size {page_size}; this build expects {PAGE_SIZE}"
74        )));
75    }
76    let page_count = u32::from_le_bytes(buf[20..24].try_into().unwrap());
77    let schema_root_page = u32::from_le_bytes(buf[24..28].try_into().unwrap());
78    Ok(DbHeader {
79        page_count,
80        schema_root_page,
81    })
82}