Skip to main content

uqa_storage/mvcc/
overlay.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Transaction-private evaluated replacements, retained command views and undo.
8
9use std::num::NonZeroU64;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use parking_lot::Mutex;
13use triomphe::Arc as StrongArc;
14use uqa_core::memory::{BudgetedSharedMap, BudgetedVec, MemoryBudget, MemoryReservation};
15
16use crate::read_control::StorageReadControl;
17use crate::StorageSavepointId;
18
19use super::key::RecordKey;
20use super::{PreparedRecordCommit, PreparedRecordWrite, RecordWrite, VersionError, VersionResult};
21
22mod cursor;
23pub use cursor::{PrivateRecordCursor, PrivateRecordEntry};
24mod selection;
25pub(super) use selection::PrivateRecordSelection;
26mod retained;
27mod scopes;
28pub use scopes::PrivateRevisionScope;
29use scopes::RevisionScopes;
30pub(in crate::mvcc) mod run;
31mod run_set;
32mod tiers;
33use retained::{RetainedSources, Sources};
34use run::SpilledRunWriter;
35use run_set::RunSet;
36use tiers::{TieredChange, TieredCursor};
37
38/// The resident bytes of a memory-tier change beyond its key and value, as an estimate: its tree node and entry, the shared key and value handles and their allocation headers.
39const CHANGE_OVERHEAD: usize = 192;
40/// The memory tier spills once the transaction's allowance is more than this fraction used.
41const PRESSURE_DIVISOR: usize = 2;
42/// The memory tier spills only once it holds at least this fraction of the allowance, so that memory held elsewhere does not spill a run for every write.
43const RESIDENT_DIVISOR: usize = 16;
44
45struct Change {
46    write: PreparedRecordWrite,
47    identity: PrivateRecordRevision,
48}
49
50type Records = BudgetedSharedMap<RecordKey, Change>;
51
52/// Process-local identity of an evaluated private batch. Identities are never reused across transactions or undo branches; they are not durable commit sequences.
53#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
54pub struct PrivateRecordRevision(NonZeroU64);
55
56impl PrivateRecordRevision {
57    pub(in crate::mvcc) fn allocate() -> VersionResult<Self> {
58        static NEXT: AtomicU64 = AtomicU64::new(1);
59        uqa_core::atomic::try_update_u64(&NEXT, Ordering::Relaxed, Ordering::Relaxed, |id| {
60            id.checked_add(1)
61        })
62        .map(|id| Self(NonZeroU64::new(id).expect("private revisions start at one")))
63        .map_err(|_| VersionError::PrivateRevisionExhausted)
64    }
65
66    #[cfg(test)]
67    pub(crate) fn for_tests() -> Self {
68        Self::allocate().expect("test private revision")
69    }
70
71    pub fn as_u64(self) -> u64 {
72        self.0.get()
73    }
74
75    /// Decode an identity written to a spill file of this process.
76    fn from_u64(value: u64) -> VersionResult<Self> {
77        NonZeroU64::new(value)
78            .map(Self)
79            .ok_or(VersionError::InvalidEncoding(
80                "zero private record revision",
81            ))
82    }
83}
84
85/// A changed key and its private batch identity, without retaining or copying its payload.
86pub struct PrivateRecordKey {
87    key: RecordKey,
88    revision: PrivateRecordRevision,
89}
90
91impl PrivateRecordKey {
92    pub fn key(&self) -> &[u8] {
93        self.key.bytes()
94    }
95
96    pub fn revision(&self) -> PrivateRecordRevision {
97        self.revision
98    }
99}
100
101struct Savepoint {
102    id: StorageSavepointId,
103    records: Records,
104    resident: usize,
105    runs: RunSet,
106    sources: RetainedSources,
107    scopes: Option<RevisionScopes>,
108    revision: Option<PrivateRecordRevision>,
109}
110
111/// The changes of one transaction in two tiers: the newest in a persistent ordered root in memory, older ones in immutable spilled runs. A key's change in memory shadows its changes in runs, and a newer run shadows an older one.
112struct State {
113    records: Records,
114    /// The estimated bytes the memory tier holds, which decide when it spills.
115    resident: usize,
116    runs: RunSet,
117    sources: Sources,
118    scopes: Option<RevisionScopes>,
119    revision: Option<PrivateRecordRevision>,
120    savepoints: BudgetedVec<Savepoint>,
121}
122
123impl State {
124    /// The newest change of `key`, from either tier.
125    fn change(
126        &self,
127        key: &[u8],
128        control: &StorageReadControl,
129    ) -> VersionResult<Option<TieredChange<'_>>> {
130        tiers::lookup(&self.records, &self.runs, key, control)
131    }
132
133    /// The smallest memory tier that spills, which is also the size of the smallest runs.
134    fn least_spill(&self) -> usize {
135        self.records.budget().limit() / RESIDENT_DIVISOR
136    }
137
138    /// Move the memory tier into a new run when the transaction's allowance is under pressure: more than half of it is used and the memory tier holds a share of it worth a run. The allowance measures what this tier actually holds, so changes that share their values with an earlier owner do not spill on their own. Failure leaves both tiers unchanged.
139    fn make_room(&mut self, control: &StorageReadControl) -> VersionResult<()> {
140        let budget = self.records.budget();
141        if self.records.is_empty()
142            || self.resident < self.least_spill()
143            || budget.used() <= budget.limit() / PRESSURE_DIVISOR
144        {
145            return Ok(());
146        }
147        let memory = self.records.budget().clone();
148        let mut writer =
149            SpilledRunWriter::new(self.records.len() as u64, self.resident as u64, &memory)?;
150        for (_, change) in &self.records {
151            writer.push(
152                change.write.key(),
153                change.write.expected(),
154                change.write.kind(),
155                change.identity,
156                change.write.value(),
157                control,
158            )?;
159        }
160        let run = writer
161            .finish()?
162            .expect("a nonempty memory tier spills a nonempty run");
163        let base = self.least_spill() as u64;
164        self.runs = self.runs.with_run(run, base, &memory, control)?;
165        self.records = Records::new(&memory);
166        self.resident = 0;
167        Ok(())
168    }
169
170    fn savepoint_position(&self, id: StorageSavepointId) -> VersionResult<usize> {
171        self.savepoints
172            .iter()
173            .rposition(|savepoint| savepoint.id == id)
174            .ok_or(VersionError::SavepointMissing(id))
175    }
176
177    fn truncate_savepoints(&mut self, len: usize) {
178        if len == 0 {
179            self.savepoints = BudgetedVec::new(self.records.budget());
180        } else {
181            self.savepoints.truncate(len);
182        }
183    }
184}
185
186struct Owner {
187    state: Mutex<State>,
188}
189
190/// One transaction's record changes. No provider lock or native transaction is retained between operations.
191///
192/// Command views and savepoints share immutable ordered roots and the spilled runs beneath them. Replacements copy only their search paths; obsolete payloads are released as soon as the last referencing root or returned record is dropped. Rollback restores a root without allocating. Once more than half of the transaction's allowance is used, the memory tier moves into a run in an encrypted temporary file, so the size of a transaction is bounded by disk rather than by its allowance.
193pub struct PrivateRecordChanges {
194    owner: StrongArc<Owner>,
195}
196
197impl PrivateRecordChanges {
198    /// An independent writable owner over the same evaluated roots. Later writes and savepoint operations do not change the original owner; unchanged records and spill runs keep their existing allocation leases.
199    pub fn fork(&self) -> Self {
200        let state = self.owner.state.lock();
201        Self {
202            owner: StrongArc::new(Owner {
203                state: Mutex::new(State {
204                    records: state.records.clone(),
205                    resident: state.resident,
206                    runs: state.runs.clone(),
207                    sources: state.sources.clone(),
208                    scopes: state.scopes.as_ref().map(RevisionScopes::fork),
209                    revision: state.revision,
210                    savepoints: BudgetedVec::new(state.records.budget()),
211                }),
212            }),
213        }
214    }
215
216    pub(super) fn share_owner(&self) -> Self {
217        Self {
218            owner: StrongArc::clone(&self.owner),
219        }
220    }
221
222    pub(super) fn write_kind(
223        &self,
224        key: &[u8],
225        control: &StorageReadControl,
226    ) -> VersionResult<Option<super::commit::RecordWriteKind>> {
227        control.check()?;
228        let state = self.owner.state.lock();
229        Ok(state.change(key, control)?.map(|change| change.kind()))
230    }
231
232    pub fn new(memory: &MemoryBudget) -> Self {
233        Self::with_revision_scope(memory, None)
234    }
235
236    pub(in crate::mvcc) fn with_revision_scope(
237        memory: &MemoryBudget,
238        scope: Option<PrivateRevisionScope>,
239    ) -> Self {
240        Self {
241            owner: StrongArc::new(Owner {
242                state: Mutex::new(State {
243                    records: Records::new(memory),
244                    resident: 0,
245                    runs: RunSet::empty(),
246                    sources: Sources::new(memory),
247                    scopes: scope.map(|scope| RevisionScopes::new(memory, scope)),
248                    revision: None,
249                    savepoints: BudgetedVec::new(memory),
250                }),
251            }),
252        }
253    }
254
255    /// Stage one atomic batch of already evaluated replacements. Repeated changes to a private record must preserve its original committed precondition.
256    pub fn apply(
257        &self,
258        writes: &[RecordWrite<'_>],
259        control: &StorageReadControl,
260    ) -> VersionResult<()> {
261        let memory = self.owner.state.lock().records.budget().clone();
262        let owned_control = StorageReadControl::new(&memory, control.cancellation());
263        let prepared = PreparedRecordCommit::new(writes, &owned_control)?;
264        let writes = prepared.resident().ok_or(VersionError::InvalidEncoding(
265            "a supplied batch is in memory",
266        ))?;
267        self.apply_owned(writes, control)
268    }
269
270    pub(crate) fn apply_owned(
271        &self,
272        writes: &[PreparedRecordWrite],
273        control: &StorageReadControl,
274    ) -> VersionResult<()> {
275        if writes.is_empty() {
276            return Ok(());
277        }
278        let mut state = self.owner.state.lock();
279        for (index, write) in writes.iter().enumerate() {
280            control.cancellation().check()?;
281            if let Some(previous) = state.change(write.key(), control)? {
282                if previous.expected() != write.expected() {
283                    return Err(VersionError::WriteConflict {
284                        mutation: index,
285                        expected: write.expected(),
286                        actual: previous.expected(),
287                    });
288                }
289            }
290        }
291        state.make_room(control)?;
292        let identity = PrivateRecordRevision::allocate()?;
293        let scopes = state
294            .scopes
295            .as_ref()
296            .map(|scopes| scopes.with_writes(writes, identity, control))
297            .transpose()?;
298        let invalidates_source = writes
299            .iter()
300            .any(|write| state.sources.get(write.key()).is_some_and(Option::is_some));
301        if writes.len() == 1 && !invalidates_source {
302            let write = &writes[0];
303            control.cancellation().check()?;
304            let resident = replaced_resident(&state.records, state.resident, write);
305            state.records.try_insert(
306                write.shared_key(),
307                Change {
308                    write: write.clone(),
309                    identity,
310                },
311            )?;
312            state.resident = resident;
313            state.scopes = scopes;
314            state.revision = Some(identity);
315            return Ok(());
316        }
317        let mut records = state.records.clone();
318        let mut sources = state.sources.clone();
319        let mut resident = state.resident;
320        for write in writes {
321            control.cancellation().check()?;
322            resident = replaced_resident(&records, resident, write);
323            records.try_insert(
324                write.shared_key(),
325                Change {
326                    write: write.clone(),
327                    identity,
328                },
329            )?;
330            if sources.get(write.key()).is_some_and(Option::is_some) {
331                sources.try_insert(write.shared_key(), None)?;
332            }
333        }
334        control.cancellation().check()?;
335        // Candidate roots own every reservation before this single atomic publication.
336        state.records = records;
337        state.sources = sources;
338        state.scopes = scopes;
339        state.resident = resident;
340        state.revision = Some(identity);
341        Ok(())
342    }
343
344    /// Stage every write of `prepared` one at a time, so that a batch larger than the allowance spills as it is staged. Unlike `apply`, a failure can leave a prefix of the writes staged; it serves changes that are discarded when it fails.
345    pub(in crate::mvcc) fn apply_prepared(
346        &self,
347        prepared: &PreparedRecordCommit,
348        control: &StorageReadControl,
349    ) -> VersionResult<()> {
350        let mut writes = prepared.writes();
351        while let Some(write) = writes.next(control)? {
352            self.apply_owned(std::slice::from_ref(&write), control)?;
353        }
354        Ok(())
355    }
356
357    pub fn has_written(&self) -> bool {
358        let state = self.owner.state.lock();
359        !state.records.is_empty() || !state.runs.is_empty()
360    }
361
362    pub fn snapshot(&self) -> VersionResult<PrivateRecordSnapshot> {
363        let state = self.owner.state.lock();
364        let memory = state
365            .records
366            .budget()
367            .reserve(std::mem::size_of::<PrivateRecordSnapshot>())?;
368        Ok(PrivateRecordSnapshot {
369            records: state.records.clone(),
370            runs: state.runs.clone(),
371            sources: state.sources.snapshot(),
372            scopes: state
373                .scopes
374                .as_ref()
375                .map(RevisionScopes::snapshot)
376                .transpose()?,
377            revision: state.revision,
378            _memory: memory,
379        })
380    }
381
382    pub fn savepoint(&self, id: StorageSavepointId) -> VersionResult<()> {
383        let mut state = self.owner.state.lock();
384        let records = state.records.clone();
385        let resident = state.resident;
386        let runs = state.runs.clone();
387        let sources = state.sources.snapshot();
388        let scopes = state.scopes.as_ref().map(RevisionScopes::fork);
389        let revision = state.revision;
390        state.savepoints.push(Savepoint {
391            id,
392            records,
393            resident,
394            runs,
395            sources,
396            scopes,
397            revision,
398        })?;
399        Ok(())
400    }
401
402    /// Release the nearest matching identity and its nested savepoints, retaining every write.
403    pub fn release_savepoint(&self, id: StorageSavepointId) -> VersionResult<()> {
404        let mut state = self.owner.state.lock();
405        let position = state.savepoint_position(id)?;
406        state.truncate_savepoints(position);
407        Ok(())
408    }
409
410    /// Undo only this owner's changes, retaining the target savepoint for another rollback.
411    pub fn rollback_to_savepoint(&self, id: StorageSavepointId) -> VersionResult<()> {
412        let mut state = self.owner.state.lock();
413        let position = state.savepoint_position(id)?;
414        state.records = state.savepoints[position].records.clone();
415        state.resident = state.savepoints[position].resident;
416        state.runs = state.savepoints[position].runs.clone();
417        let sources = state.savepoints[position].sources.clone();
418        state.sources.restore(&sources);
419        state.scopes = state.savepoints[position]
420            .scopes
421            .as_ref()
422            .map(RevisionScopes::fork);
423        state.revision = state.savepoints[position].revision;
424        state.truncate_savepoints(position + 1);
425        Ok(())
426    }
427
428    pub fn rollback(&self) -> VersionResult<()> {
429        let mut state = self.owner.state.lock();
430        let memory = state.records.budget().clone();
431        state.records = Records::new(&memory);
432        state.resident = 0;
433        state.runs = RunSet::empty();
434        state.sources = Sources::new(&memory);
435        state.scopes = state.scopes.as_ref().map(RevisionScopes::empty);
436        state.revision = None;
437        state.truncate_savepoints(0);
438        Ok(())
439    }
440
441    /// Capture immutable final replacements without reevaluating or copying their payloads. Changes that spilled are merged into one spilled run of final replacements, which commits read one block at a time.
442    pub fn prepare(&self, control: &StorageReadControl) -> VersionResult<PreparedRecordCommit> {
443        control.cancellation().check()?;
444        let state = self.owner.state.lock();
445        let mut changes = TieredCursor::new(
446            Some(&state.records),
447            &state.runs,
448            std::ops::Bound::Unbounded,
449            control,
450        )?;
451        if state.runs.is_empty() {
452            let mut writes = BudgetedVec::new(control.memory());
453            writes.reserve(state.records.len())?;
454            while let Some(change) = changes.next(control)? {
455                writes.push(change.write(control)?)?;
456            }
457            return PreparedRecordCommit::from_unique_owned(writes, control);
458        }
459        let (entries, entry_bytes) = state.runs.size();
460        let mut writer = SpilledRunWriter::new(
461            entries.saturating_add(state.records.len() as u64),
462            entry_bytes.saturating_add(state.resident as u64),
463            control.memory(),
464        )?;
465        while let Some(change) = changes.next(control)? {
466            match &change {
467                TieredChange::Resident(resident) => writer.push(
468                    resident.write.key(),
469                    resident.write.expected(),
470                    resident.write.kind(),
471                    resident.identity,
472                    resident.write.value(),
473                    control,
474                )?,
475                TieredChange::Spilled { entry, run } => writer.push_spilled(entry, run, control)?,
476            }
477        }
478        let run = writer.finish()?.expect("a spilled transaction has changes");
479        PreparedRecordCommit::from_spilled_run(run, control)
480    }
481}
482
483/// Fixed private visibility for a command or retained source cursor; tombstones remain distinguishable from an unchanged key.
484pub struct PrivateRecordSnapshot {
485    records: Records,
486    runs: RunSet,
487    sources: RetainedSources,
488    scopes: Option<Box<PrivateRecordSnapshot>>,
489    revision: Option<PrivateRecordRevision>,
490    _memory: MemoryReservation,
491}
492
493impl PrivateRecordSnapshot {
494    /// Identity of this complete private root. Undo restores the saved identity; successful replacement batches receive identities never used by another root or transaction.
495    pub fn revision(&self) -> Option<PrivateRecordRevision> {
496        self.revision
497    }
498
499    pub(super) fn last_before(
500        &self,
501        prefix: &[u8],
502        before: Option<&[u8]>,
503        control: &StorageReadControl,
504    ) -> VersionResult<Option<PreparedRecordWrite>> {
505        control.check()?;
506        let mut upper = BudgetedVec::new(control.memory());
507        upper.extend_from_slice(prefix)?;
508        let upper = match upper.iter().rposition(|byte| *byte != u8::MAX) {
509            Some(last) => {
510                upper[last] += 1;
511                upper.truncate(last + 1);
512                Some(upper)
513            }
514            None => None,
515        };
516        let end = match (before, upper.as_deref()) {
517            (Some(before), Some(upper)) => Some(before.min(upper)),
518            (before, upper) => before.or(upper),
519        };
520        let bound = end.map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
521        let resident = self
522            .records
523            .last_before::<[u8]>(bound)
524            .map(|(_, change)| TieredChange::Resident(change));
525        let spilled = self.runs.last_before(bound, control)?;
526        // The memory tier is newer, so it wins a tie.
527        let last = match (resident, spilled) {
528            (Some(resident), Some(spilled)) if spilled.key() > resident.key() => Some(spilled),
529            (Some(resident), _) => Some(resident),
530            (None, spilled) => spilled,
531        };
532        last.filter(|change| change.key().starts_with(prefix))
533            .map(|change| change.write(control))
534            .transpose()
535    }
536
537    /// Retain this exact private revision without following later writes or copying its values.
538    pub(super) fn try_clone(&self) -> VersionResult<Self> {
539        let memory = self.records.budget().reserve(std::mem::size_of::<Self>())?;
540        Ok(Self {
541            records: self.records.clone(),
542            runs: self.runs.clone(),
543            sources: self.sources.clone(),
544            scopes: self
545                .scopes
546                .as_ref()
547                .map(|scopes| scopes.try_clone().map(Box::new))
548                .transpose()?,
549            revision: self.revision,
550            _memory: memory,
551        })
552    }
553
554    /// Select changed keys on this retained command boundary, including deletions. Undo restores their earlier identities, while later branches and other transactions receive distinct identities.
555    pub fn scan_keys(
556        &self,
557        prefix: &[u8],
558        after: Option<&[u8]>,
559        limit: usize,
560        control: &StorageReadControl,
561    ) -> VersionResult<BudgetedVec<PrivateRecordKey>> {
562        control.check()?;
563        let mut result = BudgetedVec::new(control.memory());
564        if limit == 0 {
565            return Ok(result);
566        }
567        let start = after.filter(|after| *after >= prefix).unwrap_or(prefix);
568        let mut changes = TieredCursor::new(
569            Some(&self.records),
570            &self.runs,
571            std::ops::Bound::Included(start),
572            control,
573        )?;
574        while let Some(change) = changes.next(control)? {
575            control.check()?;
576            let key = change.key();
577            if !key.starts_with(prefix) {
578                break;
579            }
580            if after.is_some_and(|after| key <= after) {
581                continue;
582            }
583            result.push(PrivateRecordKey {
584                key: change.shared_key(),
585                revision: change.identity(),
586            })?;
587            if result.len() == limit {
588                break;
589            }
590        }
591        Ok(result)
592    }
593
594    pub(super) fn visit_merged<P: super::projection::Projection>(
595        &self,
596        committed: &dyn super::CommittedRecordSnapshot,
597        prefix: &[u8],
598        after: Option<&[u8]>,
599        limit: usize,
600        control: &StorageReadControl,
601        visit: &mut super::projection::Visitor<'_, P>,
602    ) -> VersionResult<()> {
603        control.cancellation().check()?;
604        if limit == 0 {
605            return Ok(());
606        }
607        let mut changes = self.cursor(prefix, after, control)?;
608        let mut pending = changes.next(control)?;
609        let mut count = 0;
610        let mut running = true;
611        let mut emit = |key: &[u8], record: P::Record<'_>| -> VersionResult<bool> {
612            control.cancellation().check()?;
613            count += 1;
614            let more = visit(key, record)?;
615            control.cancellation().check()?;
616            Ok(more && count < limit)
617        };
618        P::visit(committed, prefix, after, control, &mut |key, record| {
619            while let Some(entry) = pending.as_ref().filter(|entry| entry.key() < key) {
620                running = P::private(entry, control, &mut emit)?;
621                if !running {
622                    return Ok(false);
623                }
624                pending = changes.next(control)?;
625            }
626            if let Some(entry) = pending.as_ref().filter(|entry| entry.key() == key) {
627                running = P::private(entry, control, &mut emit)?;
628                if running {
629                    pending = changes.next(control)?;
630                }
631            } else {
632                running = emit(key, record)?;
633            }
634            Ok(running)
635        })?;
636        while running {
637            let Some(entry) = pending.as_ref() else {
638                break;
639            };
640            running = P::private(entry, control, &mut emit)?;
641            if running {
642                pending = changes.next(control)?;
643            }
644        }
645        Ok(())
646    }
647
648    /// Select the original revision and presence from the entry without reading a spilled value.
649    pub(super) fn metadata(
650        &self,
651        key: &[u8],
652        control: &StorageReadControl,
653    ) -> VersionResult<Option<super::RecordMetadata>> {
654        control.cancellation().check()?;
655        Ok(tiers::lookup(&self.records, &self.runs, key, control)?.map(|change| change.metadata()))
656    }
657
658    pub fn get(
659        &self,
660        key: &[u8],
661        control: &StorageReadControl,
662    ) -> VersionResult<Option<PreparedRecordWrite>> {
663        control.cancellation().check()?;
664        tiers::lookup(&self.records, &self.runs, key, control)?
665            .map(|change| change.write(control))
666            .transpose()
667    }
668
669    /// Visit the private replacements whose keys start with `prefix` and follow `after`, including deletions, in key order until `visit` returns false. Each replacement's value is loaded only when it is visited, so a caller can bound what it keeps by bytes.
670    pub fn visit(
671        &self,
672        prefix: &[u8],
673        after: Option<&[u8]>,
674        control: &StorageReadControl,
675        visit: &mut dyn FnMut(&PreparedRecordWrite) -> VersionResult<bool>,
676    ) -> VersionResult<()> {
677        control.cancellation().check()?;
678        let start = after.filter(|after| *after >= prefix).unwrap_or(prefix);
679        let mut changes = TieredCursor::new(
680            Some(&self.records),
681            &self.runs,
682            std::ops::Bound::Included(start),
683            control,
684        )?;
685        while let Some(change) = changes.next(control)? {
686            control.cancellation().check()?;
687            let key = change.key();
688            if !key.starts_with(prefix) {
689                break;
690            }
691            if after.is_some_and(|after| key <= after) {
692                continue;
693            }
694            if !visit(&change.write(control)?)? {
695                break;
696            }
697        }
698        Ok(())
699    }
700
701    /// Return a bounded ordered page of private replacements, including deletions.
702    pub fn scan(
703        &self,
704        prefix: &[u8],
705        after: Option<&[u8]>,
706        limit: usize,
707        control: &StorageReadControl,
708    ) -> VersionResult<BudgetedVec<PreparedRecordWrite>> {
709        control.cancellation().check()?;
710        let mut result = BudgetedVec::new(control.memory());
711        if limit == 0 {
712            return Ok(result);
713        }
714        let start = after.filter(|after| *after >= prefix).unwrap_or(prefix);
715        let mut changes = TieredCursor::new(
716            Some(&self.records),
717            &self.runs,
718            std::ops::Bound::Included(start),
719            control,
720        )?;
721        while let Some(change) = changes.next(control)? {
722            control.cancellation().check()?;
723            let key = change.key();
724            if !key.starts_with(prefix) {
725                break;
726            }
727            if after.is_some_and(|after| key <= after) {
728                continue;
729            }
730            result.push(change.write(control)?)?;
731            if result.len() == limit {
732                break;
733            }
734        }
735        Ok(result)
736    }
737}
738
739/// The estimated memory a change takes in the memory tier.
740fn resident_bytes(write: &PreparedRecordWrite) -> usize {
741    write
742        .key()
743        .len()
744        .saturating_add(write.value().map_or(0, <[u8]>::len))
745        .saturating_add(CHANGE_OVERHEAD)
746}
747
748fn replaced_resident(records: &Records, resident: usize, write: &PreparedRecordWrite) -> usize {
749    let previous = records
750        .get(write.key())
751        .map_or(0, |change| resident_bytes(&change.write));
752    resident
753        .saturating_sub(previous)
754        .saturating_add(resident_bytes(write))
755}
756
757#[cfg(test)]
758mod tests;