AkurAI Build
Menu

Public repository / 9df51040bf1687e4dc8a91557f02df52e8d5cf3a

AkurAI-Framework / crates/storage/src/pager.rs

Latest change 59a461a1d8da66d64b852d3052f0b8b5dbfa5f26 - feat(storage): single-file pager — fixed pages, alloc, durable sync by Ólafur Búi Ólafsson

//! The pager: a single file viewed as an array of fixed-size pages.
//!
//! This is the only thing that touches the database file's bytes. It offers
//! four operations — read a page, write a page, allocate a new (zeroed) page at
//! the end, and `sync` to force everything durable. Higher layers never seek;
//! they speak in [`PageId`]s.

use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;

/// Bytes per page. 4 KiB matches the typical OS page and disk block, so a page
/// write maps to one block write where the platform allows it.
pub const PAGE_SIZE: usize = 4096;

/// A page's index in the file (0-based). Page N occupies bytes
/// `[N * PAGE_SIZE, (N+1) * PAGE_SIZE)`.
pub type PageId = u64;

/// A fixed-size page of bytes.
pub type Page = [u8; PAGE_SIZE];

/// Reads and writes fixed-size pages in a single file.
pub struct Pager {
    file: File,
}

impl Pager {
    /// Open the store at `path`, creating it if absent. The file is opened for
    /// reading and writing.
    pub fn open(path: impl AsRef<Path>) -> io::Result<Pager> {
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .truncate(false)
            .open(path)?;
        Ok(Pager { file })
    }

    /// Number of whole pages currently in the file. A trailing partial page
    /// (which a clean writer never creates) is not counted.
    pub fn page_count(&self) -> io::Result<u64> {
        Ok(self.file.metadata()?.len() / PAGE_SIZE as u64)
    }

    /// Append a fresh zeroed page and return its id.
    pub fn allocate(&mut self) -> io::Result<PageId> {
        let id = self.page_count()?;
        self.write_page(id, &[0u8; PAGE_SIZE])?;
        Ok(id)
    }

    /// Read page `id`. Errors if the page is past the end of the file.
    pub fn read_page(&mut self, id: PageId) -> io::Result<Page> {
        if id >= self.page_count()? {
            return Err(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                format!("page {id} is out of range"),
            ));
        }
        let mut buf = [0u8; PAGE_SIZE];
        self.file.seek(SeekFrom::Start(id * PAGE_SIZE as u64))?;
        self.file.read_exact(&mut buf)?;
        Ok(buf)
    }

    /// Write `data` to page `id`. `id` may be the current page count, which
    /// extends the file by one page; it may not skip past the end.
    pub fn write_page(&mut self, id: PageId, data: &Page) -> io::Result<()> {
        if id > self.page_count()? {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("cannot write page {id}: would leave a gap"),
            ));
        }
        self.file.seek(SeekFrom::Start(id * PAGE_SIZE as u64))?;
        self.file.write_all(data)
    }

    /// Flush all writes to durable storage (data + metadata). Call this at a
    /// commit point; without it, writes may sit in OS cache and be lost on a
    /// crash — exactly the failure mode that corrupts a database.
    pub fn sync(&self) -> io::Result<()> {
        self.file.sync_all()
    }
}