1use super::boundary::is_hash_boundary_config;
7use super::cid::Cid;
8use super::config::Config;
9use super::encoding::INIT_LEVEL;
10use super::error::Error;
11use super::node::Node;
12use super::store::Store;
13use super::tree::Tree;
14
15use rayon::prelude::*;
16
17const SORTED_BUILDER_NODE_BATCH: usize = 256;
18
19#[derive(Debug)]
20struct BuiltNode {
21 cid: Cid,
22 first_key: Vec<u8>,
23 bytes: Vec<u8>,
24}
25
26#[derive(Clone, Debug)]
27struct NodeSummary {
28 cid: Cid,
29 first_key: Vec<u8>,
30}
31
32pub struct BatchBuilder<S: Store> {
54 store: S,
55 config: Config,
56 entries: Vec<(Vec<u8>, Vec<u8>)>,
58}
59
60pub struct SortedBatchBuilder<S: Store> {
67 store: S,
68 config: Config,
69 current: Node,
70 last_key: Option<Vec<u8>>,
71 leaf_nodes: Vec<NodeSummary>,
72 pending_nodes: Vec<BuiltNode>,
73}
74
75impl<S: Store + Clone + Send + Sync> BatchBuilder<S>
76where
77 S::Error: Send + Sync,
78{
79 pub fn new(store: S, config: Config) -> Self {
86 Self {
87 store,
88 config,
89 entries: Vec::new(),
90 }
91 }
92
93 pub fn add(&mut self, key: Vec<u8>, val: Vec<u8>) {
102 self.entries.push((key, val));
103 }
104
105 pub fn build(mut self) -> Result<Tree, Error> {
118 if self.entries.is_empty() {
120 return Ok(Tree {
121 root: None,
122 config: self.config,
123 });
124 }
125
126 self.entries.sort_by(|a, b| a.0.cmp(&b.0));
128
129 let chunks = self.parallel_chunk(&self.entries)?;
131
132 self.build_from_chunks(chunks)
134 }
135
136 fn parallel_chunk(&self, entries: &[(Vec<u8>, Vec<u8>)]) -> Result<Vec<NodeSummary>, Error> {
148 if entries.is_empty() {
149 return Ok(vec![]);
150 }
151
152 let hash_boundaries: Vec<bool> = entries
155 .par_iter()
156 .map(|(k, v)| is_hash_boundary_config(&self.config, k, v))
157 .collect();
158
159 let chunk_ranges = chunk_ranges_from_hash_boundaries(&self.config, &hash_boundaries);
160
161 let config = &self.config;
163
164 let nodes: Vec<BuiltNode> = chunk_ranges
165 .par_iter()
166 .map(|range| {
167 let mut node = new_builder_node(config, true, INIT_LEVEL);
168 reserve_node_entries(&mut node, *range.end() - *range.start() + 1);
169
170 for i in range.clone() {
171 node.keys.push(entries[i].0.clone());
172 node.vals.push(entries[i].1.clone());
173 }
174
175 let first_key = node.keys.first().cloned().unwrap_or_default();
176 let bytes = node.to_bytes();
177 let cid = Cid::from_bytes(&bytes);
178 BuiltNode {
179 cid,
180 first_key,
181 bytes,
182 }
183 })
184 .collect();
185
186 self.persist_nodes(&nodes)?;
187 Ok(nodes
188 .into_iter()
189 .map(|node| NodeSummary {
190 cid: node.cid,
191 first_key: node.first_key,
192 })
193 .collect())
194 }
195
196 fn build_from_chunks(&self, mut level_nodes: Vec<NodeSummary>) -> Result<Tree, Error> {
209 if level_nodes.is_empty() {
211 return Ok(Tree {
212 root: None,
213 config: self.config.clone(),
214 });
215 }
216
217 if level_nodes.len() == 1 {
219 return Ok(Tree {
220 root: Some(level_nodes.remove(0).cid),
221 config: self.config.clone(),
222 });
223 }
224
225 let mut level = INIT_LEVEL;
226
227 while level_nodes.len() > 1 {
229 level += 1;
230 level_nodes = self.build_level(level_nodes, level)?;
231 }
232
233 Ok(Tree {
234 root: level_nodes.into_iter().next().map(|node| node.cid),
235 config: self.config.clone(),
236 })
237 }
238
239 fn build_level(
252 &self,
253 children: Vec<NodeSummary>,
254 level: u8,
255 ) -> Result<Vec<NodeSummary>, Error> {
256 if children.is_empty() {
257 return Ok(vec![]);
258 }
259
260 let hash_boundaries: Vec<bool> = children
264 .par_iter()
265 .map(|child| {
266 is_hash_boundary_config(&self.config, &child.first_key, child.cid.as_bytes())
267 })
268 .collect();
269 let chunk_ranges = chunk_ranges_from_hash_boundaries(&self.config, &hash_boundaries);
270
271 let config = &self.config;
272 let nodes: Vec<BuiltNode> = chunk_ranges
273 .par_iter()
274 .map(|range| {
275 let start = *range.start();
276 let end = *range.end();
277
278 let mut node = new_builder_node(config, false, level);
279 reserve_node_entries(&mut node, end - start + 1);
280
281 for child in children.iter().take(end + 1).skip(start) {
282 node.keys.push(child.first_key.clone());
283 node.vals.push(child.cid.0.to_vec());
284 }
285
286 let first_key = node.keys.first().cloned().unwrap_or_default();
287 let bytes = node.to_bytes();
288 let cid = Cid::from_bytes(&bytes);
289 BuiltNode {
290 cid,
291 first_key,
292 bytes,
293 }
294 })
295 .collect();
296
297 self.persist_nodes(&nodes)?;
298 Ok(nodes
299 .into_iter()
300 .map(|node| NodeSummary {
301 cid: node.cid,
302 first_key: node.first_key,
303 })
304 .collect())
305 }
306
307 fn persist_nodes(&self, nodes: &[BuiltNode]) -> Result<(), Error> {
308 persist_nodes(&self.store, nodes)
309 }
310}
311
312impl<S: Store + Clone + Send + Sync> SortedBatchBuilder<S>
313where
314 S::Error: Send + Sync,
315{
316 pub fn new(store: S, config: Config) -> Self {
318 let current = new_builder_node(&config, true, INIT_LEVEL);
319 Self {
320 store,
321 config,
322 current,
323 last_key: None,
324 leaf_nodes: Vec::new(),
325 pending_nodes: Vec::new(),
326 }
327 }
328
329 pub fn add(&mut self, key: Vec<u8>, val: Vec<u8>) -> Result<(), Error> {
335 if let Some(previous) = &self.last_key {
336 if key < *previous {
337 return Err(Error::UnsortedInput {
338 previous: previous.clone(),
339 next: key,
340 });
341 }
342 }
343
344 let is_hash_boundary = is_hash_boundary_config(&self.config, &key, &val);
345 self.last_key = Some(key.clone());
346 self.current.keys.push(key);
347 self.current.vals.push(val);
348
349 let count = self.current.keys.len();
350 if count >= self.config.min_chunk_size
351 && (count >= self.config.max_chunk_size || is_hash_boundary)
352 {
353 self.flush_leaf()?;
354 }
355
356 Ok(())
357 }
358
359 pub fn build(mut self) -> Result<Tree, Error> {
361 self.flush_leaf()?;
362 self.flush_pending_nodes()?;
363 let builder = BatchBuilder::new(self.store.clone(), self.config.clone());
364 builder.build_from_chunks(self.leaf_nodes)
365 }
366
367 fn flush_leaf(&mut self) -> Result<(), Error> {
368 if self.current.keys.is_empty() {
369 return Ok(());
370 }
371
372 let node = std::mem::replace(
373 &mut self.current,
374 new_builder_node(&self.config, true, INIT_LEVEL),
375 );
376 let first_key = node.keys.first().cloned().unwrap_or_default();
377 let bytes = node.to_bytes();
378 let cid = Cid::from_bytes(&bytes);
379 self.leaf_nodes.push(NodeSummary {
380 cid: cid.clone(),
381 first_key: first_key.clone(),
382 });
383 self.pending_nodes.push(BuiltNode {
384 cid,
385 first_key,
386 bytes,
387 });
388 if self.pending_nodes.len() >= SORTED_BUILDER_NODE_BATCH {
389 self.flush_pending_nodes()?;
390 }
391 Ok(())
392 }
393
394 fn flush_pending_nodes(&mut self) -> Result<(), Error> {
395 persist_nodes(&self.store, &self.pending_nodes)?;
396 self.pending_nodes.clear();
397 Ok(())
398 }
399}
400
401fn new_builder_node(config: &Config, leaf: bool, level: u8) -> Node {
402 Node::builder()
403 .leaf(leaf)
404 .level(level)
405 .min_chunk_size(config.min_chunk_size)
406 .max_chunk_size(config.max_chunk_size)
407 .chunking_factor(config.chunking_factor)
408 .hash_seed(config.hash_seed)
409 .encoding(config.encoding.clone())
410 .build()
411}
412
413fn reserve_node_entries(node: &mut Node, additional: usize) {
414 node.keys.reserve_exact(additional);
415 node.vals.reserve_exact(additional);
416}
417
418fn persist_nodes<S: Store>(store: &S, nodes: &[BuiltNode]) -> Result<(), Error>
419where
420 S::Error: Send + Sync,
421{
422 if nodes.is_empty() {
423 return Ok(());
424 }
425
426 let entries = nodes
427 .iter()
428 .map(|node| (node.cid.as_bytes(), node.bytes.as_slice()))
429 .collect::<Vec<_>>();
430 store
431 .batch_put(&entries)
432 .map_err(|e| Error::Store(Box::new(e)))
433}
434
435pub(crate) fn chunk_ranges_from_hash_boundaries(
436 config: &Config,
437 hash_boundaries: &[bool],
438) -> Vec<std::ops::RangeInclusive<usize>> {
439 if hash_boundaries.is_empty() {
440 return Vec::new();
441 }
442
443 let mut chunk_ranges = Vec::new();
444 let mut start = 0;
445
446 for (i, is_hash_boundary) in hash_boundaries.iter().enumerate() {
447 let count = i - start + 1;
448 if count < config.min_chunk_size {
449 continue;
450 }
451
452 if count >= config.max_chunk_size || *is_hash_boundary {
453 chunk_ranges.push(start..=i);
454 start = i + 1;
455 }
456 }
457
458 if start < hash_boundaries.len() {
459 chunk_ranges.push(start..=(hash_boundaries.len() - 1));
460 }
461
462 chunk_ranges
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::prolly::store::BatchOp;
469 use std::collections::BTreeMap;
470 use std::sync::{Arc, Mutex};
471
472 #[derive(Clone, Default)]
473 struct CountingStore {
474 inner: Arc<Mutex<CountingStoreInner>>,
475 }
476
477 #[derive(Default)]
478 struct CountingStoreInner {
479 data: BTreeMap<Vec<u8>, Vec<u8>>,
480 get_calls: usize,
481 put_calls: usize,
482 batch_put_calls: usize,
483 }
484
485 #[derive(Debug)]
486 struct CountingStoreError(String);
487
488 impl std::fmt::Display for CountingStoreError {
489 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490 write!(f, "CountingStore error: {}", self.0)
491 }
492 }
493
494 impl std::error::Error for CountingStoreError {}
495
496 impl Store for CountingStore {
497 type Error = CountingStoreError;
498
499 fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
500 let mut inner = self
501 .inner
502 .lock()
503 .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
504 inner.get_calls += 1;
505 Ok(inner.data.get(key).cloned())
506 }
507
508 fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
509 let mut inner = self
510 .inner
511 .lock()
512 .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
513 inner.put_calls += 1;
514 inner.data.insert(key.to_vec(), value.to_vec());
515 Ok(())
516 }
517
518 fn delete(&self, key: &[u8]) -> Result<(), Self::Error> {
519 let mut inner = self
520 .inner
521 .lock()
522 .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
523 inner.data.remove(key);
524 Ok(())
525 }
526
527 fn batch(&self, ops: &[BatchOp]) -> Result<(), Self::Error> {
528 let mut inner = self
529 .inner
530 .lock()
531 .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
532 for op in ops {
533 match op {
534 BatchOp::Upsert { key, value } => {
535 inner.data.insert(key.to_vec(), value.to_vec());
536 }
537 BatchOp::Delete { key } => {
538 inner.data.remove(*key);
539 }
540 }
541 }
542 Ok(())
543 }
544
545 fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<(), Self::Error> {
546 let mut inner = self
547 .inner
548 .lock()
549 .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
550 inner.batch_put_calls += 1;
551 for (key, value) in entries {
552 inner.data.insert(key.to_vec(), value.to_vec());
553 }
554 Ok(())
555 }
556 }
557
558 #[test]
559 fn batch_builder_persists_levels_with_batched_writes_without_readback() {
560 let store = CountingStore::default();
561 let config = Config::builder()
562 .min_chunk_size(2)
563 .max_chunk_size(4)
564 .chunking_factor(2)
565 .build();
566 let mut builder = BatchBuilder::new(store.clone(), config);
567
568 for i in 0..64 {
569 builder.add(
570 format!("k{i:03}").into_bytes(),
571 format!("v{i:03}").into_bytes(),
572 );
573 }
574
575 let tree = builder.build().unwrap();
576 assert!(tree.root.is_some());
577
578 let inner = store.inner.lock().unwrap();
579 assert_eq!(inner.get_calls, 0);
580 assert_eq!(inner.put_calls, 0);
581 assert!(inner.batch_put_calls > 1);
582 }
583
584 #[test]
585 fn batch_builder_applies_max_chunk_size_to_current_chunk_not_global_index() {
586 let store = CountingStore::default();
587 let config = Config::builder()
588 .min_chunk_size(4)
589 .max_chunk_size(4)
590 .chunking_factor(2)
591 .build();
592 let mut builder = BatchBuilder::new(store.clone(), config);
593
594 for i in 0..64 {
595 builder.add(
596 format!("k{i:03}").into_bytes(),
597 format!("v{i:03}").into_bytes(),
598 );
599 }
600
601 let tree = builder.build().unwrap();
602 assert!(tree.root.is_some());
603
604 let inner = store.inner.lock().unwrap();
605 let mut leaf_lengths = Vec::new();
606 let mut level_one_lengths = Vec::new();
607 let mut root_length = None;
608
609 for bytes in inner.data.values() {
610 let node = Node::from_bytes(bytes).unwrap();
611 if node.leaf {
612 leaf_lengths.push(node.len());
613 } else if node.level == 1 {
614 level_one_lengths.push(node.len());
615 }
616
617 if Some(Cid::from_bytes(bytes)) == tree.root {
618 root_length = Some(node.len());
619 }
620 }
621
622 leaf_lengths.sort_unstable();
623 level_one_lengths.sort_unstable();
624
625 assert_eq!(leaf_lengths, vec![4; 16]);
626 assert_eq!(level_one_lengths, vec![4; 4]);
627 assert_eq!(root_length, Some(4));
628 }
629
630 #[test]
631 fn builder_node_entry_reservation_preserves_node_shape() {
632 let config = Config::default();
633 let mut node = new_builder_node(&config, true, INIT_LEVEL);
634
635 reserve_node_entries(&mut node, 17);
636
637 assert!(node.keys.capacity() >= 17);
638 assert!(node.vals.capacity() >= 17);
639 assert!(node.keys.is_empty());
640 assert!(node.vals.is_empty());
641 assert!(node.leaf);
642 assert_eq!(node.level, INIT_LEVEL);
643 }
644
645 #[test]
646 fn batch_builder_parallel_internal_level_preserves_child_order() {
647 let store = CountingStore::default();
648 let config = Config::builder()
649 .min_chunk_size(4)
650 .max_chunk_size(4)
651 .chunking_factor(u32::MAX)
652 .build();
653 let builder = BatchBuilder::new(store.clone(), config);
654 let children = (0..16)
655 .map(|idx| NodeSummary {
656 cid: Cid::from_bytes(format!("child-{idx:03}").as_bytes()),
657 first_key: format!("k{idx:03}").into_bytes(),
658 })
659 .collect::<Vec<_>>();
660
661 let level = builder.build_level(children, 1).unwrap();
662
663 assert_eq!(level.len(), 4);
664 let inner = store.inner.lock().unwrap();
665 for (group_idx, summary) in level.iter().enumerate() {
666 assert_eq!(
667 summary.first_key,
668 format!("k{:03}", group_idx * 4).into_bytes()
669 );
670 let bytes = inner.data.get(summary.cid.as_bytes()).unwrap();
671 let node = Node::from_bytes(bytes).unwrap();
672 let expected_keys = (group_idx * 4..group_idx * 4 + 4)
673 .map(|idx| format!("k{idx:03}").into_bytes())
674 .collect::<Vec<_>>();
675 assert_eq!(node.keys, expected_keys);
676 assert_eq!(node.vals.len(), 4);
677 }
678 }
679
680 #[test]
681 fn sorted_batch_builder_matches_batch_builder_for_sorted_entries() {
682 let config = Config::builder()
683 .min_chunk_size(4)
684 .max_chunk_size(16)
685 .chunking_factor(8)
686 .build();
687 let batch_store = CountingStore::default();
688 let sorted_store = CountingStore::default();
689 let mut batch = BatchBuilder::new(batch_store, config.clone());
690 let mut sorted = SortedBatchBuilder::new(sorted_store, config);
691
692 for i in 0..257 {
693 let key = format!("k{i:04}").into_bytes();
694 let val = format!("value-{i:04}").into_bytes();
695 batch.add(key.clone(), val.clone());
696 sorted.add(key, val).unwrap();
697 }
698
699 let batch_tree = batch.build().unwrap();
700 let sorted_tree = sorted.build().unwrap();
701
702 assert_eq!(batch_tree.root, sorted_tree.root);
703 }
704
705 #[test]
706 fn sorted_batch_builder_rejects_out_of_order_keys() {
707 let store = CountingStore::default();
708 let config = Config::default();
709 let mut builder = SortedBatchBuilder::new(store, config);
710
711 builder.add(b"b".to_vec(), b"1".to_vec()).unwrap();
712 let err = builder.add(b"a".to_vec(), b"2".to_vec()).unwrap_err();
713
714 assert!(matches!(err, Error::UnsortedInput { .. }));
715 }
716}