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    /// Physical key history. Indexed bases avoid unrelated payload reads;
563    /// resident and unindexed sources still verify each logical key.
564    pub fn try_for_each_key_entity(
565        &self,
566        key: &str,
567        mut callback: impl FnMut(&UnifiedEntity) -> bool,
568    ) -> Result<(), StoreError> {
569        if let Some(cursor) = self.try_paged_key_physical_cursor(key)? {
570            for record in cursor {
571                let (entity, _) = record?;
572                if !callback(&entity) {
573                    break;
574                }
575            }
576            return Ok(());
577        }
578        self.try_for_each_entity(usize::MAX, |entity| {
579            crate::storage::segment_key_lookup::SegmentKeyLookup::key(entity) != Some(key)
580                || callback(entity)
581        })
582    }
583
584    pub fn try_scan_for_each(
585        &self,
586        snapshot: Option<&SnapshotContext>,
587        limit_bytes: usize,
588        mut callback: impl FnMut(&UnifiedEntity) -> bool,
589    ) -> Result<(), StoreError> {
590        self.try_for_each_entity(limit_bytes, |entity| {
591            !Self::scan_entity_visible(snapshot, entity) || callback(entity)
592        })
593    }
594
595    /// The disk base has no segment zone summaries yet: scan all candidates
596    /// and retain the caller's residual predicate. Resident mode keeps pruning.
597    pub fn try_scan_for_each_zoned_with_stats(
598        &self,
599        snapshot: Option<&SnapshotContext>,
600        zone_preds: &[(&str, ZoneColPred<'_>)],
601        limit_bytes: usize,
602        callback: impl FnMut(&UnifiedEntity) -> bool,
603    ) -> Result<SegmentScanStats, StoreError> {
604        if !self.has_backing() {
605            return Ok(self.scan_for_each_zoned_with_stats(snapshot, zone_preds, callback));
606        }
607        self.try_scan_for_each(snapshot, limit_bytes, callback)?;
608        Ok(SegmentScanStats {
609            segments_total: 1,
610            segments_scanned: 1,
611            segments_pruned: 0,
612        })
613    }
614
615    /// Preserve the resident projection fast path; cold mode streams candidates.
616    /// Hosts own admission for retained projections independently of row decoding.
617    pub fn try_scan_project_zoned_with_stats<T, F>(
618        &self,
619        snapshot: Option<&SnapshotContext>,
620        zone_preds: &[(&str, ZoneColPred<'_>)],
621        read_bytes_max: usize,
622        project: F,
623    ) -> Result<(Vec<T>, SegmentScanStats), StoreError>
624    where
625        T: Send,
626        F: Fn(&UnifiedEntity) -> Option<T> + Sync,
627    {
628        if !self.has_backing() {
629            return Ok(self.scan_project_zoned_with_stats(snapshot, zone_preds, project));
630        }
631        let mut projected = Vec::new();
632        let stats = self.try_scan_for_each_zoned_with_stats(
633            snapshot,
634            zone_preds,
635            read_bytes_max,
636            |entity| {
637                if let Some(value) = project(entity) {
638                    projected.push(value);
639                }
640                true
641            },
642        )?;
643        Ok((projected, stats))
644    }
645
646    /// Point-read allowance applies to each row. Hosts separately admit the
647    /// retained output vector; this method is not a global result quota.
648    pub fn try_get_many(
649        &self,
650        ids: &[EntityId],
651        read_bytes_max: usize,
652    ) -> Result<Vec<Option<UnifiedEntity>>, StoreError> {
653        if !self.has_backing() && read_bytes_max == usize::MAX {
654            return Ok(self.get_many(ids));
655        }
656        ids.iter()
657            .map(|id| self.try_get(*id, read_bytes_max))
658            .collect()
659    }
660
661    pub fn try_for_each_id(
662        &self,
663        ids: &[EntityId],
664        read_bytes_max: usize,
665        mut visit: impl FnMut(usize, &UnifiedEntity) -> bool,
666    ) -> Result<(), StoreError> {
667        if self.has_backing() {
668            for (position, id) in ids.iter().enumerate() {
669                if let Some(entity) = self.try_get(*id, read_bytes_max)? {
670                    if !visit(position, &entity) {
671                        break;
672                    }
673                }
674            }
675            return Ok(());
676        }
677        let mut stopped = false;
678        let mut failure = None;
679        self.for_each_id(ids, |position, entity| {
680            if stopped {
681                return;
682            }
683            let required_bytes = if read_bytes_max == usize::MAX {
684                0
685            } else {
686                crate::storage::memory_size::entity_bytes(entity)
687            };
688            if required_bytes > read_bytes_max {
689                failure = Some(StoreError::ReadLimitExceeded {
690                    required_bytes,
691                    limit_bytes: read_bytes_max,
692                });
693                stopped = true;
694            } else {
695                stopped = !visit(position, entity);
696            }
697        });
698        failure.map_or(Ok(()), Err)
699    }
700
701    /// Inserts must not overwrite a physical identity in the published base.
702    pub(super) fn reject_backing_duplicate(&self, id: EntityId) -> Result<(), SegmentError> {
703        let backing = self.backing.read();
704        let Some(backing) = backing.as_ref() else {
705            return Ok(());
706        };
707        if !backing.removed.contains(&id)
708            && backing
709                .source
710                .get_record(id, backing.read_bytes_max)
711                .map_err(|error| SegmentError::BackingRead(error.to_string()))?
712                .is_some()
713        {
714            return Err(SegmentError::AlreadyExists(id));
715        }
716        Ok(())
717    }
718
719    /// Metadata reads carry the same I/O failure contract as entity reads.
720    pub fn try_get_metadata(
721        &self,
722        id: EntityId,
723        limit_bytes: usize,
724    ) -> Result<Option<Metadata>, StoreError> {
725        let backing = self.backing.read();
726        if self.get_with(id, |_| ()).is_some() {
727            return Ok(self.get_metadata(id));
728        }
729        let Some(backing) = backing.as_ref() else {
730            return Ok(None);
731        };
732        if backing.removed.contains(&id) {
733            return Ok(None);
734        }
735        backing
736            .source
737            .get_record(id, limit_bytes.min(backing.read_bytes_max))?
738            .map(|record| {
739                record
740                    .decode_with_budget(
741                        backing.format_version,
742                        limit_bytes.min(backing.read_bytes_max),
743                    )
744                    .map(|(_, metadata)| metadata)
745            })
746            .transpose()
747            .map(Option::flatten)
748    }
749
750    pub(super) fn materialize_backing_entity(&self, id: EntityId) -> Result<(), SegmentError> {
751        if self.get_with(id, |_| ()).is_some() {
752            return Ok(());
753        }
754        let record = {
755            let backing = self.backing.read();
756            let Some(backing) = backing.as_ref() else {
757                return Ok(());
758            };
759            if backing.removed.contains(&id) {
760                return Ok(());
761            }
762            backing
763                .source
764                .get_record(id, backing.read_bytes_max)
765                .and_then(|record| {
766                    record
767                        .map(|record| {
768                            record
769                                .decode_with_budget(backing.format_version, backing.read_bytes_max)
770                        })
771                        .transpose()
772                })
773                .map_err(|error| SegmentError::BackingRead(error.to_string()))?
774        };
775        let Some((mut entity, metadata)) = record else {
776            return Ok(());
777        };
778        self.restore_owned_row_schemas(std::iter::once(&mut entity));
779        self.restore_snapshot_entity(entity)?;
780        // This row was already included in the persisted cardinality.
781        self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
782        if let Some(metadata) = metadata {
783            self.set_metadata(id, metadata)?;
784        }
785        Ok(())
786    }
787
788    pub(super) fn remember_backing_delete(&self, id: EntityId) {
789        if let Some(backing) = self.backing.write().as_mut() {
790            backing.removed.insert(id);
791        }
792    }
793
794    pub fn backing_overlay(
795        &self,
796        mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
797    ) -> Option<(Arc<dyn CollectionRead + Send + Sync>, Arc<FrozenOverlay>)> {
798        let backing = self.backing.read();
799        let backing = backing.as_ref()?;
800        let mut overlay: FrozenOverlay = backing.removed.iter().map(|id| (*id, None)).collect();
801        self.for_each_entity_with_metadata(|entity, metadata| {
802            if let Some(bytes) = encode(entity, metadata) {
803                overlay.insert(entity.id, Some(Arc::from(bytes)));
804            }
805            true
806        });
807        Some((Arc::clone(&backing.source), Arc::new(overlay)))
808    }
809
810    pub fn current_backing_overlay(
811        &self,
812    ) -> Option<(Arc<dyn CollectionRead + Send + Sync>, Arc<FrozenOverlay>)> {
813        let version = self.backing.read().as_ref()?.format_version;
814        self.backing_overlay(|entity, metadata| {
815            Some(serialize_entity_record(entity, metadata.as_ref(), version))
816        })
817    }
818
819    fn current_backing_key_overlay(
820        &self,
821        key: &str,
822    ) -> Result<Option<(Arc<dyn CollectionRead + Send + Sync>, Arc<FrozenOverlay>)>, StoreError>
823    {
824        let backing = self.backing.read();
825        let Some(backing) = backing.as_ref() else {
826            return Ok(None);
827        };
828        let mut overlay: FrozenOverlay = backing.removed.iter().map(|id| (*id, None)).collect();
829        let schema = self.column_schema();
830        let key_column = schema
831            .as_ref()
832            .and_then(|schema| schema.iter().position(|name| name == "key"));
833        let mut failure = None;
834        self.for_each_entity_source(|segment, entity| {
835            let key_matches = match (&entity.data, key_column) {
836                (crate::storage::entity::EntityData::Row(row), Some(index)) if row.named.is_none() && row.schema.is_none() => {
837                    matches!(row.columns.get(index), Some(reddb_types::Value::Text(stored)) if stored.as_ref() == key)
838                }
839                _ => crate::storage::segment_key_lookup::SegmentKeyLookup::key(entity) == Some(key),
840            };
841            let bytes = if key_matches {
842                let required_bytes = crate::storage::memory_size::entity_bytes(entity);
843                if required_bytes > backing.read_bytes_max {
844                    failure = Some(StoreError::ReadLimitExceeded {
845                        required_bytes,
846                        limit_bytes: backing.read_bytes_max,
847                    });
848                    return false;
849                }
850                let metadata = segment.get_metadata(entity.id);
851                Some(Arc::from(serialize_entity_record(entity, metadata.as_ref(), backing.format_version)))
852            } else {
853                // A row renamed away from the requested key must still hide
854                // the old base candidate. No unrelated payload/metadata copy.
855                None
856            };
857            overlay.insert(entity.id, bytes);
858            true
859        });
860        if let Some(error) = failure {
861            return Err(error);
862        }
863        Ok(Some((Arc::clone(&backing.source), Arc::new(overlay))))
864    }
865}