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}
32
33pub struct Pager {
34    file: File,
35    pub meta: IndexMeta,
36}
37
38impl Pager {
39    /// Create a brand-new index file. Writes page 0 (metadata) and
40    /// allocates page 1 as an empty leaf (the initial root).
41    pub fn create(path: &Path) -> IsamResult<Self> {
42        let mut file = OpenOptions::new()
43            .read(true)
44            .write(true)
45            .create(true)
46            .truncate(true)
47            .open(path)?;
48
49        // Write page 0 — metadata.
50        let meta_bytes = Self::encode_meta(1, 2, 0, 0); // root=1, 2 pages total, versions 0,0
51        file.write_all(&meta_bytes)?;
52
53        // Write page 1 — empty leaf root.
54        let leaf = Self::empty_leaf_page();
55        file.write_all(&leaf)?;
56
57        file.flush()?;
58
59        Ok(Self {
60            file,
61            meta: IndexMeta {
62                root_page_id: 1,
63                page_count: 2,
64                key_schema_version: 0,
65                val_schema_version: 0,
66            },
67        })
68    }
69
70    /// Open an existing index file and read its metadata header.
71    pub fn open(path: &Path) -> IsamResult<Self> {
72        let file = OpenOptions::new().read(true).write(true).open(path)?;
73        let mut pager = Self {
74            file,
75            meta: IndexMeta {
76                root_page_id: 0,
77                page_count: 0,
78                key_schema_version: 0,
79                val_schema_version: 0,
80            },
81        };
82        pager.read_meta()?;
83        Ok(pager)
84    }
85
86    // ------------------------------------------------------------------ //
87    //  Page I/O
88    // ------------------------------------------------------------------ //
89
90    /// Read page `id` into a fresh `PAGE_SIZE`-byte buffer.
91    pub fn read_page(&mut self, id: u32) -> IsamResult<Vec<u8>> {
92        let offset = id as u64 * PAGE_SIZE as u64;
93        self.file.seek(SeekFrom::Start(offset))?;
94        let mut buf = vec![0u8; PAGE_SIZE];
95        self.file.read_exact(&mut buf)?;
96        Ok(buf)
97    }
98
99    /// Write `data` (must be exactly `PAGE_SIZE` bytes) to page `id`.
100    pub fn write_page(&mut self, id: u32, data: &[u8]) -> IsamResult<()> {
101        assert_eq!(data.len(), PAGE_SIZE, "page must be exactly PAGE_SIZE bytes");
102        let offset = id as u64 * PAGE_SIZE as u64;
103        self.file.seek(SeekFrom::Start(offset))?;
104        self.file.write_all(data)?;
105        Ok(())
106    }
107
108    /// Allocate a new blank page at the end of the file and return its id.
109    pub fn alloc_page(&mut self) -> IsamResult<u32> {
110        let new_id = self.meta.page_count;
111        // Extend the file by one page of zeros.
112        let offset = new_id as u64 * PAGE_SIZE as u64;
113        self.file.seek(SeekFrom::Start(offset))?;
114        self.file.write_all(&[0u8; PAGE_SIZE])?;
115        self.meta.page_count += 1;
116        self.flush_meta()?;
117        Ok(new_id)
118    }
119
120    // ------------------------------------------------------------------ //
121    //  Metadata helpers
122    // ------------------------------------------------------------------ //
123
124    /// Re-read page 0 and populate `self.meta`.
125    fn read_meta(&mut self) -> IsamResult<()> {
126        self.file.seek(SeekFrom::Start(0))?;
127        let mut buf = [0u8; PAGE_SIZE];
128        self.file.read_exact(&mut buf)?;
129
130        if &buf[0..8] != MAGIC {
131            return Err(IsamError::CorruptIndex(
132                "bad magic number in index header".into(),
133            ));
134        }
135        let page_size = u32::from_le_bytes(buf[8..12].try_into().unwrap());
136        if page_size as usize != PAGE_SIZE {
137            return Err(IsamError::CorruptIndex(format!(
138                "page_size mismatch: file has {page_size}, library expects {PAGE_SIZE}"
139            )));
140        }
141        self.meta.root_page_id = u32::from_le_bytes(buf[12..16].try_into().unwrap());
142        self.meta.page_count = u32::from_le_bytes(buf[16..20].try_into().unwrap());
143        self.meta.key_schema_version = u32::from_le_bytes(buf[20..24].try_into().unwrap());
144        self.meta.val_schema_version = u32::from_le_bytes(buf[24..28].try_into().unwrap());
145        Ok(())
146    }
147
148    /// Write the current `self.meta` back to page 0.
149    pub fn flush_meta(&mut self) -> IsamResult<()> {
150        let bytes = Self::encode_meta(
151            self.meta.root_page_id,
152            self.meta.page_count,
153            self.meta.key_schema_version,
154            self.meta.val_schema_version,
155        );
156        self.file.seek(SeekFrom::Start(0))?;
157        self.file.write_all(&bytes)?;
158        Ok(())
159    }
160
161    /// Flush OS write buffers.
162    pub fn flush(&mut self) -> IsamResult<()> {
163        self.file.flush()?;
164        Ok(())
165    }
166
167    /// Flush and fsync for durability.
168    pub fn fsync(&mut self) -> IsamResult<()> {
169        self.file.flush()?;
170        self.file.sync_all()?;
171        Ok(())
172    }
173
174    // ------------------------------------------------------------------ //
175    //  Static encoding helpers
176    // ------------------------------------------------------------------ //
177
178    fn encode_meta(root_page_id: u32, page_count: u32, key_schema_version: u32, val_schema_version: u32) -> Vec<u8> {
179        let mut buf = vec![0u8; PAGE_SIZE];
180        buf[0..8].copy_from_slice(MAGIC);
181        let page_size = PAGE_SIZE as u32;
182        buf[8..12].copy_from_slice(&page_size.to_le_bytes());
183        buf[12..16].copy_from_slice(&root_page_id.to_le_bytes());
184        buf[16..20].copy_from_slice(&page_count.to_le_bytes());
185        buf[20..24].copy_from_slice(&key_schema_version.to_le_bytes());
186        buf[24..28].copy_from_slice(&val_schema_version.to_le_bytes());
187        buf
188    }
189
190    /// Returns an empty leaf page (all zeros except the page_type byte).
191    pub fn empty_leaf_page() -> Vec<u8> {
192        let mut buf = vec![0u8; PAGE_SIZE];
193        buf[0] = PAGE_TYPE_LEAF;
194        // num_entries (u16) at offset 1 = 0
195        // next_leaf_id (u32) at offset 3 = 0  (0 = end of list)
196        buf
197    }
198
199    /// Returns an empty internal page.
200    pub fn empty_internal_page() -> Vec<u8> {
201        let mut buf = vec![0u8; PAGE_SIZE];
202        buf[0] = PAGE_TYPE_INTERNAL;
203        buf
204    }
205}
206
207// Page type constants shared with the B-tree layer.
208pub const PAGE_TYPE_LEAF: u8 = 0;
209pub const PAGE_TYPE_INTERNAL: u8 = 1;