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::BoundaryDetector;
7use super::cid::Cid;
8use super::config::Config;
9use super::encoding::INIT_LEVEL;
10use super::engine::ProllyEngine;
11use super::error::Error;
12use super::node::Node;
13use super::store::{AsyncStore, NodePublication, PublicationOrigin, Store};
14use super::tree::Tree;
15
16use rayon::prelude::*;
17
18mod size;
19pub(crate) use size::EncodedNodeSizer;
20mod streaming;
21pub(crate) use streaming::{EmittedNode, HierarchicalEmitter, LevelEmitter};
22
23pub(crate) const SORTED_BUILDER_NODE_BATCH: usize = 4_096;
24const PARALLEL_BOUNDARY_HASH_MIN_ENTRIES: usize = 1_024;
25
26#[derive(Debug)]
27struct BuiltNode {
28    cid: Cid,
29    first_key: Vec<u8>,
30    count: u64,
31    bytes: Vec<u8>,
32}
33
34pub(crate) struct DeferredNode {
35    pub(crate) cid: Cid,
36    pub(crate) bytes: Vec<u8>,
37    pub(crate) node: Node,
38}
39
40#[derive(Clone, Debug)]
41pub(crate) struct NodeSummary {
42    pub(crate) cid: Cid,
43    pub(crate) first_key: Vec<u8>,
44    pub(crate) count: u64,
45}
46
47/// Batch builder for parallel tree construction.
48///
49/// Enables efficient bulk loading of data into a Prolly tree with parallel
50/// boundary detection and node creation using rayon.
51///
52/// # Example
53/// ```
54/// use prolly::{BatchBuilder, MemStore, Config};
55/// use std::sync::Arc;
56///
57/// let store = Arc::new(MemStore::new());
58/// let config = Config::default();
59/// let mut builder = BatchBuilder::new(store, config);
60///
61/// builder.add(b"key1".to_vec(), b"val1".to_vec());
62/// builder.add(b"key2".to_vec(), b"val2".to_vec());
63/// builder.add(b"key3".to_vec(), b"val3".to_vec());
64///
65/// let tree = builder.build().unwrap();
66/// ```
67///
68pub struct BatchBuilder<S: Store> {
69    store: S,
70    config: Config,
71    origin: PublicationOrigin,
72    /// Key-value pairs to insert (will be sorted before build)
73    entries: Vec<(Vec<u8>, Vec<u8>)>,
74}
75
76/// Streaming bulk builder for entries that are already sorted by key.
77///
78/// Unlike [`BatchBuilder`], this builder does not retain all leaf key/value
79/// pairs. It flushes leaf nodes as soon as the same content-defined boundary
80/// rules used by [`BatchBuilder`] allow it, then builds upper levels from the
81/// compact child summaries.
82pub struct SortedBatchBuilder<S: Store> {
83    store: S,
84    config: Config,
85    origin: PublicationOrigin,
86    pending_entry: Option<(Vec<u8>, Vec<u8>)>,
87    pending_nodes: Vec<BuiltNode>,
88    hierarchy: HierarchicalEmitter,
89}
90
91/// Async-first bulk builder for unsorted entries.
92///
93/// Entries are retained until [`AsyncBatchBuilder::build`], then the engine's
94/// canonical mutation planner sorts, deduplicates, builds, validates, and
95/// publishes the resulting tree.
96pub struct AsyncBatchBuilder<S: AsyncStore> {
97    engine: ProllyEngine<S>,
98    origin: PublicationOrigin,
99    entries: Vec<(Vec<u8>, Vec<u8>)>,
100}
101
102/// Memory-bounded async builder for entries already sorted by key.
103///
104/// Finalized node batches are published as they are emitted. Those nodes remain
105/// unreachable until [`AsyncSortedBatchBuilder::build`] returns the root.
106pub struct AsyncSortedBatchBuilder<S: AsyncStore> {
107    engine: ProllyEngine<S>,
108    origin: PublicationOrigin,
109    pending_entry: Option<(Vec<u8>, Vec<u8>)>,
110    pending_nodes: Vec<DeferredNode>,
111    hierarchy: HierarchicalEmitter,
112}
113
114impl<S> AsyncBatchBuilder<S>
115where
116    S: AsyncStore,
117    S::Error: Send + Sync,
118{
119    /// Create an async bulk builder.
120    pub fn new(store: S, config: Config) -> Self {
121        Self::new_with_origin(store, config, PublicationOrigin::TreeBuild)
122    }
123
124    pub(crate) fn new_with_origin(store: S, config: Config, origin: PublicationOrigin) -> Self {
125        Self {
126            engine: ProllyEngine::new(store, config),
127            origin,
128            entries: Vec::new(),
129        }
130    }
131
132    /// Add an entry. Input order is unrestricted.
133    pub fn add(&mut self, key: Vec<u8>, val: Vec<u8>) {
134        self.entries.push((key, val));
135    }
136
137    /// Build and publish the canonical tree.
138    pub async fn build(self) -> Result<Tree, Error> {
139        self.engine
140            .build_from_entries_with_origin(self.entries, self.origin)
141            .await
142    }
143}
144
145impl<S> AsyncSortedBatchBuilder<S>
146where
147    S: AsyncStore,
148    S::Error: Send + Sync,
149{
150    /// Create a memory-bounded async sorted builder.
151    pub fn new(store: S, config: Config) -> Self {
152        Self::new_with_origin(store, config, PublicationOrigin::TreeBuild)
153    }
154
155    pub(crate) fn new_with_origin(store: S, config: Config, origin: PublicationOrigin) -> Self {
156        let hierarchy = HierarchicalEmitter::new(config.clone())
157            .expect("configuration contains a registered persisted tree format");
158        Self {
159            engine: ProllyEngine::new(store, config),
160            origin,
161            pending_entry: None,
162            pending_nodes: Vec::new(),
163            hierarchy,
164        }
165    }
166
167    /// Add the next nondecreasing key/value pair.
168    ///
169    /// Duplicate keys replace the pending value. A decreasing key is rejected
170    /// before it can publish any node derived from that key.
171    pub async fn add(&mut self, key: Vec<u8>, val: Vec<u8>) -> Result<(), Error> {
172        if let Some((previous, previous_value)) = &mut self.pending_entry {
173            if key < *previous {
174                return Err(Error::UnsortedInput {
175                    previous: previous.clone(),
176                    next: key,
177                });
178            }
179            if key == *previous {
180                *previous_value = val;
181                return Ok(());
182            }
183        }
184
185        self.flush_pending_entry().await?;
186        self.pending_entry = Some((key, val));
187        Ok(())
188    }
189
190    async fn flush_pending_entry(&mut self) -> Result<(), Error> {
191        let Some((key, val)) = self.pending_entry.take() else {
192            return Ok(());
193        };
194        let emitted = self.hierarchy.push_leaf(key, val)?;
195        self.collect_emitted(emitted).await
196    }
197
198    async fn collect_emitted(&mut self, emitted: Vec<EmittedNode>) -> Result<(), Error> {
199        self.pending_nodes
200            .extend(emitted.into_iter().map(|emitted| DeferredNode {
201                cid: emitted.summary.cid,
202                bytes: emitted.bytes,
203                node: emitted.node,
204            }));
205        if self.pending_nodes.len() >= SORTED_BUILDER_NODE_BATCH {
206            self.flush_pending_nodes().await?;
207        }
208        Ok(())
209    }
210
211    async fn flush_pending_nodes(&mut self) -> Result<(), Error> {
212        self.engine
213            .publish_builder_nodes(&self.pending_nodes, self.origin)
214            .await?;
215        self.pending_nodes.clear();
216        Ok(())
217    }
218
219    /// Finish and publish the remaining nodes, then return the visible root.
220    pub async fn build(mut self) -> Result<Tree, Error> {
221        self.flush_pending_entry().await?;
222        let (root, emitted) = self.hierarchy.finish()?;
223        self.collect_emitted(emitted).await?;
224        self.flush_pending_nodes().await?;
225        Ok(Tree {
226            root: root.map(|summary| summary.cid),
227            config: self.engine.config().clone(),
228        })
229    }
230}
231
232impl<S: Store + Clone + Send + Sync> BatchBuilder<S>
233where
234    S::Error: Send + Sync,
235{
236    /// Create a new BatchBuilder with the given store and configuration.
237    ///
238    /// # Arguments
239    /// * `store` - Storage backend implementing the `Store` trait
240    /// * `config` - Tree configuration (chunking parameters, encoding, etc.)
241    ///
242    pub fn new(store: S, config: Config) -> Self {
243        Self::new_with_origin(store, config, PublicationOrigin::TreeBuild)
244    }
245
246    pub(crate) fn new_with_origin(store: S, config: Config, origin: PublicationOrigin) -> Self {
247        Self {
248            store,
249            config,
250            origin,
251            entries: Vec::new(),
252        }
253    }
254
255    /// Add a key-value pair to the builder.
256    ///
257    /// Entries will be sorted by key before building the tree.
258    ///
259    /// # Arguments
260    /// * `key` - The key bytes
261    /// * `val` - The value bytes
262    ///
263    pub fn add(&mut self, key: Vec<u8>, val: Vec<u8>) {
264        self.entries.push((key, val));
265    }
266
267    /// Build the tree from the added entries using parallel chunking.
268    ///
269    /// This method:
270    /// 1. Sorts entries by key
271    /// 2. Partitions entries into chunks using parallel boundary detection
272    /// 3. Creates leaf nodes in parallel
273    /// 4. Builds internal nodes level by level
274    ///
275    /// # Returns
276    /// * `Ok(Tree)` - The constructed tree
277    /// * `Err(Error)` - If storage operations fail
278    ///
279    pub fn build(mut self) -> Result<Tree, Error> {
280        // Handle empty case
281        if self.entries.is_empty() {
282            return Ok(Tree {
283                root: None,
284                config: self.config,
285            });
286        }
287
288        // Sort entries by key
289        self.entries.sort_by(|a, b| a.0.cmp(&b.0));
290        let mut unique = Vec::with_capacity(self.entries.len());
291        for (key, value) in self.entries {
292            if unique
293                .last()
294                .is_some_and(|(previous, _): &(Vec<u8>, Vec<u8>)| previous == &key)
295            {
296                unique.last_mut().expect("duplicate has a predecessor").1 = value;
297            } else {
298                unique.push((key, value));
299            }
300        }
301        self.entries = unique;
302
303        // Parallel chunk building
304        let chunks = self.parallel_chunk(&self.entries)?;
305
306        // Build tree bottom-up
307        self.build_from_chunks(chunks)
308    }
309
310    /// Partition entries into chunks in parallel using boundary detection.
311    ///
312    /// Uses rayon for parallel boundary detection and leaf node creation.
313    ///
314    /// # Arguments
315    /// * `entries` - Sorted key-value pairs
316    ///
317    /// # Returns
318    /// * `Ok(Vec<NodeSummary>)` - summaries of the created leaf nodes
319    /// * `Err(Error)` - If storage operations fail
320    ///
321    fn parallel_chunk(&self, entries: &[(Vec<u8>, Vec<u8>)]) -> Result<Vec<NodeSummary>, Error> {
322        if entries.is_empty() {
323            return Ok(vec![]);
324        }
325
326        let chunk_ranges =
327            chunk_ranges_for_entries_parallel(&self.config, INIT_LEVEL, entries, None)?;
328
329        // Create leaf nodes in parallel, then persist them in one batched write.
330        let config = &self.config;
331
332        let nodes: Vec<BuiltNode> = chunk_ranges
333            .par_iter()
334            .map(|range| {
335                let mut node = new_builder_node(config, true, INIT_LEVEL);
336                reserve_node_entries(&mut node, *range.end() - *range.start() + 1);
337
338                for i in range.clone() {
339                    node.keys.push(entries[i].0.clone());
340                    node.vals.push(entries[i].1.clone());
341                }
342
343                let first_key = node.keys.first().cloned().unwrap_or_default();
344                let bytes = node.to_bytes();
345                let cid = Cid::from_bytes(&bytes);
346                BuiltNode {
347                    cid,
348                    first_key,
349                    count: (range.end() - range.start() + 1) as u64,
350                    bytes,
351                }
352            })
353            .collect();
354
355        self.persist_nodes(&nodes)?;
356        Ok(nodes
357            .into_iter()
358            .map(|node| NodeSummary {
359                cid: node.cid,
360                first_key: node.first_key,
361                count: node.count,
362            })
363            .collect())
364    }
365
366    /// Build internal nodes from leaf CIDs, level by level.
367    ///
368    /// Constructs the tree bottom-up by creating internal nodes that
369    /// reference the nodes from the previous level.
370    ///
371    /// # Arguments
372    /// * `level_nodes` - Summaries of nodes at the current (leaf) level
373    ///
374    /// # Returns
375    /// * `Ok(Tree)` - The constructed tree with root
376    /// * `Err(Error)` - If storage operations fail
377    ///
378    pub(crate) fn build_from_chunks(
379        &self,
380        mut level_nodes: Vec<NodeSummary>,
381    ) -> Result<Tree, Error> {
382        // Handle empty case
383        if level_nodes.is_empty() {
384            return Ok(Tree {
385                root: None,
386                config: self.config.clone(),
387            });
388        }
389
390        // Handle single node case - it becomes the root
391        if level_nodes.len() == 1 {
392            return Ok(Tree {
393                root: Some(level_nodes.remove(0).cid),
394                config: self.config.clone(),
395            });
396        }
397
398        let mut level = INIT_LEVEL;
399
400        // Build internal nodes level by level until we have a single root
401        while level_nodes.len() > 1 {
402            level += 1;
403            level_nodes = self.build_level(level_nodes, level)?;
404        }
405
406        Ok(Tree {
407            root: level_nodes.into_iter().next().map(|node| node.cid),
408            config: self.config.clone(),
409        })
410    }
411
412    /// Build upper levels serially and persist them in one store batch.
413    ///
414    /// Mutation writers generally rebuild only a small internal frontier. For
415    /// that workload, avoiding per-level parallel scheduling and store calls is
416    /// materially cheaper while producing the same canonical root.
417    #[cfg(test)]
418    pub(crate) fn build_from_chunks_serial(
419        &self,
420        level_nodes: Vec<NodeSummary>,
421    ) -> Result<(Tree, usize, usize), Error> {
422        let (tree, pending) = self.build_from_chunks_serial_deferred(level_nodes)?;
423        let written_nodes = pending.len();
424        let written_bytes = pending.iter().map(|node| node.bytes.len()).sum();
425        let entries = pending
426            .iter()
427            .map(|node| (node.cid.as_bytes(), node.bytes.as_slice()))
428            .collect::<Vec<_>>();
429        self.store
430            .publish_nodes(NodePublication::new(&entries, self.origin))
431            .map_err(|error| Error::Store(Box::new(error)))?;
432        Ok((tree, written_nodes, written_bytes))
433    }
434
435    pub(crate) fn build_from_chunks_serial_deferred(
436        &self,
437        mut level_nodes: Vec<NodeSummary>,
438    ) -> Result<(Tree, Vec<DeferredNode>), Error> {
439        if level_nodes.is_empty() {
440            return Ok((
441                Tree {
442                    root: None,
443                    config: self.config.clone(),
444                },
445                Vec::new(),
446            ));
447        }
448        if level_nodes.len() == 1 {
449            return Ok((
450                Tree {
451                    root: Some(level_nodes.remove(0).cid),
452                    config: self.config.clone(),
453                },
454                Vec::new(),
455            ));
456        }
457
458        let mut level = INIT_LEVEL;
459        let mut pending = Vec::<DeferredNode>::new();
460        while level_nodes.len() > 1 {
461            level += 1;
462            level_nodes = self.build_level_serial(level_nodes, level, &mut pending)?;
463        }
464
465        Ok((
466            Tree {
467                root: level_nodes.into_iter().next().map(|node| node.cid),
468                config: self.config.clone(),
469            },
470            pending,
471        ))
472    }
473
474    /// Build a single level of internal nodes from child summaries.
475    ///
476    /// Creates internal nodes that reference the child nodes, using
477    /// boundary detection to determine node boundaries.
478    ///
479    /// # Arguments
480    /// * `children` - Summaries of child nodes
481    /// * `level` - The level number for the new internal nodes
482    ///
483    /// # Returns
484    /// * `Ok(Vec<NodeSummary>)` - summaries of the created internal nodes
485    /// * `Err(Error)` - If storage operations fail
486    fn build_level(
487        &self,
488        children: Vec<NodeSummary>,
489        level: u8,
490    ) -> Result<Vec<NodeSummary>, Error> {
491        if children.is_empty() {
492            return Ok(vec![]);
493        }
494
495        let internal_entries = children
496            .iter()
497            .map(|child| (child.first_key.clone(), child.cid.as_bytes().to_vec()))
498            .collect::<Vec<_>>();
499        let child_counts = children.iter().map(|child| child.count).collect::<Vec<_>>();
500        let chunk_ranges = chunk_ranges_for_entries_parallel(
501            &self.config,
502            level,
503            &internal_entries,
504            Some(&child_counts),
505        )?;
506
507        let config = &self.config;
508        let nodes: Vec<BuiltNode> = chunk_ranges
509            .par_iter()
510            .map(|range| {
511                let start = *range.start();
512                let end = *range.end();
513
514                let mut node = new_builder_node(config, false, level);
515                reserve_node_entries(&mut node, end - start + 1);
516
517                for child in children.iter().take(end + 1).skip(start) {
518                    node.keys.push(child.first_key.clone());
519                    node.vals.push(child.cid.0.to_vec());
520                    node.child_counts.push(child.count);
521                }
522
523                let first_key = node.keys.first().cloned().unwrap_or_default();
524                let bytes = node.to_bytes();
525                let cid = Cid::from_bytes(&bytes);
526                BuiltNode {
527                    cid,
528                    first_key,
529                    count: children[start..=end].iter().map(|child| child.count).sum(),
530                    bytes,
531                }
532            })
533            .collect();
534
535        self.persist_nodes(&nodes)?;
536        Ok(nodes
537            .into_iter()
538            .map(|node| NodeSummary {
539                cid: node.cid,
540                first_key: node.first_key,
541                count: node.count,
542            })
543            .collect())
544    }
545
546    fn build_level_serial(
547        &self,
548        children: Vec<NodeSummary>,
549        level: u8,
550        pending: &mut Vec<DeferredNode>,
551    ) -> Result<Vec<NodeSummary>, Error> {
552        let mut emitter = LevelEmitter::new(self.config.clone(), false, level)?;
553        let mut summaries = Vec::new();
554        for child in children {
555            emitter.push_child_with(child, |emitted| {
556                summaries.push(emitted.summary.clone());
557                pending.push(DeferredNode {
558                    cid: emitted.summary.cid,
559                    bytes: emitted.bytes,
560                    node: emitted.node,
561                });
562            })?;
563        }
564        if let Some(emitted) = emitter.finish()? {
565            summaries.push(emitted.summary.clone());
566            pending.push(DeferredNode {
567                cid: emitted.summary.cid,
568                bytes: emitted.bytes,
569                node: emitted.node,
570            });
571        }
572        Ok(summaries)
573    }
574
575    pub(crate) fn build_level_serial_deferred(
576        &self,
577        children: Vec<NodeSummary>,
578        level: u8,
579    ) -> Result<(Vec<NodeSummary>, Vec<DeferredNode>), Error> {
580        let mut pending = Vec::new();
581        let summaries = self.build_level_serial(children, level, &mut pending)?;
582        Ok((summaries, pending))
583    }
584
585    fn persist_nodes(&self, nodes: &[BuiltNode]) -> Result<(), Error> {
586        persist_nodes(&self.store, nodes, self.origin)
587    }
588}
589
590impl<S: Store + Clone + Send + Sync> SortedBatchBuilder<S>
591where
592    S::Error: Send + Sync,
593{
594    /// Create a sorted streaming builder.
595    pub fn new(store: S, config: Config) -> Self {
596        Self::new_with_origin(store, config, PublicationOrigin::TreeBuild)
597    }
598
599    pub(crate) fn new_with_origin(store: S, config: Config, origin: PublicationOrigin) -> Self {
600        let hierarchy = HierarchicalEmitter::new(config.clone())
601            .expect("configuration contains a registered persisted tree format");
602        Self {
603            store,
604            config,
605            origin,
606            pending_entry: None,
607            pending_nodes: Vec::new(),
608            hierarchy,
609        }
610    }
611
612    /// Add the next sorted key/value pair.
613    ///
614    /// Keys must be added in nondecreasing byte order. Duplicate keys are
615    /// accepted here for parity with [`BatchBuilder`], though callers usually
616    /// provide unique keys.
617    pub fn add(&mut self, key: Vec<u8>, val: Vec<u8>) -> Result<(), Error> {
618        if let Some((previous, previous_value)) = &mut self.pending_entry {
619            if key < *previous {
620                return Err(Error::UnsortedInput {
621                    previous: previous.clone(),
622                    next: key,
623                });
624            }
625            if key == *previous {
626                *previous_value = val;
627                return Ok(());
628            }
629        }
630
631        self.flush_pending_entry()?;
632        self.pending_entry = Some((key, val));
633        Ok(())
634    }
635
636    /// Add one key/value pair when the internal caller has already proved the
637    /// stream is strictly increasing and duplicate-free.
638    #[cfg(test)]
639    pub(crate) fn add_unique_sorted(&mut self, key: Vec<u8>, val: Vec<u8>) -> Result<(), Error> {
640        debug_assert!(match self.pending_entry.as_ref() {
641            Some((previous, _)) => previous < &key,
642            None => true,
643        });
644        self.flush_pending_entry()?;
645        self.pending_entry = Some((key, val));
646        Ok(())
647    }
648
649    /// Reuse an already-persisted canonical leaf when the stream is currently
650    /// aligned to a leaf boundary. Returns `false` when callers must replay the
651    /// leaf's entries to re-establish alignment.
652    #[cfg(test)]
653    pub(crate) fn add_complete_leaf(&mut self, leaf: NodeSummary) -> Result<bool, Error> {
654        self.flush_pending_entry()?;
655        if !self.hierarchy.leaf_is_empty() {
656            return Ok(false);
657        }
658        let emitted = self.hierarchy.push_complete_leaf(leaf)?;
659        self.collect_emitted(emitted)?;
660        Ok(true)
661    }
662
663    fn flush_pending_entry(&mut self) -> Result<(), Error> {
664        let Some((key, val)) = self.pending_entry.take() else {
665            return Ok(());
666        };
667        let emitted = self.hierarchy.push_leaf(key, val)?;
668        self.collect_emitted(emitted)?;
669        Ok(())
670    }
671
672    /// Build a tree from the streamed entries.
673    pub fn build(mut self) -> Result<Tree, Error> {
674        self.flush_pending_entry()?;
675        let (root, emitted) = self.hierarchy.finish()?;
676        self.collect_emitted(emitted)?;
677        self.flush_pending_nodes()?;
678        Ok(Tree {
679            root: root.map(|summary| summary.cid),
680            config: self.config,
681        })
682    }
683
684    fn collect_emitted(&mut self, emitted: Vec<EmittedNode>) -> Result<(), Error> {
685        self.pending_nodes
686            .extend(emitted.into_iter().map(|emitted| BuiltNode {
687                cid: emitted.summary.cid,
688                first_key: emitted.summary.first_key,
689                count: emitted.summary.count,
690                bytes: emitted.bytes,
691            }));
692        if self.pending_nodes.len() >= SORTED_BUILDER_NODE_BATCH {
693            self.flush_pending_nodes()?;
694        }
695        Ok(())
696    }
697
698    fn flush_pending_nodes(&mut self) -> Result<(), Error> {
699        persist_nodes(&self.store, &self.pending_nodes, self.origin)?;
700        self.pending_nodes.clear();
701        Ok(())
702    }
703}
704
705fn new_builder_node(config: &Config, leaf: bool, level: u8) -> Node {
706    Node::builder()
707        .leaf(leaf)
708        .level(level)
709        .tree_format(config.format.clone())
710        .build()
711}
712fn chunk_ranges_for_entries_parallel(
713    config: &Config,
714    level: u8,
715    entries: &[(Vec<u8>, Vec<u8>)],
716    child_counts: Option<&[u64]>,
717) -> Result<Vec<std::ops::RangeInclusive<usize>>, Error> {
718    if entries.len() < PARALLEL_BOUNDARY_HASH_MIN_ENTRIES {
719        return chunk_ranges_for_entries_impl(config, level, entries, child_counts, false);
720    }
721    chunk_ranges_for_entries_impl(config, level, entries, child_counts, true)
722}
723
724fn chunk_ranges_for_entries_impl(
725    config: &Config,
726    level: u8,
727    entries: &[(Vec<u8>, Vec<u8>)],
728    child_counts: Option<&[u64]>,
729    parallel_hashing: bool,
730) -> Result<Vec<std::ops::RangeInclusive<usize>>, Error> {
731    if level == 0 {
732        if child_counts.is_some() {
733            return Err(Error::InvalidNode);
734        }
735    } else if child_counts.map_or(true, |counts| counts.len() != entries.len()) {
736        return Err(Error::InvalidNode);
737    }
738    let mut detector = BoundaryDetector::new(config.format.chunking.clone(), level.into())?;
739    if parallel_hashing && detector.supports_independent_hashing() {
740        let boundaries_and_upper_bytes = entries
741            .par_iter()
742            .map(|(key, value)| {
743                (
744                    detector
745                        .independent_hash_boundary(key, value)
746                        .expect("independent boundary support was checked"),
747                    (key.len() as u64)
748                        .saturating_add(value.len() as u64)
749                        .saturating_add(64),
750                )
751            })
752            .collect::<Vec<_>>();
753        let max_upper_bytes = boundaries_and_upper_bytes
754            .iter()
755            .map(|(_, bytes)| *bytes)
756            .max()
757            .unwrap_or(0);
758        let spec = &config.format.chunking;
759        let empty_size = EncodedNodeSizer::new(config.format.clone(), level == 0, level)?.size();
760        if empty_size.saturating_add(max_upper_bytes.saturating_mul(spec.max))
761            <= spec.hard_max_node_bytes
762        {
763            return Ok(chunk_ranges_from_independent_counts(
764                spec,
765                &boundaries_and_upper_bytes,
766            ));
767        }
768        let hash_boundaries = boundaries_and_upper_bytes
769            .into_iter()
770            .map(|(boundary, _)| boundary)
771            .collect::<Vec<_>>();
772        return chunk_ranges_from_independent_hashes(
773            config,
774            level,
775            entries,
776            child_counts,
777            &hash_boundaries,
778        );
779    }
780
781    let mut ranges = Vec::new();
782    let mut start = 0;
783    let mut sizer = EncodedNodeSizer::new(config.format.clone(), level == 0, level)?;
784    for (index, (key, value)) in entries.iter().enumerate() {
785        let child_count = child_counts.map(|counts| counts[index]);
786        let previous_key = (index > start).then(|| entries[index - 1].0.as_slice());
787        let mut encoded_size = sizer.size_after(previous_key, key, value, child_count)?;
788        if index > start && encoded_size > config.format.chunking.hard_max_node_bytes {
789            ranges.push(start..=(index - 1));
790            start = index;
791            detector.reset();
792            sizer.reset();
793            encoded_size = sizer.size_after(None, key, value, child_count)?;
794        }
795        if encoded_size > config.format.chunking.hard_max_node_bytes {
796            return Err(Error::EntryTooLarge {
797                encoded_bytes: encoded_size,
798                limit: config.format.chunking.hard_max_node_bytes,
799            });
800        }
801        let encoded_entry_bytes = encoded_size.saturating_sub(sizer.size());
802        sizer.push_sized(key, value, child_count, encoded_size)?;
803        if detector.observe(key, value, encoded_entry_bytes as usize)? {
804            ranges.push(start..=index);
805            start = index + 1;
806            sizer.reset();
807        }
808    }
809    if start < entries.len() {
810        ranges.push(start..=(entries.len() - 1));
811    }
812    Ok(ranges)
813}
814
815fn chunk_ranges_from_independent_counts(
816    spec: &super::format::ChunkingSpec,
817    boundaries_and_upper_bytes: &[(bool, u64)],
818) -> Vec<std::ops::RangeInclusive<usize>> {
819    let mut ranges = Vec::new();
820    let mut start = 0;
821    for (index, (hash_boundary, _)) in boundaries_and_upper_bytes.iter().enumerate() {
822        let count = (index - start + 1) as u64;
823        if count >= spec.max || (count >= spec.min && *hash_boundary) {
824            ranges.push(start..=index);
825            start = index + 1;
826        }
827    }
828    if start < boundaries_and_upper_bytes.len() {
829        ranges.push(start..=(boundaries_and_upper_bytes.len() - 1));
830    }
831    ranges
832}
833
834fn chunk_ranges_from_independent_hashes(
835    config: &Config,
836    level: u8,
837    entries: &[(Vec<u8>, Vec<u8>)],
838    child_counts: Option<&[u64]>,
839    hash_boundaries: &[bool],
840) -> Result<Vec<std::ops::RangeInclusive<usize>>, Error> {
841    debug_assert_eq!(entries.len(), hash_boundaries.len());
842    let spec = &config.format.chunking;
843    let mut ranges = Vec::new();
844    let mut start = 0;
845    let mut sizer = EncodedNodeSizer::new(config.format.clone(), level == 0, level)?;
846
847    for (index, ((key, value), hash_boundary)) in entries.iter().zip(hash_boundaries).enumerate() {
848        let child_count = child_counts.map(|counts| counts[index]);
849        let previous_key = (index > start).then(|| entries[index - 1].0.as_slice());
850        let mut encoded_size = sizer.size_after(previous_key, key, value, child_count)?;
851        if index > start && encoded_size > spec.hard_max_node_bytes {
852            ranges.push(start..=(index - 1));
853            start = index;
854            sizer.reset();
855            encoded_size = sizer.size_after(None, key, value, child_count)?;
856        }
857        if encoded_size > spec.hard_max_node_bytes {
858            return Err(Error::EntryTooLarge {
859                encoded_bytes: encoded_size,
860                limit: spec.hard_max_node_bytes,
861            });
862        }
863        sizer.push_sized(key, value, child_count, encoded_size)?;
864        let count = (index - start + 1) as u64;
865        let boundary = encoded_size >= spec.hard_max_node_bytes
866            || count >= spec.max
867            || (count >= spec.min && *hash_boundary);
868        if boundary {
869            ranges.push(start..=index);
870            start = index + 1;
871            sizer.reset();
872        }
873    }
874    if start < entries.len() {
875        ranges.push(start..=(entries.len() - 1));
876    }
877    Ok(ranges)
878}
879
880fn reserve_node_entries(node: &mut Node, additional: usize) {
881    node.keys.reserve_exact(additional);
882    node.vals.reserve_exact(additional);
883}
884
885fn persist_nodes<S: Store + Clone>(
886    store: &S,
887    nodes: &[BuiltNode],
888    origin: PublicationOrigin,
889) -> Result<(), Error>
890where
891    S::Error: Send + Sync,
892{
893    if nodes.is_empty() {
894        return Ok(());
895    }
896
897    // BuiltNode is private to the canonical builder. Its CID and encoded bytes
898    // are produced together, so publication need not decode and validate them
899    // again; every later cache admission still validates persisted bytes.
900    let entries = nodes
901        .iter()
902        .map(|node| (node.cid.as_bytes(), node.bytes.as_slice()))
903        .collect::<Vec<_>>();
904    store
905        .publish_nodes(NodePublication::new(&entries, origin))
906        .map_err(|error| Error::Store(Box::new(error)))
907}
908
909#[cfg(test)]
910mod tests {
911    use super::*;
912    use crate::prolly::store::BatchOp;
913    use crate::MemStore;
914    use std::collections::BTreeMap;
915    use std::sync::{Arc, Mutex};
916
917    #[test]
918    fn exact_incremental_sizer_matches_serialized_nodes() {
919        for layout in [
920            super::super::format::NodeLayoutSpec::PrefixCompressed,
921            super::super::format::NodeLayoutSpec::Plain,
922            super::super::format::NodeLayoutSpec::OffsetTable,
923        ] {
924            for leaf in [true, false] {
925                let format = super::super::format::TreeFormat {
926                    node_layout: layout.clone(),
927                    ..Default::default()
928                };
929                let level = if leaf { 0 } else { 1 };
930                let mut sizer = EncodedNodeSizer::new(format.clone(), leaf, level).unwrap();
931                let mut node = Node::builder()
932                    .leaf(leaf)
933                    .level(level)
934                    .tree_format(format)
935                    .build();
936
937                assert_eq!(sizer.size(), node.encoded_len() as u64);
938                for index in 0..300_u64 {
939                    let key = format!("shared-prefix-{index:020}").into_bytes();
940                    let value_len = match index {
941                        126 => 127,
942                        127 => 128,
943                        198 => 16_383,
944                        199 => 16_384,
945                        _ => (index as usize % 41) + 1,
946                    };
947                    let value = vec![index as u8; value_len];
948                    let child_count = (!leaf).then_some(match index {
949                        126 => 127,
950                        127 => 128,
951                        198 => 16_383,
952                        199 => 16_384,
953                        _ => index + 1,
954                    });
955
956                    let previous_key = node.keys.last().map(Vec::as_slice);
957                    let predicted = sizer
958                        .size_after(previous_key, &key, &value, child_count)
959                        .unwrap();
960                    sizer.push(previous_key, &key, &value, child_count).unwrap();
961                    node.keys.push(key);
962                    node.vals.push(value);
963                    if let Some(count) = child_count {
964                        node.child_counts.push(count);
965                    }
966
967                    assert_eq!(predicted, sizer.size());
968                    assert_eq!(
969                        sizer.size(),
970                        node.encoded_len() as u64,
971                        "layout={layout:?} leaf={leaf} index={index}"
972                    );
973                }
974
975                for index in 300..16_385_u64 {
976                    let key = format!("shared-prefix-{index:020}").into_bytes();
977                    let value = *b"x";
978                    let child_count = (!leaf).then_some(index + 1);
979                    sizer
980                        .push(
981                            node.keys.last().map(Vec::as_slice),
982                            &key,
983                            &value,
984                            child_count,
985                        )
986                        .unwrap();
987                    node.keys.push(key);
988                    node.vals.push(value.to_vec());
989                    if let Some(count) = child_count {
990                        node.child_counts.push(count);
991                    }
992                    if matches!(index, 16_382..=16_384) {
993                        assert_eq!(
994                            sizer.size(),
995                            node.encoded_len() as u64,
996                            "count varint transition layout={layout:?} leaf={leaf} index={index}"
997                        );
998                    }
999                }
1000
1001                sizer.reset();
1002                assert_eq!(
1003                    sizer.size(),
1004                    Node::builder()
1005                        .leaf(leaf)
1006                        .level(level)
1007                        .tree_format(node.format.clone())
1008                        .build()
1009                        .encoded_len() as u64
1010                );
1011            }
1012        }
1013    }
1014
1015    #[derive(Clone, Default)]
1016    struct CountingStore {
1017        inner: Arc<Mutex<CountingStoreInner>>,
1018    }
1019
1020    #[derive(Default)]
1021    struct CountingStoreInner {
1022        data: BTreeMap<Vec<u8>, Vec<u8>>,
1023        get_calls: usize,
1024        put_calls: usize,
1025        batch_put_calls: usize,
1026    }
1027
1028    #[derive(Debug)]
1029    struct CountingStoreError(String);
1030
1031    impl std::fmt::Display for CountingStoreError {
1032        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1033            write!(f, "CountingStore error: {}", self.0)
1034        }
1035    }
1036
1037    impl std::error::Error for CountingStoreError {}
1038
1039    impl Store for CountingStore {
1040        type Error = CountingStoreError;
1041
1042        fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
1043            let mut inner = self
1044                .inner
1045                .lock()
1046                .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
1047            inner.get_calls += 1;
1048            Ok(inner.data.get(key).cloned())
1049        }
1050
1051        fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
1052            let mut inner = self
1053                .inner
1054                .lock()
1055                .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
1056            inner.put_calls += 1;
1057            inner.data.insert(key.to_vec(), value.to_vec());
1058            Ok(())
1059        }
1060
1061        fn delete(&self, key: &[u8]) -> Result<(), Self::Error> {
1062            let mut inner = self
1063                .inner
1064                .lock()
1065                .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
1066            inner.data.remove(key);
1067            Ok(())
1068        }
1069
1070        fn batch(&self, ops: &[BatchOp]) -> Result<(), Self::Error> {
1071            let mut inner = self
1072                .inner
1073                .lock()
1074                .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
1075            for op in ops {
1076                match op {
1077                    BatchOp::Upsert { key, value } => {
1078                        inner.data.insert(key.to_vec(), value.to_vec());
1079                    }
1080                    BatchOp::Delete { key } => {
1081                        inner.data.remove(*key);
1082                    }
1083                }
1084            }
1085            Ok(())
1086        }
1087
1088        fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<(), Self::Error> {
1089            let mut inner = self
1090                .inner
1091                .lock()
1092                .map_err(|e| CountingStoreError(format!("lock poisoned: {}", e)))?;
1093            inner.batch_put_calls += 1;
1094            for (key, value) in entries {
1095                inner.data.insert(key.to_vec(), value.to_vec());
1096            }
1097            Ok(())
1098        }
1099    }
1100
1101    #[test]
1102    fn batch_builder_persists_levels_with_batched_writes_without_readback() {
1103        let store = CountingStore::default();
1104        let config = Config::builder()
1105            .min_chunk_size(2)
1106            .max_chunk_size(4)
1107            .chunking_factor(2)
1108            .build();
1109        let mut builder = BatchBuilder::new(store.clone(), config);
1110
1111        for i in 0..64 {
1112            builder.add(
1113                format!("k{i:03}").into_bytes(),
1114                format!("v{i:03}").into_bytes(),
1115            );
1116        }
1117
1118        let tree = builder.build().unwrap();
1119        assert!(tree.root.is_some());
1120
1121        let inner = store.inner.lock().unwrap();
1122        assert_eq!(inner.get_calls, 0);
1123        assert_eq!(inner.put_calls, 0);
1124        assert!(inner.batch_put_calls > 1);
1125    }
1126
1127    #[test]
1128    fn batch_builder_applies_max_chunk_size_to_current_chunk_not_global_index() {
1129        let store = CountingStore::default();
1130        let config = Config::builder()
1131            .min_chunk_size(4)
1132            .max_chunk_size(4)
1133            .chunking_factor(2)
1134            .build();
1135        let mut builder = BatchBuilder::new(store.clone(), config);
1136
1137        for i in 0..64 {
1138            builder.add(
1139                format!("k{i:03}").into_bytes(),
1140                format!("v{i:03}").into_bytes(),
1141            );
1142        }
1143
1144        let tree = builder.build().unwrap();
1145        assert!(tree.root.is_some());
1146
1147        let inner = store.inner.lock().unwrap();
1148        let mut leaf_lengths = Vec::new();
1149        let mut level_one_lengths = Vec::new();
1150        let mut root_length = None;
1151
1152        for bytes in inner.data.values() {
1153            let node = Node::from_bytes(bytes).unwrap();
1154            if node.leaf {
1155                leaf_lengths.push(node.len());
1156            } else if node.level == 1 {
1157                level_one_lengths.push(node.len());
1158            }
1159
1160            if Some(Cid::from_bytes(bytes)) == tree.root {
1161                root_length = Some(node.len());
1162            }
1163        }
1164
1165        leaf_lengths.sort_unstable();
1166        level_one_lengths.sort_unstable();
1167
1168        assert_eq!(leaf_lengths, vec![4; 16]);
1169        assert_eq!(level_one_lengths, vec![4; 4]);
1170        assert_eq!(root_length, Some(4));
1171    }
1172
1173    #[test]
1174    fn builder_node_entry_reservation_preserves_node_shape() {
1175        let config = Config::default();
1176        let mut node = new_builder_node(&config, true, INIT_LEVEL);
1177
1178        reserve_node_entries(&mut node, 17);
1179
1180        assert!(node.keys.capacity() >= 17);
1181        assert!(node.vals.capacity() >= 17);
1182        assert!(node.keys.is_empty());
1183        assert!(node.vals.is_empty());
1184        assert!(node.leaf);
1185        assert_eq!(node.level, INIT_LEVEL);
1186    }
1187
1188    #[test]
1189    fn batch_builder_parallel_internal_level_preserves_child_order() {
1190        let store = CountingStore::default();
1191        let config = Config::builder()
1192            .min_chunk_size(4)
1193            .max_chunk_size(4)
1194            .chunking_factor(u32::MAX)
1195            .build();
1196        let builder = BatchBuilder::new(store.clone(), config);
1197        let children = (0..16)
1198            .map(|idx| NodeSummary {
1199                cid: Cid::from_bytes(format!("child-{idx:03}").as_bytes()),
1200                first_key: format!("k{idx:03}").into_bytes(),
1201                count: 1,
1202            })
1203            .collect::<Vec<_>>();
1204
1205        let level = builder.build_level(children, 1).unwrap();
1206
1207        assert_eq!(level.len(), 4);
1208        let inner = store.inner.lock().unwrap();
1209        for (group_idx, summary) in level.iter().enumerate() {
1210            assert_eq!(
1211                summary.first_key,
1212                format!("k{:03}", group_idx * 4).into_bytes()
1213            );
1214            let bytes = inner.data.get(summary.cid.as_bytes()).unwrap();
1215            let node = Node::from_bytes(bytes).unwrap();
1216            let expected_keys = (group_idx * 4..group_idx * 4 + 4)
1217                .map(|idx| format!("k{idx:03}").into_bytes())
1218                .collect::<Vec<_>>();
1219            assert_eq!(node.keys, expected_keys);
1220            assert_eq!(node.vals.len(), 4);
1221        }
1222    }
1223
1224    #[test]
1225    fn serial_internal_build_matches_parallel_root() {
1226        let config = Config::builder()
1227            .min_chunk_size(4)
1228            .max_chunk_size(16)
1229            .chunking_factor(8)
1230            .build();
1231        let parallel_store = CountingStore::default();
1232        let serial_store = CountingStore::default();
1233        let children = (0..257)
1234            .map(|idx| NodeSummary {
1235                cid: Cid::from_bytes(format!("child-{idx:03}").as_bytes()),
1236                first_key: format!("k{idx:03}").into_bytes(),
1237                count: 1,
1238            })
1239            .collect::<Vec<_>>();
1240
1241        let parallel = BatchBuilder::new(parallel_store, config.clone())
1242            .build_from_chunks(children.clone())
1243            .unwrap();
1244        let (serial, written_nodes, written_bytes) =
1245            BatchBuilder::new(serial_store.clone(), config)
1246                .build_from_chunks_serial(children)
1247                .unwrap();
1248
1249        assert_eq!(serial.root, parallel.root);
1250        assert!(written_nodes > 1);
1251        assert!(written_bytes > 0);
1252        assert_eq!(serial_store.inner.lock().unwrap().batch_put_calls, 1);
1253    }
1254
1255    #[test]
1256    fn sorted_batch_builder_matches_batch_builder_for_sorted_entries() {
1257        let config = Config::builder()
1258            .min_chunk_size(4)
1259            .max_chunk_size(16)
1260            .chunking_factor(8)
1261            .build();
1262        let batch_store = CountingStore::default();
1263        let sorted_store = CountingStore::default();
1264        let mut batch = BatchBuilder::new(batch_store, config.clone());
1265        let mut sorted = SortedBatchBuilder::new(sorted_store, config);
1266
1267        for i in 0..257 {
1268            let key = format!("k{i:04}").into_bytes();
1269            let val = format!("value-{i:04}").into_bytes();
1270            batch.add(key.clone(), val.clone());
1271            sorted.add(key, val).unwrap();
1272        }
1273
1274        let batch_tree = batch.build().unwrap();
1275        let sorted_tree = sorted.build().unwrap();
1276
1277        assert_eq!(batch_tree.root, sorted_tree.root);
1278    }
1279
1280    #[test]
1281    fn builders_coalesce_duplicate_keys_with_last_value_winning() {
1282        let config = Config::builder()
1283            .min_chunk_size(2)
1284            .max_chunk_size(8)
1285            .chunking_factor(4)
1286            .build();
1287        let entries = vec![
1288            (b"a".to_vec(), b"old".to_vec()),
1289            (b"a".to_vec(), b"new".to_vec()),
1290            (b"b".to_vec(), b"value".to_vec()),
1291        ];
1292
1293        let batch_store = Arc::new(MemStore::new());
1294        let mut batch = BatchBuilder::new(batch_store, config.clone());
1295        for (key, value) in entries.clone() {
1296            batch.add(key, value);
1297        }
1298        let batch_tree = batch.build().unwrap();
1299
1300        let sorted_store = Arc::new(MemStore::new());
1301        let mut sorted = SortedBatchBuilder::new(sorted_store, config.clone());
1302        for (key, value) in entries {
1303            sorted.add(key, value).unwrap();
1304        }
1305        let sorted_tree = sorted.build().unwrap();
1306
1307        let unique_store = Arc::new(MemStore::new());
1308        let mut unique = BatchBuilder::new(unique_store, config);
1309        unique.add(b"a".to_vec(), b"new".to_vec());
1310        unique.add(b"b".to_vec(), b"value".to_vec());
1311        let unique_tree = unique.build().unwrap();
1312
1313        assert_eq!(batch_tree.root, unique_tree.root);
1314        assert_eq!(sorted_tree.root, unique_tree.root);
1315    }
1316
1317    #[test]
1318    fn sorted_batch_builder_rejects_out_of_order_keys() {
1319        let store = CountingStore::default();
1320        let config = Config::default();
1321        let mut builder = SortedBatchBuilder::new(store, config);
1322
1323        builder.add(b"b".to_vec(), b"1".to_vec()).unwrap();
1324        let err = builder.add(b"a".to_vec(), b"2".to_vec()).unwrap_err();
1325
1326        assert!(matches!(err, Error::UnsortedInput { .. }));
1327    }
1328
1329    #[test]
1330    fn sorted_builder_keeps_only_height_bounded_hierarchy_state() {
1331        let store = CountingStore::default();
1332        let config = Config::builder()
1333            .min_chunk_size(8)
1334            .max_chunk_size(16)
1335            .chunking_factor(16)
1336            .build();
1337        let mut builder = SortedBatchBuilder::new(store, config);
1338        let mut max_levels = 0;
1339        let mut max_buffered_entries = 0;
1340
1341        for index in 0..100_000 {
1342            builder
1343                .add(
1344                    format!("key-{index:08}").into_bytes(),
1345                    format!("value-{index:08}").into_bytes(),
1346                )
1347                .unwrap();
1348            max_levels = max_levels.max(builder.hierarchy.active_levels());
1349            max_buffered_entries = max_buffered_entries.max(builder.hierarchy.buffered_entries());
1350        }
1351
1352        let tree = builder.build().unwrap();
1353        assert!(tree.root.is_some());
1354        assert!(max_levels <= 8, "active levels={max_levels}");
1355        assert!(
1356            max_buffered_entries <= max_levels * 16,
1357            "buffered entries={max_buffered_entries}, active levels={max_levels}"
1358        );
1359    }
1360
1361    #[test]
1362    fn sorted_builder_reuses_cascade_scratch_allocation() {
1363        let store = CountingStore::default();
1364        let config = Config::builder()
1365            .min_chunk_size(2)
1366            .max_chunk_size(4)
1367            .chunking_factor(4)
1368            .build();
1369        let mut builder = SortedBatchBuilder::new(store, config);
1370
1371        for index in 0..32 {
1372            builder
1373                .add(
1374                    format!("key-{index:08}").into_bytes(),
1375                    format!("value-{index:08}").into_bytes(),
1376                )
1377                .unwrap();
1378        }
1379        let allocation = builder.hierarchy.cascade_scratch_allocation();
1380        assert!(allocation.0 > 0);
1381
1382        for index in 32..10_000 {
1383            builder
1384                .add(
1385                    format!("key-{index:08}").into_bytes(),
1386                    format!("value-{index:08}").into_bytes(),
1387                )
1388                .unwrap();
1389            assert_eq!(builder.hierarchy.cascade_scratch_allocation(), allocation);
1390        }
1391    }
1392}