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    /// Reconcile a verified physical count while a recovered store is still
291    /// exclusively owned and has not been made available to readers.
292    #[doc(hidden)]
293    pub fn set_recovered_backing_count(&self, root_page: u32, count: u64) -> bool {
294        let mut backing = self.backing.write();
295        let Some(backing) = backing.as_mut() else {
296            return false;
297        };
298        if backing.root_page != root_page {
299            return false;
300        }
301        backing.total_entities = count;
302        true
303    }
304
305    /// Whether a projected base posting has been replaced or physically
306    /// removed. The host must fence publication/mutations while merging the
307    /// projected base with resident rows. This never reads a base payload.
308    pub fn backing_row_is_shadowed(&self, id: EntityId) -> bool {
309        let backing = self.backing.read();
310        backing
311            .as_ref()
312            .is_none_or(|base| base.removed.contains(&id))
313            || self.get_with(id, |_| ()).is_some()
314    }
315
316    /// Preserve I/O and decode failures instead of treating them as absent rows.
317    pub fn try_get(
318        &self,
319        id: EntityId,
320        limit_bytes: usize,
321    ) -> Result<Option<UnifiedEntity>, StoreError> {
322        let backing = self.backing.read();
323        let limit_bytes = backing
324            .as_ref()
325            .map_or(limit_bytes, |base| limit_bytes.min(base.read_bytes_max));
326        if let Some(result) = self.get_with(id, |entity| {
327            let required_bytes = if limit_bytes == usize::MAX {
328                0
329            } else {
330                crate::storage::memory_size::entity_bytes(entity)
331            };
332            if required_bytes > limit_bytes {
333                Err(StoreError::ReadLimitExceeded {
334                    required_bytes,
335                    limit_bytes,
336                })
337            } else {
338                Ok(entity.clone())
339            }
340        }) {
341            let mut entity = result?;
342            self.restore_owned_row_schemas(std::iter::once(&mut entity));
343            return Ok(Some(entity));
344        }
345        let Some(backing) = backing.as_ref() else {
346            return Ok(None);
347        };
348        if backing.removed.contains(&id) {
349            return Ok(None);
350        }
351        let Some(record) = backing
352            .source
353            .get_record(id, limit_bytes.min(backing.read_bytes_max))?
354        else {
355            return Ok(None);
356        };
357        let (mut entity, _) = record.decode_with_budget(
358            backing.format_version,
359            limit_bytes.min(backing.read_bytes_max),
360        )?;
361        self.restore_owned_row_schemas(std::iter::once(&mut entity));
362        Ok(Some(entity))
363    }
364
365    /// Fallible keyed projection. Resident mode keeps the segment key indexes.
366    /// Prepared bases seek physical candidates through their paired key index;
367    /// unindexed legacy sources retain a one-row streaming fallback. Hosts must
368    /// fence publication, and callbacks must not mutate or re-enter this collection.
369    pub fn try_project_key_entity<T>(
370        &self,
371        key: &str,
372        snapshot: Option<&SnapshotContext>,
373        versioned: bool,
374        limit_bytes: usize,
375        mut project: impl FnMut(&UnifiedEntity) -> T,
376    ) -> Result<Option<T>, StoreError> {
377        let backing = self.backing.read();
378        let limit_bytes = backing
379            .as_ref()
380            .map_or(limit_bytes, |base| limit_bytes.min(base.read_bytes_max));
381        let mut best = self
382            .project_key_entity(key, snapshot, versioned, |entity| {
383                let required_bytes = if limit_bytes == usize::MAX {
384                    0
385                } else {
386                    crate::storage::memory_size::entity_bytes(entity)
387                };
388                if required_bytes > limit_bytes {
389                    Err(StoreError::ReadLimitExceeded {
390                        required_bytes,
391                        limit_bytes,
392                    })
393                } else {
394                    Ok((entity.xmin, project(entity)))
395                }
396            })
397            .transpose()?;
398        let Some(backing) = backing.as_ref() else {
399            return Ok(best.map(|(_, projected)| projected));
400        };
401        if !versioned && best.is_some() {
402            return Ok(best.map(|(_, projected)| projected));
403        }
404        let mut cursor = match backing.source.key_cursor(key)? {
405            Some(cursor) => cursor,
406            None => backing.source.cursor()?,
407        };
408        while let Some(id) = cursor.peek_id()? {
409            // A physical replacement or deletion masks the old base value,
410            // even when it no longer matches this logical key.
411            if backing.removed.contains(&id) || self.get_with(id, |_| ()).is_some() {
412                cursor.skip_record()?;
413                continue;
414            }
415            let record = cursor
416                .next_record(limit_bytes)?
417                .expect("invariant: peeked physical row exists");
418            let (mut entity, _) = record.decode_with_budget(backing.format_version, limit_bytes)?;
419            self.restore_owned_row_schemas(std::iter::once(&mut entity));
420            if crate::storage::segment_key_lookup::SegmentKeyLookup::key(&entity) != Some(key)
421                || (versioned && !Self::scan_entity_visible(snapshot, &entity))
422            {
423                continue;
424            }
425            if best.as_ref().is_none_or(|(xmin, _)| entity.xmin >= *xmin) {
426                best = Some((entity.xmin, project(&entity)));
427            }
428            if !versioned {
429                break;
430            }
431        }
432        Ok(best.map(|(_, projected)| projected))
433    }
434
435    /// Visit the merged physical view in ID order without hydrating the base.
436    /// Only overlay identities and one decoded base record are retained. Like
437    /// resident visitors, callbacks must not mutate or re-enter this collection.
438    pub fn try_for_each_entity(
439        &self,
440        limit_bytes: usize,
441        mut callback: impl FnMut(&UnifiedEntity) -> bool,
442    ) -> Result<(), StoreError> {
443        let backing_guard = self.backing.read();
444        let Some(backing) = backing_guard.as_ref() else {
445            self.for_each_entity(callback);
446            return Ok(());
447        };
448        self.try_for_each_backed_entity(backing, limit_bytes, 0, |entity| callback(&entity))
449    }
450
451    fn try_for_each_backed_entity(
452        &self,
453        backing: &BackingRead,
454        limit_bytes: usize,
455        mut skip: usize,
456        mut callback: impl FnMut(std::borrow::Cow<'_, UnifiedEntity>) -> bool,
457    ) -> Result<(), StoreError> {
458        let limit_bytes = limit_bytes.min(backing.read_bytes_max);
459        let mut identities = Vec::new();
460        self.for_each_entity(|entity| {
461            identities.push(entity.id);
462            true
463        });
464        identities.sort_unstable();
465        identities.dedup();
466        let mut identities = identities.into_iter().peekable();
467        let mut cursor = backing.source.cursor()?;
468        loop {
469            let base = cursor.peek_id()?;
470            let resident = identities.peek().copied();
471            if let Some(id) = resident.filter(|id| base.is_none_or(|base| *id <= base)) {
472                if base == Some(id) {
473                    cursor.skip_record()?;
474                }
475                identities.next();
476                let result = self.get_with(id, |entity| {
477                    if skip > 0 {
478                        skip -= 1;
479                        return Ok(true);
480                    }
481                    let required_bytes = crate::storage::memory_size::entity_bytes(entity);
482                    if required_bytes > limit_bytes {
483                        return Err(StoreError::ReadLimitExceeded {
484                            required_bytes,
485                            limit_bytes,
486                        });
487                    }
488                    Ok(callback(std::borrow::Cow::Borrowed(entity)))
489                });
490                if result.transpose()? == Some(false) {
491                    return Ok(());
492                }
493                continue;
494            }
495            let Some(id) = base else {
496                return Ok(());
497            };
498            if backing.removed.contains(&id) {
499                cursor.skip_record()?;
500                continue;
501            }
502            if skip > 0 {
503                skip -= 1;
504                cursor.skip_record()?;
505                continue;
506            }
507            let record = cursor
508                .next_record(limit_bytes)?
509                .expect("invariant: peeked physical row exists");
510            let (mut entity, _) = record.decode_with_budget(
511                backing.format_version,
512                limit_bytes.min(backing.read_bytes_max),
513            )?;
514            self.restore_owned_row_schemas(std::iter::once(&mut entity));
515            if !callback(std::borrow::Cow::Owned(entity)) {
516                return Ok(());
517            }
518        }
519    }
520
521    /// Page the same base/overlay physical view used by fallible query reads.
522    /// Prefix records are skipped without decoding; only the requested page is
523    /// cloned. The manager's counter includes base rows and resident mutations.
524    pub fn try_page_in_id_order(
525        &self,
526        offset: usize,
527        limit: usize,
528        limit_bytes: usize,
529    ) -> Result<(usize, Vec<UnifiedEntity>), StoreError> {
530        let backing_guard = self.backing.read();
531        let total = self.count();
532        if offset >= total || limit == 0 {
533            return Ok((total, Vec::new()));
534        }
535        let mut items = Vec::new();
536        let mut retained_bytes = 0_usize;
537        let mut error = None;
538        let mut capture = |mut entity: std::borrow::Cow<'_, UnifiedEntity>| {
539            // Eager file recovery sees these named records before runtime
540            // rehydrates the declared column schema. Reproduce that layout
541            // only for decoded base rows; live overlays keep their wire shape.
542            if let std::borrow::Cow::Owned(entity) = &mut entity {
543                self.restore_recovery_row_layout(entity, None);
544            }
545            let required_bytes =
546                retained_bytes.saturating_add(crate::storage::memory_size::entity_bytes(&entity));
547            if required_bytes > limit_bytes {
548                error = Some(StoreError::ReadLimitExceeded {
549                    required_bytes,
550                    limit_bytes,
551                });
552                return false;
553            }
554            retained_bytes = required_bytes;
555            items.push(entity.into_owned());
556            items.len() < limit
557        };
558        if let Some(backing) = backing_guard.as_ref() {
559            self.try_for_each_backed_entity(backing, limit_bytes, offset, &mut capture)?;
560        } else {
561            let mut skip = offset;
562            self.for_each_entity_in_id_order(|entity| {
563                if skip > 0 {
564                    skip -= 1;
565                    return true;
566                }
567                capture(std::borrow::Cow::Borrowed(entity))
568            });
569        }
570        if let Some(error) = error {
571            return Err(error);
572        }
573        self.restore_owned_row_schemas(items.iter_mut());
574        Ok((total, items))
575    }
576
577    /// Page a caller-filtered logical view while keeping the visible total exact.
578    /// Unlike post-page filtering, this never leaks the number or position of
579    /// rows rejected by `predicate`.
580    pub fn try_page_in_id_order_where(
581        &self,
582        offset: usize,
583        limit: usize,
584        limit_bytes: usize,
585        mut predicate: impl FnMut(&UnifiedEntity) -> bool,
586    ) -> Result<(usize, Vec<UnifiedEntity>), StoreError> {
587        let mut visible = 0_usize;
588        let mut retained_bytes = 0_usize;
589        let mut items = Vec::new();
590        let mut error = None;
591        self.try_scan_for_each(None, limit_bytes, |entity| {
592            if !predicate(entity) {
593                return true;
594            }
595            let position = visible;
596            visible = visible.saturating_add(1);
597            if position < offset || items.len() >= limit {
598                return true;
599            }
600            let required_bytes =
601                retained_bytes.saturating_add(crate::storage::memory_size::entity_bytes(entity));
602            if required_bytes > limit_bytes {
603                error = Some(StoreError::ReadLimitExceeded {
604                    required_bytes,
605                    limit_bytes,
606                });
607                return false;
608            }
609            retained_bytes = required_bytes;
610            items.push(entity.clone());
611            true
612        })?;
613        if let Some(error) = error {
614            return Err(error);
615        }
616        self.restore_owned_row_schemas(items.iter_mut());
617        Ok((visible, items))
618    }
619
620    /// Physical key history. Indexed bases avoid unrelated payload reads;
621    /// resident and unindexed sources still verify each logical key.
622    pub fn try_for_each_key_entity(
623        &self,
624        key: &str,
625        mut callback: impl FnMut(&UnifiedEntity) -> bool,
626    ) -> Result<(), StoreError> {
627        if let Some(cursor) = self.try_paged_key_physical_cursor(key)? {
628            for record in cursor {
629                let (entity, _) = record?;
630                if !callback(&entity) {
631                    break;
632                }
633            }
634            return Ok(());
635        }
636        self.try_for_each_entity(usize::MAX, |entity| {
637            crate::storage::segment_key_lookup::SegmentKeyLookup::key(entity) != Some(key)
638                || callback(entity)
639        })
640    }
641
642    pub fn try_scan_for_each(
643        &self,
644        snapshot: Option<&SnapshotContext>,
645        limit_bytes: usize,
646        mut callback: impl FnMut(&UnifiedEntity) -> bool,
647    ) -> Result<(), StoreError> {
648        self.try_for_each_entity(limit_bytes, |entity| {
649            !Self::scan_entity_visible(snapshot, entity) || callback(entity)
650        })
651    }
652
653    /// The disk base has no segment zone summaries yet: scan all candidates
654    /// and retain the caller's residual predicate. Resident mode keeps pruning.
655    pub fn try_scan_for_each_zoned_with_stats(
656        &self,
657        snapshot: Option<&SnapshotContext>,
658        zone_preds: &[(&str, ZoneColPred<'_>)],
659        limit_bytes: usize,
660        callback: impl FnMut(&UnifiedEntity) -> bool,
661    ) -> Result<SegmentScanStats, StoreError> {
662        if !self.has_backing() {
663            return Ok(self.scan_for_each_zoned_with_stats(snapshot, zone_preds, callback));
664        }
665        self.try_scan_for_each(snapshot, limit_bytes, callback)?;
666        Ok(SegmentScanStats {
667            segments_total: 1,
668            segments_scanned: 1,
669            segments_pruned: 0,
670        })
671    }
672
673    /// Preserve the resident projection fast path; cold mode streams candidates.
674    /// Hosts own admission for retained projections independently of row decoding.
675    pub fn try_scan_project_zoned_with_stats<T, F>(
676        &self,
677        snapshot: Option<&SnapshotContext>,
678        zone_preds: &[(&str, ZoneColPred<'_>)],
679        read_bytes_max: usize,
680        project: F,
681    ) -> Result<(Vec<T>, SegmentScanStats), StoreError>
682    where
683        T: Send,
684        F: Fn(&UnifiedEntity) -> Option<T> + Sync,
685    {
686        if !self.has_backing() {
687            return Ok(self.scan_project_zoned_with_stats(snapshot, zone_preds, project));
688        }
689        let mut projected = Vec::new();
690        let stats = self.try_scan_for_each_zoned_with_stats(
691            snapshot,
692            zone_preds,
693            read_bytes_max,
694            |entity| {
695                if let Some(value) = project(entity) {
696                    projected.push(value);
697                }
698                true
699            },
700        )?;
701        Ok((projected, stats))
702    }
703
704    /// Point-read allowance applies to each row. Hosts separately admit the
705    /// retained output vector; this method is not a global result quota.
706    pub fn try_get_many(
707        &self,
708        ids: &[EntityId],
709        read_bytes_max: usize,
710    ) -> Result<Vec<Option<UnifiedEntity>>, StoreError> {
711        if !self.has_backing() && read_bytes_max == usize::MAX {
712            return Ok(self.get_many(ids));
713        }
714        ids.iter()
715            .map(|id| self.try_get(*id, read_bytes_max))
716            .collect()
717    }
718
719    pub fn try_for_each_id(
720        &self,
721        ids: &[EntityId],
722        read_bytes_max: usize,
723        mut visit: impl FnMut(usize, &UnifiedEntity) -> bool,
724    ) -> Result<(), StoreError> {
725        if self.has_backing() {
726            for (position, id) in ids.iter().enumerate() {
727                if let Some(entity) = self.try_get(*id, read_bytes_max)? {
728                    if !visit(position, &entity) {
729                        break;
730                    }
731                }
732            }
733            return Ok(());
734        }
735        let mut stopped = false;
736        let mut failure = None;
737        self.for_each_id(ids, |position, entity| {
738            if stopped {
739                return;
740            }
741            let required_bytes = if read_bytes_max == usize::MAX {
742                0
743            } else {
744                crate::storage::memory_size::entity_bytes(entity)
745            };
746            if required_bytes > read_bytes_max {
747                failure = Some(StoreError::ReadLimitExceeded {
748                    required_bytes,
749                    limit_bytes: read_bytes_max,
750                });
751                stopped = true;
752            } else {
753                stopped = !visit(position, entity);
754            }
755        });
756        failure.map_or(Ok(()), Err)
757    }
758
759    /// Inserts must not overwrite a physical identity in the published base.
760    pub(super) fn reject_backing_duplicate(&self, id: EntityId) -> Result<(), SegmentError> {
761        let backing = self.backing.read();
762        let Some(backing) = backing.as_ref() else {
763            return Ok(());
764        };
765        if !backing.removed.contains(&id)
766            && backing
767                .source
768                .get_record(id, backing.read_bytes_max)
769                .map_err(|error| SegmentError::BackingRead(error.to_string()))?
770                .is_some()
771        {
772            return Err(SegmentError::AlreadyExists(id));
773        }
774        Ok(())
775    }
776
777    /// Metadata reads carry the same I/O failure contract as entity reads.
778    pub fn try_get_metadata(
779        &self,
780        id: EntityId,
781        limit_bytes: usize,
782    ) -> Result<Option<Metadata>, StoreError> {
783        let backing = self.backing.read();
784        if self.get_with(id, |_| ()).is_some() {
785            return Ok(self.get_metadata(id));
786        }
787        let Some(backing) = backing.as_ref() else {
788            return Ok(None);
789        };
790        if backing.removed.contains(&id) {
791            return Ok(None);
792        }
793        backing
794            .source
795            .get_record(id, limit_bytes.min(backing.read_bytes_max))?
796            .map(|record| {
797                record
798                    .decode_with_budget(
799                        backing.format_version,
800                        limit_bytes.min(backing.read_bytes_max),
801                    )
802                    .map(|(_, metadata)| metadata)
803            })
804            .transpose()
805            .map(Option::flatten)
806    }
807
808    pub(super) fn materialize_backing_entity(&self, id: EntityId) -> Result<(), SegmentError> {
809        if self.get_with(id, |_| ()).is_some() {
810            return Ok(());
811        }
812        let record = {
813            let backing = self.backing.read();
814            let Some(backing) = backing.as_ref() else {
815                return Ok(());
816            };
817            if backing.removed.contains(&id) {
818                return Ok(());
819            }
820            backing
821                .source
822                .get_record(id, backing.read_bytes_max)
823                .and_then(|record| {
824                    record
825                        .map(|record| {
826                            record
827                                .decode_with_budget(backing.format_version, backing.read_bytes_max)
828                        })
829                        .transpose()
830                })
831                .map_err(|error| SegmentError::BackingRead(error.to_string()))?
832        };
833        let Some((mut entity, metadata)) = record else {
834            return Ok(());
835        };
836        self.restore_owned_row_schemas(std::iter::once(&mut entity));
837        self.restore_snapshot_entity(entity)?;
838        // This row was already included in the persisted cardinality.
839        self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
840        if let Some(metadata) = metadata {
841            self.set_metadata(id, metadata)?;
842        }
843        Ok(())
844    }
845
846    pub(super) fn remember_backing_delete(&self, id: EntityId) {
847        if let Some(backing) = self.backing.write().as_mut() {
848            backing.removed.insert(id);
849        }
850    }
851
852    pub fn backing_overlay(
853        &self,
854        mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
855    ) -> Option<(Arc<dyn CollectionRead + Send + Sync>, Arc<FrozenOverlay>)> {
856        let backing = self.backing.read();
857        let backing = backing.as_ref()?;
858        let mut overlay: FrozenOverlay = backing.removed.iter().map(|id| (*id, None)).collect();
859        self.for_each_entity_with_metadata(|entity, metadata| {
860            if let Some(bytes) = encode(entity, metadata) {
861                overlay.insert(entity.id, Some(Arc::from(bytes)));
862            }
863            true
864        });
865        Some((Arc::clone(&backing.source), Arc::new(overlay)))
866    }
867
868    pub fn current_backing_overlay(
869        &self,
870    ) -> Option<(Arc<dyn CollectionRead + Send + Sync>, Arc<FrozenOverlay>)> {
871        let version = self.backing.read().as_ref()?.format_version;
872        self.backing_overlay(|entity, metadata| {
873            Some(serialize_entity_record(entity, metadata.as_ref(), version))
874        })
875    }
876
877    fn current_backing_key_overlay(
878        &self,
879        key: &str,
880    ) -> Result<Option<(Arc<dyn CollectionRead + Send + Sync>, Arc<FrozenOverlay>)>, StoreError>
881    {
882        let backing = self.backing.read();
883        let Some(backing) = backing.as_ref() else {
884            return Ok(None);
885        };
886        let mut overlay: FrozenOverlay = backing.removed.iter().map(|id| (*id, None)).collect();
887        let schema = self.column_schema();
888        let key_column = schema
889            .as_ref()
890            .and_then(|schema| schema.iter().position(|name| name == "key"));
891        let mut failure = None;
892        self.for_each_entity_source(|segment, entity| {
893            let key_matches = match (&entity.data, key_column) {
894                (crate::storage::entity::EntityData::Row(row), Some(index)) if row.named.is_none() && row.schema.is_none() => {
895                    matches!(row.columns.get(index), Some(reddb_types::Value::Text(stored)) if stored.as_ref() == key)
896                }
897                _ => crate::storage::segment_key_lookup::SegmentKeyLookup::key(entity) == Some(key),
898            };
899            let bytes = if key_matches {
900                let required_bytes = crate::storage::memory_size::entity_bytes(entity);
901                if required_bytes > backing.read_bytes_max {
902                    failure = Some(StoreError::ReadLimitExceeded {
903                        required_bytes,
904                        limit_bytes: backing.read_bytes_max,
905                    });
906                    return false;
907                }
908                let metadata = segment.get_metadata(entity.id);
909                Some(Arc::from(serialize_entity_record(entity, metadata.as_ref(), backing.format_version)))
910            } else {
911                // A row renamed away from the requested key must still hide
912                // the old base candidate. No unrelated payload/metadata copy.
913                None
914            };
915            overlay.insert(entity.id, bytes);
916            true
917        });
918        if let Some(error) = failure {
919            return Err(error);
920        }
921        Ok(Some((Arc::clone(&backing.source), Arc::new(overlay))))
922    }
923}