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