#![allow(dead_code)]
use line_ending::LineEnding;
use llkv_btree::bplus_tree::BPlusTree;
use llkv_btree::codecs::{BigEndianIdCodec, BigEndianKeyCodec, KeyCodec};
use llkv_btree::errors::Error;
use llkv_btree::pager::Pager;
use llkv_btree::traits::GraphvizExt;
use llkv_btree::{views::key_view::KeyRef, views::value_view::ValueRef};
use rand::rngs::StdRng;
use rand::{SeedableRng, seq::SliceRandom};
use rustc_hash::FxHashMap;
use std::sync::{Arc, Mutex};
struct TestPagerState {
pages: FxHashMap<u64, Arc<[u8]>>,
next_id: u64,
}
pub struct TestPager {
page_size: usize,
state: Mutex<TestPagerState>,
}
impl TestPager {
pub fn new(page_size: usize) -> Self {
Self {
page_size,
state: Mutex::new(TestPagerState {
pages: FxHashMap::default(),
next_id: 1,
}),
}
}
}
impl Pager for TestPager {
type Id = u64;
type Page = Arc<[u8]>;
fn read_batch(&self, ids: &[Self::Id]) -> Result<FxHashMap<Self::Id, Self::Page>, Error> {
let state = self.state.lock().unwrap();
Ok(ids
.iter()
.filter_map(|id| state.pages.get(id).map(|p| (*id, p.clone())))
.collect())
}
fn write_batch(&self, pages: &[(Self::Id, &[u8])]) -> Result<(), Error> {
let mut state = self.state.lock().unwrap();
for (id, data) in pages {
state.pages.insert(*id, Arc::from(*data));
}
Ok(())
}
fn alloc_ids(&self, count: usize) -> Result<Vec<Self::Id>, Error> {
let mut state = self.state.lock().unwrap();
let start = state.next_id;
state.next_id += count as u64;
Ok((start..state.next_id).collect())
}
fn dealloc_ids(&self, ids: &[Self::Id]) -> Result<(), Error> {
let mut state = self.state.lock().unwrap();
for id in ids {
state.pages.remove(id);
}
Ok(())
}
fn page_size_hint(&self) -> Option<usize> {
Some(self.page_size)
}
fn materialize_owned(&self, bytes: &[u8]) -> Result<Self::Page, Error> {
Ok(Arc::from(bytes))
}
}
#[derive(Clone)]
pub struct SharedPager {
inner: Arc<Mutex<Inner>>,
}
struct Inner {
pages: FxHashMap<u64, Arc<[u8]>>,
next_id: u64,
page_size: usize,
last_root: Option<u64>,
}
impl SharedPager {
pub fn new(page_size: usize) -> Self {
Self {
inner: Arc::new(Mutex::new(Inner {
pages: FxHashMap::default(),
next_id: 1,
page_size,
last_root: None,
})),
}
}
pub fn last_root(&self) -> Option<u64> {
self.inner.lock().unwrap().last_root
}
}
impl Pager for SharedPager {
type Id = u64;
type Page = Arc<[u8]>;
fn read_batch(&self, ids: &[Self::Id]) -> Result<FxHashMap<Self::Id, Self::Page>, Error> {
let g = self.inner.lock().unwrap();
Ok(ids
.iter()
.filter_map(|id| g.pages.get(id).map(|p| (*id, p.clone())))
.collect())
}
fn write_batch(&self, pages: &[(Self::Id, &[u8])]) -> Result<(), Error> {
let mut g = self.inner.lock().unwrap();
for (id, data) in pages {
if data.len() > g.page_size {
return Err(Error::Corrupt("page overflow"));
}
g.pages.insert(*id, Arc::from(*data));
}
Ok(())
}
fn alloc_ids(&self, count: usize) -> Result<Vec<Self::Id>, Error> {
let mut g = self.inner.lock().unwrap();
let start = g.next_id;
g.next_id += count as u64;
Ok((start..g.next_id).collect())
}
fn dealloc_ids(&self, ids: &[Self::Id]) -> Result<(), Error> {
let mut g = self.inner.lock().unwrap();
for id in ids {
g.pages.remove(id);
}
Ok(())
}
fn page_size_hint(&self) -> Option<usize> {
Some(self.inner.lock().unwrap().page_size)
}
fn materialize_owned(&self, bytes: &[u8]) -> Result<Self::Page, Error> {
Ok(Arc::from(bytes))
}
fn on_root_changed(&self, new_root: Self::Id) -> Result<(), Error> {
self.inner.lock().unwrap().last_root = Some(new_root);
Ok(())
}
}
pub type TreeU64<P> = BPlusTree<P, BigEndianKeyCodec<u64>, BigEndianIdCodec<u64>>;
pub type U64Tree = TreeU64<TestPager>;
pub fn create_tree() -> Result<TreeU64<TestPager>, Error> {
let pager = TestPager::new(256);
BPlusTree::<_, BigEndianKeyCodec<u64>, BigEndianIdCodec<u64>>::create_empty(pager, None)
}
pub fn populate_tree(tree: &mut TreeU64<TestPager>) -> Result<Vec<(u64, Vec<u8>)>, Error> {
const N: usize = 20_000;
let mut rng = StdRng::seed_from_u64(42);
let mut keys: Vec<u64> = (0..N as u64).collect();
keys.shuffle(&mut rng);
let shared_value = b"this is a duplicated value".to_vec();
let mut items = Vec::with_capacity(N);
let mut expected = Vec::with_capacity(N);
for (i, &k) in keys.iter().enumerate() {
let value = if i % 5 == 0 {
shared_value.clone()
} else {
format!("unique_value_{i}").into_bytes()
};
items.push((k, value.clone()));
expected.push((k, value));
}
expected.sort_by_key(|e| e.0);
let owned: Vec<_> = items.iter().map(|(k, v)| (*k, v.as_slice())).collect();
tree.insert_many(&owned)?;
Ok(expected)
}
pub fn expect_val_u64<P>(tree: &TreeU64<P>, k: u64, expected: Option<&[u8]>)
where
P: Pager<Id = u64>,
P::Page: Clone,
{
let got = tree.get(&k).unwrap();
match (got, expected) {
(None, None) => {}
(Some(vref), Some(bytes)) => assert_eq!(&*vref, bytes),
(g, e) => panic!(
"for key {}, got {:?}, expected {:?}",
k,
g.map(|v| v.as_ref().to_vec()),
e.map(|b| b.to_vec())
),
}
}
pub fn snapshot_as_vec_u64<P>(tree: &TreeU64<P>) -> Vec<(u64, Vec<u8>)>
where
P: Pager<Id = u64>,
P::Page: Clone,
{
tree.iter()
.unwrap()
.map(|(k, v)| {
(
BigEndianKeyCodec::<u64>::decode_from(k.as_ref()).unwrap(),
v.as_ref().to_vec(),
)
})
.collect()
}
pub fn assert_dots_eq_u64<P>(a: &TreeU64<P>, b: &TreeU64<P>, stage: &str)
where
P: Pager<Id = u64>,
P::Page: Clone,
{
let da = a.to_dot().expect("to_dot(A)");
let db = b.to_dot().expect("to_dot(B)");
if da != db {
if let Ok(prefix) = std::env::var("LLKV_DUMP_DOT") {
let _ = std::fs::write(format!("{}_A_{}.dot", prefix, stage), &da);
let _ = std::fs::write(format!("{}_B_{}.dot", prefix, stage), &db);
}
let al: Vec<_> = da.lines().collect();
let bl: Vec<_> = db.lines().collect();
for i in 0..al.len().max(bl.len()) {
let l = al.get(i).copied().unwrap_or("<EOF>");
let r = bl.get(i).copied().unwrap_or("<EOF>");
if l != r {
panic!(
"DOT mismatch @ {stage} (line {}):\nA: {l}\nB: {r}\n--- (set LLKV_DUMP_DOT to dump full graphs)",
i + 1
);
}
}
panic!("DOT mismatch @ {stage}, but could not locate differing line");
}
}
pub fn reopen_latest(pager: &SharedPager) -> TreeU64<SharedPager> {
let root = pager.last_root().expect("no last_root recorded");
BPlusTree::new(pager.clone(), root, None)
}
pub fn collect_and_validate_iter<P: Pager, KC: KeyCodec>(
iter: impl Iterator<Item = (KeyRef<P::Page>, ValueRef<P::Page>)>,
expected_keys: &[KC::Key],
expected_values: &[&[u8]],
) where
P::Page: Clone,
KC::Key: PartialEq + std::fmt::Debug,
{
let results: Vec<(KC::Key, Vec<u8>)> = iter
.map(|(k_ref, v_ref)| {
(
KC::decode_from(k_ref.as_ref()).unwrap(),
v_ref.as_ref().to_vec(),
)
})
.collect();
let expected_values_vec: Vec<Vec<u8>> = expected_values.iter().map(|v| v.to_vec()).collect();
assert_eq!(
results.len(),
expected_keys.len(),
"Iterator yielded a different number of items."
);
for i in 0..results.len() {
assert_eq!(
results[i].0, expected_keys[i],
"Keys do not match at index {}",
i
);
assert_eq!(
results[i].1, expected_values_vec[i],
"Values do not match at index {}",
i
);
}
}
pub fn diff_dump(who: &str, got: &[(u64, Vec<u8>)], want: &[(u64, Vec<u8>)]) {
use std::collections::BTreeMap;
let gm: BTreeMap<u64, &Vec<u8>> = got.iter().map(|(k, v)| (*k, v)).collect();
let wm: BTreeMap<u64, &Vec<u8>> = want.iter().map(|(k, v)| (*k, v)).collect();
let missing: Vec<u64> = wm
.keys()
.filter(|k| !gm.contains_key(k))
.take(10)
.cloned()
.collect();
let extra: Vec<u64> = gm
.keys()
.filter(|k| !wm.contains_key(k))
.take(10)
.cloned()
.collect();
let mut first_val_mismatch: Option<(u64, Vec<u8>, Vec<u8>)> = None;
for k in wm.keys() {
if let (Some(gv), Some(wv)) = (gm.get(k), wm.get(k))
&& *gv != *wv
{
first_val_mismatch = Some((*k, (*gv).clone(), (*wv).clone()));
break;
}
}
println!("DIFF [{}]:", who);
if !missing.is_empty() {
println!(" missing keys (sample): {:?}", missing);
}
if !extra.is_empty() {
println!(" extra keys (sample): {:?}", extra);
}
if let Some((k, g, w)) = first_val_mismatch {
println!(
" first value mismatch at key {}: got {:?} want {:?}",
k, g, w
);
}
}
pub fn assert_matches_golden(actual: &str, golden_path: &str) {
use std::fs;
use std::path::Path;
let p = Path::new(golden_path);
if std::env::var("UPDATE_SNAPSHOTS").as_deref() == Ok("1") {
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, actual.as_bytes()).unwrap();
eprintln!("updated snapshot: {golden_path}");
} else {
let expected = LineEnding::normalize(
&fs::read_to_string(p).expect("missing golden; set UPDATE_SNAPSHOTS=1"),
);
assert_eq!(actual, expected, "snapshot mismatch: {golden_path}",);
}
}