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, plus the `KIND_HNSW` cell tag for vector ANN
28/// indexes. All Phase 7 storage additions (VECTOR cells, JSON cells,
29/// HNSW index nodes) live inside the v4 envelope.
30/// - Version 5 (Phase 8c): adds the `KIND_FTS_POSTING` cell tag for
31/// persisted FTS posting lists. Bumped **on demand** — a database
32/// without any FTS index keeps writing v4. The first save with at
33/// least one FTS index attached writes v5 instead. Decoders accept
34/// both v4 and v5; v5 reading a v4-shaped DB just sees zero FTS
35/// indexes in `sqlrite_master`. See [Phase 8 plan Q10].
36pub const FORMAT_VERSION_V4: u16 = 4;
37pub const FORMAT_VERSION_V5: u16 = 5;
38/// The version a brand-new write defaults to when no FTS index forces
39/// a bump. Existing databases keep their on-disk version unchanged
40/// across reads + non-FTS writes; FTS-bearing saves switch to V5.
41pub const FORMAT_VERSION_BASELINE: u16 = FORMAT_VERSION_V4;
42
43/// Parsed header. `page_count` includes page 0 itself.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct DbHeader {
46 pub page_count: u32,
47 pub schema_root_page: u32,
48 /// On-disk format version this header carries. Tracked explicitly
49 /// so save can preserve a v4 file as v4 (no FTS) or bump it to v5
50 /// (FTS present), per Phase 8c's on-demand bump strategy.
51 pub format_version: u16,
52}
53
54/// Encodes the header into a `PAGE_SIZE`-sized buffer.
55pub fn encode_header(h: &DbHeader) -> [u8; PAGE_SIZE] {
56 let mut buf = [0u8; PAGE_SIZE];
57 buf[0..16].copy_from_slice(MAGIC);
58 buf[16..18].copy_from_slice(&h.format_version.to_le_bytes());
59 buf[18..20].copy_from_slice(&(PAGE_SIZE as u16).to_le_bytes());
60 buf[20..24].copy_from_slice(&h.page_count.to_le_bytes());
61 buf[24..28].copy_from_slice(&h.schema_root_page.to_le_bytes());
62 buf
63}
64
65/// Decodes the header from a `PAGE_SIZE`-sized buffer. Returns an error if
66/// magic bytes, format version, or page size don't match what we wrote.
67/// Both V4 and V5 are accepted; the result's `format_version` echoes
68/// what was on disk so a no-op resave preserves it.
69pub fn decode_header(buf: &[u8]) -> Result<DbHeader> {
70 if buf.len() != PAGE_SIZE {
71 return Err(SQLRiteError::Internal(format!(
72 "header buffer length {} != PAGE_SIZE {PAGE_SIZE}",
73 buf.len()
74 )));
75 }
76 if &buf[0..16] != MAGIC {
77 return Err(SQLRiteError::General(
78 "file is not a SQLRite database (bad magic bytes)".to_string(),
79 ));
80 }
81 let version = u16::from_le_bytes(buf[16..18].try_into().unwrap());
82 if version != FORMAT_VERSION_V4 && version != FORMAT_VERSION_V5 {
83 return Err(SQLRiteError::General(format!(
84 "unsupported SQLRite format version {version}; this build understands \
85 {FORMAT_VERSION_V4} and {FORMAT_VERSION_V5}"
86 )));
87 }
88 let page_size = u16::from_le_bytes(buf[18..20].try_into().unwrap()) as usize;
89 if page_size != PAGE_SIZE {
90 return Err(SQLRiteError::General(format!(
91 "unsupported page size {page_size}; this build expects {PAGE_SIZE}"
92 )));
93 }
94 let page_count = u32::from_le_bytes(buf[20..24].try_into().unwrap());
95 let schema_root_page = u32::from_le_bytes(buf[24..28].try_into().unwrap());
96 Ok(DbHeader {
97 page_count,
98 schema_root_page,
99 format_version: version,
100 })
101}