Skip to main content

akar_storage/
page_manager.rs

1//! Page Manager — allocates and frees database pages.
2//!
3//! Wraps the `FreeSpaceManager` and `FileHandle` to provide page-level
4//! allocation and deallocation. On allocation, the FSM is consulted first;
5//! if no free pages are available, the file is extended.
6//!
7//! Ported from C++ `src/storage/page_manager.cpp`.
8
9use crate::free_space_manager::{FreeSpaceManager, INVALID_PAGE_IDX, PageRange};
10use std::path::PathBuf;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13
14/// Manages page allocation and deallocation for a database file.
15pub struct PageManager {
16    /// Path to the database file (for extending).
17    db_path: PathBuf,
18    /// Page size in bytes.
19    page_size: usize,
20    /// Total number of pages in the file (allocated + free).
21    total_pages: AtomicU64,
22    /// Free space manager tracks reusable pages.
23    fsm: Arc<FreeSpaceManager>,
24    /// Last allocated page index (monotonically increasing).
25    /// Used for extending the file when FSM has no free pages.
26    next_page: AtomicU64,
27}
28
29impl PageManager {
30    /// Create a new page manager.
31    ///
32    /// `existing_pages` is the number of pages already present in the file
33    /// (determined from file metadata on open).
34    pub fn new(db_path: PathBuf, page_size: usize, existing_pages: u64, fsm: Arc<FreeSpaceManager>) -> Self {
35        Self {
36            db_path,
37            page_size,
38            total_pages: AtomicU64::new(existing_pages),
39            fsm,
40            next_page: AtomicU64::new(existing_pages),
41        }
42    }
43
44    /// Page size in bytes.
45    pub fn page_size(&self) -> usize {
46        self.page_size
47    }
48
49    /// Total number of pages managed (allocated + free).
50    pub fn total_pages(&self) -> u64 {
51        self.total_pages.load(Ordering::Acquire)
52    }
53
54    /// Allocate a new page index.
55    ///
56    /// Tries the FSM first for a free page; if none available,
57    /// extends the file (incrementing `next_page`).
58    pub fn allocate_page(&self) -> u64 {
59        // Try FSM first
60        if let Some(range) = self.fsm.pop_free_pages(1) {
61            return range.start_page_idx;
62        }
63
64        // No free pages — extend the file
65        let page_idx = self.next_page.fetch_add(1, Ordering::SeqCst);
66        self.total_pages.fetch_add(1, Ordering::SeqCst);
67        page_idx
68    }
69
70    /// Free a previously allocated page.
71    ///
72    /// The page is added back to the FSM for future reuse.
73    pub fn free_page(&self, page_idx: u64) {
74        if page_idx == INVALID_PAGE_IDX {
75            return;
76        }
77        self.fsm.add_free_pages(PageRange::new(page_idx, 1));
78    }
79
80    /// Free a range of pages.
81    pub fn free_pages(&self, start_page_idx: u64, num_pages: u64) {
82        if num_pages == 0 || start_page_idx == INVALID_PAGE_IDX {
83            return;
84        }
85        self.fsm.add_free_pages(PageRange::new(start_page_idx, num_pages));
86    }
87
88    /// Access the underlying free space manager.
89    pub fn fsm(&self) -> &Arc<FreeSpaceManager> {
90        &self.fsm
91    }
92
93    /// Get the byte offset for a page index.
94    pub fn page_offset(&self, page_idx: u64) -> u64 {
95        page_idx * self.page_size as u64
96    }
97
98    /// Extend the database file by `num_pages` pages.
99    ///
100    /// This writes zero-filled pages to the end of the file.
101    /// Returns the starting page index of the newly extended region.
102    #[cfg(not(target_arch = "wasm32"))]
103    pub fn extend_file(&self, num_pages: u64) -> std::io::Result<u64> {
104        use std::io::Write;
105
106        let start_page = self.total_pages.load(Ordering::Acquire);
107        let extend_bytes = (num_pages as usize) * self.page_size;
108
109        let mut file = std::fs::OpenOptions::new()
110            .create(true)
111            .append(true)
112            .open(&self.db_path)?;
113
114        // Write zero-filled pages
115        let zeros = vec![0u8; extend_bytes];
116        file.write_all(&zeros)?;
117        file.flush()?;
118
119        self.total_pages.fetch_add(num_pages, Ordering::SeqCst);
120
121        Ok(start_page)
122    }
123
124    #[cfg(target_arch = "wasm32")]
125    pub fn extend_file(&self, num_pages: u64) -> std::io::Result<u64> {
126        let start_page = self.total_pages.load(Ordering::Acquire);
127        self.total_pages.fetch_add(num_pages, Ordering::SeqCst);
128        Ok(start_page)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::sync::Arc;
136
137    fn make_pm() -> (PageManager, tempfile::TempDir) {
138        let dir = tempfile::tempdir().unwrap();
139        let db_path = dir.path().join("test.db");
140        let fsm = Arc::new(FreeSpaceManager::new());
141        let pm = PageManager::new(db_path, 4096, 0, fsm);
142        (pm, dir)
143    }
144
145    #[test]
146    fn test_allocate_extends_file() {
147        let (pm, _dir) = make_pm();
148        assert_eq!(pm.total_pages(), 0);
149
150        let p1 = pm.allocate_page();
151        assert_eq!(p1, 0);
152        assert_eq!(pm.total_pages(), 1);
153
154        let p2 = pm.allocate_page();
155        assert_eq!(p2, 1);
156        assert_eq!(pm.total_pages(), 2);
157    }
158
159    #[test]
160    fn test_free_and_reuse() {
161        let (pm, _dir) = make_pm();
162
163        let p1 = pm.allocate_page();
164        let _p2 = pm.allocate_page();
165        assert_eq!(pm.total_pages(), 2);
166
167        // Free p1 — should be reused on next allocation
168        pm.free_page(p1);
169
170        let p3 = pm.allocate_page();
171        assert_eq!(p3, p1); // reused
172        assert_eq!(pm.total_pages(), 2); // no extension
173    }
174
175    #[test]
176    fn test_allocate_multiple_no_fsm() {
177        let (pm, _dir) = make_pm();
178
179        for i in 0..10 {
180            let p = pm.allocate_page();
181            assert_eq!(p, i);
182        }
183        assert_eq!(pm.total_pages(), 10);
184    }
185}