Skip to content

Commit 6a230e9

Browse files
committed
feat(txn)!: return scan rows as Bytes
BREAKING: the `ReadTxn` scans return `Vec<(Bytes, Bytes)>` rather than `Vec<(Vec<u8>, Vec<u8>)>`, matching `get`. Done now for the same reason `get` was: the signature is the part that cannot change after 0.1. Rows still cost an allocation each — keys are prefix- compressed, so a full key is not contiguous in the page and has to be reassembled — but with the type in place that becomes an internal optimisation rather than another breaking change. Callers that stage scan output into pages they own — the compaction repack loader, the history relocator, the rekey segment walk — copy at the boundary, which is what they already did.
1 parent e707d87 commit 6a230e9

9 files changed

Lines changed: 51 additions & 38 deletions

File tree

src/btree/tests/basic.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ async fn forward_scan_returns_sorted() {
180180
let got = tree.collect_range(b"k0010", b"k0020").await.unwrap();
181181
let keys: Vec<String> = got
182182
.into_iter()
183-
.map(|(k, _)| String::from_utf8(k).unwrap())
183+
.map(|(k, _)| String::from_utf8(k.to_vec()).unwrap())
184184
.collect();
185185
let expected: Vec<String> = (10..20).map(|i| format!("k{i:04}")).collect();
186186
assert_eq!(keys, expected);
@@ -206,7 +206,7 @@ async fn collect_all_returns_every_key_including_the_top_of_the_keyspace() {
206206

207207
let all = tree.collect_all().await.unwrap();
208208
assert_eq!(all.len(), 202);
209-
let keys: Vec<&[u8]> = all.iter().map(|(k, _)| k.as_slice()).collect();
209+
let keys: Vec<&[u8]> = all.iter().map(|(k, _)| k.as_ref()).collect();
210210
assert!(keys.windows(2).all(|w| w[0] < w[1]), "not ascending");
211211
assert_eq!(keys[200], &[0xFF; 256]);
212212
assert_eq!(keys[201], beyond.as_slice());
@@ -816,7 +816,7 @@ async fn bulk_loader_builds_a_multi_level_tree_that_scans_in_order() {
816816
let all = tree.collect_all().await.unwrap();
817817
assert_eq!(all.len(), n);
818818
for (i, (key, value)) in all.iter().enumerate() {
819-
assert_eq!(key.as_slice(), format!("k-{i:05}").as_bytes());
819+
assert_eq!(key.as_ref(), format!("k-{i:05}").as_bytes());
820820
let want = if i % 11 == 0 { &spilled } else { &inline };
821821
assert_eq!(value, want, "value mismatch at record {i}");
822822
}

src/btree/tests/tree_ops.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ async fn scan_rev_returns_descending() {
100100
let got = tree.scan_rev(b"k0010", b"k0020").await.unwrap();
101101
let keys: Vec<String> = got
102102
.into_iter()
103-
.map(|(k, _)| String::from_utf8(k).unwrap())
103+
.map(|(k, _)| String::from_utf8(k.to_vec()).unwrap())
104104
.collect();
105105
let expected: Vec<String> = (10..20).rev().map(|i| format!("k{i:04}")).collect();
106106
assert_eq!(keys, expected);
@@ -115,7 +115,7 @@ async fn scan_prefix_short_circuits() {
115115
tree.put(word.as_bytes(), &v).await.unwrap();
116116
}
117117
let got = tree.scan_prefix(b"app").await.unwrap();
118-
let keys: Vec<&[u8]> = got.iter().map(|(k, _)| k.as_slice()).collect();
118+
let keys: Vec<&[u8]> = got.iter().map(|(k, _)| k.as_ref()).collect();
119119
assert_eq!(keys, vec![b"apple".as_ref(), b"apply".as_ref()]);
120120
}
121121

src/btree/tree/read.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,15 @@ impl<V: Vfs> BTree<V> {
170170

171171
/// Resolve a `LeafValue` to its raw bytes. Follows overflow chains as
172172
/// needed.
173-
pub(super) async fn resolve_leaf_value(&self, v: &LeafValue) -> Result<Vec<u8>> {
173+
pub(super) async fn resolve_leaf_value(&self, v: &LeafValue) -> Result<Bytes> {
174174
match v {
175-
LeafValue::Inline(b) => Ok(b.clone()),
175+
LeafValue::Inline(b) => Ok(Bytes::copy_from_slice(b)),
176176
LeafValue::Overflow {
177177
total_len,
178178
root_page_id,
179-
} => overflow::read_chain(&self.pager, self.realm_id, *root_page_id, *total_len).await,
179+
} => overflow::read_chain(&self.pager, self.realm_id, *root_page_id, *total_len)
180+
.await
181+
.map(Bytes::from),
180182
}
181183
}
182184
}

src/btree/tree/scan.rs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Range, reverse, and prefix scans.
22
3+
use bytes::Bytes;
4+
35
use crate::Result;
46
use crate::vfs::Vfs;
57

@@ -33,13 +35,13 @@ impl<V: Vfs> BTree<V> {
3335
/// lets the write path skip sibling-pointer `CoW` (saves ~2 leaf rewrites
3436
/// per put) at the cost of one extra internal-node lookup per leaf
3537
/// boundary crossed during a scan.
36-
pub async fn collect_range(&self, start: &[u8], end: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
38+
pub async fn collect_range(&self, start: &[u8], end: &[u8]) -> Result<Vec<(Bytes, Bytes)>> {
3739
if self.root_page_id == 0 {
3840
return Ok(Vec::new());
3941
}
4042
let mut path = self.path_to_leaf_for_key(start).await?;
4143
let mut seen_leaves = scan_guard();
42-
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
44+
let mut out: Vec<(Bytes, Bytes)> = Vec::new();
4345
loop {
4446
let leaf_id = *path.last().expect("non-empty path");
4547
seen_leaves.insert(leaf_id)?;
@@ -50,7 +52,7 @@ impl<V: Vfs> BTree<V> {
5052
}
5153
if k.as_slice() >= start {
5254
let val = self.resolve_leaf_value(v).await?;
53-
out.push((k.clone(), val));
55+
out.push((Bytes::copy_from_slice(k), val));
5456
}
5557
}
5658
match self.next_leaf_after(&path).await? {
@@ -68,22 +70,22 @@ impl<V: Vfs> BTree<V> {
6870
/// bound is beyond the valid key domain: a bounded "scan everything" would
6971
/// silently drop records at the top of the keyspace (`[0xFF; N]` and any
7072
/// key extending it). Callers that need the whole tree must use this.
71-
pub async fn collect_all(&self) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
73+
pub async fn collect_all(&self) -> Result<Vec<(Bytes, Bytes)>> {
7274
if self.root_page_id == 0 {
7375
return Ok(Vec::new());
7476
}
7577
// The empty key sorts below every stored key, so the descent lands on
7678
// the leftmost leaf.
7779
let mut path = self.path_to_leaf_for_key(&[]).await?;
7880
let mut seen_leaves = scan_guard();
79-
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
81+
let mut out: Vec<(Bytes, Bytes)> = Vec::new();
8082
loop {
8183
let leaf_id = *path.last().expect("non-empty path");
8284
seen_leaves.insert(leaf_id)?;
8385
let leaf = self.read_leaf(leaf_id).await?;
8486
for (k, v) in &leaf.records {
8587
let val = self.resolve_leaf_value(v).await?;
86-
out.push((k.clone(), val));
88+
out.push((Bytes::copy_from_slice(k), val));
8789
}
8890
match self.next_leaf_after(&path).await? {
8991
Some(next_path) => path = next_path,
@@ -109,13 +111,13 @@ impl<V: Vfs> BTree<V> {
109111
&self,
110112
start: &[u8],
111113
limit: usize,
112-
) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
114+
) -> Result<Vec<(Bytes, Bytes)>> {
113115
if self.root_page_id == 0 || limit == 0 {
114116
return Ok(Vec::new());
115117
}
116118
let mut path = self.path_to_leaf_for_key(start).await?;
117119
let mut seen_leaves = scan_guard();
118-
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::with_capacity(limit);
120+
let mut out: Vec<(Bytes, Bytes)> = Vec::with_capacity(limit);
119121
loop {
120122
let leaf_id = *path.last().expect("non-empty path");
121123
seen_leaves.insert(leaf_id)?;
@@ -128,7 +130,7 @@ impl<V: Vfs> BTree<V> {
128130
return Ok(out);
129131
}
130132
let val = self.resolve_leaf_value(v).await?;
131-
out.push((k.clone(), val));
133+
out.push((Bytes::copy_from_slice(k), val));
132134
}
133135
if out.len() == limit {
134136
return Ok(out);
@@ -155,7 +157,7 @@ impl<V: Vfs> BTree<V> {
155157
prefix: &[u8],
156158
start: &[u8],
157159
limit: usize,
158-
) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
160+
) -> Result<Vec<(Bytes, Bytes)>> {
159161
let mut batch = self.collect_batch_from(start, limit).await?;
160162
if let Some(end) = batch.iter().position(|(key, _)| !key.starts_with(prefix)) {
161163
batch.truncate(end);
@@ -179,21 +181,21 @@ impl<V: Vfs> BTree<V> {
179181

180182
/// Reverse range scan: `start` inclusive, `end` exclusive. Returns results
181183
/// in descending key order. Collects matching records forward then reverses.
182-
pub async fn scan_rev(&self, start: &[u8], end: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
184+
pub async fn scan_rev(&self, start: &[u8], end: &[u8]) -> Result<Vec<(Bytes, Bytes)>> {
183185
let mut forward = self.collect_range(start, end).await?;
184186
forward.reverse();
185187
Ok(forward)
186188
}
187189

188190
/// Prefix scan: returns all records whose key starts with `prefix`, in
189191
/// ascending order.
190-
pub async fn scan_prefix(&self, prefix: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
192+
pub async fn scan_prefix(&self, prefix: &[u8]) -> Result<Vec<(Bytes, Bytes)>> {
191193
if self.root_page_id == 0 {
192194
return Ok(Vec::new());
193195
}
194196
let mut path = self.path_to_leaf_for_key(prefix).await?;
195197
let mut seen_leaves = scan_guard();
196-
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
198+
let mut out: Vec<(Bytes, Bytes)> = Vec::new();
197199
loop {
198200
let leaf_id = *path.last().expect("non-empty path");
199201
seen_leaves.insert(leaf_id)?;
@@ -208,7 +210,7 @@ impl<V: Vfs> BTree<V> {
208210
break;
209211
}
210212
let val = self.resolve_leaf_value(v).await?;
211-
out.push((k.clone(), val));
213+
out.push((Bytes::copy_from_slice(k), val));
212214
}
213215
if past_prefix {
214216
return Ok(out);

src/compaction/helpers.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ pub(super) async fn stream_dense_tree<V: Vfs + Clone>(
7777
if !keep(&key) {
7878
continue;
7979
}
80-
loader.push(key, value).await?;
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?;
8183
// A compacted page is written once and never read back, so flushing
8284
// it out mid-batch costs nothing and keeps the pool from growing
8385
// with the tree instead of with the budget.
@@ -136,7 +138,7 @@ pub(super) async fn segment_rows_from<V: Vfs + Clone>(
136138
let mut out = Vec::with_capacity(rows.len());
137139
for (key, value) in rows {
138140
let meta = Catalog::decode_segment_meta(&value)?;
139-
out.push((key, meta));
141+
out.push((key.to_vec(), meta));
140142
}
141143
Ok(out)
142144
}

src/txn/db/history_carry.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,14 @@ impl<V: Vfs + Clone> Db<V> {
8080
return Ok(unmoved);
8181
}
8282

83-
let rows = base_tree.collect_all().await?;
83+
// The loader stages these into pages it owns, so the scan's borrowed
84+
// view is copied out here.
85+
let rows: Vec<(Vec<u8>, Vec<u8>)> = base_tree
86+
.collect_all()
87+
.await?
88+
.into_iter()
89+
.map(|(key, value)| (key.to_vec(), value.to_vec()))
90+
.collect();
8491
let entry_count = u64::try_from(rows.len()).ok();
8592
let mut relocated = BTree::open(
8693
self.pager.clone(),

src/txn/db/rekey/segments.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ impl<V: Vfs + Clone> Db<V> {
500500
.into_iter()
501501
.map(|(key, value)| {
502502
Ok(SegmentEntry {
503-
key,
503+
key: key.to_vec(),
504504
meta: Catalog::decode_segment_meta(&value)?,
505505
})
506506
})

src/txn/read/txn.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ impl<'db, V: Vfs + Clone> ReadTxn<'db, V> {
122122
/// To take the first few rows at or after a key, use
123123
/// [`scan_from`](Self::scan_from) — bounding by count there is what keeps a
124124
/// short read short.
125-
pub async fn scan(&self, start: &[u8], end: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
125+
pub async fn scan(&self, start: &[u8], end: &[u8]) -> Result<Vec<(Bytes, Bytes)>> {
126126
self.check_abort()?;
127127
self.tree().collect_range(start, end).await
128128
}
@@ -145,7 +145,7 @@ impl<'db, V: Vfs + Clone> ReadTxn<'db, V> {
145145
/// that is its immediate successor in the key ordering, so paging this way
146146
/// never skips a record and never returns one twice. A batch shorter than
147147
/// `limit` means the tree ended.
148-
pub async fn scan_from(&self, start: &[u8], limit: usize) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
148+
pub async fn scan_from(&self, start: &[u8], limit: usize) -> Result<Vec<(Bytes, Bytes)>> {
149149
self.check_abort()?;
150150
self.tree().collect_batch_from(start, limit).await
151151
}
@@ -162,19 +162,19 @@ impl<'db, V: Vfs + Clone> ReadTxn<'db, V> {
162162
prefix: &[u8],
163163
start: &[u8],
164164
limit: usize,
165-
) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
165+
) -> Result<Vec<(Bytes, Bytes)>> {
166166
self.check_abort()?;
167167
self.tree()
168168
.collect_prefix_batch_from(prefix, start, limit)
169169
.await
170170
}
171171

172-
pub async fn scan_rev(&self, start: &[u8], end: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
172+
pub async fn scan_rev(&self, start: &[u8], end: &[u8]) -> Result<Vec<(Bytes, Bytes)>> {
173173
self.check_abort()?;
174174
self.tree().scan_rev(start, end).await
175175
}
176176

177-
pub async fn scan_prefix(&self, prefix: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
177+
pub async fn scan_prefix(&self, prefix: &[u8]) -> Result<Vec<(Bytes, Bytes)>> {
178178
self.check_abort()?;
179179
self.tree().scan_prefix(prefix).await
180180
}

tests/txn_basic.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ async fn scan_from_stops_at_limit() {
212212
let r = db.begin_read().await.unwrap();
213213
let rows = r.scan_from(b"row:0000", 10).await.unwrap();
214214
assert_eq!(rows.len(), 10);
215-
assert_eq!(rows[0].0, b"row:0000");
216-
assert_eq!(rows[9].0, b"row:0009");
215+
assert_eq!(rows[0].0.as_ref(), b"row:0000");
216+
assert_eq!(rows[9].0.as_ref(), b"row:0009");
217217
}
218218

219219
#[tokio::test(flavor = "current_thread")]
@@ -222,9 +222,9 @@ async fn scan_from_starts_at_or_after_key() {
222222
let r = db.begin_read().await.unwrap();
223223
// Start key need not exist: the scan lands on the first row at or after it.
224224
let rows = r.scan_from(b"row:0020", 3).await.unwrap();
225-
assert_eq!(rows[0].0, b"row:0020");
225+
assert_eq!(rows[0].0.as_ref(), b"row:0020");
226226
let rows = r.scan_from(b"row:0019z", 1).await.unwrap();
227-
assert_eq!(rows[0].0, b"row:0020");
227+
assert_eq!(rows[0].0.as_ref(), b"row:0020");
228228
}
229229

230230
#[tokio::test(flavor = "current_thread")]
@@ -233,7 +233,7 @@ async fn scan_from_short_batch_means_end_of_tree() {
233233
let r = db.begin_read().await.unwrap();
234234
let rows = r.scan_from(b"row:0045", 10).await.unwrap();
235235
assert_eq!(rows.len(), 5);
236-
assert_eq!(rows[4].0, b"row:0049");
236+
assert_eq!(rows[4].0.as_ref(), b"row:0049");
237237
assert!(r.scan_from(b"zzz", 10).await.unwrap().is_empty());
238238
assert!(r.scan_from(b"row:0000", 0).await.unwrap().is_empty());
239239
}
@@ -251,9 +251,9 @@ async fn scan_from_resume_protocol_pages_every_row_once() {
251251
if batch.is_empty() {
252252
break;
253253
}
254-
cursor = batch.last().unwrap().0.clone();
254+
cursor = batch.last().unwrap().0.to_vec();
255255
cursor.push(0x00);
256-
seen.extend(batch.into_iter().map(|(k, _)| k));
256+
seen.extend(batch.into_iter().map(|(k, _)| k.to_vec()));
257257
}
258258
assert_eq!(seen.len(), 50);
259259
let mut expected: Vec<Vec<u8>> = (0..50)

0 commit comments

Comments
 (0)