use crate::error::{ChiselError, Result};
use crate::page::{
self, PageType, CHECKSUM_OFFSET, DATA_PAGE_HEADER_SIZE, PAGE_ID_NONE, PAGE_SIZE,
};
use crate::page_cache::PageCache;
const ENTRY_SIZE: usize = 16;
pub const ENTRIES_PER_LEAF: usize = (CHECKSUM_OFFSET - DATA_PAGE_HEADER_SIZE) / ENTRY_SIZE;
const CHILD_PTR_SIZE: usize = 8;
const PTRS_PER_INTERIOR: usize = (CHECKSUM_OFFSET - DATA_PAGE_HEADER_SIZE) / CHILD_PTR_SIZE;
const MAX_DEPTH: u32 = 6;
const FLAG_LEAF: u8 = 0x01;
pub(crate) const FLAG_INTERIOR: u8 = 0x02;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HandleFlags {
Live,
Deleted,
Overflow,
}
impl HandleFlags {
fn to_u8(self) -> u8 {
match self {
HandleFlags::Live => 0x01,
HandleFlags::Deleted => 0x00,
HandleFlags::Overflow => 0x02,
}
}
fn from_u8(v: u8) -> HandleFlags {
match v {
0x01 => HandleFlags::Live,
0x02 => HandleFlags::Overflow,
_ => HandleFlags::Deleted,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HandleEntry {
pub page_id: u64,
pub slot_index: u16,
pub flags: HandleFlags,
pub tag: u32,
pub client_byte: u8,
}
pub struct HandleTable {
depth: u32, }
impl Default for HandleTable {
fn default() -> Self {
Self::new()
}
}
impl HandleTable {
pub fn new() -> HandleTable {
HandleTable { depth: 0 }
}
pub fn create_root(&mut self, cache: &mut PageCache) -> Result<u64> {
let page_id = cache.new_page()?;
debug_assert_ne!(
page_id, 0,
"handle-table pages must not use page id 0 (reserved as the zero-child sentinel)"
);
let buf = cache.get_mut(page_id)?;
buf.fill(0); buf[0] = PageType::HandleTable as u8;
buf[1] = FLAG_LEAF;
buf[2] = page::current_version(page::PageType::HandleTable); page::stamp_checksum(buf);
self.depth = 0;
Ok(page_id)
}
pub fn lookup(
&self,
cache: &mut PageCache,
root: u64,
handle: u64,
) -> Result<Option<HandleEntry>> {
if root == PAGE_ID_NONE {
return Ok(None);
}
let Some((leaf_page, index)) = self.find_leaf(cache, root, handle)? else {
return Ok(None);
};
let buf = cache.get(leaf_page)?;
let entry = Self::read_entry(buf, index);
if entry.flags == HandleFlags::Deleted {
Ok(None)
} else {
Ok(Some(entry))
}
}
pub fn insert(
&mut self,
cache: &mut PageCache,
root: u64,
handle: u64,
entry: &HandleEntry,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
freed: &mut Vec<u64>,
) -> Result<u64> {
let mut current_root = root;
while handle >= self.capacity() {
current_root = self.grow(cache, current_root, alloc)?;
}
self.insert_recursive(cache, current_root, handle, entry, self.depth, alloc, freed)
}
pub fn delete(
&mut self,
cache: &mut PageCache,
root: u64,
handle: u64,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
freed: &mut Vec<u64>,
) -> Result<(u64, Option<HandleEntry>)> {
if root == PAGE_ID_NONE {
return Ok((root, None));
}
if handle >= self.capacity() {
return Ok((root, None));
}
self.delete_recursive(cache, root, handle, self.depth, alloc, freed)
}
fn delete_recursive(
&mut self,
cache: &mut PageCache,
page: u64,
handle: u64,
level: u32,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
freed: &mut Vec<u64>,
) -> Result<(u64, Option<HandleEntry>)> {
if level == 0 {
let index = (handle as usize) % ENTRIES_PER_LEAF;
let entry = {
let buf = cache.get(page)?;
Self::read_entry(buf, index)
};
match entry.flags {
HandleFlags::Deleted => {
Ok((page, None))
}
HandleFlags::Live | HandleFlags::Overflow => {
let new_leaf = alloc(cache)?;
debug_assert_ne!(new_leaf, 0); cache.copy_page(page, new_leaf)?;
freed.push(page);
let tombstone = HandleEntry {
page_id: 0,
slot_index: 0,
flags: HandleFlags::Deleted,
tag: 0,
client_byte: 0,
};
{
let new_buf = cache.get_mut(new_leaf)?;
Self::write_entry(new_buf, index, &tombstone);
page::stamp_checksum(new_buf);
}
Ok((new_leaf, Some(entry)))
}
}
} else {
let child_span = self.span_at_level(level);
let child_idx = (handle / child_span) as usize;
let child_page = {
let buf = cache.get(page)?;
let offset = DATA_PAGE_HEADER_SIZE + child_idx * CHILD_PTR_SIZE;
u64::from_le_bytes(buf[offset..offset + 8].try_into().unwrap())
};
if child_page == 0 {
return Ok((page, None));
}
let (new_child, prev_entry) = self.delete_recursive(
cache,
child_page,
handle % child_span,
level - 1,
alloc,
freed,
)?;
if prev_entry.is_none() {
return Ok((page, None));
}
let new_page = alloc(cache)?;
debug_assert_ne!(new_page, 0); cache.copy_page(page, new_page)?;
freed.push(page);
{
let new_buf = cache.get_mut(new_page)?;
let offset = DATA_PAGE_HEADER_SIZE + child_idx * CHILD_PTR_SIZE;
new_buf[offset..offset + 8].copy_from_slice(&new_child.to_le_bytes());
page::stamp_checksum(new_buf);
}
Ok((new_page, prev_entry))
}
}
pub fn iter_live(&self, cache: &mut PageCache, root: u64) -> Result<Vec<(u64, HandleEntry)>> {
if root == PAGE_ID_NONE {
return Ok(Vec::new());
}
let mut result = Vec::new();
self.iter_recursive(cache, root, 0, self.depth, &mut result)?;
Ok(result)
}
pub fn set_depth(&mut self, depth: u32) {
self.depth = depth;
}
pub fn recover_depth(cache: &mut PageCache, root: u64) -> Result<u32> {
if root == PAGE_ID_NONE {
return Ok(0);
}
let mut depth = 0u32;
let mut current = root;
loop {
let buf = cache.get(current)?;
if buf[1] != FLAG_INTERIOR {
break;
}
if buf[0] != PageType::HandleTable as u8 {
return Err(ChiselError::CorruptPage { page_id: current });
}
depth += 1;
if depth > MAX_DEPTH {
return Err(ChiselError::CorruptPage { page_id: current });
}
let child_offset = page::DATA_PAGE_HEADER_SIZE;
let child = u64::from_le_bytes(buf[child_offset..child_offset + 8].try_into().unwrap());
if child == 0 {
break;
}
current = child;
}
Ok(depth)
}
pub fn depth(&self) -> u32 {
self.depth
}
fn capacity(&self) -> u64 {
let mut cap = ENTRIES_PER_LEAF as u64;
for _ in 0..self.depth {
cap = cap.saturating_mul(PTRS_PER_INTERIOR as u64);
}
cap
}
fn grow(
&mut self,
cache: &mut PageCache,
old_root: u64,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<u64> {
let new_root = alloc(cache)?;
debug_assert_ne!(new_root, 0);
let buf = cache.get_mut(new_root)?;
buf.fill(0);
buf[0] = PageType::HandleTable as u8;
buf[1] = FLAG_INTERIOR;
buf[2] = page::current_version(page::PageType::HandleTable); buf[DATA_PAGE_HEADER_SIZE..DATA_PAGE_HEADER_SIZE + 8]
.copy_from_slice(&old_root.to_le_bytes());
page::stamp_checksum(buf);
self.depth += 1;
Ok(new_root)
}
#[allow(clippy::too_many_arguments)]
fn insert_recursive(
&self,
cache: &mut PageCache,
page_id: u64,
handle: u64,
entry: &HandleEntry,
level: u32,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
freed: &mut Vec<u64>,
) -> Result<u64> {
let new_page = alloc(cache)?;
debug_assert_ne!(new_page, 0);
{
let old_buf = cache.get(page_id)?;
let old_data: [u8; PAGE_SIZE] = *old_buf;
let new_buf = cache.get_mut(new_page)?;
new_buf.copy_from_slice(&old_data);
}
freed.push(page_id);
if level == 0 {
let index = (handle % ENTRIES_PER_LEAF as u64) as usize;
let buf = cache.get_mut(new_page)?;
Self::write_entry(buf, index, entry);
page::stamp_checksum(buf);
Ok(new_page)
} else {
let child_span = self.span_at_level(level);
let child_idx = (handle / child_span) as usize;
let child_page = {
let buf = cache.get(new_page)?;
let offset = DATA_PAGE_HEADER_SIZE + child_idx * CHILD_PTR_SIZE;
u64::from_le_bytes(buf[offset..offset + 8].try_into().unwrap())
};
let actual_child = if child_page == 0 {
if level == 1 {
let leaf = alloc(cache)?;
debug_assert_ne!(leaf, 0); let buf = cache.get_mut(leaf)?;
buf.fill(0);
buf[0] = PageType::HandleTable as u8;
buf[1] = FLAG_LEAF;
buf[2] = page::current_version(page::PageType::HandleTable); page::stamp_checksum(buf);
leaf
} else {
let interior = alloc(cache)?;
debug_assert_ne!(interior, 0); let buf = cache.get_mut(interior)?;
buf.fill(0);
buf[0] = PageType::HandleTable as u8;
buf[1] = FLAG_INTERIOR;
buf[2] = page::current_version(page::PageType::HandleTable); page::stamp_checksum(buf);
interior
}
} else {
child_page
};
let new_child = self.insert_recursive(
cache,
actual_child,
handle % child_span,
entry,
level - 1,
alloc,
freed,
)?;
let buf = cache.get_mut(new_page)?;
let offset = DATA_PAGE_HEADER_SIZE + child_idx * CHILD_PTR_SIZE;
buf[offset..offset + 8].copy_from_slice(&new_child.to_le_bytes());
page::stamp_checksum(buf);
Ok(new_page)
}
}
fn find_leaf(
&self,
cache: &mut PageCache,
page_id: u64,
handle: u64,
) -> Result<Option<(u64, usize)>> {
let cap = self.capacity();
if cap != u64::MAX && handle >= cap {
return Ok(None);
}
if self.depth == 0 {
let index = (handle % ENTRIES_PER_LEAF as u64) as usize;
return Ok(Some((page_id, index)));
}
let mut current = page_id;
let mut remaining = handle;
for level in (1..=self.depth).rev() {
let child_span = self.span_at_level(level);
let child_idx = (remaining / child_span) as usize;
if child_idx >= PTRS_PER_INTERIOR {
return Ok(None);
}
remaining %= child_span;
let buf = cache.get(current)?;
let offset = DATA_PAGE_HEADER_SIZE + child_idx * CHILD_PTR_SIZE;
let child = u64::from_le_bytes(buf[offset..offset + 8].try_into().unwrap());
if child == 0 {
return Ok(None);
}
current = child;
}
let index = (remaining % ENTRIES_PER_LEAF as u64) as usize;
Ok(Some((current, index)))
}
fn span_at_level(&self, level: u32) -> u64 {
let mut span = ENTRIES_PER_LEAF as u64;
for _ in 1..level {
span = span.saturating_mul(PTRS_PER_INTERIOR as u64);
}
span
}
fn iter_recursive(
&self,
cache: &mut PageCache,
page_id: u64,
base_handle: u64,
level: u32,
result: &mut Vec<(u64, HandleEntry)>,
) -> Result<()> {
if level == 0 {
let buf = cache.get(page_id)?;
for i in 0..ENTRIES_PER_LEAF {
let entry = Self::read_entry(buf, i);
if entry.flags != HandleFlags::Deleted {
result.push((base_handle.saturating_add(i as u64), entry));
}
}
} else {
let child_span = self.span_at_level(level);
let children: Vec<(usize, u64)> = {
let buf = cache.get(page_id)?;
(0..PTRS_PER_INTERIOR)
.map(|i| {
let offset = DATA_PAGE_HEADER_SIZE + i * CHILD_PTR_SIZE;
let child = u64::from_le_bytes(buf[offset..offset + 8].try_into().unwrap());
(i, child)
})
.filter(|(_, child)| *child != 0)
.collect()
};
for (i, child) in children {
let next_base = base_handle.saturating_add((i as u64).saturating_mul(child_span));
self.iter_recursive(cache, child, next_base, level - 1, result)?;
}
}
Ok(())
}
fn read_entry(buf: &[u8; PAGE_SIZE], index: usize) -> HandleEntry {
let base = DATA_PAGE_HEADER_SIZE + index * ENTRY_SIZE;
HandleEntry {
page_id: u64::from_le_bytes(buf[base..base + 8].try_into().unwrap()),
slot_index: u16::from_le_bytes(buf[base + 8..base + 10].try_into().unwrap()),
flags: HandleFlags::from_u8(buf[base + 10]),
tag: u32::from_le_bytes(buf[base + 11..base + 15].try_into().unwrap()),
client_byte: buf[base + 15],
}
}
fn write_entry(buf: &mut [u8; PAGE_SIZE], index: usize, entry: &HandleEntry) {
let base = DATA_PAGE_HEADER_SIZE + index * ENTRY_SIZE;
buf[base..base + 8].copy_from_slice(&entry.page_id.to_le_bytes());
buf[base + 8..base + 10].copy_from_slice(&entry.slot_index.to_le_bytes());
buf[base + 10] = entry.flags.to_u8();
buf[base + 11..base + 15].copy_from_slice(&entry.tag.to_le_bytes());
buf[base + 15] = entry.client_byte;
}
}
#[cfg(test)]
impl HandleTable {
fn insert_t(
&mut self,
cache: &mut PageCache,
root: u64,
handle: u64,
entry: &HandleEntry,
) -> Result<u64> {
self.insert(
cache,
root,
handle,
entry,
&mut |c| c.new_page(),
&mut Vec::new(),
)
}
fn delete_t(
&mut self,
cache: &mut PageCache,
root: u64,
handle: u64,
) -> Result<(u64, Option<HandleEntry>)> {
self.delete(cache, root, handle, &mut |c| c.new_page(), &mut Vec::new())
}
pub(crate) fn collect_page_ids(
&self,
cache: &mut PageCache,
root: u64,
out: &mut Vec<u64>,
) -> Result<()> {
if root == PAGE_ID_NONE {
return Ok(());
}
self.collect_recursive(cache, root, self.depth, out)
}
fn collect_recursive(
&self,
cache: &mut PageCache,
page: u64,
level: u32,
out: &mut Vec<u64>,
) -> Result<()> {
out.push(page);
if level > 0 {
let children: Vec<u64> = {
let buf = cache.get(page)?;
(0..PTRS_PER_INTERIOR)
.map(|i| {
let off = DATA_PAGE_HEADER_SIZE + i * CHILD_PTR_SIZE;
u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
})
.filter(|c| *c != 0)
.collect()
};
for child in children {
self.collect_recursive(cache, child, level - 1, out)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::page_cache::PageCache;
use crate::page_io::PageIo;
use tempfile::NamedTempFile;
fn make_cache() -> PageCache {
let file = NamedTempFile::new().unwrap();
let io = PageIo::open(file.path(), false).unwrap();
let mut cache = PageCache::new(
io,
1024 * crate::page::PAGE_SIZE as u64,
0,
crate::DrainInsertion::LruTail,
crate::SpillwayLocation::InMemory,
);
cache.set_next_page_id(2);
cache
}
#[test]
fn capacity_and_span_saturate_on_out_of_range_depth() {
let ht = HandleTable { depth: 30 };
assert_eq!(ht.capacity(), u64::MAX);
assert_eq!(ht.span_at_level(30), u64::MAX);
}
#[test]
fn recover_depth_rejects_cyclic_spine() {
let mut cache = make_cache();
let id = cache.new_page().unwrap();
{
let buf = cache.get_mut(id).unwrap();
buf.fill(0);
buf[0] = PageType::HandleTable as u8;
buf[1] = FLAG_INTERIOR;
buf[2] = page::PAGE_FORMAT_VERSION_CURRENT;
buf[DATA_PAGE_HEADER_SIZE..DATA_PAGE_HEADER_SIZE + 8]
.copy_from_slice(&id.to_le_bytes()); page::stamp_checksum(buf);
}
let err = match HandleTable::recover_depth(&mut cache, id) {
Ok(d) => panic!("recover_depth accepted a cyclic spine (depth {d})"),
Err(e) => e,
};
assert!(
matches!(err, ChiselError::CorruptPage { .. }),
"expected CorruptPage, got {err:?}"
);
}
#[test]
fn lookup_sparse_range_in_depth1_tree_returns_none() {
let mut cache = make_cache();
let mut ht = HandleTable::new();
let root0 = ht.create_root(&mut cache).unwrap();
let entry = HandleEntry {
page_id: 42,
slot_index: 7,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
let root1 = ht.insert_t(&mut cache, root0, 0, &entry).unwrap();
let root2 = ht.insert_t(&mut cache, root1, 510, &entry).unwrap();
assert_eq!(ht.depth(), 1);
{
let buf = cache.get_mut(root2).unwrap();
let flags_offset = DATA_PAGE_HEADER_SIZE + 10; buf[flags_offset] = HandleFlags::Live.to_u8();
page::stamp_checksum(buf);
}
let result = ht.lookup(&mut cache, root2, 1020).unwrap();
assert_eq!(
result, None,
"sparse handle must report absent, not a bogus entry"
);
}
#[test]
fn delete_returns_some_for_live_entry() {
let mut cache = make_cache();
let mut ht = HandleTable::new();
let root = ht.create_root(&mut cache).unwrap();
let live_entry = HandleEntry {
page_id: 42,
slot_index: 7,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
let root_after_insert = ht.insert_t(&mut cache, root, 100, &live_entry).unwrap();
let (new_root, prev_entry) = ht.delete_t(&mut cache, root_after_insert, 100).unwrap();
assert_ne!(
new_root, root_after_insert,
"delete of a Live entry must COW the leaf"
);
let entry = prev_entry.expect("delete of a Live entry must return Some(entry)");
assert_eq!(entry.page_id, 42);
assert_eq!(entry.slot_index, 7);
assert_eq!(entry.flags, HandleFlags::Live);
}
#[test]
fn delete_returns_some_for_overflow_entry() {
let mut cache = make_cache();
let mut ht = HandleTable::new();
let root = ht.create_root(&mut cache).unwrap();
let overflow_entry = HandleEntry {
page_id: 99,
slot_index: 0,
flags: HandleFlags::Overflow,
tag: 0,
client_byte: 0,
};
let root_after_insert = ht.insert_t(&mut cache, root, 200, &overflow_entry).unwrap();
let (new_root, prev_entry) = ht.delete_t(&mut cache, root_after_insert, 200).unwrap();
assert_ne!(
new_root, root_after_insert,
"delete of an Overflow entry must COW the leaf"
);
let entry = prev_entry.expect("delete of an Overflow entry must return Some(entry)");
assert_eq!(entry.page_id, 99);
assert_eq!(entry.flags, HandleFlags::Overflow);
}
#[test]
fn delete_returns_none_for_already_deleted() {
let mut cache = make_cache();
let mut ht = HandleTable::new();
let root = ht.create_root(&mut cache).unwrap();
let live_entry = HandleEntry {
page_id: 42,
slot_index: 7,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
let root_after_insert = ht.insert_t(&mut cache, root, 100, &live_entry).unwrap();
let (root_after_first_delete, _) = ht.delete_t(&mut cache, root_after_insert, 100).unwrap();
let (root_after_second_delete, prev_entry) = ht
.delete_t(&mut cache, root_after_first_delete, 100)
.unwrap();
assert_eq!(
prev_entry, None,
"delete of an already-tombstoned handle must return None"
);
assert_eq!(
root_after_second_delete, root_after_first_delete,
"no-op delete must not COW the tree (root unchanged)"
);
}
#[test]
fn delete_returns_none_for_absent_handle_in_existing_subtree() {
let mut cache = make_cache();
let mut ht = HandleTable::new();
let root = ht.create_root(&mut cache).unwrap();
let live_entry = HandleEntry {
page_id: 42,
slot_index: 7,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
let root_after_insert = ht.insert_t(&mut cache, root, 100, &live_entry).unwrap();
let (new_root, prev_entry) = ht.delete_t(&mut cache, root_after_insert, 200).unwrap();
assert_eq!(
prev_entry, None,
"delete of an absent slot must return None"
);
assert_eq!(
new_root, root_after_insert,
"delete of an absent slot must not COW the tree"
);
}
#[test]
fn delete_does_not_grow_tree_for_handle_beyond_capacity() {
let mut cache = make_cache();
let mut ht = HandleTable::new();
let root = ht.create_root(&mut cache).unwrap();
assert_eq!(ht.depth(), 0);
let (new_root, prev_entry) = ht.delete_t(&mut cache, root, u64::MAX).unwrap();
assert_eq!(prev_entry, None);
assert_eq!(
new_root, root,
"delete beyond capacity must not COW the tree"
);
assert_eq!(
ht.depth(),
0,
"delete beyond capacity must NOT grow the tree (insert would have grown; delete must not)"
);
}
#[test]
fn lookup_handle_beyond_capacity_returns_none() {
let mut cache = make_cache();
let mut ht = HandleTable::new();
let root0 = ht.create_root(&mut cache).unwrap();
let entry = HandleEntry {
page_id: 42,
slot_index: 7,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
let root1 = ht.insert_t(&mut cache, root0, 0, &entry).unwrap();
let root2 = ht
.insert_t(&mut cache, root1, ENTRIES_PER_LEAF as u64, &entry)
.unwrap();
assert_eq!(ht.depth(), 1);
let first_over = (ENTRIES_PER_LEAF as u64) * (PTRS_PER_INTERIOR as u64);
assert_eq!(
ht.lookup(&mut cache, root2, first_over).unwrap(),
None,
"handle at exact capacity must be reported absent, not trigger a fatal cache.get"
);
assert_eq!(
ht.lookup(&mut cache, root2, u64::MAX).unwrap(),
None,
"u64::MAX handle must not panic nor read past the page"
);
}
#[test]
fn lookup_out_of_range_handle_at_depth_0_returns_none() {
let mut cache = make_cache();
let mut ht = HandleTable::new();
let root = ht.create_root(&mut cache).unwrap();
let entry = HandleEntry {
page_id: 42,
slot_index: 7,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
let root = ht.insert_t(&mut cache, root, 6, &entry).unwrap();
assert_eq!(ht.depth(), 0, "single insert must not grow the tree");
assert_eq!(
ht.lookup(&mut cache, root, 6).unwrap(),
Some(entry),
"in-range handle must resolve to its entry"
);
let aliasing = 6 + ENTRIES_PER_LEAF as u64;
assert_eq!(
ht.lookup(&mut cache, root, aliasing).unwrap(),
None,
"out-of-range handle at depth 0 must be absent, not alias an occupied slot"
);
}
fn ht_cache(max_pages: usize) -> PageCache {
let file = NamedTempFile::new().unwrap();
let io = PageIo::open(file.path(), false).unwrap();
let mut cache = PageCache::new(
io,
max_pages as u64 * crate::page::PAGE_SIZE as u64,
0,
crate::DrainInsertion::LruTail,
crate::SpillwayLocation::InMemory,
);
cache.set_next_page_id(2);
cache
}
#[test]
fn test_handle_table_insert_and_lookup() {
let mut cache = ht_cache(64);
let mut ht = HandleTable::new();
let root = ht.create_root(&mut cache).unwrap();
let entry = HandleEntry {
page_id: 10,
slot_index: 3,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
let new_root = ht.insert_t(&mut cache, root, 0, &entry).unwrap();
let found = ht.lookup(&mut cache, new_root, 0).unwrap().unwrap();
assert_eq!(found.page_id, 10);
assert_eq!(found.slot_index, 3);
}
#[test]
fn test_handle_table_multiple_entries() {
let mut cache = ht_cache(64);
let mut ht = HandleTable::new();
let mut root = ht.create_root(&mut cache).unwrap();
for i in 0..10u64 {
let entry = HandleEntry {
page_id: 100 + i,
slot_index: i as u16,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
root = ht.insert_t(&mut cache, root, i, &entry).unwrap();
}
for i in 0..10u64 {
let found = ht.lookup(&mut cache, root, i).unwrap().unwrap();
assert_eq!(found.page_id, 100 + i);
assert_eq!(found.slot_index, i as u16);
}
}
#[test]
fn test_handle_table_cow_returns_new_root() {
let mut cache = ht_cache(64);
let mut ht = HandleTable::new();
let root1 = ht.create_root(&mut cache).unwrap();
let entry = HandleEntry {
page_id: 10,
slot_index: 0,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
let root2 = ht.insert_t(&mut cache, root1, 0, &entry).unwrap();
assert_ne!(root1, root2);
}
#[test]
fn test_handle_table_grows_to_depth_one() {
let mut cache = ht_cache(2048);
let mut ht = HandleTable::new();
let mut root = ht.create_root(&mut cache).unwrap();
for i in 0..(ENTRIES_PER_LEAF as u64 + 10) {
let entry = HandleEntry {
page_id: i,
slot_index: 0,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
root = ht.insert_t(&mut cache, root, i, &entry).unwrap();
}
assert_eq!(ht.depth(), 1, "520 handles must grow the tree to depth 1");
for i in 0..(ENTRIES_PER_LEAF as u64 + 10) {
let found = ht.lookup(&mut cache, root, i).unwrap().unwrap();
assert_eq!(found.page_id, i);
}
}
#[test]
fn test_handle_table_grows_to_depth_two() {
const DEPTH2_BOUNDARY: u64 = ENTRIES_PER_LEAF as u64 * PTRS_PER_INTERIOR as u64; let mut cache = ht_cache(256);
let mut ht = HandleTable::new();
let mut root = ht.create_root(&mut cache).unwrap();
let handles = [
0u64,
5,
509,
ENTRIES_PER_LEAF as u64,
DEPTH2_BOUNDARY,
DEPTH2_BOUNDARY + 7,
1_000_000,
];
for &h in &handles {
let entry = HandleEntry {
page_id: h.wrapping_add(100),
slot_index: (h % 1021) as u16,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
root = ht.insert_t(&mut cache, root, h, &entry).unwrap();
}
assert_eq!(
ht.depth(),
2,
"a handle >= capacity(1) must grow the tree to depth 2"
);
for &h in &handles {
let found = ht
.lookup(&mut cache, root, h)
.unwrap()
.unwrap_or_else(|| panic!("handle {h} missing after depth-2 grow"));
assert_eq!(
found.page_id,
h.wrapping_add(100),
"page_id mismatch for {h}"
);
assert_eq!(
found.slot_index,
(h % 1021) as u16,
"slot_index mismatch for {h}"
);
}
assert!(ht
.lookup(&mut cache, root, DEPTH2_BOUNDARY + 1)
.unwrap()
.is_none());
}
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(96))]
#[test]
fn prop_handle_table_insert_lookup_matches_oracle(
ops in proptest::collection::vec(
(0u64..1_100_000u64, 0u64..=u32::MAX as u64),
1..40usize,
)
) {
let mut cache = ht_cache(4096);
let mut ht = HandleTable::new();
let mut root = ht.create_root(&mut cache).unwrap();
let mut oracle: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
for (handle, page_id) in &ops {
let entry = HandleEntry {
page_id: *page_id,
slot_index: 0,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
root = ht.insert_t(&mut cache, root, *handle, &entry).unwrap();
oracle.insert(*handle, *page_id); }
for (handle, expected) in &oracle {
let found = ht.lookup(&mut cache, root, *handle).unwrap();
proptest::prop_assert!(found.is_some(), "handle {} vanished", handle);
proptest::prop_assert_eq!(found.unwrap().page_id, *expected);
}
}
#[test]
fn prop_handle_table_recover_depth_matches(handle in 0u64..600_000_000u64) {
let mut cache = ht_cache(256);
let mut ht = HandleTable::new();
let mut root = ht.create_root(&mut cache).unwrap();
let entry = HandleEntry {
page_id: 1,
slot_index: 0,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
root = ht.insert_t(&mut cache, root, handle, &entry).unwrap();
proptest::prop_assert_eq!(HandleTable::recover_depth(&mut cache, root).unwrap(), ht.depth());
}
}
#[test]
fn test_handle_table_delete() {
let mut cache = ht_cache(64);
let mut ht = HandleTable::new();
let mut root = ht.create_root(&mut cache).unwrap();
let entry = HandleEntry {
page_id: 10,
slot_index: 0,
flags: HandleFlags::Live,
tag: 0,
client_byte: 0,
};
root = ht.insert_t(&mut cache, root, 0, &entry).unwrap();
let (new_root, prev) = ht.delete_t(&mut cache, root, 0).unwrap();
root = new_root;
assert_eq!(
prev,
Some(entry),
"delete of a live entry must return the original entry"
);
let found = ht.lookup(&mut cache, root, 0).unwrap();
assert!(found.is_none());
}
#[test]
fn insert_lookup_roundtrips_client_byte() {
let mut cache = make_cache();
let mut ht = HandleTable::new();
let root = ht.create_root(&mut cache).unwrap();
let e = HandleEntry {
page_id: 42,
slot_index: 3,
flags: HandleFlags::Live,
tag: 9,
client_byte: 200,
};
let root = ht.insert_t(&mut cache, root, 1, &e).unwrap();
let got = ht.lookup(&mut cache, root, 1).unwrap().unwrap();
assert_eq!(got.client_byte, 200);
assert_eq!(got.tag, 9, "tag neighbor must be undisturbed");
}
#[test]
fn handle_entry_tag_round_trips_through_a_leaf_slot() {
let mut buf = [0u8; PAGE_SIZE];
let entry = HandleEntry {
page_id: 42,
slot_index: 7,
flags: HandleFlags::Live,
tag: 0xDEADBEEF,
client_byte: 0,
};
HandleTable::write_entry(&mut buf, 3, &entry);
let read = HandleTable::read_entry(&buf, 3);
assert_eq!(read, entry);
assert_eq!(read.tag, 0xDEADBEEF);
let zeroed = HandleTable::read_entry(&[0u8; PAGE_SIZE], 0);
assert_eq!(zeroed.tag, 0);
}
}