Skip to main content

commonware_storage/index/partitioned/ordered/
cursor.rs

1//! A mutable cursor over a single translated key's values.
2//!
3//! A [Cursor] iterates and edits the value run for one translated key. That run lives in one of two
4//! representations -- an inline sorted-array [Partition] or a spilled `BTreeMap` (see the parent
5//! module) -- abstracted behind [Backing]. The iteration protocol (the [State] machine, the
6//! must-call-`next` guards, and the metric bookkeeping) is identical for both representations, so it
7//! is written once here; [Backing] holds the only thing that differs: the positional store
8//! operations.
9
10use super::partition::Partition;
11use crate::index::Cursor as CursorTrait;
12use commonware_runtime::telemetry::metrics::{Counter, Gauge};
13use std::{
14    collections::{btree_map, hash_map, BTreeMap, HashMap},
15    ops::Range,
16};
17
18const MUST_CALL_NEXT: &str = "must call Cursor::next()";
19const NO_ACTIVE_ITEM: &str = "no active item in Cursor";
20
21/// Position of a [Cursor] within its key's value run (offset 0 is the run's first value).
22enum State {
23    /// Before the first `next()` or after an `insert()`/`delete()`: the next `next()` returns the
24    /// value at run offset `from`.
25    NeedNext { from: usize },
26    /// `next()` returned the value at run offset `offset`; `update`/`delete`/`insert` are valid.
27    Active { offset: usize },
28    /// `next()` returned `None`; only `insert()` (which appends) is valid.
29    Done,
30}
31
32/// The store holding a [Cursor]'s value run, abstracting the inline and spilled representations
33/// behind a common positional interface. All offsets are relative to the start of the key's run.
34enum Backing<'a, K: Ord + Copy, V> {
35    /// Inline sorted-array partition: the key's values occupy the contiguous index range `run`.
36    ///
37    /// `run.start` is fixed for the cursor's lifetime: the cursor borrows the partition exclusively
38    /// and only ever adds or removes its own key's values, so nothing shifts the entries before the
39    /// run. `run.end` is adjusted by one on each insert/remove to stay aligned with the array.
40    Soa {
41        partition: &'a mut Partition<K, V>,
42        key: K,
43        run: Range<usize>,
44    },
45    /// Spilled partition: the key's values live in the side-table's `BTreeMap`, re-resolved on each
46    /// access (spilling is the rare case, so the extra descent is off the hot path).
47    Spilled {
48        spilled: &'a mut HashMap<usize, BTreeMap<K, Vec<V>>>,
49        partition: usize,
50        key: K,
51    },
52}
53
54impl<K: Ord + Copy, V> Backing<'_, K, V> {
55    /// The number of values in the run (zero if the key is absent).
56    fn len(&self) -> usize {
57        match self {
58            Self::Soa { run, .. } => run.len(),
59            Self::Spilled {
60                spilled,
61                partition,
62                key,
63            } => spilled
64                .get(partition)
65                .and_then(|inner| inner.get(key))
66                .map_or(0, Vec::len),
67        }
68    }
69
70    /// The value at run offset `off`. The caller ensures `off < len()`.
71    fn get(&self, off: usize) -> &V {
72        match self {
73            Self::Soa { partition, run, .. } => partition.value_at(run.start + off),
74            Self::Spilled {
75                spilled,
76                partition,
77                key,
78            } => &spilled
79                .get(partition)
80                .and_then(|inner| inner.get(key))
81                .expect("active cursor must reference a present key")[off],
82        }
83    }
84
85    /// Overwrite the value at run offset `off`. The caller ensures `off < len()`.
86    fn set(&mut self, off: usize, value: V) {
87        match self {
88            Self::Soa { partition, run, .. } => partition.set(run.start + off, value),
89            Self::Spilled {
90                spilled,
91                partition,
92                key,
93            } => {
94                spilled
95                    .get_mut(partition)
96                    .and_then(|inner| inner.get_mut(key))
97                    .expect("active cursor must reference a present key")[off] = value;
98            }
99        }
100    }
101
102    /// Insert `value` at run offset `off`, returning whether this created the key (its first value).
103    fn insert(&mut self, off: usize, value: V) -> bool {
104        match self {
105            Self::Soa {
106                partition,
107                key,
108                run,
109            } => {
110                #[allow(unstable_name_collisions)]
111                let created = run.is_empty(); // empty run => key absent => this creates it
112                partition.insert_at(run.start + off, *key, value);
113                run.end += 1;
114                created
115            }
116            Self::Spilled {
117                spilled,
118                partition,
119                key,
120            } => match spilled.entry(*partition).or_default().entry(*key) {
121                btree_map::Entry::Occupied(mut run) => {
122                    run.get_mut().insert(off, value);
123                    false
124                }
125                btree_map::Entry::Vacant(run) => {
126                    run.insert(vec![value]);
127                    true
128                }
129            },
130        }
131    }
132
133    /// Remove the value at run offset `off`, returning whether that emptied (and so removed) the key.
134    fn remove(&mut self, off: usize) -> bool {
135        match self {
136            Self::Soa { partition, run, .. } => {
137                partition.remove(run.start + off);
138                run.end -= 1;
139                #[allow(unstable_name_collisions)]
140                run.is_empty()
141            }
142            Self::Spilled {
143                spilled,
144                partition,
145                key,
146            } => {
147                let hash_map::Entry::Occupied(mut part) = spilled.entry(*partition) else {
148                    unreachable!("active cursor must reference a present partition")
149                };
150                let btree_map::Entry::Occupied(mut run) = part.get_mut().entry(*key) else {
151                    unreachable!("active cursor must reference a present key")
152                };
153                run.get_mut().remove(off);
154                if !run.get().is_empty() {
155                    return false;
156                }
157                // Removed the key's last value; drop the key, and de-spill the partition (back to an
158                // empty sorted array) if that was its last key.
159                run.remove();
160                if part.get().is_empty() {
161                    part.remove();
162                }
163                true
164            }
165        }
166    }
167}
168
169/// A [crate::index::Cursor] over a single translated key's values.
170///
171/// Both representations -- the inline sorted array and the spilled `BTreeMap` -- share one
172/// iteration protocol; see the module docs.
173pub struct Cursor<'a, K: Ord + Copy, V> {
174    backing: Backing<'a, K, V>,
175    state: State,
176    keys: &'a Gauge,
177    items: &'a Gauge,
178    pruned: &'a Counter,
179}
180
181impl<'a, K: Ord + Copy, V> Cursor<'a, K, V> {
182    /// A cursor over a key's values held inline in a sorted-array partition. `run` is the (non-empty)
183    /// index range of `key`'s values within the partition.
184    pub(super) const fn soa(
185        partition: &'a mut Partition<K, V>,
186        key: K,
187        run: Range<usize>,
188        keys: &'a Gauge,
189        items: &'a Gauge,
190        pruned: &'a Counter,
191    ) -> Self {
192        Self {
193            backing: Backing::Soa {
194                partition,
195                key,
196                run,
197            },
198            state: State::NeedNext { from: 0 },
199            keys,
200            items,
201            pruned,
202        }
203    }
204
205    /// A cursor over a key's values held in a spilled partition's `BTreeMap`.
206    pub(super) const fn spilled(
207        spilled: &'a mut HashMap<usize, BTreeMap<K, Vec<V>>>,
208        partition: usize,
209        key: K,
210        keys: &'a Gauge,
211        items: &'a Gauge,
212        pruned: &'a Counter,
213    ) -> Self {
214        Self {
215            backing: Backing::Spilled {
216                spilled,
217                partition,
218                key,
219            },
220            state: State::NeedNext { from: 0 },
221            keys,
222            items,
223            pruned,
224        }
225    }
226}
227
228impl<K: Ord + Copy + Send + Sync, V: Send + Sync> CursorTrait for Cursor<'_, K, V> {
229    type Value = V;
230
231    fn next(&mut self) -> Option<&V> {
232        let off = match self.state {
233            State::Done => return None,
234            State::NeedNext { from } => from,
235            State::Active { offset } => offset + 1,
236        };
237        if off >= self.backing.len() {
238            self.state = State::Done;
239            return None;
240        }
241        self.state = State::Active { offset: off };
242        Some(self.backing.get(off))
243    }
244
245    fn update(&mut self, value: V) {
246        match self.state {
247            State::NeedNext { .. } => panic!("{MUST_CALL_NEXT}"),
248            State::Done => panic!("{NO_ACTIVE_ITEM}"),
249            State::Active { offset } => self.backing.set(offset, value),
250        }
251    }
252
253    fn insert(&mut self, value: V) {
254        match self.state {
255            State::NeedNext { .. } => panic!("{MUST_CALL_NEXT}"),
256            State::Active { offset } => {
257                // Place immediately after the current value (never a new key, so the return is
258                // ignored); `next()` then returns the value after the inserted one, skipping both
259                // the current and the inserted.
260                self.backing.insert(offset + 1, value);
261                self.items.inc();
262                self.state = State::NeedNext { from: offset + 2 };
263            }
264            State::Done => {
265                // Append at the run end, re-creating the key if it was emptied.
266                let end = self.backing.len();
267                if self.backing.insert(end, value) {
268                    self.keys.inc();
269                }
270                self.items.inc();
271            }
272        }
273    }
274
275    fn delete(&mut self) {
276        let offset = match self.state {
277            State::NeedNext { .. } => panic!("{MUST_CALL_NEXT}"),
278            State::Done => panic!("{NO_ACTIVE_ITEM}"),
279            State::Active { offset } => offset,
280        };
281        if self.backing.remove(offset) {
282            // Removed the key's last value; the key is gone.
283            self.keys.dec();
284        }
285        self.items.dec();
286        self.pruned.inc();
287
288        // The value after the deleted one shifted into `offset`.
289        self.state = State::NeedNext { from: offset };
290    }
291}