Skip to main content

reddb_engine/storage/manager/
backing.rs

1//! Disk-backed collection state. Publication remains a host-owned write fence.
2
3use super::*;
4use crate::storage::collection_read::{
5    CollectionCursor, CollectionRead, FrozenOverlay, MergedCollection,
6};
7use crate::storage::error::StoreError;
8use crate::storage::metadata_codec::serialize_entity_record;
9use std::collections::HashSet;
10
11pub(super) struct BackingRead {
12    pub source: Arc<dyn CollectionRead + Send + Sync>,
13    pub root_page: u32,
14    pub total_entities: u64,
15    pub format_version: u32,
16    pub read_bytes_max: usize,
17    pub removed: HashSet<EntityId>,
18}
19
20/// Frozen base/overlay cursor for a lazy query source. The source generation
21/// remains pinned through the owned page cursor, not a manager topology lock.
22pub struct PagedEntityCursor {
23    cursor: Box<dyn CollectionCursor>,
24    format_version: u32,
25    read_bytes_max: usize,
26    snapshot: Option<SnapshotContext>,
27    visible_only: bool,
28    schema: Option<Arc<Vec<String>>>,
29    key: Option<String>,
30    finished: bool,
31}
32
33impl Iterator for PagedEntityCursor {
34    type Item = Result<(UnifiedEntity, Option<Metadata>), StoreError>;
35    fn next(&mut self) -> Option<Self::Item> {
36        if self.finished {
37            return None;
38        }
39        loop {
40            let record = match self.cursor.next_record(self.read_bytes_max) {
41                Ok(Some(record)) => record,
42                Ok(None) => {
43                    self.finished = true;
44                    return None;
45                }
46                Err(error) => {
47                    self.finished = true;
48                    return Some(Err(error));
49                }
50            };
51            let (mut entity, metadata) =
52                match record.decode_with_budget(self.format_version, self.read_bytes_max) {
53                    Ok(record) => record,
54                    Err(error) => {
55                        self.finished = true;
56                        return Some(Err(error));
57                    }
58                };
59            if self.visible_only
60                && !SegmentManager::scan_entity_visible(self.snapshot.as_ref(), &entity)
61            {
62                continue;
63            }
64            if let super::super::entity::EntityData::Row(row) = &mut entity.data {
65                if row.named.is_none() && row.schema.is_none() {
66                    row.schema = self.schema.clone();
67                }
68            }
69            if self.key.as_deref().is_some_and(|key| {
70                crate::storage::segment_key_lookup::SegmentKeyLookup::key(&entity) != Some(key)
71            }) {
72                continue;
73            }
74            return Some(Ok((entity, metadata)));
75        }
76    }
77}
78
79impl SegmentManager {
80    /// Visit the physical base and resident replacements without an ID-order
81    /// workspace. This is for order-independent metadata reductions, not SQL
82    /// ordering. The host must fence writers throughout this reduction so a
83    /// materialized base row cannot be counted twice. Callbacks must not mutate
84    /// or re-enter this collection.
85    pub fn try_for_each_entity_unordered(
86        &self,
87        limit_bytes: usize,
88        mut callback: impl FnMut(&UnifiedEntity) -> bool,
89    ) -> Result<(), StoreError> {
90        let backing_guard = self.backing.read();
91        let Some(backing) = backing_guard.as_ref() else {
92            self.for_each_entity(callback);
93            return Ok(());
94        };
95        let limit_bytes = limit_bytes.min(backing.read_bytes_max);
96        let mut cursor = backing.source.cursor()?;
97        while let Some(id) = cursor.peek_id()? {
98            if backing.removed.contains(&id) || self.get_with(id, |_| ()).is_some() {
99                cursor.skip_record()?;
100                continue;
101            }
102            let record = cursor
103                .next_record(limit_bytes)?
104                .expect("invariant: peeked physical row exists");
105            let (mut entity, _) = record.decode_with_budget(backing.format_version, limit_bytes)?;
106            self.restore_owned_row_schemas(std::iter::once(&mut entity));
107            if !callback(&entity) {
108                return Ok(());
109            }
110        }
111        let mut result = Ok(());
112        self.for_each_entity(|entity| {
113            let required_bytes = crate::storage::memory_size::entity_bytes(entity);
114            if required_bytes > limit_bytes {
115                result = Err(StoreError::ReadLimitExceeded {
116                    required_bytes,
117                    limit_bytes,
118                });
119                return false;
120            }
121            callback(entity)
122        });
123        result
124    }
125
126    /// The host must fence publication while capturing the base and overlay.
127    /// This retains O(dirty overlay) state and decodes at most one base row.
128    pub fn try_paged_entity_cursor(
129        &self,
130        snapshot: Option<SnapshotContext>,
131    ) -> Result<Option<PagedEntityCursor>, StoreError> {
132        self.try_paged_cursor(snapshot, true, None)
133    }
134
135    /// Raw physical records and their frozen metadata, including superseded
136    /// versions. Hosts fence publication while capturing this cursor.
137    pub fn try_paged_physical_cursor(&self) -> Result<Option<PagedEntityCursor>, StoreError> {
138        self.try_paged_cursor(None, false, None)
139    }
140
141    /// Freeze only this key's payloads and metadata, retaining other dirty IDs
142    /// as masks. Every decoded candidate is checked against the logical key.
143    pub fn try_paged_key_entity_cursor(
144        &self,
145        key: &str,
146        snapshot: Option<SnapshotContext>,
147    ) -> Result<Option<PagedEntityCursor>, StoreError> {
148        self.try_paged_cursor(snapshot, true, Some(key))
149    }
150
151    /// Like the visible keyed cursor, but includes superseded physical history.
152    pub fn try_paged_key_physical_cursor(
153        &self,
154        key: &str,
155    ) -> Result<Option<PagedEntityCursor>, StoreError> {
156        self.try_paged_cursor(None, false, Some(key))
157    }
158
159    fn try_paged_cursor(
160        &self,
161        snapshot: Option<SnapshotContext>,
162        visible_only: bool,
163        key: Option<&str>,
164    ) -> Result<Option<PagedEntityCursor>, StoreError> {
165        let Some((format_version, read_bytes_max)) = self
166            .backing
167            .read()
168            .as_ref()
169            .map(|base| (base.format_version, base.read_bytes_max))
170        else {
171            return Ok(None);
172        };
173        let view = match key {
174            Some(key) => self.current_backing_key_overlay(key)?,
175            None => self.current_backing_overlay(),
176        };
177        let Some((base, overlay)) = view else {
178            return Ok(None);
179        };
180        let merged = MergedCollection::new(base.as_ref(), overlay);
181        let cursor = match key {
182            Some(key) => match merged.owned_key_cursor(key)? {
183                Some(cursor) => cursor,
184                None => merged.owned_cursor()?,
185            },
186            None => merged.owned_cursor()?,
187        };
188        Ok(Some(PagedEntityCursor {
189            cursor,
190            format_version,
191            read_bytes_max,
192            snapshot,
193            visible_only,
194            schema: self.column_schema(),
195            key: key.map(str::to_owned),
196            finished: false,
197        }))
198    }
199
200    pub fn has_backing(&self) -> bool {
201        self.backing.read().is_some()
202    }
203
204    /// Identity of the immutable base currently installed by the host.
205    pub fn backing_root_page(&self) -> Option<u32> {
206        self.backing.read().as_ref().map(|base| base.root_page)
207    }
208
209    /// The host applies the same row allowance to model-specific expansion
210    /// such as decrypting a compressed Vault value after native record decode.
211    pub fn backing_read_bytes_max(&self) -> Option<usize> {
212        self.backing
213            .read()
214            .as_ref()
215            .map(|backing| backing.read_bytes_max)
216    }
217
218    /// The host must fence writers and publish the new durable tree first.
219    /// Captured resident readers retain their old sources through weak tracking.
220    pub fn install_backing(
221        &self,
222        source: Arc<dyn CollectionRead + Send + Sync>,
223        root_page: u32,
224        format_version: u32,
225        read_bytes_max: usize,
226        total_entities: u64,
227    ) {
228        let mut backing = self.backing.write();
229        let mut consolidation = self.consolidation.write();
230        let mut growing = self.growing.write();
231        let mut sealed = self.sealed.write();
232        let mut retired = self.retired.write();
233        if let Some(source) = growing.take() {
234            self.restore_sequence_id_next(source.read().sequence_id_next());
235            retired.push(Arc::downgrade(&source));
236        }
237        for source in sealed.drain(..) {
238            self.restore_sequence_id_next(source.read().sequence_id_next());
239            retired.push(Arc::downgrade(&source));
240        }
241        *consolidation = None;
242        self.entity_segment.write().clear();
243        self.total_entities_atomic
244            .store(total_entities, Ordering::Relaxed);
245        *backing = Some(BackingRead {
246            source,
247            root_page,
248            total_entities,
249            format_version,
250            read_bytes_max,
251            removed: HashSet::new(),
252        });
253    }
254
255    /// Fence acquisition of new base readers while a host checks all native
256    /// cursor/tree pins and applies WAL-protected inserts. The source must have
257    /// no strong or weak owners outside this manager. The host still owns the
258    /// mutation/publication fence and must not re-enter backing methods here.
259    /// A successful projection updates the root and its exact physical count.
260    #[doc(hidden)]
261    pub fn try_with_unshared_backing<T>(
262        &self,
263        root_page: u32,
264        update: impl FnOnce() -> (T, Option<(u32, u64)>),
265    ) -> Option<T> {
266        let mut backing = self.backing.write();
267        let backing = backing.as_mut()?;
268        if backing.root_page != root_page
269            || !backing.removed.is_empty()
270            || Arc::get_mut(&mut backing.source).is_none()
271        {
272            return None;
273        }
274        let (result, published) = update();
275        if let Some((root_page, total_entities)) = published {
276            backing.root_page = root_page;
277            backing.total_entities = total_entities;
278        }
279        Some(result)
280    }
281
282    pub fn backing_count_at_root(&self, root_page: u32) -> Option<u64> {
283        self.backing
284            .read()
285            .as_ref()
286            .filter(|base| base.root_page == root_page)
287            .map(|base| base.total_entities)
288    }
289
290    /// Whether a projected base posting has been replaced or physically
291    /// removed. The host must fence publication/mutations while merging the
292    /// projected base with resident rows. This never reads a base payload.
293    pub fn backing_row_is_shadowed(&self, id: EntityId) -> bool {
294        let backing = self.backing.read();
295        backing
296            .as_ref()
297            .is_none_or(|base| base.removed.contains(&id))
298            || self.get_with(id, |_| ()).is_some()
299    }
300
301    /// Preserve I/O and decode failures instead of treating them as absent rows.
302    pub fn try_get(
303        &self,
304        id: EntityId,
305        limit_bytes: usize,
306    ) -> Result<Option<UnifiedEntity>, StoreError> {
307        let backing = self.backing.read();
308        let limit_bytes = backing
309            .as_ref()
310            .map_or(limit_bytes, |base| limit_bytes.min(base.read_bytes_max));
311        if let Some(result) = self.get_with(id, |entity| {
312            let required_bytes = if limit_bytes == usize::MAX {
313                0
314            } else {
315                crate::storage::memory_size::entity_bytes(entity)
316            };
317            if required_bytes > limit_bytes {
318                Err(StoreError::ReadLimitExceeded {
319                    required_bytes,
320                    limit_bytes,
321                })
322            } else {
323                Ok(entity.clone())
324            }
325        }) {
326            let mut entity = result?;
327            self.restore_owned_row_schemas(std::iter::once(&mut entity));
328            return Ok(Some(entity));
329        }
330        let Some(backing) = backing.as_ref() else {
331            return Ok(None);
332        };
333        if backing.removed.contains(&id) {
334            return Ok(None);
335        }
336        let Some(record) = backing
337            .source
338            .get_record(id, limit_bytes.min(backing.read_bytes_max))?
339        else {
340            return Ok(None);
341        };
342        let (mut entity, _) = record.decode_with_budget(
343            backing.format_version,
344            limit_bytes.min(backing.read_bytes_max),
345        )?;
346        self.restore_owned_row_schemas(std::iter::once(&mut entity));
347        Ok(Some(entity))
348    }
349
350    /// Fallible keyed projection. Resident mode keeps the segment key indexes.
351    /// Prepared bases seek physical candidates through their paired key index;
352    /// unindexed legacy sources retain a one-row streaming fallback. Hosts must
353    /// fence publication, and callbacks must not mutate or re-enter this collection.
354    pub fn try_project_key_entity<T>(
355        &self,
356        key: &str,
357        snapshot: Option<&SnapshotContext>,
358        versioned: bool,
359        limit_bytes: usize,
360        mut project: impl FnMut(&UnifiedEntity) -> T,
361    ) -> Result<Option<T>, StoreError> {
362        let backing = self.backing.read();
363        let limit_bytes = backing
364            .as_ref()
365            .map_or(limit_bytes, |base| limit_bytes.min(base.read_bytes_max));
366        let mut best = self
367            .project_key_entity(key, snapshot, versioned, |entity| {
368                let required_bytes = if limit_bytes == usize::MAX {
369                    0
370                } else {
371                    crate::storage::memory_size::entity_bytes(entity)
372                };
373                if required_bytes > limit_bytes {
374                    Err(StoreError::ReadLimitExceeded {
375                        required_bytes,
376                        limit_bytes,
377                    })
378                } else {
379                    Ok((entity.xmin, project(entity)))
380                }
381            })
382            .transpose()?;
383        let Some(backing) = backing.as_ref() else {
384            return Ok(best.map(|(_, projected)| projected));
385        };
386        if !versioned && best.is_some() {
387            return Ok(best.map(|(_, projected)| projected));
388        }
389        let mut cursor = match backing.source.key_cursor(key)? {
390            Some(cursor) => cursor,
391            None => backing.source.cursor()?,
392        };
393        while let Some(id) = cursor.peek_id()? {
394            // A physical replacement or deletion masks the old base value,
395            // even when it no longer matches this logical key.
396            if backing.removed.contains(&id) || self.get_with(id, |_| ()).is_some() {
397                cursor.skip_record()?;
398                continue;
399            }
400            let record = cursor
401                .next_record(limit_bytes)?
402                .expect("invariant: peeked physical row exists");
403            let (mut entity, _) = record.decode_with_budget(backing.format_version, limit_bytes)?;
404            self.restore_owned_row_schemas(std::iter::once(&mut entity));
405            if crate::storage::segment_key_lookup::SegmentKeyLookup::key(&entity) != Some(key)
406                || (versioned && !Self::scan_entity_visible(snapshot, &entity))
407            {
408                continue;
409            }
410            if best.as_ref().is_none_or(|(xmin, _)| entity.xmin >= *xmin) {
411                best = Some((entity.xmin, project(&entity)));
412            }
413            if !versioned {
414                break;
415            }
416        }
417        Ok(best.map(|(_, projected)| projected))
418    }
419
420    /// Visit the merged physical view in ID order without hydrating the base.
421    /// Only overlay identities and one decoded base record are retained. Like
422    /// resident visitors, callbacks must not mutate or re-enter this collection.
423    pub fn try_for_each_entity(
424        &self,
425        limit_bytes: usize,
426        mut callback: impl FnMut(&UnifiedEntity) -> bool,
427    ) -> Result<(), StoreError> {
428        let backing_guard = self.backing.read();
429        let Some(backing) = backing_guard.as_ref() else {
430            self.for_each_entity(callback);
431            return Ok(());
432        };
433        self.try_for_each_backed_entity(backing, limit_bytes, 0, |entity| callback(&entity))
434    }
435
436    fn try_for_each_backed_entity(
437        &self,
438        backing: &BackingRead,
439        limit_bytes: usize,
440        mut skip: usize,
441        mut callback: impl FnMut(std::borrow::Cow<'_, UnifiedEntity>) -> bool,
442    ) -> Result<(), StoreError> {
443        let limit_bytes = limit_bytes.min(backing.read_bytes_max);
444        let mut identities = Vec::new();
445        self.for_each_entity(|entity| {
446            identities.push(entity.id);
447            true
448        });
449        identities.sort_unstable();
450        identities.dedup();
451        let mut identities = identities.into_iter().peekable();
452        let mut cursor = backing.source.cursor()?;
453        loop {
454            let base = cursor.peek_id()?;
455            let resident = identities.peek().copied();
456            if let Some(id) = resident.filter(|id| base.is_none_or(|base| *id <= base)) {
457                if base == Some(id) {
458                    cursor.skip_record()?;
459                }
460                identities.next();
461                let result = self.get_with(id, |entity| {
462                    if skip > 0 {
463                        skip -= 1;
464                        return Ok(true);
465                    }
466                    let required_bytes = crate::storage::memory_size::entity_bytes(entity);
467                    if required_bytes > limit_bytes {
468                        return Err(StoreError::ReadLimitExceeded {
469                            required_bytes,
470                            limit_bytes,
471                        });
472                    }
473                    Ok(callback(std::borrow::Cow::Borrowed(entity)))
474                });
475                if result.transpose()? == Some(false) {
476                    return Ok(());
477                }
478                continue;
479            }
480            let Some(id) = base else {
481                return Ok(());
482            };
483            if backing.removed.contains(&id) {
484                cursor.skip_record()?;
485                continue;
486            }
487            if skip > 0 {
488                skip -= 1;
489                cursor.skip_record()?;
490                continue;
491            }
492            let record = cursor
493                .next_record(limit_bytes)?
494                .expect("invariant: peeked physical row exists");
495            let (mut entity, _) = record.decode_with_budget(
496                backing.format_version,
497                limit_bytes.min(backing.read_bytes_max),
498            )?;
499            self.restore_owned_row_schemas(std::iter::once(&mut entity));
500            if !callback(std::borrow::Cow::Owned(entity)) {
501                return Ok(());
502            }
503        }
504    }
505
506    /// Page the same base/overlay physical view used by fallible query reads.
507    /// Prefix records are skipped without decoding; only the requested page is
508    /// cloned. The manager's counter includes base rows and resident mutations.
509    pub fn try_page_in_id_order(
510        &self,
511        offset: usize,
512        limit: usize,
513        limit_bytes: usize,
514    ) -> Result<(usize, Vec<UnifiedEntity>), StoreError> {
515        let backing_guard = self.backing.read();
516        let total = self.count();
517        if offset >= total || limit == 0 {
518            return Ok((total, Vec::new()));
519        }
520        let mut items = Vec::new();
521        let mut retained_bytes = 0_usize;
522        let mut error = None;
523        let mut capture = |mut entity: std::borrow::Cow<'_, UnifiedEntity>| {
524            // Eager file recovery sees these named records before runtime
525            // rehydrates the declared column schema. Reproduce that layout
526            // only for decoded base rows; live overlays keep their wire shape.
527            if let std::borrow::Cow::Owned(entity) = &mut entity {
528                self.restore_recovery_row_layout(entity, None);
529            }
530            let required_bytes =
531                retained_bytes.saturating_add(crate::storage::memory_size::entity_bytes(&entity));
532            if required_bytes > limit_bytes {
533                error = Some(StoreError::ReadLimitExceeded {
534                    required_bytes,
535                    limit_bytes,
536                });
537                return false;
538            }
539            retained_bytes = required_bytes;
540            items.push(entity.into_owned());
541            items.len() < limit
542        };
543        if let Some(backing) = backing_guard.as_ref() {
544            self.try_for_each_backed_entity(backing, limit_bytes, offset, &mut capture)?;
545        } else {
546            let mut skip = offset;
547            self.for_each_entity_in_id_order(|entity| {
548                if skip > 0 {
549                    skip -= 1;
550                    return true;
551                }
552                capture(std::borrow::Cow::Borrowed(entity))
553            });
554        }
555        if let Some(error) = error {
556            return Err(error);
557        }
558        self.restore_owned_row_schemas(items.iter_mut());
559        Ok((total, items))
560    }
561
562    /// Page a caller-filtered logical view while keeping the visible total exact.
563    /// Unlike post-page filtering, this never leaks the number or position of
564    /// rows rejected by `predicate`.
565    pub fn try_page_in_id_order_where(
566        &self,
567        offset: usize,
568        limit: usize,
569        limit_bytes: usize,
570        mut predicate: impl FnMut(&UnifiedEntity) -> bool,
571    ) -> Result<(usize, Vec<UnifiedEntity>), StoreError> {
572        let mut visible = 0_usize;
573        let mut retained_bytes = 0_usize;
574        let mut items = Vec::new();
575        let mut error = None;
576        self.try_scan_for_each(None, limit_bytes, |entity| {
577            if !predicate(entity) {
578                return true;
579            }
580            let position = visible;
581            visible = visible.saturating_add(1);
582            if position < offset || items.len() >= limit {
583                return true;
584            }
585            let required_bytes =
586                retained_bytes.saturating_add(crate::storage::memory_size::entity_bytes(entity));
587            if required_bytes > limit_bytes {
588                error = Some(StoreError::ReadLimitExceeded {
589                    required_bytes,
590                    limit_bytes,
591                });
592                return false;
593            }
594            retained_bytes = required_bytes;
595            items.push(entity.clone());
596            true
597        })?;
598        if let Some(error) = error {
599            return Err(error);
600        }
601        self.restore_owned_row_schemas(items.iter_mut());
602        Ok((visible, items))
603    }
604
605    /// Physical key history. Indexed bases avoid unrelated payload reads;
606    /// resident and unindexed sources still verify each logical key.
607    pub fn try_for_each_key_entity(
608        &self,
609        key: &str,
610        mut callback: impl FnMut(&UnifiedEntity) -> bool,
611    ) -> Result<(), StoreError> {
612        if let Some(cursor) = self.try_paged_key_physical_cursor(key)? {
613            for record in cursor {
614                let (entity, _) = record?;
615                if !callback(&entity) {
616                    break;
617                }
618            }
619            return Ok(());
620        }
621        self.try_for_each_entity(usize::MAX, |entity| {
622            crate::storage::segment_key_lookup::SegmentKeyLookup::key(entity) != Some(key)
623                || callback(entity)
624        })
625    }
626
627    pub fn try_scan_for_each(
628        &self,
629        snapshot: Option<&SnapshotContext>,
630        limit_bytes: usize,
631        mut callback: impl FnMut(&UnifiedEntity) -> bool,
632    ) -> Result<(), StoreError> {
633        self.try_for_each_entity(limit_bytes, |entity| {
634            !Self::scan_entity_visible(snapshot, entity) || callback(entity)
635        })
636    }
637
638    /// The disk base has no segment zone summaries yet: scan all candidates
639    /// and retain the caller's residual predicate. Resident mode keeps pruning.
640    pub fn try_scan_for_each_zoned_with_stats(
641        &self,
642        snapshot: Option<&SnapshotContext>,
643        zone_preds: &[(&str, ZoneColPred<'_>)],
644        limit_bytes: usize,
645        callback: impl FnMut(&UnifiedEntity) -> bool,
646    ) -> Result<SegmentScanStats, StoreError> {
647        if !self.has_backing() {
648            return Ok(self.scan_for_each_zoned_with_stats(snapshot, zone_preds, callback));
649        }
650        self.try_scan_for_each(snapshot, limit_bytes, callback)?;
651        Ok(SegmentScanStats {
652            segments_total: 1,
653            segments_scanned: 1,
654            segments_pruned: 0,
655        })
656    }
657
658    /// Preserve the resident projection fast path; cold mode streams candidates.
659    /// Hosts own admission for retained projections independently of row decoding.
660    pub fn try_scan_project_zoned_with_stats<T, F>(
661        &self,
662        snapshot: Option<&SnapshotContext>,
663        zone_preds: &[(&str, ZoneColPred<'_>)],
664        read_bytes_max: usize,
665        project: F,
666    ) -> Result<(Vec<T>, SegmentScanStats), StoreError>
667    where
668        T: Send,
669        F: Fn(&UnifiedEntity) -> Option<T> + Sync,
670    {
671        if !self.has_backing() {
672            return Ok(self.scan_project_zoned_with_stats(snapshot, zone_preds, project));
673        }
674        let mut projected = Vec::new();
675        let stats = self.try_scan_for_each_zoned_with_stats(
676            snapshot,
677            zone_preds,
678            read_bytes_max,
679            |entity| {
680                if let Some(value) = project(entity) {
681                    projected.push(value);
682                }
683                true
684            },
685        )?;
686        Ok((projected, stats))
687    }
688
689    /// Point-read allowance applies to each row. Hosts separately admit the
690    /// retained output vector; this method is not a global result quota.
691    pub fn try_get_many(
692        &self,
693        ids: &[EntityId],
694        read_bytes_max: usize,
695    ) -> Result<Vec<Option<UnifiedEntity>>, StoreError> {
696        if !self.has_backing() && read_bytes_max == usize::MAX {
697            return Ok(self.get_many(ids));
698        }
699        ids.iter()
700            .map(|id| self.try_get(*id, read_bytes_max))
701            .collect()
702    }
703
704    pub fn try_for_each_id(
705        &self,
706        ids: &[EntityId],
707        read_bytes_max: usize,
708        mut visit: impl FnMut(usize, &UnifiedEntity) -> bool,
709    ) -> Result<(), StoreError> {
710        if self.has_backing() {
711            for (position, id) in ids.iter().enumerate() {
712                if let Some(entity) = self.try_get(*id, read_bytes_max)? {
713                    if !visit(position, &entity) {
714                        break;
715                    }
716                }
717            }
718            return Ok(());
719        }
720        let mut stopped = false;
721        let mut failure = None;
722        self.for_each_id(ids, |position, entity| {
723            if stopped {
724                return;
725            }
726            let required_bytes = if read_bytes_max == usize::MAX {
727                0
728            } else {
729                crate::storage::memory_size::entity_bytes(entity)
730            };
731            if required_bytes > read_bytes_max {
732                failure = Some(StoreError::ReadLimitExceeded {
733                    required_bytes,
734                    limit_bytes: read_bytes_max,
735                });
736                stopped = true;
737            } else {
738                stopped = !visit(position, entity);
739            }
740        });
741        failure.map_or(Ok(()), Err)
742    }
743
744    /// Inserts must not overwrite a physical identity in the published base.
745    pub(super) fn reject_backing_duplicate(&self, id: EntityId) -> Result<(), SegmentError> {
746        let backing = self.backing.read();
747        let Some(backing) = backing.as_ref() else {
748            return Ok(());
749        };
750        if !backing.removed.contains(&id)
751            && backing
752                .source
753                .get_record(id, backing.read_bytes_max)
754                .map_err(|error| SegmentError::BackingRead(error.to_string()))?
755                .is_some()
756        {
757            return Err(SegmentError::AlreadyExists(id));
758        }
759        Ok(())
760    }
761
762    /// Metadata reads carry the same I/O failure contract as entity reads.
763    pub fn try_get_metadata(
764        &self,
765        id: EntityId,
766        limit_bytes: usize,
767    ) -> Result<Option<Metadata>, StoreError> {
768        let backing = self.backing.read();
769        if self.get_with(id, |_| ()).is_some() {
770            return Ok(self.get_metadata(id));
771        }
772        let Some(backing) = backing.as_ref() else {
773            return Ok(None);
774        };
775        if backing.removed.contains(&id) {
776            return Ok(None);
777        }
778        backing
779            .source
780            .get_record(id, limit_bytes.min(backing.read_bytes_max))?
781            .map(|record| {
782                record
783                    .decode_with_budget(
784                        backing.format_version,
785                        limit_bytes.min(backing.read_bytes_max),
786                    )
787                    .map(|(_, metadata)| metadata)
788            })
789            .transpose()
790            .map(Option::flatten)
791    }
792
793    pub(super) fn materialize_backing_entity(&self, id: EntityId) -> Result<(), SegmentError> {
794        if self.get_with(id, |_| ()).is_some() {
795            return Ok(());
796        }
797        let record = {
798            let backing = self.backing.read();
799            let Some(backing) = backing.as_ref() else {
800                return Ok(());
801            };
802            if backing.removed.contains(&id) {
803                return Ok(());
804            }
805            backing
806                .source
807                .get_record(id, backing.read_bytes_max)
808                .and_then(|record| {
809                    record
810                        .map(|record| {
811                            record
812                                .decode_with_budget(backing.format_version, backing.read_bytes_max)
813                        })
814                        .transpose()
815                })
816                .map_err(|error| SegmentError::BackingRead(error.to_string()))?
817        };
818        let Some((mut entity, metadata)) = record else {
819            return Ok(());
820        };
821        self.restore_owned_row_schemas(std::iter::once(&mut entity));
822        self.restore_snapshot_entity(entity)?;
823        // This row was already included in the persisted cardinality.
824        self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
825        if let Some(metadata) = metadata {
826            self.set_metadata(id, metadata)?;
827        }
828        Ok(())
829    }
830
831    pub(super) fn remember_backing_delete(&self, id: EntityId) {
832        if let Some(backing) = self.backing.write().as_mut() {
833            backing.removed.insert(id);
834        }
835    }
836
837    pub fn backing_overlay(
838        &self,
839        mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
840    ) -> Option<(Arc<dyn CollectionRead + Send + Sync>, Arc<FrozenOverlay>)> {
841        let backing = self.backing.read();
842        let backing = backing.as_ref()?;
843        let mut overlay: FrozenOverlay = backing.removed.iter().map(|id| (*id, None)).collect();
844        self.for_each_entity_with_metadata(|entity, metadata| {
845            if let Some(bytes) = encode(entity, metadata) {
846                overlay.insert(entity.id, Some(Arc::from(bytes)));
847            }
848            true
849        });
850        Some((Arc::clone(&backing.source), Arc::new(overlay)))
851    }
852
853    pub fn current_backing_overlay(
854        &self,
855    ) -> Option<(Arc<dyn CollectionRead + Send + Sync>, Arc<FrozenOverlay>)> {
856        let version = self.backing.read().as_ref()?.format_version;
857        self.backing_overlay(|entity, metadata| {
858            Some(serialize_entity_record(entity, metadata.as_ref(), version))
859        })
860    }
861
862    fn current_backing_key_overlay(
863        &self,
864        key: &str,
865    ) -> Result<Option<(Arc<dyn CollectionRead + Send + Sync>, Arc<FrozenOverlay>)>, StoreError>
866    {
867        let backing = self.backing.read();
868        let Some(backing) = backing.as_ref() else {
869            return Ok(None);
870        };
871        let mut overlay: FrozenOverlay = backing.removed.iter().map(|id| (*id, None)).collect();
872        let schema = self.column_schema();
873        let key_column = schema
874            .as_ref()
875            .and_then(|schema| schema.iter().position(|name| name == "key"));
876        let mut failure = None;
877        self.for_each_entity_source(|segment, entity| {
878            let key_matches = match (&entity.data, key_column) {
879                (crate::storage::entity::EntityData::Row(row), Some(index)) if row.named.is_none() && row.schema.is_none() => {
880                    matches!(row.columns.get(index), Some(reddb_types::Value::Text(stored)) if stored.as_ref() == key)
881                }
882                _ => crate::storage::segment_key_lookup::SegmentKeyLookup::key(entity) == Some(key),
883            };
884            let bytes = if key_matches {
885                let required_bytes = crate::storage::memory_size::entity_bytes(entity);
886                if required_bytes > backing.read_bytes_max {
887                    failure = Some(StoreError::ReadLimitExceeded {
888                        required_bytes,
889                        limit_bytes: backing.read_bytes_max,
890                    });
891                    return false;
892                }
893                let metadata = segment.get_metadata(entity.id);
894                Some(Arc::from(serialize_entity_record(entity, metadata.as_ref(), backing.format_version)))
895            } else {
896                // A row renamed away from the requested key must still hide
897                // the old base candidate. No unrelated payload/metadata copy.
898                None
899            };
900            overlay.insert(entity.id, bytes);
901            true
902        });
903        if let Some(error) = failure {
904            return Err(error);
905        }
906        Ok(Some((Arc::clone(&backing.source), Arc::new(overlay))))
907    }
908}