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 SLOT_SIZE: usize = 8;
const SLOTS_PER_PAGE: usize = (CHECKSUM_OFFSET - DATA_PAGE_HEADER_SIZE) / SLOT_SIZE;
const MAX_DEPTH: u32 = 6;
fn read_slot(buf: &[u8; PAGE_SIZE], index: usize) -> u64 {
let off = DATA_PAGE_HEADER_SIZE + index * SLOT_SIZE;
u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
}
fn write_slot(buf: &mut [u8; PAGE_SIZE], index: usize, value: u64) {
let off = DATA_PAGE_HEADER_SIZE + index * SLOT_SIZE;
buf[off..off + 8].copy_from_slice(&value.to_le_bytes());
}
fn init_page(
cache: &mut PageCache,
page_type: PageType,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<u64> {
let id = alloc(cache)?;
debug_assert_ne!(id, 0, "membership-index pages must not use page id 0");
let buf = cache.get_mut(id)?;
buf.fill(0);
buf[0] = page_type as u8;
buf[1] = page::current_version(page_type); page::stamp_checksum(buf);
Ok(id)
}
fn free_subtree(cache: &mut PageCache, root: u64, depth: u32, freed: &mut Vec<u64>) -> Result<()> {
if root == PAGE_ID_NONE {
return Ok(());
}
if depth > 0 {
let children: Vec<u64> = {
let buf = cache.get(root)?;
(0..SLOTS_PER_PAGE)
.map(|idx| read_slot(buf, idx))
.filter(|&child| child != 0)
.collect()
};
for child in children {
free_subtree(cache, child, depth - 1, freed)?;
}
}
freed.push(root);
Ok(())
}
pub(crate) struct RadixU64 {
pub depth: u32,
}
impl RadixU64 {
#[allow(dead_code)] pub fn new() -> RadixU64 {
RadixU64 { depth: 0 }
}
pub fn create_root(
&mut self,
cache: &mut PageCache,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<u64> {
self.depth = 0;
init_page(cache, PageType::MembershipLeaf, alloc)
}
fn capacity(&self) -> u64 {
let mut cap = SLOTS_PER_PAGE as u64;
for _ in 0..self.depth {
cap = cap.saturating_mul(SLOTS_PER_PAGE as u64);
}
cap
}
fn span_at_level(&self, level: u32) -> u64 {
let mut span = SLOTS_PER_PAGE as u64;
for _ in 1..level {
span = span.saturating_mul(SLOTS_PER_PAGE as u64);
}
span
}
fn find_leaf(
&self,
cache: &mut PageCache,
root: u64,
key: u64,
) -> Result<Option<(u64, usize)>> {
let cap = self.capacity();
if cap != u64::MAX && key >= cap {
return Ok(None);
}
if self.depth == 0 {
return Ok(Some((root, (key % SLOTS_PER_PAGE as u64) as usize)));
}
let mut current = root;
let mut remaining = key;
for level in (1..=self.depth).rev() {
let span = self.span_at_level(level);
let child_idx = (remaining / span) as usize;
if child_idx >= SLOTS_PER_PAGE {
return Ok(None);
}
remaining %= span;
let child = read_slot(cache.get(current)?, child_idx);
if child == 0 {
return Ok(None);
}
current = child;
}
Ok(Some((
current,
(remaining % SLOTS_PER_PAGE as u64) as usize,
)))
}
pub fn lookup(&self, cache: &mut PageCache, root: u64, key: u64) -> Result<u64> {
if root == PAGE_ID_NONE {
return Ok(0);
}
let Some((leaf, idx)) = self.find_leaf(cache, root, key)? else {
return Ok(0);
};
Ok(read_slot(cache.get(leaf)?, idx))
}
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::MembershipInterior as u8;
buf[1] = page::current_version(PageType::MembershipInterior);
write_slot(buf, 0, old_root); page::stamp_checksum(buf);
self.depth += 1;
Ok(new_root)
}
pub fn insert(
&mut self,
cache: &mut PageCache,
root: u64,
key: u64,
value: u64,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
freed: &mut Vec<u64>,
) -> Result<u64> {
debug_assert_ne!(value, 0, "0 is the absent sentinel; cannot be stored");
let mut current_root = root;
while key >= self.capacity() {
current_root = self.grow(cache, current_root, alloc)?;
}
self.insert_recursive(cache, current_root, key, value, self.depth, alloc, freed)
}
#[allow(clippy::too_many_arguments)]
fn insert_recursive(
&self,
cache: &mut PageCache,
page: u64,
key: u64,
value: u64,
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);
cache.copy_page(page, new_page)?;
freed.push(page);
if level == 0 {
let idx = (key % SLOTS_PER_PAGE as u64) as usize;
let buf = cache.get_mut(new_page)?;
write_slot(buf, idx, value);
page::stamp_checksum(buf);
Ok(new_page)
} else {
let span = self.span_at_level(level);
let child_idx = (key / span) as usize;
let child = read_slot(cache.get(new_page)?, child_idx);
let actual_child = if child == 0 {
let pt = if level == 1 {
PageType::MembershipLeaf
} else {
PageType::MembershipInterior
};
let id = alloc(cache)?;
let buf = cache.get_mut(id)?;
buf.fill(0);
buf[0] = pt as u8;
buf[1] = page::current_version(pt);
page::stamp_checksum(buf);
id
} else {
child
};
let new_child = self.insert_recursive(
cache,
actual_child,
key % span,
value,
level - 1,
alloc,
freed,
)?;
let buf = cache.get_mut(new_page)?;
write_slot(buf, child_idx, new_child);
page::stamp_checksum(buf);
Ok(new_page)
}
}
pub fn delete(
&mut self,
cache: &mut PageCache,
root: u64,
key: u64,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
freed: &mut Vec<u64>,
) -> Result<(u64, u64)> {
if root == PAGE_ID_NONE || key >= self.capacity() {
return Ok((root, 0));
}
self.delete_recursive(cache, root, key, self.depth, alloc, freed)
}
fn delete_recursive(
&self,
cache: &mut PageCache,
page: u64,
key: u64,
level: u32,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
freed: &mut Vec<u64>,
) -> Result<(u64, u64)> {
if level == 0 {
let idx = (key % SLOTS_PER_PAGE as u64) as usize;
let prev = read_slot(cache.get(page)?, idx);
if prev == 0 {
return Ok((page, 0));
}
let new_leaf = alloc(cache)?;
debug_assert_ne!(new_leaf, 0);
cache.copy_page(page, new_leaf)?;
{
let buf = cache.get_mut(new_leaf)?;
write_slot(buf, idx, 0);
page::stamp_checksum(buf);
}
freed.push(page);
Ok((new_leaf, prev))
} else {
let span = self.span_at_level(level);
let child_idx = (key / span) as usize;
let child = read_slot(cache.get(page)?, child_idx);
if child == 0 {
return Ok((page, 0));
}
let (new_child, prev) =
self.delete_recursive(cache, child, key % span, level - 1, alloc, freed)?;
if prev == 0 {
return Ok((page, 0));
}
let new_page = alloc(cache)?;
debug_assert_ne!(new_page, 0);
cache.copy_page(page, new_page)?;
{
let buf = cache.get_mut(new_page)?;
write_slot(buf, child_idx, new_child);
page::stamp_checksum(buf);
}
freed.push(page);
Ok((new_page, prev))
}
}
pub fn iter(&self, cache: &mut PageCache, root: u64) -> Result<Vec<(u64, u64)>> {
if root == PAGE_ID_NONE {
return Ok(Vec::new());
}
let mut out = Vec::new();
self.iter_recursive(cache, root, 0, self.depth, &mut out)?;
Ok(out)
}
fn iter_recursive(
&self,
cache: &mut PageCache,
page: u64,
base: u64,
level: u32,
out: &mut Vec<(u64, u64)>,
) -> Result<()> {
if level == 0 {
let buf = cache.get(page)?;
for i in 0..SLOTS_PER_PAGE {
let v = read_slot(buf, i);
if v != 0 {
out.push((base.saturating_add(i as u64), v));
}
}
} else {
let span = self.span_at_level(level);
let children: Vec<(usize, u64)> = {
let buf = cache.get(page)?;
(0..SLOTS_PER_PAGE)
.map(|i| (i, read_slot(buf, i)))
.filter(|(_, c)| *c != 0)
.collect()
};
for (i, child) in children {
let next_base = base.saturating_add((i as u64).saturating_mul(span));
self.iter_recursive(cache, child, next_base, level - 1, out)?;
}
}
Ok(())
}
pub fn iter_bounded(
&self,
cache: &mut PageCache,
root: u64,
limit: usize,
) -> Result<Vec<(u64, u64)>> {
let mut out = Vec::new();
if root == PAGE_ID_NONE || limit == 0 {
return Ok(out);
}
self.iter_bounded_recursive(cache, root, 0, self.depth, limit, &mut out)?;
Ok(out)
}
fn iter_bounded_recursive(
&self,
cache: &mut PageCache,
page: u64,
base: u64,
level: u32,
limit: usize,
out: &mut Vec<(u64, u64)>,
) -> Result<()> {
if out.len() >= limit {
return Ok(());
}
if level == 0 {
let buf = cache.get(page)?;
for i in 0..SLOTS_PER_PAGE {
if out.len() >= limit {
break;
}
let v = read_slot(buf, i);
if v != 0 {
out.push((base.saturating_add(i as u64), v));
}
}
} else {
let span = self.span_at_level(level);
let children: Vec<(usize, u64)> = {
let buf = cache.get(page)?;
(0..SLOTS_PER_PAGE)
.map(|i| (i, read_slot(buf, i)))
.filter(|(_, c)| *c != 0)
.collect()
};
for (i, child) in children {
if out.len() >= limit {
break;
}
let next_base = base.saturating_add((i as u64).saturating_mul(span));
self.iter_bounded_recursive(cache, child, next_base, level - 1, limit, out)?;
}
}
Ok(())
}
pub fn any_present(&self, cache: &mut PageCache, root: u64) -> Result<bool> {
if root == PAGE_ID_NONE {
return Ok(false);
}
self.any_recursive(cache, root, self.depth)
}
fn any_recursive(&self, cache: &mut PageCache, page: u64, level: u32) -> Result<bool> {
if level == 0 {
let buf = cache.get(page)?;
for i in 0..SLOTS_PER_PAGE {
if read_slot(buf, i) != 0 {
return Ok(true);
}
}
Ok(false)
} else {
let children: Vec<u64> = {
let buf = cache.get(page)?;
(0..SLOTS_PER_PAGE)
.map(|i| read_slot(buf, i))
.filter(|c| *c != 0)
.collect()
};
for child in children {
if self.any_recursive(cache, child, level - 1)? {
return Ok(true);
}
}
Ok(false)
}
}
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[0] != PageType::MembershipInterior as u8 {
break;
}
depth += 1;
if depth > MAX_DEPTH {
return Err(ChiselError::CorruptPage { page_id: current });
}
let child = read_slot(buf, 0);
if child == 0 {
break;
}
current = child;
}
Ok(depth)
}
}
const INNER_ROOT_BITS: u32 = 58;
const INNER_ROOT_MASK: u64 = (1u64 << INNER_ROOT_BITS) - 1;
fn pack_inner(root: u64, depth: u32) -> u64 {
debug_assert!(root <= INNER_ROOT_MASK, "page id exceeds 2^58");
debug_assert!(
depth < (1 << (64 - INNER_ROOT_BITS)),
"inner depth too large"
);
((depth as u64) << INNER_ROOT_BITS) | root
}
fn unpack_inner(packed: u64) -> (u64, u32) {
(packed & INNER_ROOT_MASK, (packed >> INNER_ROOT_BITS) as u32)
}
pub(crate) struct MembershipIndex {
outer_depth: u32,
}
impl MembershipIndex {
pub fn new() -> MembershipIndex {
MembershipIndex { outer_depth: 0 }
}
pub fn set_outer_depth(&mut self, depth: u32) {
self.outer_depth = depth;
}
pub fn insert(
&mut self,
cache: &mut PageCache,
outer_root: u64,
tag: u32,
handle: u64,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
freed: &mut Vec<u64>,
) -> Result<u64> {
let mut outer = RadixU64 {
depth: self.outer_depth,
};
let root = if outer_root == PAGE_ID_NONE {
outer.create_root(cache, alloc)?
} else {
outer_root
};
let (mut inner_root, inner_depth) = unpack_inner(outer.lookup(cache, root, tag as u64)?);
if inner_root != 0 && inner_depth > MAX_DEPTH {
return Err(ChiselError::CorruptPage {
page_id: inner_root,
});
}
let mut inner = RadixU64 { depth: inner_depth };
if inner_root == 0 {
inner_root = inner.create_root(cache, alloc)?;
}
let new_inner_root = inner.insert(cache, inner_root, handle, 1, alloc, freed)?;
let packed = pack_inner(new_inner_root, inner.depth);
let new_outer_root = outer.insert(cache, root, tag as u64, packed, alloc, freed)?;
self.outer_depth = outer.depth;
Ok(new_outer_root)
}
pub fn remove(
&mut self,
cache: &mut PageCache,
outer_root: u64,
tag: u32,
handle: u64,
alloc: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
freed: &mut Vec<u64>,
) -> Result<(u64, bool)> {
if outer_root == PAGE_ID_NONE {
return Ok((outer_root, false));
}
let mut outer = RadixU64 {
depth: self.outer_depth,
};
let (inner_root, inner_depth) = unpack_inner(outer.lookup(cache, outer_root, tag as u64)?);
if inner_root != 0 && inner_depth > MAX_DEPTH {
return Err(ChiselError::CorruptPage {
page_id: inner_root,
});
}
if inner_root == 0 {
return Ok((outer_root, false));
}
let mut inner = RadixU64 { depth: inner_depth };
let (new_inner_root, prev) = inner.delete(cache, inner_root, handle, alloc, freed)?;
if prev == 0 {
return Ok((outer_root, false));
}
let new_outer_root = if inner.any_present(cache, new_inner_root)? {
outer.insert(
cache,
outer_root,
tag as u64,
pack_inner(new_inner_root, inner.depth),
alloc,
freed,
)?
} else {
free_subtree(cache, new_inner_root, inner.depth, freed)?;
let (r, _) = outer.delete(cache, outer_root, tag as u64, alloc, freed)?;
r
};
self.outer_depth = outer.depth;
Ok((new_outer_root, true))
}
#[allow(dead_code)] pub fn contains(
&self,
cache: &mut PageCache,
outer_root: u64,
tag: u32,
handle: u64,
) -> Result<bool> {
if outer_root == PAGE_ID_NONE {
return Ok(false);
}
let outer = RadixU64 {
depth: self.outer_depth,
};
let (inner_root, inner_depth) = unpack_inner(outer.lookup(cache, outer_root, tag as u64)?);
if inner_root != 0 && inner_depth > MAX_DEPTH {
return Err(ChiselError::CorruptPage {
page_id: inner_root,
});
}
if inner_root == 0 {
return Ok(false);
}
let inner = RadixU64 { depth: inner_depth };
Ok(inner.lookup(cache, inner_root, handle)? != 0)
}
pub fn handles_for_tag(
&self,
cache: &mut PageCache,
outer_root: u64,
tag: u32,
) -> Result<Vec<u64>> {
if outer_root == PAGE_ID_NONE {
return Ok(Vec::new());
}
let outer = RadixU64 {
depth: self.outer_depth,
};
let (inner_root, inner_depth) = unpack_inner(outer.lookup(cache, outer_root, tag as u64)?);
if inner_root != 0 && inner_depth > MAX_DEPTH {
return Err(ChiselError::CorruptPage {
page_id: inner_root,
});
}
if inner_root == 0 {
return Ok(Vec::new());
}
let inner = RadixU64 { depth: inner_depth };
Ok(inner
.iter(cache, inner_root)?
.into_iter()
.map(|(h, _)| h)
.collect())
}
pub fn handles_for_tag_bounded(
&self,
cache: &mut PageCache,
outer_root: u64,
tag: u32,
limit: usize,
) -> Result<Vec<u64>> {
if outer_root == PAGE_ID_NONE {
return Ok(Vec::new());
}
let outer = RadixU64 {
depth: self.outer_depth,
};
let (inner_root, inner_depth) = unpack_inner(outer.lookup(cache, outer_root, tag as u64)?);
if inner_root != 0 && inner_depth > MAX_DEPTH {
return Err(ChiselError::CorruptPage {
page_id: inner_root,
});
}
if inner_root == 0 {
return Ok(Vec::new());
}
let inner = RadixU64 { depth: inner_depth };
Ok(inner
.iter_bounded(cache, inner_root, limit)?
.into_iter()
.map(|(h, _)| h)
.collect())
}
}
#[cfg(test)]
impl RadixU64 {
fn insert_t(&mut self, cache: &mut PageCache, root: u64, key: u64, value: u64) -> Result<u64> {
self.insert(
cache,
root,
key,
value,
&mut |c| c.new_page(),
&mut Vec::new(),
)
}
fn delete_t(&mut self, cache: &mut PageCache, root: u64, key: u64) -> Result<(u64, u64)> {
self.delete(cache, root, key, &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..SLOTS_PER_PAGE)
.map(|i| read_slot(buf, i))
.filter(|c| *c != 0)
.collect()
};
for child in children {
self.collect_recursive(cache, child, level - 1, out)?;
}
}
Ok(())
}
}
#[cfg(test)]
impl MembershipIndex {
fn insert_t(
&mut self,
cache: &mut PageCache,
outer_root: u64,
tag: u32,
handle: u64,
) -> Result<u64> {
self.insert(
cache,
outer_root,
tag,
handle,
&mut |c| c.new_page(),
&mut Vec::new(),
)
}
fn remove_t(
&mut self,
cache: &mut PageCache,
outer_root: u64,
tag: u32,
handle: u64,
) -> Result<(u64, bool)> {
self.remove(
cache,
outer_root,
tag,
handle,
&mut |c| c.new_page(),
&mut Vec::new(),
)
}
pub(crate) fn collect_page_ids(
&self,
cache: &mut PageCache,
outer_root: u64,
out: &mut Vec<u64>,
) -> Result<()> {
if outer_root == PAGE_ID_NONE {
return Ok(());
}
let outer = RadixU64 {
depth: self.outer_depth,
};
outer.collect_page_ids(cache, outer_root, out)?;
for (_tag, packed) in outer.iter(cache, outer_root)? {
let (inner_root, inner_depth) = unpack_inner(packed);
if inner_root != 0 {
let inner = RadixU64 { depth: inner_depth };
inner.collect_page_ids(cache, inner_root, out)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::page_cache::PageCache;
use crate::page_io::PageIo;
use tempfile::NamedTempFile;
fn cache(max_pages: usize) -> PageCache {
let file = NamedTempFile::new().unwrap();
let io = PageIo::open(file.path(), false).unwrap();
let mut c = PageCache::new(
io,
max_pages as u64 * crate::page::PAGE_SIZE as u64,
0,
crate::DrainInsertion::LruTail,
crate::SpillwayLocation::InMemory,
);
c.set_next_page_id(2);
c
}
#[test]
fn insert_lookup_delete_single_level() {
let mut c = cache(64);
let mut t = RadixU64::new();
let root = t
.create_root(&mut c, &mut |c: &mut PageCache| c.new_page())
.unwrap();
assert_eq!(t.lookup(&mut c, root, 5).unwrap(), 0);
let r = t.insert_t(&mut c, root, 5, 99).unwrap();
assert_eq!(t.lookup(&mut c, r, 5).unwrap(), 99);
assert!(t.any_present(&mut c, r).unwrap());
let (r2, prev) = t.delete_t(&mut c, r, 5).unwrap();
assert_eq!(prev, 99);
assert_eq!(t.lookup(&mut c, r2, 5).unwrap(), 0);
assert!(!t.any_present(&mut c, r2).unwrap());
}
#[test]
fn free_subtree_reclaims_every_page_of_a_multi_level_tree() {
let mut c = cache(8192);
let mut t = RadixU64::new();
let mut root = t
.create_root(&mut c, &mut |c: &mut PageCache| c.new_page())
.unwrap();
for k in 0..(SLOTS_PER_PAGE as u64 + 51) {
root = t.insert_t(&mut c, root, k, 1).unwrap();
}
assert_eq!(
t.depth, 1,
"more than SLOTS_PER_PAGE keys must grow to depth 1"
);
let mut freed = Vec::new();
free_subtree(&mut c, root, t.depth, &mut freed).unwrap();
assert_eq!(
freed.len(),
3,
"free_subtree must free the whole depth-1 tree (root + 2 leaves), not just the root"
);
let mut uniq = freed.clone();
uniq.sort_unstable();
uniq.dedup();
assert_eq!(uniq.len(), freed.len(), "each page freed exactly once");
}
#[test]
fn capacity_and_span_saturate_on_out_of_range_depth() {
let r = RadixU64 { depth: 30 };
assert_eq!(r.capacity(), u64::MAX);
assert_eq!(r.span_at_level(30), u64::MAX);
}
#[test]
fn recover_depth_rejects_cyclic_spine() {
let mut c = cache(64);
let id = c.new_page().unwrap();
{
let buf = c.get_mut(id).unwrap();
buf.fill(0);
buf[0] = PageType::MembershipInterior as u8;
buf[1] = page::PAGE_FORMAT_VERSION_CURRENT;
write_slot(buf, 0, id); page::stamp_checksum(buf);
}
let err = match RadixU64::recover_depth(&mut c, 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 corrupt_inner_depth_is_corrupt_page() {
let mut c = cache(64);
let mut idx = MembershipIndex::new();
let root = idx.insert_t(&mut c, PAGE_ID_NONE, 7, 5).unwrap();
{
let buf = c.get_mut(root).unwrap();
let packed = read_slot(buf, 7);
assert_ne!(packed, 0, "tag 7 should occupy outer slot 7");
let corrupt = (packed & INNER_ROOT_MASK) | (30u64 << INNER_ROOT_BITS);
write_slot(buf, 7, corrupt);
page::stamp_checksum(buf);
}
let err = match idx.handles_for_tag(&mut c, root, 7) {
Ok(hs) => panic!("handles_for_tag accepted a corrupt inner depth: {hs:?}"),
Err(e) => e,
};
assert!(
matches!(err, ChiselError::CorruptPage { .. }),
"expected CorruptPage, got {err:?}"
);
}
#[test]
fn iter_bounded_saturates_prefix_on_corrupt_max_depth() {
let mut c = cache(64);
let id = c.new_page().unwrap();
{
let buf = c.get_mut(id).unwrap();
buf.fill(0);
buf[0] = PageType::MembershipInterior as u8;
buf[1] = page::PAGE_FORMAT_VERSION_CURRENT;
write_slot(buf, 20, id); page::stamp_checksum(buf);
}
let t = RadixU64 { depth: MAX_DEPTH };
let _ = t.iter_bounded(&mut c, id, 8).unwrap();
}
#[test]
fn grows_and_iterates_across_levels() {
let mut c = cache(8192);
let mut t = RadixU64::new();
let mut root = t
.create_root(&mut c, &mut |c: &mut PageCache| c.new_page())
.unwrap();
for k in [0u64, 1, 1021, 2000, 1_000_000] {
root = t.insert_t(&mut c, root, k, k + 7).unwrap();
}
assert!(t.depth >= 1, "tree should have grown");
for k in [0u64, 1, 1021, 2000, 1_000_000] {
assert_eq!(t.lookup(&mut c, root, k).unwrap(), k + 7);
}
let mut pairs = t.iter(&mut c, root).unwrap();
pairs.sort();
assert_eq!(
pairs,
vec![
(0, 7),
(1, 8),
(1021, 1028),
(2000, 2007),
(1_000_000, 1_000_007)
]
);
}
#[test]
fn iter_bounded_caps_collection() {
let mut c = cache(8192);
let mut t = RadixU64::new();
let mut root = t
.create_root(&mut c, &mut |c: &mut PageCache| c.new_page())
.unwrap();
for k in 0..50u64 {
root = t.insert_t(&mut c, root, k, k + 1).unwrap();
}
assert_eq!(t.iter_bounded(&mut c, root, 10).unwrap().len(), 10);
assert_eq!(t.iter_bounded(&mut c, root, 100).unwrap().len(), 50);
}
#[test]
fn delete_absent_is_noop() {
let mut c = cache(64);
let mut t = RadixU64::new();
let root = t
.create_root(&mut c, &mut |c: &mut PageCache| c.new_page())
.unwrap();
let (r, prev) = t.delete_t(&mut c, root, 42).unwrap();
assert_eq!(prev, 0);
assert_eq!(r, root, "no COW for an absent key");
}
#[test]
fn recover_depth_matches_after_grow() {
let mut c = cache(8192);
let mut t = RadixU64::new();
let mut root = t
.create_root(&mut c, &mut |c: &mut PageCache| c.new_page())
.unwrap();
root = t.insert_t(&mut c, root, 5_000_000, 1).unwrap();
let recovered = RadixU64::recover_depth(&mut c, root).unwrap();
assert_eq!(recovered, t.depth);
}
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(96))]
#[test]
fn prop_radix_insert_lookup_matches_oracle(
ops in proptest::collection::vec(
// value != 0: 0 is the absent sentinel (insert debug-asserts non-zero).
(0u64..2_500_000u64, 1u64..=u32::MAX as u64),
1..40usize,
)
) {
let mut c = cache(4096);
let mut t = RadixU64::new();
let mut root = t.create_root(&mut c, &mut |c: &mut PageCache| c.new_page()).unwrap();
let mut oracle: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
for (key, value) in &ops {
root = t.insert_t(&mut c, root, *key, *value).unwrap();
oracle.insert(*key, *value); }
for (key, expected) in &oracle {
proptest::prop_assert_eq!(t.lookup(&mut c, root, *key).unwrap(), *expected);
}
let mut absent = 7u64;
while oracle.contains_key(&absent) {
absent += 1;
}
proptest::prop_assert_eq!(t.lookup(&mut c, root, absent).unwrap(), 0);
}
#[test]
fn prop_radix_recover_depth_matches(key in 0u64..1_100_000_000u64) {
let mut c = cache(256);
let mut t = RadixU64::new();
let mut root = t.create_root(&mut c, &mut |c: &mut PageCache| c.new_page()).unwrap();
root = t.insert_t(&mut c, root, key, 1).unwrap();
proptest::prop_assert_eq!(RadixU64::recover_depth(&mut c, root).unwrap(), t.depth);
}
}
#[test]
fn membership_insert_contains_remove() {
let mut c = cache(8192);
let mut idx = MembershipIndex::new();
let mut root = PAGE_ID_NONE;
root = idx.insert_t(&mut c, root, 7, 100).unwrap();
root = idx.insert_t(&mut c, root, 7, 200).unwrap();
root = idx.insert_t(&mut c, root, 9, 300).unwrap();
assert!(idx.contains(&mut c, root, 7, 100).unwrap());
assert!(idx.contains(&mut c, root, 7, 200).unwrap());
assert!(!idx.contains(&mut c, root, 7, 300).unwrap());
let mut h7 = idx.handles_for_tag(&mut c, root, 7).unwrap();
h7.sort();
assert_eq!(h7, vec![100, 200]);
assert_eq!(idx.handles_for_tag(&mut c, root, 9).unwrap(), vec![300]);
let (root2, removed) = idx.remove_t(&mut c, root, 7, 100).unwrap();
assert!(removed);
assert!(!idx.contains(&mut c, root2, 7, 100).unwrap());
assert!(idx.contains(&mut c, root2, 7, 200).unwrap());
let (_root3, removed_again) = idx.remove_t(&mut c, root2, 7, 100).unwrap();
assert!(!removed_again, "removing an absent member reports false");
}
#[test]
fn radix_lookup_out_of_range_key_at_depth_0_is_absent() {
let mut c = cache(8192);
let mut t = RadixU64::new();
let root = t
.create_root(&mut c, &mut |c: &mut PageCache| c.new_page())
.unwrap();
let root = t.insert_t(&mut c, root, 5, 99).unwrap();
assert_eq!(t.depth, 0, "single insert must not grow the tree");
assert_eq!(
t.lookup(&mut c, root, 5).unwrap(),
99,
"in-range key resolves"
);
let aliasing = 5 + SLOTS_PER_PAGE as u64;
assert_eq!(
t.lookup(&mut c, root, aliasing).unwrap(),
0,
"out-of-range key at depth 0 must be absent, not alias an occupied slot"
);
}
#[test]
fn handles_for_tag_bounded_caps_results() {
let mut c = cache(16384);
let mut idx = MembershipIndex::new();
let mut root = PAGE_ID_NONE;
for h in 0..10u64 {
root = idx.insert_t(&mut c, root, 3, 1000 + h).unwrap();
}
assert_eq!(
idx.handles_for_tag_bounded(&mut c, root, 3, 4)
.unwrap()
.len(),
4
);
assert_eq!(
idx.handles_for_tag_bounded(&mut c, root, 3, 100)
.unwrap()
.len(),
10
);
assert_eq!(
idx.handles_for_tag_bounded(&mut c, root, 3, 5)
.unwrap()
.len(),
5
);
}
#[test]
fn inner_grow_roundtrip() {
let mut c = cache(1_000_000);
let mut idx = MembershipIndex::new();
let mut root = PAGE_ID_NONE;
let n = 1100u64;
for h in 0..n {
root = idx.insert_t(&mut c, root, 7, h).unwrap();
}
let mut got = idx.handles_for_tag(&mut c, root, 7).unwrap();
got.sort();
assert_eq!(got, (0..n).collect::<Vec<_>>());
for h in 0..n {
let (r, removed) = idx.remove_t(&mut c, root, 7, h).unwrap();
root = r;
assert!(removed);
}
assert!(idx.handles_for_tag(&mut c, root, 7).unwrap().is_empty());
}
}