Skip to main content

storage/index/
fold.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The folded index: seq to (segment, byte offset), and what indexing
7//! one line means.
8//!
9//! The maps are built from the segments, and [`crate::index::LedgerIndex`]
10//! keeps them for the life of a process so a query costs one directory
11//! listing plus the bytes appended since the last look. They are
12//! persisted only inside the views snapshot, as a projection: `scanned`
13//! says how far each segment was folded, so a segment that shrank or
14//! vanished since rebuilds them rather than being trusted
15//! (`crates/storage/spec/Index.lean` §8-4).
16//!
17//! The storage is one implicit-seq column over lines and a span table
18//! over runs ([`Entries`], [`RunTable`]). The public queries are the
19//! contract; the layout is theirs to change, and `tests` holds the
20//! BTreeMap-shaped oracle that pins the answers.
21
22use std::cmp::Ordering;
23use std::collections::BTreeMap;
24
25use kernel::{RunId, Seq};
26
27mod entries;
28use entries::{Entries, Seqs};
29
30#[derive(serde::Serialize, serde::Deserialize)]
31pub(crate) struct Folded {
32    pub(crate) scanned: BTreeMap<String, u64>,
33    entries: Entries,
34    runs: RunTable,
35}
36
37impl Folded {
38    pub(crate) fn empty() -> Folded {
39        Folded {
40            scanned: BTreeMap::new(),
41            entries: Entries::empty(),
42            runs: RunTable::empty(),
43        }
44    }
45
46    /// Indexes the complete lines of `tail`, which begins at `from` in
47    /// the segment. Returns how many bytes those complete lines took.
48    pub(crate) fn fold_segment(&mut self, name: &str, from: u64, tail: &[u8]) -> u64 {
49        let mut offset = from;
50        let mut complete_bytes = 0u64;
51        for line in tail.split_inclusive(|b| *b == b'\n') {
52            if line.last().copied() != Some(b'\n') {
53                break;
54            }
55            let body = line.get(..line.len().saturating_sub(1)).unwrap_or(line);
56            if !body.is_empty() {
57                self.insert_line(name, offset, body);
58            }
59            let len = u64::try_from(line.len()).unwrap_or(0);
60            offset = offset.saturating_add(len);
61            complete_bytes = complete_bytes.saturating_add(len);
62        }
63        complete_bytes
64    }
65
66    /// What indexing one line means, in one place: where it sits, and
67    /// whose it is. A line that does not parse is skipped rather than
68    /// reported — the caller is looking at a ledger that may be damaged,
69    /// and an index is how such a ledger gets repaired.
70    ///
71    /// The seq column and the run span table are written here and only
72    /// here, so the two cannot tell different stories about who wrote a
73    /// line: the seq join that carries a run's name is registered with
74    /// its spans in the same call.
75    pub(crate) fn insert_line(&mut self, name: &str, offset: u64, body: &[u8]) {
76        if let Some(located) = locate(body) {
77            self.insert_located(name, offset, located);
78        }
79    }
80
81    /// Indexes a line whose place in the history a caller has already
82    /// read, so the line is not parsed a second time.
83    pub(crate) fn insert_located(&mut self, name: &str, offset: u64, located: Located) {
84        self.entries.insert(located.seq, name, offset);
85        if let Some(run) = located.run {
86            self.runs.add(run, located.seq);
87        }
88    }
89
90    /// Where one line sits, and under which segment name.
91    pub(crate) fn loc_of(&self, seq: Seq) -> Option<(&str, u64)> {
92        self.entries.loc_of(seq)
93    }
94
95    /// The seq values one run wrote, newest first, below `before`.
96    pub(crate) fn run_seqs_before(
97        &self,
98        run: RunId,
99        before: Option<Seq>,
100    ) -> impl Iterator<Item = Seq> {
101        self.runs.seqs_before(run, before)
102    }
103
104    pub(crate) fn tail_seq(&self) -> Option<Seq> {
105        self.entries.tail_seq()
106    }
107
108    pub(crate) fn seqs(&self) -> Seqs<'_> {
109        self.entries.seqs()
110    }
111
112    pub(crate) fn seqs_from(&self, from: Seq) -> Seqs<'_> {
113        self.entries.seqs_from(from)
114    }
115
116    pub(crate) fn len(&self) -> usize {
117        self.entries.len()
118    }
119
120    pub(crate) fn is_empty(&self) -> bool {
121        self.entries.len() == 0
122    }
123}
124
125/// run → the seq values it wrote, as ascending disjoint spans.
126///
127/// A run is a burst of contiguous writes, so its seq set collapses into
128/// spans of consecutive values: the tail span extends on touch, and
129/// truly interleaved runs carry a handful of spans instead of one id per
130/// line. Spans hold only values both endpoints were seen at — two spans
131/// merge exactly when the gap between them has been written too — so a
132/// span never claims a seq nobody wrote.
133#[derive(serde::Serialize, serde::Deserialize)]
134struct RunTable {
135    /// Sorted by run, so one lookup is one binary search.
136    runs: Vec<RunSpans>,
137}
138
139/// One run's spans: ascending, disjoint, and never adjacent, because
140/// adjacent spans merge the moment they touch.
141#[derive(serde::Serialize, serde::Deserialize)]
142struct RunSpans {
143    run: RunId,
144    spans: Vec<Span>,
145}
146
147/// An inclusive span of seq values, `start..=end`.
148#[derive(Clone, Copy, serde::Serialize, serde::Deserialize)]
149struct Span {
150    start: Seq,
151    end: Seq,
152}
153
154impl RunTable {
155    fn empty() -> RunTable {
156        RunTable { runs: Vec::new() }
157    }
158
159    fn add(&mut self, run: RunId, seq: Seq) {
160        match self.runs.binary_search_by(|held| held.run.cmp(&run)) {
161            Ok(at) => {
162                if let Some(held) = self.runs.get_mut(at) {
163                    held.add(seq);
164                }
165            }
166            Err(at) => self.runs.insert(
167                at,
168                RunSpans {
169                    run,
170                    spans: vec![Span {
171                        start: seq,
172                        end: seq,
173                    }],
174                },
175            ),
176        }
177    }
178
179    /// Lazy, span by span from the newest: a caller that takes `n`
180    /// values walks `n` values, however long the run is.
181    fn seqs_before(&self, run: RunId, before: Option<Seq>) -> impl Iterator<Item = Seq> {
182        let spans: &[Span] = match self
183            .runs
184            .binary_search_by(|held| held.run.cmp(&run))
185            .ok()
186            .and_then(|at| self.runs.get(at))
187        {
188            Some(held) => &held.spans,
189            None => &[],
190        };
191        // The highest value a caller may be answered; `None` when
192        // `before` is the first seq there is, so nothing sits below it.
193        let ceiling = match before {
194            None => Some(u64::MAX),
195            Some(before) => before.value().checked_sub(1),
196        };
197        spans
198            .iter()
199            .rev()
200            .filter_map(move |span| {
201                let top = ceiling?.min(span.end.value());
202                (top >= span.start.value()).then_some((span.start.value(), top))
203            })
204            .flat_map(|(low, top)| (low..=top).rev().map(Seq::new))
205    }
206}
207
208impl RunSpans {
209    fn add(&mut self, seq: Seq) {
210        let at = match self.spans.binary_search_by(|span| span.place(seq)) {
211            Ok(_) => return, // the span already covers this value
212            Err(at) => at,
213        };
214        let joins_below = self.spans.get(at).is_some_and(|span| span.abuts_after(seq));
215        let joins_above = at
216            .checked_sub(1)
217            .and_then(|at| self.spans.get(at))
218            .is_some_and(|span| span.abuts_before(seq));
219        match (joins_above, joins_below) {
220            (true, true) => {
221                let stretch = self.spans.get(at).map(|span| span.end);
222                let reach = at.checked_sub(1).and_then(|at| self.spans.get_mut(at));
223                if let (Some(stretch), Some(reach)) = (stretch, reach) {
224                    reach.end = stretch;
225                }
226                self.spans.remove(at);
227            }
228            (true, false) => {
229                if let Some(reach) = at.checked_sub(1).and_then(|at| self.spans.get_mut(at)) {
230                    reach.end = seq;
231                }
232            }
233            (false, true) => {
234                if let Some(reach) = self.spans.get_mut(at) {
235                    reach.start = seq;
236                }
237            }
238            (false, false) => self.spans.insert(
239                at,
240                Span {
241                    start: seq,
242                    end: seq,
243                },
244            ),
245        }
246    }
247}
248
249impl Span {
250    /// Where this span sits around `seq`, as `binary_search_by` wants
251    /// it: the ordering of the element (this span) against the target.
252    fn place(&self, seq: Seq) -> Ordering {
253        if seq < self.start {
254            Ordering::Greater
255        } else if self.end < seq {
256            Ordering::Less
257        } else {
258            Ordering::Equal
259        }
260    }
261
262    /// True when `seq` is the value right below this span, so writing
263    /// it stretches the span up one without leaving a hole.
264    fn abuts_before(&self, seq: Seq) -> bool {
265        self.end.value().checked_add(1) == Some(seq.value())
266    }
267
268    /// True when `seq` is the value right above this span.
269    fn abuts_after(&self, seq: Seq) -> bool {
270        seq.value().checked_add(1) == Some(self.start.value())
271    }
272}
273
274/// Where a line sits in the history and whose it is.
275pub struct Located {
276    pub seq: Seq,
277    /// `None` when the line names no run that reads back as one. The
278    /// line is still indexed by seq: an index over a damaged ledger is
279    /// exactly what a repair path needs, and a line dropped here would
280    /// be invisible to every reader.
281    pub run: Option<RunId>,
282}
283
284/// Reads two fields off one parse. Indexing must not depend on the
285/// record parsing cleanly as an `EventRecord`, so this asks the raw
286/// document rather than the typed one.
287pub(crate) fn locate(line: &[u8]) -> Option<Located> {
288    let value: serde_json::Value = serde_json::from_slice(line).ok()?;
289    let seq = Seq::new(value.get("seq")?.as_u64()?);
290    let run = value
291        .get("run")
292        .and_then(serde_json::Value::as_str)
293        .and_then(|raw| RunId::parse(raw).ok());
294    Some(Located { seq, run })
295}
296
297#[cfg(test)]
298#[allow(
299    clippy::unwrap_used,
300    clippy::expect_used,
301    clippy::panic,
302    clippy::indexing_slicing,
303    reason = "test code"
304)]
305mod tests;