#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
use crate::btree_v2::NodeInfo;
pub(crate) const NODE_SIZE: u32 = 512;
const SPLIT_PERCENT: u8 = 100;
const MERGE_PERCENT: u8 = 40;
struct PlannedNode {
depth: u16,
records: Vec<usize>,
children: Vec<usize>,
}
pub(crate) struct BTreeV2Plan {
tree_type: u8,
node_size: u32,
record_size: u16,
depth: u16,
total_records: u64,
nodes: Vec<PlannedNode>,
info: NodeInfo,
}
pub(crate) struct BTreeV2Image {
pub(crate) header: Vec<u8>,
pub(crate) nodes: Vec<u8>,
}
pub(crate) const fn header_size(offset_size: u8, length_size: u8) -> usize {
4 + 1 + 1 + 4 + 2 + 2 + 1 + 1 + offset_size as usize + 2 + length_size as usize + 4
}
fn distribute(total: usize, parts: usize) -> Vec<usize> {
debug_assert!(parts > 0, "a node always has at least one child");
let base = total / parts;
let remainder = total % parts;
(0..parts)
.map(|i| if i < remainder { base + 1 } else { base })
.collect()
}
fn capacity_as_usize(capacity: u64) -> usize {
usize::try_from(capacity).unwrap_or(usize::MAX)
}
impl BTreeV2Plan {
pub(crate) fn new(
tree_type: u8,
record_count: usize,
record_size: u16,
node_size: u32,
offset_size: u8,
) -> Option<BTreeV2Plan> {
let (info, depth) =
NodeInfo::for_record_count(node_size, record_size, offset_size, record_count as u64)?;
let mut nodes = Vec::new();
if record_count == 0 {
nodes.push(PlannedNode {
depth: 0,
records: Vec::new(),
children: Vec::new(),
});
} else {
let mut next_record = 0usize;
plan_subtree(record_count, depth, &info, &mut next_record, &mut nodes)?;
debug_assert_eq!(next_record, record_count, "every record is placed once");
}
Some(BTreeV2Plan {
tree_type,
node_size,
record_size,
depth,
total_records: record_count as u64,
nodes,
info,
})
}
pub(crate) fn nodes_size(&self) -> u64 {
self.nodes.len() as u64 * self.node_size as u64
}
pub(crate) fn serialize(
&self,
records: &[u8],
nodes_address: u64,
offset_size: u8,
length_size: u8,
) -> BTreeV2Image {
let rs = self.record_size as usize;
debug_assert_eq!(
records.len() as u64,
self.total_records * rs as u64,
"record buffer must hold exactly the records the plan placed"
);
let mut subtree_total = vec![0u64; self.nodes.len()];
for (i, node) in self.nodes.iter().enumerate() {
let below: u64 = node.children.iter().map(|&c| subtree_total[c]).sum();
subtree_total[i] = node.records.len() as u64 + below;
}
let address_of = |index: usize| nodes_address + index as u64 * self.node_size as u64;
let mut nodes = Vec::with_capacity(self.nodes.len() * self.node_size as usize);
for (i, node) in self.nodes.iter().enumerate() {
let start = nodes.len();
nodes.extend_from_slice(if node.depth == 0 { b"BTLF" } else { b"BTIN" });
nodes.push(0); nodes.push(self.tree_type);
for &r in &node.records {
nodes.extend_from_slice(&records[r * rs..(r + 1) * rs]);
}
if node.depth > 0 {
let nrec_width = self.info.max_nrec_size();
let total_width = self.info.total_nrec_size(node.depth);
for &child in &node.children {
write_uint(&mut nodes, address_of(child), offset_size as usize);
write_uint(
&mut nodes,
self.nodes[child].records.len() as u64,
nrec_width,
);
write_uint(&mut nodes, subtree_total[child], total_width);
}
}
let checksum = crate::checksum::jenkins_lookup3(&nodes[start..]);
nodes.extend_from_slice(&checksum.to_le_bytes());
debug_assert!(
nodes.len() - start <= self.node_size as usize,
"a planned node overflows its node size"
);
nodes.resize(start + self.node_size as usize, 0);
debug_assert_eq!(address_of(i) - nodes_address, start as u64);
}
let root = self.nodes.last().expect("a plan always has a root");
let mut header = Vec::with_capacity(header_size(offset_size, length_size));
header.extend_from_slice(b"BTHD");
header.push(0); header.push(self.tree_type);
header.extend_from_slice(&self.node_size.to_le_bytes());
header.extend_from_slice(&self.record_size.to_le_bytes());
header.extend_from_slice(&self.depth.to_le_bytes());
header.push(SPLIT_PERCENT);
header.push(MERGE_PERCENT);
write_uint(
&mut header,
address_of(self.nodes.len() - 1),
offset_size as usize,
);
#[expect(
clippy::cast_possible_truncation,
reason = "the root's own record count is bounded by its level's capacity, which \
`NodeInfo` derives from the node size — far below u16::MAX for any node \
size this crate emits"
)]
let root_nrec = root.records.len() as u16;
header.extend_from_slice(&root_nrec.to_le_bytes());
write_uint(&mut header, self.total_records, length_size as usize);
let checksum = crate::checksum::jenkins_lookup3(&header);
header.extend_from_slice(&checksum.to_le_bytes());
debug_assert_eq!(header.len(), header_size(offset_size, length_size));
BTreeV2Image { header, nodes }
}
#[cfg(test)]
fn depth(&self) -> u16 {
self.depth
}
}
fn min_records(depth: u16) -> u64 {
1u64.checked_shl(depth as u32 + 1)
.map_or(u64::MAX, |v| v - 1)
}
fn plan_subtree(
count: usize,
depth: u16,
info: &NodeInfo,
next_record: &mut usize,
out: &mut Vec<PlannedNode>,
) -> Option<usize> {
debug_assert!(
count as u64 <= info.cum_max_nrec(depth),
"a subtree was handed more records than its depth can hold"
);
if (count as u64) < min_records(depth) {
return None;
}
if depth == 0 {
let records = (*next_record..*next_record + count).collect();
*next_record += count;
out.push(PlannedNode {
depth,
records,
children: Vec::new(),
});
return Some(out.len() - 1);
}
let child_capacity = capacity_as_usize(info.cum_max_nrec(depth - 1));
let k = count
.saturating_sub(child_capacity)
.div_ceil(child_capacity + 1)
.max(1);
debug_assert!(
k as u64 <= info.max_nrec(depth),
"a node was given more records than its level can hold"
);
let group_sizes = distribute(count - k, k + 1);
let mut records = Vec::with_capacity(k);
let mut children = Vec::with_capacity(k + 1);
for (i, &size) in group_sizes.iter().enumerate() {
children.push(plan_subtree(size, depth - 1, info, next_record, out)?);
if i < k {
records.push(*next_record);
*next_record += 1;
}
}
out.push(PlannedNode {
depth,
records,
children,
});
Some(out.len() - 1)
}
fn write_uint(buf: &mut Vec<u8>, value: u64, width: usize) {
for i in 0..width {
#[expect(
clippy::cast_possible_truncation,
reason = "masked to one byte by the shift-and-truncate"
)]
buf.push((value >> (i * 8)) as u8);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records};
const OFFSET_SIZE: u8 = 8;
const LENGTH_SIZE: u8 = 8;
fn numbered_records(count: usize, record_size: u16) -> Vec<u8> {
let mut buf = Vec::with_capacity(count * record_size as usize);
for i in 0..count as u64 {
let mut rec = vec![0u8; record_size as usize];
rec[..8].copy_from_slice(&i.to_le_bytes());
buf.extend_from_slice(&rec);
}
buf
}
fn round_trip(count: usize, record_size: u16) -> (u16, Vec<u64>) {
let plan =
BTreeV2Plan::new(8, count, record_size, NODE_SIZE, OFFSET_SIZE).expect("plannable");
let records = numbered_records(count, record_size);
let nodes_address = header_size(OFFSET_SIZE, LENGTH_SIZE) as u64;
let image = plan.serialize(&records, nodes_address, OFFSET_SIZE, LENGTH_SIZE);
let mut file = image.header.clone();
file.extend_from_slice(&image.nodes);
let header = BTreeV2Header::parse(&file, 0, OFFSET_SIZE, LENGTH_SIZE).expect("header");
assert_eq!(header.node_size, NODE_SIZE);
assert_eq!(header.total_records, count as u64);
let read =
collect_btree_v2_records(&file, &header, OFFSET_SIZE, LENGTH_SIZE).expect("read");
let ids = read
.iter()
.map(|r| u64::from_le_bytes(r.data[..8].try_into().expect("8 bytes")))
.collect();
(plan.depth(), ids)
}
#[test]
fn every_record_survives_a_round_trip_in_order() {
for count in [0, 1, 29, 30, 568, 569, 570, 10_259, 10_260, 40_000] {
let (_, ids) = round_trip(count, 17);
assert_eq!(
ids,
(0..count as u64).collect::<Vec<_>>(),
"round trip of {count} records"
);
}
}
#[test]
fn depth_grows_only_when_the_level_below_is_full() {
assert_eq!(round_trip(29, 17).0, 0);
assert_eq!(round_trip(30, 17).0, 1);
assert_eq!(round_trip(569, 17).0, 1);
assert_eq!(round_trip(570, 17).0, 2);
assert_eq!(round_trip(10_259, 17).0, 2);
assert_eq!(round_trip(10_260, 17).0, 3);
}
#[test]
fn a_wider_record_reaches_depth_sooner() {
let (depth, ids) = round_trip(1_000, 24);
assert_eq!(ids, (0..1_000u64).collect::<Vec<_>>());
assert_eq!(depth, 2, "20 records per leaf, 380 per depth-1 subtree");
}
#[test]
fn no_node_exceeds_its_capacity_or_sits_empty() {
for count in [1usize, 30, 569, 570, 10_260, 40_000] {
let plan = BTreeV2Plan::new(8, count, 17, NODE_SIZE, OFFSET_SIZE).expect("plannable");
for node in &plan.nodes {
assert!(
!node.records.is_empty(),
"empty node at depth {} for {count} records",
node.depth
);
assert!(
node.records.len() as u64 <= plan.info.max_nrec(node.depth),
"node at depth {} holds {} records, capacity {}",
node.depth,
node.records.len(),
plan.info.max_nrec(node.depth)
);
assert_eq!(
node.children.len(),
if node.depth == 0 {
0
} else {
node.records.len() + 1
},
"a node's children must interleave with its records"
);
}
}
}
#[test]
fn nodes_are_all_one_node_size_long() {
let plan = BTreeV2Plan::new(8, 5_000, 17, NODE_SIZE, OFFSET_SIZE).expect("plannable");
let image = plan.serialize(
&numbered_records(5_000, 17),
4_096,
OFFSET_SIZE,
LENGTH_SIZE,
);
assert_eq!(image.nodes.len() as u64, plan.nodes_size());
assert_eq!(image.nodes.len() % NODE_SIZE as usize, 0);
}
#[test]
fn a_shape_needing_an_empty_node_is_refused() {
assert!(BTreeV2Plan::new(8, 1_000, 200, 256, OFFSET_SIZE).is_none());
assert!(BTreeV2Plan::new(8, 4, 200, 256, OFFSET_SIZE).is_none());
assert!(BTreeV2Plan::new(8, 7, 200, 256, OFFSET_SIZE).is_some());
}
}