Skip to main content

akar_storage/
page.rs

1//! Page and Frame types for the buffer manager.
2
3use std::path::PathBuf;
4
5/// Default page size: 8KB.
6pub const DEFAULT_PAGE_SIZE: usize = 8192;
7
8/// A database page number (logical identifier).
9pub type PageNum = u64;
10
11/// A frame in the buffer pool holding a cached page.
12#[derive(Debug, Clone)]
13pub struct Frame {
14    /// The page number this frame holds.
15    pub page_num: PageNum,
16    /// Reference count (number of pins).
17    pub pin_count: u32,
18    /// Whether the page has been modified since loading.
19    pub is_dirty: bool,
20    /// Clock hand flag for Clock eviction policy.
21    pub clock_ref: bool,
22    /// The actual page data.
23    pub data: Vec<u8>,
24    /// NUMA node this frame resides on (0 = unknown / single-node).
25    pub numa_node: u32,
26}
27
28impl Frame {
29    pub fn new(page_num: PageNum, data: Vec<u8>) -> Self {
30        Self {
31            page_num,
32            pin_count: 0,
33            is_dirty: false,
34            clock_ref: true,
35            data,
36            numa_node: 0,
37        }
38    }
39
40    /// Pin the frame (increment reference count).
41    #[inline(always)]
42    pub fn pin(&mut self) {
43        self.pin_count += 1;
44    }
45
46    /// Unpin the frame (decrement reference count).
47    #[inline(always)]
48    pub fn unpin(&mut self) {
49        if self.pin_count > 0 {
50            self.pin_count -= 1;
51        }
52    }
53
54    /// Check if the frame is pinned (in use).
55    #[inline(always)]
56    pub fn is_pinned(&self) -> bool {
57        self.pin_count > 0
58    }
59
60    /// Mark frame as dirty (modified).
61    #[inline(always)]
62    pub fn mark_dirty(&mut self) {
63        self.is_dirty = true;
64    }
65}
66
67/// A file handle mapping logical page numbers to file offsets.
68#[derive(Debug, Clone)]
69pub struct FileHandle {
70    /// Path to the database file.
71    pub path: PathBuf,
72    /// Number of pages currently in the file.
73    pub num_pages: u64,
74    /// Page size in bytes.
75    pub page_size: usize,
76    /// Free space manager for reusing pages.
77    pub free_space_manager: Option<std::sync::Arc<crate::free_space_manager::FreeSpaceManager>>,
78}
79
80impl FileHandle {
81    pub fn new(path: PathBuf, page_size: usize) -> Self {
82        let num_pages = if path.exists() {
83            let len = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
84            len / page_size as u64
85        } else {
86            0
87        };
88        Self {
89            path,
90            num_pages,
91            page_size,
92            free_space_manager: None,
93        }
94    }
95
96    pub fn with_free_space_manager(mut self, fsm: std::sync::Arc<crate::free_space_manager::FreeSpaceManager>) -> Self {
97        self.free_space_manager = Some(fsm);
98        self
99    }
100
101    /// Get the file offset for a given page number.
102    #[inline(always)]
103    pub fn page_offset(&self, page_num: PageNum) -> u64 {
104        page_num * self.page_size as u64
105    }
106
107    /// Read a page from disk.
108    #[cfg(not(target_arch = "wasm32"))]
109    pub fn read_page(&self, page_num: PageNum) -> std::io::Result<Vec<u8>> {
110        use std::io::{Read, Seek, SeekFrom};
111        let mut file = std::fs::File::open(&self.path)?;
112        let offset = self.page_offset(page_num);
113        file.seek(SeekFrom::Start(offset))?;
114        let mut buf = vec![0u8; self.page_size];
115        file.read_exact(&mut buf)?;
116        Ok(buf)
117    }
118
119    #[cfg(target_arch = "wasm32")]
120    pub fn read_page(&self, page_num: PageNum) -> std::io::Result<Vec<u8>> {
121        // Mock implementation for Wasm
122        Ok(vec![0u8; self.page_size])
123    }
124
125    /// Write a page to disk.
126    #[cfg(not(target_arch = "wasm32"))]
127    pub fn write_page(&self, page_num: PageNum, data: &[u8]) -> std::io::Result<()> {
128        use std::io::{Seek, SeekFrom, Write};
129        let mut file = std::fs::OpenOptions::new()
130            .write(true)
131            .create(true)
132            .truncate(false)
133            .open(&self.path)?;
134        let offset = self.page_offset(page_num);
135        file.seek(SeekFrom::Start(offset))?;
136        file.write_all(data)?;
137        Ok(())
138    }
139
140    #[cfg(target_arch = "wasm32")]
141    pub fn write_page(&self, _page_num: PageNum, _data: &[u8]) -> std::io::Result<()> {
142        // Mock implementation for Wasm
143        Ok(())
144    }
145
146    /// Allocate a new page (extend the file or reuse from FSM).
147    pub fn allocate_page(&mut self) -> PageNum {
148        if let Some(fsm) = &self.free_space_manager {
149            if let Some(range) = fsm.pop_free_pages(1) {
150                return range.start_page_idx;
151            }
152        }
153        let page_num = self.num_pages;
154        self.num_pages += 1;
155        page_num
156    }
157
158    /// Free a page for reuse.
159    pub fn free_page(&self, page_num: PageNum) {
160        if let Some(fsm) = &self.free_space_manager {
161            fsm.add_uncheckpointed_free_pages(crate::free_space_manager::PageRange::new(page_num, 1));
162        }
163    }
164
165    /// Extend the file by `num_pages` pages, writing zero-filled data.
166    ///
167    /// Returns the starting page number of the newly extended region.
168    #[cfg(not(target_arch = "wasm32"))]
169    pub fn extend_file(&mut self, num_pages: u64) -> std::io::Result<PageNum> {
170        use std::io::Write;
171
172        let start_page = self.num_pages;
173        let extend_bytes = (num_pages as usize) * self.page_size;
174
175        let mut file = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
176
177        let zeros = vec![0u8; extend_bytes];
178        file.write_all(&zeros)?;
179        file.flush()?;
180
181        self.num_pages += num_pages;
182        Ok(start_page)
183    }
184
185    #[cfg(target_arch = "wasm32")]
186    pub fn extend_file(&mut self, num_pages: u64) -> std::io::Result<PageNum> {
187        let start_page = self.num_pages;
188        self.num_pages += num_pages;
189        Ok(start_page)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_frame_pin_unpin() {
199        let mut frame = Frame::new(0, vec![0u8; DEFAULT_PAGE_SIZE]);
200        assert!(!frame.is_pinned());
201        assert_eq!(frame.pin_count, 0);
202
203        frame.pin();
204        assert!(frame.is_pinned());
205        assert_eq!(frame.pin_count, 1);
206
207        frame.unpin();
208        assert!(!frame.is_pinned());
209    }
210
211    #[test]
212    fn test_frame_dirty() {
213        let mut frame = Frame::new(1, vec![0u8; DEFAULT_PAGE_SIZE]);
214        assert!(!frame.is_dirty);
215        frame.mark_dirty();
216        assert!(frame.is_dirty);
217    }
218
219    #[test]
220    fn test_page_offset() {
221        let fh = FileHandle::new(PathBuf::from("test.db"), DEFAULT_PAGE_SIZE);
222        assert_eq!(fh.page_offset(0), 0);
223        assert_eq!(fh.page_offset(1), 8192);
224        assert_eq!(fh.page_offset(5), 40960);
225    }
226
227    #[test]
228    fn test_file_handle_fsm() {
229        let path = PathBuf::from("test_fsm.db");
230        let _ = std::fs::remove_file(&path); // Cleanup before
231
232        let fsm = std::sync::Arc::new(crate::free_space_manager::FreeSpaceManager::new());
233        let mut fh = FileHandle::new(path.clone(), DEFAULT_PAGE_SIZE).with_free_space_manager(fsm.clone());
234
235        // Allocate pages
236        let p1 = fh.allocate_page(); // 0
237        let p2 = fh.allocate_page(); // 1
238        let p3 = fh.allocate_page(); // 2
239        assert_eq!(p1, 0);
240        assert_eq!(p2, 1);
241        assert_eq!(p3, 2);
242        assert_eq!(fh.num_pages, 3);
243
244        // Free page 1
245        fh.free_page(p2);
246
247        // Finalize checkpoint so uncheckpointed pages become reusable
248        fsm.finalize_checkpoint();
249
250        // Next allocation should reuse page 1
251        let p4 = fh.allocate_page();
252        assert_eq!(p4, 1);
253        assert_eq!(fh.num_pages, 3); // num_pages should not increase
254
255        // Next allocation should give a new page 3
256        let p5 = fh.allocate_page();
257        assert_eq!(p5, 3);
258        assert_eq!(fh.num_pages, 4);
259
260        let _ = std::fs::remove_file(&path); // Cleanup after
261    }
262}