use crate::disk::DiskManager;
use crate::error::StorageError;
use crate::page::{iter_page_slots, Page, PageType, MAX_ROW_DATA_SIZE, PAGE_SIZE};
use crate::row::{row_is_v2, validate_row_format};
use crate::types::RowId;
use rustc_hash::FxHashMap;
use std::io;
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
pub const HEAP_MAGIC: &[u8; 5] = b"PHEAP";
pub const HEAP_FORMAT_VERSION: u16 = 2;
pub const HEAP_FORMAT_VERSION_WITH_OVERFLOW: u16 = 3;
const HEAP_SUPERBLOCK_OFFSET: usize = crate::page::PAGE_HEADER_SIZE;
const HEAP_SUPERBLOCK_FIRST_DATA_PAGE: u32 = 1;
const HEAP_SUPERBLOCK_VERSION_OFFSET: usize = HEAP_SUPERBLOCK_OFFSET + HEAP_MAGIC.len();
fn heap_superblock_page_versioned(version: u16) -> Page {
let page = Page::new(0, PageType::Meta);
let mut bytes = *page.as_bytes();
let mut pos = HEAP_SUPERBLOCK_OFFSET;
bytes[pos..pos + HEAP_MAGIC.len()].copy_from_slice(HEAP_MAGIC);
pos += HEAP_MAGIC.len();
bytes[pos..pos + 2].copy_from_slice(&version.to_le_bytes());
pos += 2;
bytes[pos..pos + 2].copy_from_slice(&0u16.to_le_bytes()); pos += 2;
bytes[pos..pos + 2].copy_from_slice(&(PAGE_SIZE as u16).to_le_bytes());
pos += 2;
bytes[pos..pos + 4].copy_from_slice(&HEAP_SUPERBLOCK_FIRST_DATA_PAGE.to_le_bytes());
let mut page = Page::from_bytes(&bytes).expect("fresh heap superblock is a valid page");
page.stamp_checksum();
page
}
fn heap_superblock_page() -> Page {
heap_superblock_page_versioned(HEAP_FORMAT_VERSION)
}
fn heap_first_data_page(buf: &[u8; PAGE_SIZE]) -> io::Result<(u32, u16)> {
if buf[4] != PageType::Meta as u8 {
return Ok((0, 1));
}
let mut pos = HEAP_SUPERBLOCK_OFFSET;
if &buf[pos..pos + HEAP_MAGIC.len()] != HEAP_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"bad heap superblock magic",
));
}
pos += HEAP_MAGIC.len();
let version = u16::from_le_bytes(buf[pos..pos + 2].try_into().expect("2-byte heap version"));
pos += 2;
if version != HEAP_FORMAT_VERSION && version != HEAP_FORMAT_VERSION_WITH_OVERFLOW {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported heap format version: {version}"),
));
}
pos += 2; let page_size = u16::from_le_bytes(buf[pos..pos + 2].try_into().expect("2-byte page size"));
pos += 2;
if page_size as usize != PAGE_SIZE {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported heap page size: {page_size}"),
));
}
let first_data_page = u32::from_le_bytes(
buf[pos..pos + 4]
.try_into()
.expect("4-byte first data page"),
);
Ok((first_data_page, version))
}
pub const DEFAULT_DIRTY_PAGE_BUDGET: usize = 256 * 1024 * 1024;
pub struct DirtyPageBudget {
pages: AtomicUsize,
limit_bytes: AtomicUsize,
rollback_pinned: AtomicBool,
}
impl Default for DirtyPageBudget {
fn default() -> Self {
Self::new(DEFAULT_DIRTY_PAGE_BUDGET)
}
}
impl DirtyPageBudget {
pub fn new(limit_bytes: usize) -> Self {
DirtyPageBudget {
pages: AtomicUsize::new(0),
limit_bytes: AtomicUsize::new(limit_bytes),
rollback_pinned: AtomicBool::new(false),
}
}
pub fn limit_bytes(&self) -> usize {
self.limit_bytes.load(Ordering::Relaxed)
}
pub fn set_limit_bytes(&self, limit_bytes: usize) {
self.limit_bytes.store(limit_bytes, Ordering::Relaxed);
}
pub fn charged_pages(&self) -> usize {
self.pages.load(Ordering::Relaxed)
}
pub fn set_rollback_pinned(&self, pinned: bool) {
self.rollback_pinned.store(pinned, Ordering::Relaxed);
}
pub fn is_rollback_pinned(&self) -> bool {
self.rollback_pinned.load(Ordering::Relaxed)
}
fn try_charge_page(&self) -> bool {
let limit_pages = self.limit_bytes.load(Ordering::Relaxed) / PAGE_SIZE;
if self.pages.fetch_add(1, Ordering::Relaxed) >= limit_pages {
self.pages.fetch_sub(1, Ordering::Relaxed);
return false;
}
true
}
fn force_charge(&self, pages: usize) {
self.pages.fetch_add(pages, Ordering::Relaxed);
}
fn release(&self, pages: usize) {
if pages == 0 {
return;
}
let _ = self
.pages
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |charged| {
Some(charged.saturating_sub(pages))
});
}
}
struct HotPage {
page_id: u32,
page: Page,
dirty: bool,
}
pub struct HeapFile {
disk: DiskManager,
first_data_page: u32,
pages_with_space: Vec<u32>,
in_free_list: Vec<bool>,
mmap_ptr: Option<(*const u8, usize)>,
hot_page: Option<HotPage>,
dirty_buffer: FxHashMap<u32, Page>,
dirty_budget: Arc<DirtyPageBudget>,
free_overflow_pages: Vec<u32>,
heap_version: u16,
}
impl HeapFile {
pub fn create(path: &Path) -> io::Result<Self> {
let mut disk = DiskManager::create(path)?;
let page_id = disk.allocate_page()?;
debug_assert_eq!(page_id, 0);
let superblock = heap_superblock_page();
disk.write_page(0, superblock.as_bytes())?;
disk.flush()?;
Ok(HeapFile {
disk,
first_data_page: HEAP_SUPERBLOCK_FIRST_DATA_PAGE,
pages_with_space: Vec::new(),
in_free_list: Vec::new(),
mmap_ptr: None,
hot_page: None,
dirty_buffer: FxHashMap::default(),
dirty_budget: Arc::new(DirtyPageBudget::default()),
free_overflow_pages: Vec::new(),
heap_version: HEAP_FORMAT_VERSION,
})
}
pub fn open(path: &Path) -> io::Result<Self> {
Self::open_disk(DiskManager::open(path)?)
}
pub fn open_read_only(path: &Path) -> io::Result<Self> {
Self::open_disk(DiskManager::open_read_only(path)?)
}
fn open_disk(mut disk: DiskManager) -> io::Result<Self> {
let read_only = disk.is_read_only();
let num_pages = disk.num_pages();
let (first_data_page, heap_version) = if num_pages == 0 {
(0, HEAP_FORMAT_VERSION)
} else {
let page0 = disk.read_page(0)?;
heap_first_data_page(&page0)?
};
let mut pages_with_space = Vec::new();
let mut in_free_list = vec![false; num_pages as usize];
for i in first_data_page..num_pages {
if let Ok(buf) = disk.read_page(i) {
if buf[4] == PageType::Overflow as u8 {
continue;
}
if buf[4] == 0 {
if read_only {
continue;
}
let mut fresh = Page::new(i, PageType::Data);
fresh.stamp_checksum();
let _ = disk.write_page(i, fresh.as_bytes());
pages_with_space.push(i);
in_free_list[i as usize] = true;
continue;
}
let page = Page::from_bytes_verified(&buf).map_err(io::Error::from)?;
for (_slot, row) in iter_page_slots(&buf) {
validate_row_format(row)?;
}
if page.free_space() > 64 {
pages_with_space.push(i);
in_free_list[i as usize] = true;
}
}
}
Ok(HeapFile {
disk,
first_data_page,
pages_with_space,
in_free_list,
mmap_ptr: None,
hot_page: None,
dirty_buffer: FxHashMap::default(),
dirty_budget: Arc::new(DirtyPageBudget::default()),
free_overflow_pages: Vec::new(),
heap_version,
})
}
pub fn set_dirty_budget(&mut self, budget: Arc<DirtyPageBudget>) {
let held = self.dirty_buffer.len();
self.dirty_budget.release(held);
budget.force_charge(held);
self.dirty_budget = budget;
}
pub fn dirty_page_count(&self) -> usize {
self.dirty_buffer.len()
}
pub fn format_version(&self) -> u16 {
if self.first_data_page == 0 {
1
} else {
self.heap_version
}
}
pub fn first_data_page(&self) -> u32 {
self.first_data_page
}
#[inline]
fn is_in_free_list(&self, page_id: u32) -> bool {
self.in_free_list
.get(page_id as usize)
.copied()
.unwrap_or(false)
}
#[inline]
fn mark_not_free(&mut self, page_id: u32) {
if let Some(slot) = self.in_free_list.get_mut(page_id as usize) {
*slot = false;
}
}
#[inline]
fn mark_free(&mut self, page_id: u32) {
let idx = page_id as usize;
if idx >= self.in_free_list.len() {
self.in_free_list.resize(idx + 1, false);
}
self.in_free_list[idx] = true;
}
fn park_hot_page(&mut self) -> io::Result<()> {
if !self.hot_page.as_ref().is_some_and(|hot| hot.dirty) {
self.hot_page = None;
return Ok(());
}
if !self.dirty_budget.try_charge_page() {
if self.dirty_budget.is_rollback_pinned() {
return Err(io::Error::new(
io::ErrorKind::OutOfMemory,
StorageError::TransactionTooLarge {
pages: self.dirty_budget.charged_pages(),
limit_bytes: self.dirty_budget.limit_bytes(),
},
));
}
self.flush_all_dirty()?;
self.hot_page = None;
return Ok(());
}
if let Some(hot) = self.hot_page.take() {
if self.dirty_buffer.insert(hot.page_id, hot.page).is_some() {
self.dirty_budget.release(1);
}
}
Ok(())
}
pub fn flush_hot_page(&mut self) -> io::Result<()> {
self.flush_all_dirty()
}
pub fn flush_all_dirty(&mut self) -> io::Result<()> {
if let Some(hot) = self.hot_page.as_mut() {
if hot.dirty {
hot.page.stamp_checksum();
self.disk.write_page(hot.page_id, hot.page.as_bytes())?;
hot.dirty = false;
}
}
if !self.dirty_buffer.is_empty() {
let drained: Vec<(u32, Page)> = self.dirty_buffer.drain().collect();
self.dirty_budget.release(drained.len());
for (mut page, page_id) in drained.into_iter().map(|(id, p)| (p, id)) {
page.stamp_checksum();
self.disk.write_page(page_id, page.as_bytes())?;
}
}
Ok(())
}
fn ensure_hot(&mut self, page_id: u32) -> io::Result<()> {
if let Some(hot) = &self.hot_page {
if hot.page_id == page_id {
return Ok(());
}
}
self.park_hot_page()?;
if let Some(page) = self.dirty_buffer.remove(&page_id) {
self.dirty_budget.release(1);
self.hot_page = Some(HotPage {
page_id,
page,
dirty: true,
});
return Ok(());
}
if let Some((ptr, len)) = self.mmap_ptr {
let offset = page_id as usize * PAGE_SIZE;
if offset + PAGE_SIZE <= len {
let page_bytes = unsafe { std::slice::from_raw_parts(ptr.add(offset), PAGE_SIZE) };
if let Some(page) = Page::from_bytes(page_bytes) {
self.hot_page = Some(HotPage {
page_id,
page,
dirty: false,
});
return Ok(());
}
}
}
let buf = self.disk.read_page(page_id)?;
let page = Page::from_bytes_verified(&buf).map_err(io::Error::from)?;
self.hot_page = Some(HotPage {
page_id,
page,
dirty: false,
});
Ok(())
}
fn install_fresh_hot(&mut self, page_id: u32, page: Page) -> io::Result<()> {
self.park_hot_page()?;
self.hot_page = Some(HotPage {
page_id,
page,
dirty: true,
});
Ok(())
}
pub fn enable_mmap(&mut self) {
if self.mmap_ptr.is_some() {
return;
}
if let Err(e) = self.flush_all_dirty() {
tracing::warn!(error = %e, "flush failed before mmap enable");
}
let num_pages = self.disk.num_pages();
if num_pages == 0 {
return;
}
let file_len = num_pages as usize * PAGE_SIZE;
use std::os::unix::io::AsRawFd;
let fd = self.disk.file_ref().as_raw_fd();
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
file_len,
libc::PROT_READ,
libc::MAP_PRIVATE,
fd,
0,
)
};
if ptr != libc::MAP_FAILED {
self.mmap_ptr = Some((ptr as *const u8, file_len));
}
}
pub fn disable_mmap(&mut self) {
if let Some((ptr, len)) = self.mmap_ptr.take() {
unsafe {
libc::munmap(ptr as *mut libc::c_void, len);
}
}
}
#[inline]
fn check_row_size(row_data: &[u8]) -> io::Result<()> {
if row_data.len() > MAX_ROW_DATA_SIZE {
return Err(StorageError::RowTooLarge {
size: row_data.len(),
max: MAX_ROW_DATA_SIZE,
}
.into());
}
Ok(())
}
fn ensure_heap_v3(&mut self) -> io::Result<()> {
if self.first_data_page == 0 || self.heap_version >= HEAP_FORMAT_VERSION_WITH_OVERFLOW {
return Ok(());
}
let mut buf = self.disk.read_page(0)?;
buf[HEAP_SUPERBLOCK_VERSION_OFFSET..HEAP_SUPERBLOCK_VERSION_OFFSET + 2]
.copy_from_slice(&HEAP_FORMAT_VERSION_WITH_OVERFLOW.to_le_bytes());
let mut page = Page::from_bytes(&buf)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "corrupt heap superblock"))?;
page.stamp_checksum();
self.disk.write_page(0, page.as_bytes())?;
self.heap_version = HEAP_FORMAT_VERSION_WITH_OVERFLOW;
Ok(())
}
pub fn allocate_overflow_page(&mut self) -> io::Result<u32> {
if let Some(pid) = self.free_overflow_pages.pop() {
return Ok(pid);
}
self.disable_mmap();
self.disk.allocate_page()
}
pub fn write_overflow_page(
&mut self,
page_id: u32,
next_page: u32,
chunk: &[u8],
lsn: u64,
) -> io::Result<()> {
self.ensure_heap_v3()?;
if page_id >= self.disk.num_pages() {
self.disable_mmap();
while self.disk.num_pages() <= page_id {
self.disk.allocate_page()?;
}
}
let mut page = Page::new_overflow(page_id);
page.set_overflow_chunk(next_page, chunk);
if lsn > 0 {
page.set_lsn(lsn);
}
page.stamp_checksum();
self.disk.write_page(page_id, page.as_bytes())
}
pub fn overflow_page_lsn(&self, page_id: u32) -> u64 {
if page_id < self.disk.num_pages() {
if let Ok(buf) = self.disk.read_page(page_id) {
return crate::page::page_lsn(&buf);
}
}
0
}
pub fn overflow_chain_pages(&self, first_page: u32) -> io::Result<Vec<u32>> {
let mut pages = Vec::new();
let mut pid = first_page;
let max_steps = self.disk.num_pages() as usize + 1;
while pid != crate::page::OVERFLOW_CHAIN_END && pages.len() < max_steps {
if pid >= self.disk.num_pages() {
break;
}
pages.push(pid);
let buf = self.disk.read_page(pid)?;
if buf[4] != PageType::Overflow as u8 {
break;
}
pid = crate::page::overflow_next_from_bytes(&buf);
}
Ok(pages)
}
pub fn read_overflow_value(
&self,
stub: &crate::row::OverflowStub,
) -> crate::error::Result<Vec<u8>> {
let mut out = Vec::with_capacity(stub.total_len as usize);
let mut pid = stub.first_page;
let max_steps = self.disk.num_pages() as usize + 1;
let mut steps = 0usize;
while pid != crate::page::OVERFLOW_CHAIN_END {
if pid >= self.disk.num_pages() || steps > max_steps {
return Err(StorageError::OverflowCorrupt(format!(
"overflow chain from page {} runs off the file at page {pid}",
stub.first_page
)));
}
let buf = self.disk.read_page(pid).map_err(StorageError::Io)?;
Page::from_bytes_verified(&buf)?;
if buf[4] != PageType::Overflow as u8 {
return Err(StorageError::OverflowCorrupt(format!(
"overflow chain page {pid} is not an Overflow page"
)));
}
out.extend_from_slice(crate::page::overflow_chunk_from_bytes(&buf));
pid = crate::page::overflow_next_from_bytes(&buf);
steps += 1;
if out.len() as u64 > stub.total_len {
break;
}
}
if out.len() as u64 != stub.total_len {
return Err(StorageError::OverflowCorrupt(format!(
"overflow value length {} != stub total_len {}",
out.len(),
stub.total_len
)));
}
let crc = crc32fast::hash(&out);
if crc != stub.value_crc32 {
return Err(StorageError::OverflowCorrupt(format!(
"overflow value CRC32 mismatch: computed {crc:#010x}, stub {:#010x}",
stub.value_crc32
)));
}
Ok(out)
}
pub fn release_overflow_pages(&mut self, pages: &[u32]) {
self.free_overflow_pages.extend_from_slice(pages);
}
pub fn overflow_free_list_len(&self) -> usize {
self.free_overflow_pages.len()
}
pub fn num_pages(&self) -> u32 {
self.disk.num_pages()
}
pub fn sweep_unreferenced_overflow(
&mut self,
referenced: &std::collections::HashSet<u32>,
) -> io::Result<Vec<u32>> {
let already_free: std::collections::HashSet<u32> =
self.free_overflow_pages.iter().copied().collect();
let watermark = self.disk.num_pages();
let mut reclaimed = Vec::new();
for pid in self.first_data_page..watermark {
if referenced.contains(&pid) || already_free.contains(&pid) {
continue;
}
let buf = self.disk.read_page(pid)?;
if buf[4] == PageType::Overflow as u8 {
reclaimed.push(pid);
}
}
self.free_overflow_pages.extend_from_slice(&reclaimed);
Ok(reclaimed)
}
pub fn overflow_chunk_count(value_len: usize) -> usize {
value_len.div_ceil(crate::page::OVERFLOW_PAYLOAD_CAP).max(1)
}
pub fn insert(&mut self, row_data: &[u8]) -> io::Result<RowId> {
Self::check_row_size(row_data)?;
if let Some(hot) = self.hot_page.as_mut() {
if let Some(slot) = hot.page.insert(row_data) {
hot.dirty = true;
let page_id = hot.page_id;
let became_full = hot.page.free_space() < 64;
if became_full {
if let Some(pos) = self.pages_with_space.iter().position(|p| *p == page_id) {
self.pages_with_space.swap_remove(pos);
}
self.mark_not_free(page_id);
}
return Ok(RowId {
page_id,
slot_index: slot,
});
}
}
for idx in 0..self.pages_with_space.len() {
let page_id = self.pages_with_space[idx];
self.ensure_hot(page_id)?;
let hot = self.hot_page.as_mut().expect("ensure_hot guarantees Some");
if let Some(slot) = hot.page.insert(row_data) {
hot.dirty = true;
if hot.page.free_space() < 64 {
self.pages_with_space.swap_remove(idx);
self.mark_not_free(page_id);
}
return Ok(RowId {
page_id,
slot_index: slot,
});
}
}
self.disable_mmap();
let page_id = self.disk.allocate_page()?;
let mut page = Page::new(page_id, PageType::Data);
let slot = page.insert(row_data).ok_or_else(|| {
io::Error::from(StorageError::RowTooLarge {
size: row_data.len(),
max: MAX_ROW_DATA_SIZE,
})
})?;
if page.free_space() >= 64 {
self.pages_with_space.push(page_id);
self.mark_free(page_id);
}
self.install_fresh_hot(page_id, page)?;
Ok(RowId {
page_id,
slot_index: slot,
})
}
pub fn insert_at(&mut self, rid: RowId, row_data: &[u8]) -> io::Result<()> {
if rid.page_id >= self.disk.num_pages() {
self.disable_mmap();
while self.disk.num_pages() <= rid.page_id {
let pid = self.disk.allocate_page()?;
let mut empty = Page::new(pid, PageType::Data);
empty.stamp_checksum();
self.disk.write_page(pid, empty.as_bytes())?;
}
}
self.ensure_hot(rid.page_id)?;
let hot = self.hot_page.as_mut().expect("ensure_hot guarantees Some");
if hot.page.is_blank() {
hot.page = Page::new(rid.page_id, PageType::Data);
}
if !hot.page.insert_at_slot(rid.slot_index, row_data) {
return Err(io::Error::other(format!(
"replay: row does not fit at {rid:?} (page {} slot {})",
rid.page_id, rid.slot_index
)));
}
hot.dirty = true;
Ok(())
}
#[inline]
pub fn get(&self, rid: RowId) -> Option<Vec<u8>> {
if let Some(hot) = &self.hot_page {
if hot.page_id == rid.page_id {
return hot.page.get(rid.slot_index).map(|d| d.to_vec());
}
}
if let Some(page) = self.dirty_buffer.get(&rid.page_id) {
return page.get(rid.slot_index).map(|d| d.to_vec());
}
if let Some((ptr, len)) = self.mmap_ptr {
let offset = rid.page_id as usize * PAGE_SIZE;
if offset + PAGE_SIZE <= len {
let page_bytes = unsafe { std::slice::from_raw_parts(ptr.add(offset), PAGE_SIZE) };
if rid.slot_index >= crate::page::slot_count_from_page(page_bytes) {
return None;
}
return crate::page::slot_bytes_from_page(page_bytes, rid.slot_index)
.map(|row| row.to_vec());
}
}
let buf = self.disk.read_page(rid.page_id).ok()?;
let page = Page::from_bytes_verified(&buf).ok()?;
page.get(rid.slot_index).map(|d| d.to_vec())
}
pub fn delete(&mut self, rid: RowId) -> io::Result<()> {
self.ensure_hot(rid.page_id)?;
let hot = self.hot_page.as_mut().expect("ensure_hot guarantees Some");
hot.page.delete(rid.slot_index);
hot.dirty = true;
if !self.is_in_free_list(rid.page_id) {
self.pages_with_space.push(rid.page_id);
self.mark_free(rid.page_id);
}
Ok(())
}
#[inline]
pub fn delete_with_hook<F>(&mut self, rid: RowId, hook: F) -> io::Result<bool>
where
F: FnOnce(&[u8]),
{
self.ensure_hot(rid.page_id)?;
let found = {
let hot = self.hot_page.as_mut().expect("ensure_hot guarantees Some");
let has_slot = if let Some(bytes) = hot.page.get(rid.slot_index) {
hook(bytes);
true
} else {
false
};
if has_slot {
hot.page.delete(rid.slot_index);
hot.dirty = true;
}
has_slot
};
if found && !self.is_in_free_list(rid.page_id) {
self.pages_with_space.push(rid.page_id);
self.mark_free(rid.page_id);
}
Ok(found)
}
#[inline]
pub fn with_row_bytes_mut<F>(&mut self, rid: RowId, f: F) -> io::Result<bool>
where
F: FnOnce(&mut [u8]),
{
self.ensure_hot(rid.page_id)?;
let hot = self.hot_page.as_mut().expect("ensure_hot guarantees Some");
if let Some(bytes) = hot.page.slot_bytes_mut(rid.slot_index) {
if row_is_v2(bytes) {
return Ok(false);
}
f(bytes);
hot.dirty = true;
return Ok(true);
}
Ok(false)
}
#[inline]
pub fn patch_row_shrink<F>(&mut self, rid: RowId, f: F) -> io::Result<bool>
where
F: FnOnce(&mut [u8]) -> Option<u16>,
{
self.ensure_hot(rid.page_id)?;
let hot = self.hot_page.as_mut().expect("ensure_hot guarantees Some");
let Some(bytes) = hot.page.slot_bytes_mut(rid.slot_index) else {
return Ok(false);
};
if row_is_v2(bytes) {
return Ok(false);
}
let old_len = bytes.len();
let Some(new_len) = f(bytes) else {
return Ok(false);
};
if (new_len as usize) > old_len {
return Ok(false);
}
if (new_len as usize) != old_len {
hot.page.shrink_slot(rid.slot_index, new_len);
}
hot.dirty = true;
Ok(true)
}
#[inline]
pub fn with_row_bytes<R, F>(&mut self, rid: RowId, f: F) -> io::Result<Option<R>>
where
F: FnOnce(&[u8]) -> R,
{
self.ensure_hot(rid.page_id)?;
let hot = self.hot_page.as_ref().expect("ensure_hot guarantees Some");
if let Some(bytes) = hot.page.get(rid.slot_index) {
return Ok(Some(f(bytes)));
}
Ok(None)
}
#[inline]
pub fn scan_delete_matching<P, H>(&mut self, mut pred: P, mut hook: H) -> io::Result<u64>
where
P: FnMut(&[u8]) -> bool,
H: FnMut(RowId, &[u8]),
{
let num_pages = self.disk.num_pages();
if num_pages == 0 {
return Ok(0);
}
let mut count = 0u64;
for page_id in 0..num_pages {
self.ensure_hot(page_id)?;
let mut any_deleted = false;
{
let hot = self.hot_page.as_mut().expect("ensure_hot guarantees Some");
if hot.page.is_overflow() {
continue;
}
let slot_count = hot.page.slot_count();
for slot in 0..slot_count {
let should_delete = match hot.page.get(slot) {
Some(bytes) if pred(bytes) => {
hook(
RowId {
page_id,
slot_index: slot,
},
bytes,
);
true
}
_ => false,
};
if should_delete {
hot.page.delete(slot);
any_deleted = true;
count += 1;
}
}
if any_deleted {
hot.dirty = true;
}
}
if any_deleted && !self.is_in_free_list(page_id) {
self.pages_with_space.push(page_id);
self.mark_free(page_id);
}
}
Ok(count)
}
pub fn scan_patch_matching<P, M, H>(
&mut self,
mut pred: P,
mut try_mutate: M,
mut hook: H,
) -> io::Result<(u64, Vec<RowId>)>
where
P: FnMut(&[u8]) -> bool,
M: FnMut(&mut [u8]) -> Option<u16>,
H: FnMut(RowId, &[u8]),
{
let num_pages = self.disk.num_pages();
if num_pages == 0 {
return Ok((0, Vec::new()));
}
let mut count = 0u64;
let mut fallback: Vec<RowId> = Vec::new();
for page_id in 0..num_pages {
self.ensure_hot(page_id)?;
let hot = self.hot_page.as_mut().expect("ensure_hot guarantees Some");
if hot.page.is_overflow() {
continue;
}
let slot_count = hot.page.slot_count();
let mut any_mutated = false;
for slot in 0..slot_count {
let matches = match hot.page.get(slot) {
Some(bytes) => pred(bytes),
None => false,
};
if matches {
let rid = RowId {
page_id,
slot_index: slot,
};
if hot.page.get(slot).map(row_is_v2).unwrap_or(false) {
fallback.push(rid);
continue;
}
if let Some(bytes) = hot.page.slot_bytes_mut(slot) {
let old_len = bytes.len() as u16;
if let Some(new_len) = try_mutate(bytes) {
if new_len < old_len {
hot.page.shrink_slot(slot, new_len);
}
if let Some(final_bytes) = hot.page.get(slot) {
hook(rid, final_bytes);
}
any_mutated = true;
count += 1;
} else {
fallback.push(rid);
}
}
}
}
if any_mutated {
hot.dirty = true;
}
}
Ok((count, fallback))
}
pub fn update(&mut self, rid: RowId, row_data: &[u8]) -> io::Result<RowId> {
Self::check_row_size(row_data)?;
self.ensure_hot(rid.page_id)?;
{
let hot = self.hot_page.as_mut().expect("ensure_hot guarantees Some");
if hot.page.update(rid.slot_index, row_data) {
hot.dirty = true;
return Ok(rid);
}
}
self.delete(rid)?;
self.insert(row_data)
}
pub fn scan(&self) -> impl Iterator<Item = (RowId, Vec<u8>)> + '_ {
let hot_view = self
.hot_page
.as_ref()
.map(|hot| (hot.page_id, *hot.page.as_bytes()));
(0..self.disk.num_pages()).flat_map(move |page_id| {
if let Some(page) = self.dirty_buffer.get(&page_id) {
let entries: Vec<_> = page
.iter()
.map(|(slot, data)| {
(
RowId {
page_id,
slot_index: slot,
},
data.to_vec(),
)
})
.collect();
return entries.into_iter();
}
let entries: Vec<_> = match &hot_view {
Some((hid, hbytes)) if *hid == page_id => iter_page_slots(hbytes.as_slice())
.map(|(slot, data)| {
(
RowId {
page_id,
slot_index: slot,
},
data.to_vec(),
)
})
.collect(),
_ => self
.disk
.read_page(page_id)
.ok()
.and_then(|buf| Page::from_bytes(&buf))
.map(|page| {
page.iter()
.map(|(slot, data)| {
(
RowId {
page_id,
slot_index: slot,
},
data.to_vec(),
)
})
.collect()
})
.unwrap_or_default(),
};
entries.into_iter()
})
}
#[inline]
pub fn try_for_each_row<F>(&self, mut f: F)
where
F: FnMut(RowId, &[u8]) -> std::ops::ControlFlow<()>,
{
use std::ops::ControlFlow;
let num_pages = self.disk.num_pages();
if num_pages == 0 {
return;
}
let hot_view: Option<(u32, &[u8; PAGE_SIZE])> = self
.hot_page
.as_ref()
.map(|hot| (hot.page_id, hot.page.as_bytes()));
if let Some((ptr, len)) = self.mmap_ptr {
let mapped = unsafe { std::slice::from_raw_parts(ptr, len) };
let pages_in_map = len / PAGE_SIZE;
let limit = num_pages.min(pages_in_map as u32);
'outer: for page_id in 0..limit {
if let Some(page) = self.dirty_buffer.get(&page_id) {
for (slot, data) in iter_page_slots(page.as_bytes()) {
if let ControlFlow::Break(()) = f(
RowId {
page_id,
slot_index: slot,
},
data,
) {
break 'outer;
}
}
continue;
}
let page_bytes: &[u8] = match hot_view {
Some((hid, hbytes)) if hid == page_id => hbytes.as_slice(),
_ => {
let offset = page_id as usize * PAGE_SIZE;
&mapped[offset..offset + PAGE_SIZE]
}
};
for (slot, data) in iter_page_slots(page_bytes) {
if let ControlFlow::Break(()) = f(
RowId {
page_id,
slot_index: slot,
},
data,
) {
break 'outer;
}
}
}
if let Some((hid, hbytes)) = hot_view {
if hid >= limit && hid < num_pages && !self.dirty_buffer.contains_key(&hid) {
for (slot, data) in iter_page_slots(hbytes) {
if let ControlFlow::Break(()) = f(
RowId {
page_id: hid,
slot_index: slot,
},
data,
) {
return;
}
}
}
}
for page_id in limit..num_pages {
if let Some(page) = self.dirty_buffer.get(&page_id) {
for (slot, data) in iter_page_slots(page.as_bytes()) {
if let ControlFlow::Break(()) = f(
RowId {
page_id,
slot_index: slot,
},
data,
) {
return;
}
}
}
}
return;
}
use std::os::unix::io::AsRawFd;
let fd = self.disk.file_ref().as_raw_fd();
let file_len = (num_pages as usize) * PAGE_SIZE;
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
file_len,
libc::PROT_READ,
libc::MAP_PRIVATE,
fd,
0,
)
};
if ptr != libc::MAP_FAILED {
let mapped = unsafe { std::slice::from_raw_parts(ptr as *const u8, file_len) };
'outer: for page_id in 0..num_pages {
if let Some(page) = self.dirty_buffer.get(&page_id) {
for (slot, data) in iter_page_slots(page.as_bytes()) {
if let ControlFlow::Break(()) = f(
RowId {
page_id,
slot_index: slot,
},
data,
) {
break 'outer;
}
}
continue;
}
let page_bytes: &[u8] = match hot_view {
Some((hid, hbytes)) if hid == page_id => hbytes.as_slice(),
_ => {
let offset = page_id as usize * PAGE_SIZE;
&mapped[offset..offset + PAGE_SIZE]
}
};
for (slot, data) in iter_page_slots(page_bytes) {
if let ControlFlow::Break(()) = f(
RowId {
page_id,
slot_index: slot,
},
data,
) {
break 'outer;
}
}
}
unsafe {
libc::munmap(ptr, file_len);
}
} else {
'outer: for page_id in 0..num_pages {
if let Some(page) = self.dirty_buffer.get(&page_id) {
for (slot, data) in iter_page_slots(page.as_bytes()) {
if let ControlFlow::Break(()) = f(
RowId {
page_id,
slot_index: slot,
},
data,
) {
break 'outer;
}
}
continue;
}
if let Some((hid, hbytes)) = hot_view {
if hid == page_id {
for (slot, data) in iter_page_slots(hbytes) {
if let ControlFlow::Break(()) = f(
RowId {
page_id,
slot_index: slot,
},
data,
) {
break 'outer;
}
}
continue;
}
}
let buf = match self.disk.read_page(page_id) {
Ok(b) => b,
Err(_) => continue,
};
if let Some(page) = Page::from_bytes(&buf) {
for (slot, data) in page.iter() {
if let ControlFlow::Break(()) = f(
RowId {
page_id,
slot_index: slot,
},
data,
) {
break 'outer;
}
}
}
}
}
}
#[inline]
pub fn for_each_row<F>(&self, mut f: F)
where
F: FnMut(RowId, &[u8]),
{
let num_pages = self.disk.num_pages();
if num_pages == 0 {
return;
}
let hot_view: Option<(u32, &[u8; PAGE_SIZE])> = self
.hot_page
.as_ref()
.map(|hot| (hot.page_id, hot.page.as_bytes()));
if let Some((ptr, len)) = self.mmap_ptr {
let mapped = unsafe { std::slice::from_raw_parts(ptr, len) };
let pages_in_map = len / PAGE_SIZE;
let limit = num_pages.min(pages_in_map as u32);
for page_id in 0..limit {
if let Some(page) = self.dirty_buffer.get(&page_id) {
for (slot, data) in iter_page_slots(page.as_bytes()) {
f(
RowId {
page_id,
slot_index: slot,
},
data,
);
}
continue;
}
let page_bytes: &[u8] = match hot_view {
Some((hid, hbytes)) if hid == page_id => hbytes.as_slice(),
_ => {
let offset = page_id as usize * PAGE_SIZE;
&mapped[offset..offset + PAGE_SIZE]
}
};
for (slot, data) in iter_page_slots(page_bytes) {
f(
RowId {
page_id,
slot_index: slot,
},
data,
);
}
}
if let Some((hid, hbytes)) = hot_view {
if hid >= limit && hid < num_pages && !self.dirty_buffer.contains_key(&hid) {
for (slot, data) in iter_page_slots(hbytes) {
f(
RowId {
page_id: hid,
slot_index: slot,
},
data,
);
}
}
}
for page_id in limit..num_pages {
if let Some(page) = self.dirty_buffer.get(&page_id) {
for (slot, data) in iter_page_slots(page.as_bytes()) {
f(
RowId {
page_id,
slot_index: slot,
},
data,
);
}
}
}
return;
}
use std::os::unix::io::AsRawFd;
let fd = self.disk.file_ref().as_raw_fd();
let file_len = (num_pages as usize) * PAGE_SIZE;
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
file_len,
libc::PROT_READ,
libc::MAP_PRIVATE,
fd,
0,
)
};
if ptr != libc::MAP_FAILED {
let mapped = unsafe { std::slice::from_raw_parts(ptr as *const u8, file_len) };
for page_id in 0..num_pages {
if let Some(page) = self.dirty_buffer.get(&page_id) {
for (slot, data) in iter_page_slots(page.as_bytes()) {
f(
RowId {
page_id,
slot_index: slot,
},
data,
);
}
continue;
}
let page_bytes: &[u8] = match hot_view {
Some((hid, hbytes)) if hid == page_id => hbytes.as_slice(),
_ => {
let offset = page_id as usize * PAGE_SIZE;
&mapped[offset..offset + PAGE_SIZE]
}
};
for (slot, data) in iter_page_slots(page_bytes) {
f(
RowId {
page_id,
slot_index: slot,
},
data,
);
}
}
unsafe {
libc::munmap(ptr, file_len);
}
} else {
for page_id in 0..num_pages {
if let Some(page) = self.dirty_buffer.get(&page_id) {
for (slot, data) in iter_page_slots(page.as_bytes()) {
f(
RowId {
page_id,
slot_index: slot,
},
data,
);
}
continue;
}
if let Some((hid, hbytes)) = hot_view {
if hid == page_id {
for (slot, data) in iter_page_slots(hbytes) {
f(
RowId {
page_id,
slot_index: slot,
},
data,
);
}
continue;
}
}
let buf = match self.disk.read_page(page_id) {
Ok(b) => b,
Err(_) => continue,
};
if let Some(page) = Page::from_bytes(&buf) {
for (slot, data) in page.iter() {
f(
RowId {
page_id,
slot_index: slot,
},
data,
);
}
}
}
}
}
pub fn max_page_lsn(&self) -> u64 {
use crate::page::page_lsn;
let mut max_lsn = 0u64;
if let Some(hot) = &self.hot_page {
max_lsn = max_lsn.max(hot.page.lsn());
}
for page in self.dirty_buffer.values() {
max_lsn = max_lsn.max(page.lsn());
}
for page_id in 0..self.disk.num_pages() {
if self.hot_page.as_ref().is_some_and(|h| h.page_id == page_id) {
continue;
}
if self.dirty_buffer.contains_key(&page_id) {
continue;
}
if let Ok(buf) = self.disk.read_page(page_id) {
max_lsn = max_lsn.max(page_lsn(&buf));
}
}
max_lsn
}
pub fn page_lsn(&self, page_id: u32) -> u64 {
use crate::page::page_lsn;
if let Some(hot) = &self.hot_page {
if hot.page_id == page_id {
return hot.page.lsn();
}
}
if let Some(page) = self.dirty_buffer.get(&page_id) {
return page.lsn();
}
if page_id < self.disk.num_pages() {
if let Ok(buf) = self.disk.read_page(page_id) {
return page_lsn(&buf);
}
}
0
}
pub fn stamp_all_pages_min_lsn(&mut self, barrier_lsn: u64) -> io::Result<()> {
if barrier_lsn == 0 {
return Ok(());
}
let n = self.disk.num_pages();
for page_id in 0..n {
self.set_page_lsn(page_id, barrier_lsn)?;
}
Ok(())
}
pub fn set_page_lsn(&mut self, page_id: u32, lsn: u64) -> io::Result<()> {
self.ensure_hot(page_id)?;
if let Some(hot) = self.hot_page.as_mut() {
if hot.page.lsn() < lsn {
hot.page.set_lsn(lsn);
hot.dirty = true;
}
}
Ok(())
}
pub fn verify_integrity(&self) -> crate::error::Result<()> {
for page_id in 0..self.disk.num_pages() {
let buf = self.disk.read_page(page_id)?;
Page::from_bytes_verified(&buf)?;
}
Ok(())
}
pub fn flush(&mut self) -> io::Result<()> {
self.flush_hot_page()?;
self.disk.flush()
}
pub fn discard_dirty(&mut self) {
self.hot_page = None;
self.dirty_budget.release(self.dirty_buffer.len());
self.dirty_buffer.clear();
self.disable_mmap();
}
}
impl Drop for HeapFile {
fn drop(&mut self) {
let _ = self.flush_all_dirty();
self.disable_mmap();
}
}
unsafe impl Send for HeapFile {}
unsafe impl Sync for HeapFile {}
#[cfg(test)]
mod tests {
use super::*;
use crate::row::{decode_row, encode_row};
use crate::types::*;
fn user_schema() -> Schema {
Schema {
table_name: "users".into(),
columns: vec![
ColumnDef {
name: "name".into(),
type_id: TypeId::Str,
required: true,
position: 0,
},
ColumnDef {
name: "age".into(),
type_id: TypeId::Int,
required: false,
position: 1,
},
],
}
}
fn temp_heap(name: &str) -> (HeapFile, std::path::PathBuf) {
let path = std::env::temp_dir().join(format!("powdb_heap_{name}_{}", std::process::id()));
let heap = HeapFile::create(&path).unwrap();
(heap, path)
}
#[test]
fn test_insert_and_get() {
let (mut heap, path) = temp_heap("basic");
let schema = user_schema();
let row = vec![Value::Str("Alice".into()), Value::Int(30)];
let encoded = encode_row(&schema, &row);
let rid = heap.insert(&encoded).unwrap();
let data = heap.get(rid).unwrap();
let decoded = decode_row(&schema, &data);
assert_eq!(decoded[0], Value::Str("Alice".into()));
assert_eq!(decoded[1], Value::Int(30));
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_scan_all_rows() {
let (mut heap, path) = temp_heap("scan");
let schema = user_schema();
for i in 0..100 {
let row = vec![Value::Str(format!("user_{i}")), Value::Int(i)];
heap.insert(&encode_row(&schema, &row)).unwrap();
}
let all: Vec<_> = heap.scan().collect();
assert_eq!(all.len(), 100);
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_delete_row() {
let (mut heap, path) = temp_heap("del");
let schema = user_schema();
let r1 = heap
.insert(&encode_row(
&schema,
&[Value::Str("A".into()), Value::Int(1)],
))
.unwrap();
let r2 = heap
.insert(&encode_row(
&schema,
&[Value::Str("B".into()), Value::Int(2)],
))
.unwrap();
heap.delete(r1).unwrap();
assert!(heap.get(r1).is_none());
assert!(heap.get(r2).is_some());
assert_eq!(heap.scan().count(), 1);
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_update_row() {
let (mut heap, path) = temp_heap("upd");
let schema = user_schema();
let row = vec![Value::Str("Alice".into()), Value::Int(30)];
let rid = heap.insert(&encode_row(&schema, &row)).unwrap();
let new_row = vec![Value::Str("Alice".into()), Value::Int(31)];
let new_rid = heap.update(rid, &encode_row(&schema, &new_row)).unwrap();
let decoded = decode_row(&schema, &heap.get(new_rid).unwrap());
assert_eq!(decoded[1], Value::Int(31));
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_scan_delete_matching_basic() {
let (mut heap, path) = temp_heap("sdm_basic");
let schema = user_schema();
let mut inserted = Vec::new();
for i in 0..500 {
let row = vec![Value::Str(format!("user_{i:04}")), Value::Int(i)];
inserted.push(heap.insert(&encode_row(&schema, &row)).unwrap());
}
let layout = crate::row::RowLayout::new(&schema);
let mut deleted_keys: Vec<i64> = Vec::new();
let count = heap
.scan_delete_matching(
|data| match crate::row::decode_column(&schema, &layout, data, 1) {
Value::Int(i) => i % 2 == 0,
_ => false,
},
|_rid, data| {
if let Value::Int(i) = crate::row::decode_column(&schema, &layout, data, 1) {
deleted_keys.push(i);
}
},
)
.unwrap();
assert_eq!(count, 250); assert_eq!(deleted_keys.len(), 250);
deleted_keys.sort_unstable();
let expected: Vec<i64> = (0..500).step_by(2).collect();
assert_eq!(deleted_keys, expected);
let remaining: Vec<_> = heap.scan().collect();
assert_eq!(remaining.len(), 250);
for (_, data) in &remaining {
let row = decode_row(&schema, data);
if let Value::Int(i) = &row[1] {
assert_eq!(i % 2, 1);
}
}
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_scan_delete_matching_all_or_none() {
let (mut heap, path) = temp_heap("sdm_edge");
let schema = user_schema();
for i in 0..50 {
let row = vec![Value::Str(format!("u{i}")), Value::Int(i)];
heap.insert(&encode_row(&schema, &row)).unwrap();
}
let c = heap.scan_delete_matching(|_| false, |_rid, _| {}).unwrap();
assert_eq!(c, 0);
assert_eq!(heap.scan().count(), 50);
let c = heap.scan_delete_matching(|_| true, |_rid, _| {}).unwrap();
assert_eq!(c, 50);
assert_eq!(heap.scan().count(), 0);
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_inserts_with_mmap_enabled_all_readable() {
let (mut heap, path) = temp_heap("mmap_inserts");
let schema = user_schema();
let mut rids = Vec::new();
for i in 0..200 {
let row = vec![Value::Str(format!("seed_{i:04}")), Value::Int(i)];
rids.push((i, heap.insert(&encode_row(&schema, &row)).unwrap()));
}
heap.enable_mmap();
for i in 200..2000 {
let row = vec![Value::Str(format!("seed_{i:04}")), Value::Int(i)];
rids.push((i, heap.insert(&encode_row(&schema, &row)).unwrap()));
}
for (i, rid) in &rids {
let data = heap.get(*rid).unwrap_or_else(|| panic!("row {i} missing"));
let decoded = decode_row(&schema, &data);
assert_eq!(decoded[0], Value::Str(format!("seed_{i:04}")));
assert_eq!(decoded[1], Value::Int(*i));
}
assert_eq!(heap.scan().count(), 2000);
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_oversized_insert_returns_error_and_heap_survives() {
let (mut heap, path) = temp_heap("oversized_insert");
let schema = user_schema();
let big = vec![0xABu8; PAGE_SIZE];
let err = heap.insert(&big).unwrap_err();
assert!(
err.to_string().contains("row too large"),
"unexpected error: {err}"
);
let rid = heap
.insert(&encode_row(
&schema,
&[Value::Str("ok".into()), Value::Int(1)],
))
.unwrap();
assert!(heap.get(rid).is_some());
assert_eq!(heap.scan().count(), 1);
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_insert_at_max_row_size_succeeds() {
use crate::page::MAX_ROW_DATA_SIZE;
let (mut heap, path) = temp_heap("max_row");
let exact = vec![0x42u8; MAX_ROW_DATA_SIZE];
let rid = heap.insert(&exact).unwrap();
assert_eq!(heap.get(rid).unwrap().len(), MAX_ROW_DATA_SIZE);
let over = vec![0x42u8; MAX_ROW_DATA_SIZE + 1];
let err = heap.insert(&over).unwrap_err();
assert!(
err.to_string().contains("row too large"),
"unexpected error: {err}"
);
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_oversized_update_returns_error_and_row_intact() {
let (mut heap, path) = temp_heap("oversized_update");
let schema = user_schema();
let rid = heap
.insert(&encode_row(
&schema,
&[Value::Str("Alice".into()), Value::Int(30)],
))
.unwrap();
let old_bytes = heap.get(rid).unwrap();
let big = vec![0xCDu8; PAGE_SIZE];
let err = heap.update(rid, &big).unwrap_err();
assert!(
err.to_string().contains("row too large"),
"unexpected error: {err}"
);
assert_eq!(heap.get(rid).unwrap(), old_bytes, "old row must survive");
assert_eq!(heap.scan().count(), 1);
drop(heap);
std::fs::remove_file(&path).ok();
}
fn write_chain(heap: &mut HeapFile, value: &[u8]) -> crate::row::OverflowStub {
use crate::page::{OVERFLOW_CHAIN_END, OVERFLOW_PAYLOAD_CAP};
let n = value.len().div_ceil(OVERFLOW_PAYLOAD_CAP).max(1);
let mut pages = Vec::new();
for _ in 0..n {
pages.push(heap.allocate_overflow_page().unwrap());
}
for i in 0..n {
let start = i * OVERFLOW_PAYLOAD_CAP;
let end = (start + OVERFLOW_PAYLOAD_CAP).min(value.len());
let next = if i + 1 < n {
pages[i + 1]
} else {
OVERFLOW_CHAIN_END
};
heap.write_overflow_page(pages[i], next, &value[start..end], 0)
.unwrap();
}
crate::row::OverflowStub::new(value.len() as u64, pages[0], crc32fast::hash(value))
}
#[test]
fn test_overflow_chain_roundtrip_and_v3_bump() {
let (mut heap, path) = temp_heap("ovf_chain");
assert_eq!(heap.format_version(), HEAP_FORMAT_VERSION); let value = vec![0x7Eu8; 10_000]; let stub = write_chain(&mut heap, &value);
assert_eq!(
heap.format_version(),
HEAP_FORMAT_VERSION_WITH_OVERFLOW,
"first chain write must lazily bump the heap to v3"
);
let got = heap.read_overflow_value(&stub).unwrap();
assert_eq!(got, value);
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_overflow_crc_and_length_faults_are_typed_errors() {
let (mut heap, path) = temp_heap("ovf_crc");
let value = b"the whole out-of-line value".to_vec();
let mut stub = write_chain(&mut heap, &value);
assert_eq!(heap.read_overflow_value(&stub).unwrap(), value);
stub.value_crc32 ^= 0xFFFF_FFFF;
assert!(matches!(
heap.read_overflow_value(&stub),
Err(crate::error::StorageError::OverflowCorrupt(_))
));
let mut bad_len = crate::row::OverflowStub::new(
value.len() as u64 + 100,
stub.first_page,
crc32fast::hash(&value),
);
bad_len.value_crc32 = crc32fast::hash(&value);
assert!(matches!(
heap.read_overflow_value(&bad_len),
Err(crate::error::StorageError::OverflowCorrupt(_))
));
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_overflow_free_list_reuse() {
let (mut heap, path) = temp_heap("ovf_reuse");
let stub = write_chain(&mut heap, &vec![1u8; 9000]); let pages = heap.overflow_chain_pages(stub.first_page).unwrap();
assert_eq!(pages.len(), 3);
heap.release_overflow_pages(&pages);
assert_eq!(heap.overflow_free_list_len(), 3);
let reused: Vec<u32> = (0..3)
.map(|_| heap.allocate_overflow_page().unwrap())
.collect();
let mut a = reused.clone();
a.sort_unstable();
let mut b = pages.clone();
b.sort_unstable();
assert_eq!(
a, b,
"freed chain pages must be reused, not newly allocated"
);
assert_eq!(heap.overflow_free_list_len(), 0);
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_multi_page_span() {
let (mut heap, path) = temp_heap("multipage");
let schema = user_schema();
for i in 0..500 {
let row = vec![Value::Str(format!("user_{i:04}")), Value::Int(i)];
heap.insert(&encode_row(&schema, &row)).unwrap();
}
assert_eq!(heap.scan().count(), 500);
drop(heap);
std::fs::remove_file(&path).ok();
}
fn fill_until_budget_trips(heap: &mut HeapFile, rows: usize) -> Option<io::Error> {
let schema = user_schema();
for i in 0..rows {
let row = vec![Value::Str(format!("user_{i:06}")), Value::Int(i as i64)];
if let Err(e) = heap.insert(&encode_row(&schema, &row)) {
return Some(e);
}
}
None
}
#[test]
fn pinned_budget_refuses_growth_with_a_typed_error() {
let (mut heap, path) = temp_heap("dirty_budget_pinned");
let budget = Arc::new(DirtyPageBudget::new(8 * PAGE_SIZE));
budget.set_rollback_pinned(true);
heap.set_dirty_budget(Arc::clone(&budget));
let err = fill_until_budget_trips(&mut heap, 100_000)
.expect("an 8-page budget must refuse an unbounded pinned transaction");
assert_eq!(err.kind(), io::ErrorKind::OutOfMemory);
let typed = err
.get_ref()
.and_then(|source| source.downcast_ref::<StorageError>());
assert!(
matches!(typed, Some(StorageError::TransactionTooLarge { .. })),
"expected a typed TransactionTooLarge, got: {err}"
);
assert!(
err.to_string().starts_with("cannot "),
"message must survive server-side sanitization: {err}"
);
assert!(budget.charged_pages() <= 8);
heap.discard_dirty();
budget.set_rollback_pinned(false);
assert_eq!(budget.charged_pages(), 0);
assert!(fill_until_budget_trips(&mut heap, 2_000).is_none());
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn unpinned_budget_spills_instead_of_failing() {
let (mut heap, path) = temp_heap("dirty_budget_unpinned");
let budget = Arc::new(DirtyPageBudget::new(8 * PAGE_SIZE));
heap.set_dirty_budget(Arc::clone(&budget));
assert!(fill_until_budget_trips(&mut heap, 5_000).is_none());
assert!(budget.charged_pages() <= 8);
assert_eq!(heap.scan().count(), 5_000);
drop(heap);
std::fs::remove_file(&path).ok();
}
#[test]
fn budget_accounting_returns_to_zero_after_flush() {
let (mut heap, path) = temp_heap("dirty_budget_accounting");
let budget = Arc::new(DirtyPageBudget::default());
heap.set_dirty_budget(Arc::clone(&budget));
assert!(fill_until_budget_trips(&mut heap, 2_000).is_none());
assert!(budget.charged_pages() > 0, "inserts must charge the budget");
assert_eq!(budget.charged_pages(), heap.dirty_page_count());
heap.flush_all_dirty().unwrap();
assert_eq!(budget.charged_pages(), 0);
drop(heap);
std::fs::remove_file(&path).ok();
}
}