Skip to content

Commit 1f7299a

Browse files
committed
feat(btree)!: carry inline values as Bytes end to end
BREAKING: `put_batch` takes `Vec<(Bytes, Bytes)>` and `bulk_load` / `BulkLoader::push` take their values as `Bytes`. `LeafValue::Inline` held a `Vec<u8>`, so every hop between pages copied the value even when nothing about it changed. A repack reads each record out of one page and stages it into another; a history relocation and a rekey do the same. With the read side already producing `Bytes`, each of those boundaries had to copy back down to `Vec<u8>` purely to satisfy the type. Hold it as `Bytes` instead. Values now move by refcount from the page they were read out of, through the scan, into the loader staging the page they are written to. Reads of an uncommitted or dirty leaf stop copying as well: they share what the leaf already holds. `put_batch` takes the row shape the scans return, so read output feeds straight back in without a conversion — the asymmetry only existed because the two sides were changed at different times. Keys are untouched: prefix compression means a full key is not contiguous in the page, so a record owns its key either way. Bulk load allocates 1885 -> 1646 bytes per row. `PageGuard::body` also stops copying, which it only did because the buffer behind it used to be a `Vec`.
1 parent 6a230e9 commit 1f7299a

13 files changed

Lines changed: 95 additions & 57 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ version, since none exists yet.
1414

1515
- **B+ tree surface** — sorted `bytes → bytes` store with copy-on-write shadow
1616
paging, A/B headers, ACID transactions, range scans, monotonic append, and
17-
bulk load.
17+
bulk load. Reads return `Bytes` borrowed from the page cache; scans come
18+
bounded (`scan_from`, `scan_prefix_from`) or materialising.
1819
- **Segment File API** — engine-owned, append-mostly, atomically sealed
1920
encrypted files for formats that own their own layout (vectors, columnar
2021
blocks, FTS postings, R-trees).

src/btree/leaf.rs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Leaf node operations.
22
3+
use bytes::Bytes;
4+
35
use crate::Result;
46
use crate::errors::PagedbError;
57
use crate::pager::PageGuard;
@@ -15,8 +17,14 @@ use super::node::{
1517
/// overflow chain.
1618
#[derive(Debug, Clone)]
1719
pub enum LeafValue {
18-
Inline(Vec<u8>),
19-
Overflow { total_len: u64, root_page_id: u64 },
20+
/// Held as [`Bytes`] so a value read out of one page and written into
21+
/// another — a repack, a history relocation, a rekey — moves by refcount
22+
/// instead of being copied at each hop.
23+
Inline(Bytes),
24+
Overflow {
25+
total_len: u64,
26+
root_page_id: u64,
27+
},
2028
}
2129

2230
/// Bytes an overflow reference occupies in an encoded record body: the
@@ -78,8 +86,8 @@ impl Leaf {
7886
}
7987
} else {
8088
let vlen = value_len_raw as usize;
81-
let v = body[off + 2 + suffix_len + 2..off + 2 + suffix_len + 2 + vlen].to_vec();
82-
LeafValue::Inline(v)
89+
let start = off + 2 + suffix_len + 2;
90+
LeafValue::Inline(Bytes::copy_from_slice(&body[start..start + vlen]))
8391
};
8492
// Reconstruct full key by prepending the common prefix.
8593
let mut full_key = prefix_bytes.clone();
@@ -439,14 +447,14 @@ mod tests {
439447
#[test]
440448
fn inline_round_trip() {
441449
let mut leaf = Leaf::new();
442-
leaf.upsert(b"hello", LeafValue::Inline(b"world".to_vec()));
443-
leaf.upsert(b"hello2", LeafValue::Inline(b"world2".to_vec()));
450+
leaf.upsert(b"hello", LeafValue::Inline(b"world".as_slice().into()));
451+
leaf.upsert(b"hello2", LeafValue::Inline(b"world2".as_slice().into()));
444452
let mut body = make_body();
445453
leaf.encode(&mut body).unwrap();
446454
let decoded = Leaf::decode(&body).unwrap();
447455
assert_eq!(decoded.records.len(), 2);
448456
match &decoded.records[0].1 {
449-
LeafValue::Inline(v) => assert_eq!(v, b"world"),
457+
LeafValue::Inline(v) => assert_eq!(v.as_ref(), b"world"),
450458
LeafValue::Overflow { .. } => panic!("expected inline"),
451459
}
452460
}
@@ -479,9 +487,9 @@ mod tests {
479487
#[test]
480488
fn prefix_compression_applied() {
481489
let mut leaf = Leaf::new();
482-
leaf.upsert(b"prefix/aaa", LeafValue::Inline(b"v1".to_vec()));
483-
leaf.upsert(b"prefix/bbb", LeafValue::Inline(b"v2".to_vec()));
484-
leaf.upsert(b"prefix/ccc", LeafValue::Inline(b"v3".to_vec()));
490+
leaf.upsert(b"prefix/aaa", LeafValue::Inline(b"v1".as_slice().into()));
491+
leaf.upsert(b"prefix/bbb", LeafValue::Inline(b"v2".as_slice().into()));
492+
leaf.upsert(b"prefix/ccc", LeafValue::Inline(b"v3".as_slice().into()));
485493
let mut body = make_body();
486494
leaf.encode(&mut body).unwrap();
487495
// Check prefix_len in header is 7 ("prefix/")
@@ -497,11 +505,11 @@ mod tests {
497505
fn lcp_edge_cases() {
498506
let records: Vec<(Vec<u8>, LeafValue)> = Vec::new();
499507
assert_eq!(lcp(&records), b"");
500-
let one = vec![(b"hello".to_vec(), LeafValue::Inline(vec![]))];
508+
let one = vec![(b"hello".to_vec(), LeafValue::Inline(Bytes::new()))];
501509
assert_eq!(lcp(&one), b"hello");
502510
let two = vec![
503-
(b"abc".to_vec(), LeafValue::Inline(vec![])),
504-
(b"abd".to_vec(), LeafValue::Inline(vec![])),
511+
(b"abc".to_vec(), LeafValue::Inline(Bytes::new())),
512+
(b"abd".to_vec(), LeafValue::Inline(Bytes::new())),
505513
];
506514
assert_eq!(lcp(&two), b"ab");
507515
}

src/btree/tests/basic.rs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::sync::Arc;
22
use std::sync::mpsc;
33
use std::time::Duration;
44

5+
use bytes::Bytes;
6+
57
use crate::btree::BTree;
68
use crate::btree::internal::{Internal, InternalEntry};
79
use crate::btree::leaf::{Leaf, LeafValue};
@@ -328,7 +330,7 @@ async fn read_node_rejects_authenticated_envelope_body_kind_mismatch() {
328330
let realm = RealmId::new([1; 16]);
329331

330332
let mut leaf = Leaf::new();
331-
leaf.upsert(b"k", LeafValue::Inline(b"v".to_vec()));
333+
leaf.upsert(b"k", LeafValue::Inline(b"v".as_slice().into()));
332334
let mut leaf_body = vec![0u8; body_capacity(PAGE)];
333335
leaf.encode(&mut leaf_body).unwrap();
334336

@@ -468,7 +470,7 @@ fn delete_range_rejects_leaf_sibling_cycle_without_hanging() {
468470

469471
let mut leaf = Leaf::new();
470472
leaf.right_sibling = leaf_page_id;
471-
leaf.upsert(b"k", LeafValue::Inline(b"v".to_vec()));
473+
leaf.upsert(b"k", LeafValue::Inline(b"v".as_slice().into()));
472474
let mut body = vec![0u8; body_capacity(PAGE)];
473475
leaf.encode(&mut body).unwrap();
474476
pager
@@ -607,7 +609,7 @@ fn collect_all_rejects_alternating_leaf_successors_without_hanging() {
607609

608610
for (page_id, key) in [(left_leaf, b"a"), (right_leaf, b"b")] {
609611
let mut leaf = Leaf::new();
610-
leaf.upsert(key, LeafValue::Inline(b"v".to_vec()));
612+
leaf.upsert(key, LeafValue::Inline(b"v".as_slice().into()));
611613
let mut body = vec![0u8; body_capacity(PAGE)];
612614
leaf.encode(&mut body).unwrap();
613615
pager
@@ -674,8 +676,8 @@ fn bulk_load_rejects_separator_that_cannot_fit_without_hanging() {
674676
let mut tree = fresh_tree(pager);
675677
let key_len = body_capacity(PAGE) - 32;
676678
tree.bulk_load(vec![
677-
(vec![b'a'; key_len], Vec::new()),
678-
(vec![b'b'; key_len], Vec::new()),
679+
(vec![b'a'; key_len], Bytes::new()),
680+
(vec![b'b'; key_len], Bytes::new()),
679681
])
680682
.await
681683
});
@@ -693,12 +695,12 @@ fn bulk_load_rejects_separator_that_cannot_fit_without_hanging() {
693695
async fn bulk_load_rejects_non_strict_key_order_without_poisoning_tree() {
694696
let cases = [
695697
vec![
696-
(b"b".to_vec(), b"two".to_vec()),
697-
(b"a".to_vec(), b"one".to_vec()),
698+
(b"b".to_vec(), Bytes::from_static(b"two")),
699+
(b"a".to_vec(), Bytes::from_static(b"one")),
698700
],
699701
vec![
700-
(b"a".to_vec(), b"one".to_vec()),
701-
(b"a".to_vec(), b"two".to_vec()),
702+
(b"a".to_vec(), Bytes::from_static(b"one")),
703+
(b"a".to_vec(), Bytes::from_static(b"two")),
702704
],
703705
];
704706

@@ -717,8 +719,8 @@ async fn bulk_load_rejects_non_strict_key_order_without_poisoning_tree() {
717719
assert_eq!(tree.next_page_id(), 4);
718720

719721
tree.bulk_load(vec![
720-
(b"a".to_vec(), b"one".to_vec()),
721-
(b"b".to_vec(), b"two".to_vec()),
722+
(b"a".to_vec(), b"one".as_slice().into()),
723+
(b"b".to_vec(), b"two".as_slice().into()),
722724
])
723725
.await
724726
.unwrap();
@@ -743,9 +745,12 @@ async fn bulk_loader_rejects_non_increasing_keys_across_pushes() {
743745
let mut tree = fresh_tree(pager);
744746
{
745747
let mut loader = tree.bulk_loader().unwrap();
746-
loader.push(b"b".to_vec(), b"one".to_vec()).await.unwrap();
748+
loader
749+
.push(b"b".to_vec(), b"one".as_slice().into())
750+
.await
751+
.unwrap();
747752
let error = loader
748-
.push(second_key.to_vec(), b"two".to_vec())
753+
.push(second_key.to_vec(), b"two".as_slice().into())
749754
.await
750755
.expect_err("descending and duplicate keys must both be rejected");
751756
assert!(
@@ -769,7 +774,7 @@ async fn bulk_loader_rejects_oversized_key_before_allocating() {
769774
{
770775
let mut loader = tree.bulk_loader().unwrap();
771776
let error = loader
772-
.push(vec![b'a'; key_len], Vec::new())
777+
.push(vec![b'a'; key_len], Bytes::new())
773778
.await
774779
.expect_err("a key too large to separate must be rejected");
775780
assert!(matches!(error, PagedbError::PayloadTooLarge));
@@ -800,7 +805,7 @@ async fn bulk_loader_builds_a_multi_level_tree_that_scans_in_order() {
800805
for i in 0..n {
801806
let value = if i % 11 == 0 { &spilled } else { &inline };
802807
loader
803-
.push(format!("k-{i:05}").into_bytes(), value.clone())
808+
.push(format!("k-{i:05}").into_bytes(), value.clone().into())
804809
.await
805810
.unwrap();
806811
}

src/btree/tests/generated_nodes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ fn encoded_leaf(records: &[(Vec<u8>, Vec<u8>)], overflow_every: Option<u64>) ->
7878
total_len: value.len() as u64,
7979
root_page_id,
8080
},
81-
_ => LeafValue::Inline(value.clone()),
81+
_ => LeafValue::Inline(value.clone().into()),
8282
};
8383
leaf.upsert(key, value);
8484
}

src/btree/tests/generated_structural_walks.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ async fn lay_out_graph(pager: &Arc<Pager<MemVfs>>, specs: &[PageSpec]) {
187187
leaf.left_sibling = slot_to_page_id(*left_sibling);
188188
leaf.right_sibling = slot_to_page_id(*right_sibling);
189189
for key in keys {
190-
leaf.upsert(&[*key], LeafValue::Inline(vec![*key; 4]));
190+
leaf.upsert(&[*key], LeafValue::Inline(vec![*key; 4].into()));
191191
}
192192
if leaf.encode(&mut body).is_err() {
193193
continue;

src/btree/tests/tree_ops.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::collections::BTreeMap;
22
use std::sync::Arc;
33

4+
use bytes::Bytes;
5+
46
use crate::RealmId;
57
use crate::btree::BTree;
68
use crate::crypto::CipherId;
@@ -124,13 +126,18 @@ async fn put_batch_inserts_all() {
124126
let pager = fresh_pager().await;
125127
let mut tree = fresh_tree(pager);
126128
let v = vec![0u8; 16];
127-
let batch: Vec<(Vec<u8>, Vec<u8>)> = (0..200u32)
128-
.map(|i| (format!("k{i:04}").into_bytes(), v.clone()))
129+
let batch: Vec<(Bytes, Bytes)> = (0..200u32)
130+
.map(|i| {
131+
(
132+
Bytes::from(format!("k{i:04}").into_bytes()),
133+
Bytes::from(v.clone()),
134+
)
135+
})
129136
.collect();
130137
tree.put_batch(batch.clone()).await.unwrap();
131138
for (k, expected) in &batch {
132139
let got = tree.get(k).await.unwrap();
133-
assert_eq!(got.as_deref(), Some(expected.as_slice()));
140+
assert_eq!(got.as_deref(), Some(expected.as_ref()));
134141
}
135142
}
136143

src/btree/tree/bulk/loader.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
88
use std::sync::Arc;
99

10+
use bytes::Bytes;
11+
1012
use crate::Result;
1113
use crate::errors::PagedbError;
1214
use crate::vfs::Vfs;
@@ -92,7 +94,10 @@ impl<'a, V: Vfs> BulkLoader<'a, V> {
9294
/// large for a leaf or a separator with `PayloadTooLarge`. Both checks run
9395
/// before this record allocates a page or touches the cache, so a rejected
9496
/// record leaves the tree exactly as it found it.
95-
pub async fn push(&mut self, key: Vec<u8>, value: Vec<u8>) -> Result<()> {
97+
/// `value` is taken as [`Bytes`] so a repack, which reads each record out
98+
/// of one page and stages it into another, moves it by refcount instead of
99+
/// copying it at the boundary.
100+
pub async fn push(&mut self, key: Vec<u8>, value: Bytes) -> Result<()> {
96101
if let Some(last) = &self.last_key {
97102
if key <= *last {
98103
return Err(PagedbError::BulkLoadNotMonotonic);
@@ -287,7 +292,7 @@ impl<V: Vfs> BTree<V> {
287292
/// caller holding a whole input is the incremental-apply history carry,
288293
/// which is native-only.
289294
#[cfg(any(test, not(target_arch = "wasm32")))]
290-
pub async fn bulk_load(&mut self, pairs: Vec<(Vec<u8>, Vec<u8>)>) -> Result<()> {
295+
pub async fn bulk_load(&mut self, pairs: Vec<(Vec<u8>, Bytes)>) -> Result<()> {
291296
let mut loader = self.bulk_loader()?;
292297
for (key, value) in pairs {
293298
loader.push(key, value).await?;

src/btree/tree/read.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ impl<V: Vfs> BTree<V> {
4848
) -> Result<Option<Bytes>> {
4949
match leaf.get(key) {
5050
None => Ok(None),
51-
// Uncommitted, so it lives in the decoded leaf rather than in any
52-
// page a slice could borrow from.
53-
Some(LeafValue::Inline(v)) => Ok(Some(Bytes::copy_from_slice(v))),
51+
// Uncommitted: held by the decoded leaf rather than by a page, but
52+
// still shared rather than copied.
53+
Some(LeafValue::Inline(v)) => Ok(Some(v.clone())),
5454
Some(LeafValue::Overflow {
5555
total_len,
5656
root_page_id,
@@ -136,9 +136,9 @@ impl<V: Vfs> BTree<V> {
136136
{
137137
return match leaf.get(key) {
138138
None => Ok(None),
139-
// Uncommitted, so it lives in the decoded leaf rather than in
140-
// any page a slice could borrow from.
141-
Some(LeafValue::Inline(v)) => Ok(Some(Bytes::copy_from_slice(v))),
139+
// Uncommitted: held by the decoded leaf rather than by a page,
140+
// but still shared rather than copied.
141+
Some(LeafValue::Inline(v)) => Ok(Some(v.clone())),
142142
Some(LeafValue::Overflow {
143143
total_len,
144144
root_page_id,
@@ -172,7 +172,8 @@ impl<V: Vfs> BTree<V> {
172172
/// needed.
173173
pub(super) async fn resolve_leaf_value(&self, v: &LeafValue) -> Result<Bytes> {
174174
match v {
175-
LeafValue::Inline(b) => Ok(Bytes::copy_from_slice(b)),
175+
// Refcount bump, not a copy.
176+
LeafValue::Inline(b) => Ok(b.clone()),
176177
LeafValue::Overflow {
177178
total_len,
178179
root_page_id,

src/btree/tree/write.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
33
use std::sync::Arc;
44

5+
use bytes::Bytes;
6+
57
use crate::Result;
68
use crate::errors::PagedbError;
79
use crate::vfs::Vfs;
@@ -102,7 +104,7 @@ impl<V: Vfs> BTree<V> {
102104
root_page_id,
103105
})
104106
} else {
105-
Ok(LeafValue::Inline(value.to_vec()))
107+
Ok(LeafValue::Inline(Bytes::copy_from_slice(value)))
106108
}
107109
}
108110

@@ -378,7 +380,7 @@ impl<V: Vfs> BTree<V> {
378380
///
379381
/// Per-leaf batching (amortising `CoW`) is a deferred performance
380382
/// optimisation; this implementation is correct for all inputs.
381-
pub async fn put_batch(&mut self, pairs: Vec<(Vec<u8>, Vec<u8>)>) -> Result<()> {
383+
pub async fn put_batch(&mut self, pairs: Vec<(Bytes, Bytes)>) -> Result<()> {
382384
for (k, v) in pairs {
383385
self.put(&k, &v).await?;
384386
}

src/compaction/helpers.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ pub(super) async fn stream_dense_tree<V: Vfs + Clone>(
7777
if !keep(&key) {
7878
continue;
7979
}
80-
// The loader owns what it stages into pages it is building, so the
81-
// scan's borrowed view has to be copied out here.
82-
loader.push(key.to_vec(), value.to_vec()).await?;
80+
// The value carries through by refcount; only the key, which the
81+
// staged record owns, is copied.
82+
loader.push(key.to_vec(), value).await?;
8383
// A compacted page is written once and never read back, so flushing
8484
// it out mid-batch costs nothing and keeps the pool from growing
8585
// with the tree instead of with the budget.

0 commit comments

Comments
 (0)