Skip to main content

prolly/prolly/
builder.rs

1//! Batch builder for parallel tree construction
2//!
3//! The `BatchBuilder` enables efficient bulk loading of data into a Prolly tree
4//! with parallel boundary detection and node creation using rayon.
5
6use 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
32/// Batch builder for parallel tree construction.
33///
34/// Enables efficient bulk loading of data into a Prolly tree with parallel
35/// boundary detection and node creation using rayon.
36///
37/// # Example
38/// ```
39/// use prolly::{BatchBuilder, MemStore, Config};
40/// use std::sync::Arc;
41///
42/// let store = Arc::new(MemStore::new());
43/// let config = Config::default();
44/// let mut builder = BatchBuilder::new(store, config);
45///
46/// builder.add(b"key1".to_vec(), b"val1".to_vec());
47/// builder.add(b"key2".to_vec(), b"val2".to_vec());
48/// builder.add(b"key3".to_vec(), b"val3".to_vec());
49///
50/// let tree = builder.build().unwrap();
51/// ```
52///
53pub struct BatchBuilder<S: Store> {
54    store: S,
55    config: Config,
56    /// Key-value pairs to insert (will be sorted before build)
57    entries: Vec<(Vec<u8>, Vec<u8>)>,
58}
59
60/// Streaming bulk builder for entries that are already sorted by key.
61///
62/// Unlike [`BatchBuilder`], this builder does not retain all leaf key/value
63/// pairs. It flushes leaf nodes as soon as the same content-defined boundary
64/// rules used by [`BatchBuilder`] allow it, then builds upper levels from the
65/// compact child summaries.
66pub 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    /// Create a new BatchBuilder with the given store and configuration.
80    ///
81    /// # Arguments
82    /// * `store` - Storage backend implementing the `Store` trait
83    /// * `config` - Tree configuration (chunking parameters, encoding, etc.)
84    ///
85    pub fn new(store: S, config: Config) -> Self {
86        Self {
87            store,
88            config,
89            entries: Vec::new(),
90        }
91    }
92
93    /// Add a key-value pair to the builder.
94    ///
95    /// Entries will be sorted by key before building the tree.
96    ///
97    /// # Arguments
98    /// * `key` - The key bytes
99    /// * `val` - The value bytes
100    ///
101    pub fn add(&mut self, key: Vec<u8>, val: Vec<u8>) {
102        self.entries.push((key, val));
103    }
104
105    /// Build the tree from the added entries using parallel chunking.
106    ///
107    /// This method:
108    /// 1. Sorts entries by key
109    /// 2. Partitions entries into chunks using parallel boundary detection
110    /// 3. Creates leaf nodes in parallel
111    /// 4. Builds internal nodes level by level
112    ///
113    /// # Returns
114    /// * `Ok(Tree)` - The constructed tree
115    /// * `Err(Error)` - If storage operations fail
116    ///
117    pub fn build(mut self) -> Result<Tree, Error> {
118        // Handle empty case
119        if self.entries.is_empty() {
120            return Ok(Tree {
121                root: None,
122                config: self.config,
123            });
124        }
125
126        // Sort entries by key
127        self.entries.sort_by(|a, b| a.0.cmp(&b.0));
128
129        // Parallel chunk building
130        let chunks = self.parallel_chunk(&self.entries)?;
131
132        // Build tree bottom-up
133        self.build_from_chunks(chunks)
134    }
135
136    /// Partition entries into chunks in parallel using boundary detection.
137    ///
138    /// Uses rayon for parallel boundary detection and leaf node creation.
139    ///
140    /// # Arguments
141    /// * `entries` - Sorted key-value pairs
142    ///
143    /// # Returns
144    /// * `Ok(Vec<NodeSummary>)` - summaries of the created leaf nodes
145    /// * `Err(Error)` - If storage operations fail
146    ///
147    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        // Precompute the hash predicate in parallel. Min/max rules depend on
153        // the current chunk length, so they are applied in a sequential pass.
154        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        // Create leaf nodes in parallel, then persist them in one batched write.
162        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    /// Build internal nodes from leaf CIDs, level by level.
197    ///
198    /// Constructs the tree bottom-up by creating internal nodes that
199    /// reference the nodes from the previous level.
200    ///
201    /// # Arguments
202    /// * `level_nodes` - Summaries of nodes at the current (leaf) level
203    ///
204    /// # Returns
205    /// * `Ok(Tree)` - The constructed tree with root
206    /// * `Err(Error)` - If storage operations fail
207    ///
208    fn build_from_chunks(&self, mut level_nodes: Vec<NodeSummary>) -> Result<Tree, Error> {
209        // Handle empty case
210        if level_nodes.is_empty() {
211            return Ok(Tree {
212                root: None,
213                config: self.config.clone(),
214            });
215        }
216
217        // Handle single node case - it becomes the root
218        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        // Build internal nodes level by level until we have a single root
228        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    /// Build a single level of internal nodes from child summaries.
240    ///
241    /// Creates internal nodes that reference the child nodes, using
242    /// boundary detection to determine node boundaries.
243    ///
244    /// # Arguments
245    /// * `children` - Summaries of child nodes
246    /// * `level` - The level number for the new internal nodes
247    ///
248    /// # Returns
249    /// * `Ok(Vec<NodeSummary>)` - summaries of the created internal nodes
250    /// * `Err(Error)` - If storage operations fail
251    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        // Precompute hash predicate in parallel, then apply size rules using
261        // chunk-local counts so max_chunk_size does not degenerate into
262        // one-child internal nodes after the first full chunk.
263        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    /// Create a sorted streaming builder.
317    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    /// Add the next sorted key/value pair.
330    ///
331    /// Keys must be added in nondecreasing byte order. Duplicate keys are
332    /// accepted here for parity with [`BatchBuilder`], though callers usually
333    /// provide unique keys.
334    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    /// Build a tree from the streamed entries.
360    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}