Skip to main content

highlandcows_isam/index/
pager.rs

1/// Pager — raw page read/write on the `.idx` B-tree index file.
2///
3/// ## Page 0 — metadata header (fixed layout)
4/// ```text
5/// [magic:        8 bytes ]   b"ISAMIDX\0"
6/// [page_size:    u32 LE  ]   always 4096
7/// [root_page_id: u32 LE  ]   initially 1; changes as tree grows
8/// [page_count:   u32 LE  ]   total allocated pages (including page 0)
9/// ```
10///
11/// ## Data pages (pages 1..page_count-1)
12///
13/// Each page is exactly `PAGE_SIZE` bytes.  The content is interpreted
14/// by the B-tree layer; the pager only handles raw byte I/O.
15use std::fs::{File, OpenOptions};
16use std::io::{Read, Seek, SeekFrom, Write};
17use std::path::Path;
18
19use crate::error::{IsamError, IsamResult};
20
21pub const PAGE_SIZE: usize = 4096;
22pub const MAGIC: &[u8; 8] = b"ISAMIDX\0";
23
24/// The decoded metadata from page 0.
25#[derive(Debug, Clone)]
26pub struct IndexMeta {
27    pub root_page_id: u32,
28    pub page_count: u32,
29    pub key_schema_version: u32,
30    pub val_schema_version: u32,
31    /// For secondary index files (`.sidx`): the schema version stored when
32    /// the index was last migrated via `migrate_index`.
33    /// Always 0 in primary index files (`.idx`).
34    /// Stored at bytes 28–31 of the metadata page; zero in older files
35    /// (backward-compatible default).
36    pub index_schema_version: u32,
37}
38
39pub struct Pager {
40    file: File,
41    pub meta: IndexMeta,
42}
43
44impl Pager {
45    /// Create a brand-new index file. Writes page 0 (metadata) and
46    /// allocates page 1 as an empty leaf (the initial root).
47    pub fn create(path: &Path) -> IsamResult<Self> {
48        let mut file = OpenOptions::new()
49            .read(true)
50            .write(true)
51            .create(true)
52            .truncate(true)
53            .open(path)?;
54
55        // Write page 0 — metadata.
56        let meta_bytes = Self::encode_meta(1, 2, 0, 0, 0); // root=1, 2 pages total, versions 0,0,0
57        file.write_all(&meta_bytes)?;
58
59        // Write page 1 — empty leaf root.
60        let leaf = Self::empty_leaf_page();
61        file.write_all(&leaf)?;
62
63        file.flush()?;
64
65        Ok(Self {
66            file,
67            meta: IndexMeta {
68                root_page_id: 1,
69                page_count: 2,
70                key_schema_version: 0,
71                val_schema_version: 0,
72                index_schema_version: 0,
73            },
74        })
75    }
76
77    /// Open an existing index file and read its metadata header.
78    pub fn open(path: &Path) -> IsamResult<Self> {
79        let file = OpenOptions::new().read(true).write(true).open(path)?;
80        let mut pager = Self {
81            file,
82            meta: IndexMeta {
83                root_page_id: 0,
84                page_count: 0,
85                key_schema_version: 0,
86                val_schema_version: 0,
87                index_schema_version: 0,
88            },
89        };
90        pager.read_meta()?;
91        Ok(pager)
92    }
93
94    // ------------------------------------------------------------------ //
95    //  Page I/O
96    // ------------------------------------------------------------------ //
97
98    /// Read page `id` into a fresh `PAGE_SIZE`-byte buffer.
99    pub fn read_page(&mut self, id: u32) -> IsamResult<Vec<u8>> {
100        let offset = id as u64 * PAGE_SIZE as u64;
101        self.file.seek(SeekFrom::Start(offset))?;
102        let mut buf = vec![0u8; PAGE_SIZE];
103        self.file.read_exact(&mut buf)?;
104        Ok(buf)
105    }
106
107    /// Write `data` (must be exactly `PAGE_SIZE` bytes) to page `id`.
108    pub fn write_page(&mut self, id: u32, data: &[u8]) -> IsamResult<()> {
109        assert_eq!(data.len(), PAGE_SIZE, "page must be exactly PAGE_SIZE bytes");
110        let offset = id as u64 * PAGE_SIZE as u64;
111        self.file.seek(SeekFrom::Start(offset))?;
112        self.file.write_all(data)?;
113        Ok(())
114    }
115
116    /// Allocate a new blank page at the end of the file and return its id.
117    pub fn alloc_page(&mut self) -> IsamResult<u32> {
118        let new_id = self.meta.page_count;
119        // Extend the file by one page of zeros.
120        let offset = new_id as u64 * PAGE_SIZE as u64;
121        self.file.seek(SeekFrom::Start(offset))?;
122        self.file.write_all(&[0u8; PAGE_SIZE])?;
123        self.meta.page_count += 1;
124        self.flush_meta()?;
125        Ok(new_id)
126    }
127
128    // ------------------------------------------------------------------ //
129    //  Metadata helpers
130    // ------------------------------------------------------------------ //
131
132    /// Re-read page 0 and populate `self.meta`.
133    fn read_meta(&mut self) -> IsamResult<()> {
134        self.file.seek(SeekFrom::Start(0))?;
135        let mut buf = [0u8; PAGE_SIZE];
136        self.file.read_exact(&mut buf)?;
137
138        if &buf[0..8] != MAGIC {
139            return Err(IsamError::CorruptIndex(
140                "bad magic number in index header".into(),
141            ));
142        }
143        let page_size = u32::from_le_bytes(buf[8..12].try_into().unwrap());
144        if page_size as usize != PAGE_SIZE {
145            return Err(IsamError::CorruptIndex(format!(
146                "page_size mismatch: file has {page_size}, library expects {PAGE_SIZE}"
147            )));
148        }
149        self.meta.root_page_id = u32::from_le_bytes(buf[12..16].try_into().unwrap());
150        self.meta.page_count = u32::from_le_bytes(buf[16..20].try_into().unwrap());
151        self.meta.key_schema_version = u32::from_le_bytes(buf[20..24].try_into().unwrap());
152        self.meta.val_schema_version = u32::from_le_bytes(buf[24..28].try_into().unwrap());
153        self.meta.index_schema_version = u32::from_le_bytes(buf[28..32].try_into().unwrap());
154        Ok(())
155    }
156
157    /// Write the current `self.meta` back to page 0.
158    pub fn flush_meta(&mut self) -> IsamResult<()> {
159        let bytes = Self::encode_meta(
160            self.meta.root_page_id,
161            self.meta.page_count,
162            self.meta.key_schema_version,
163            self.meta.val_schema_version,
164            self.meta.index_schema_version,
165        );
166        self.file.seek(SeekFrom::Start(0))?;
167        self.file.write_all(&bytes)?;
168        Ok(())
169    }
170
171    /// Flush OS write buffers.
172    pub fn flush(&mut self) -> IsamResult<()> {
173        self.file.flush()?;
174        Ok(())
175    }
176
177    /// Flush and fsync for durability.
178    pub fn fsync(&mut self) -> IsamResult<()> {
179        self.file.flush()?;
180        self.file.sync_all()?;
181        Ok(())
182    }
183
184    // ------------------------------------------------------------------ //
185    //  Static encoding helpers
186    // ------------------------------------------------------------------ //
187
188    fn encode_meta(root_page_id: u32, page_count: u32, key_schema_version: u32, val_schema_version: u32, index_schema_version: u32) -> Vec<u8> {
189        let mut buf = vec![0u8; PAGE_SIZE];
190        buf[0..8].copy_from_slice(MAGIC);
191        let page_size = PAGE_SIZE as u32;
192        buf[8..12].copy_from_slice(&page_size.to_le_bytes());
193        buf[12..16].copy_from_slice(&root_page_id.to_le_bytes());
194        buf[16..20].copy_from_slice(&page_count.to_le_bytes());
195        buf[20..24].copy_from_slice(&key_schema_version.to_le_bytes());
196        buf[24..28].copy_from_slice(&val_schema_version.to_le_bytes());
197        buf[28..32].copy_from_slice(&index_schema_version.to_le_bytes());
198        buf
199    }
200
201    /// Returns an empty leaf page (all zeros except the page_type byte).
202    pub fn empty_leaf_page() -> Vec<u8> {
203        let mut buf = vec![0u8; PAGE_SIZE];
204        buf[0] = PAGE_TYPE_LEAF;
205        // num_entries (u16) at offset 1 = 0
206        // next_leaf_id (u32) at offset 3 = 0  (0 = end of list)
207        buf
208    }
209
210    /// Returns an empty internal page.
211    pub fn empty_internal_page() -> Vec<u8> {
212        let mut buf = vec![0u8; PAGE_SIZE];
213        buf[0] = PAGE_TYPE_INTERNAL;
214        buf
215    }
216}
217
218// Page type constants shared with the B-tree layer.
219pub const PAGE_TYPE_LEAF: u8 = 0;
220pub const PAGE_TYPE_INTERNAL: u8 = 1;