Skip to main content

reddb_server/storage/unified/store/
impl_pages.rs

1use super::*;
2use parking_lot::RwLock;
3
4const CHECKPOINT_BATCH_BYTES: usize = 1024 * 1024;
5const CHECKPOINT_BATCH_ROWS: usize = 512;
6
7#[derive(Debug)]
8struct SystemCollectionCollision {
9    survivor: String,
10    loser: String,
11    canonical: String,
12    shared_physical_state: bool,
13    resume_counter_drift: bool,
14    merged_resume: Option<reddb_file::native_resume::CollectionResume>,
15}
16
17#[derive(Debug, Default)]
18struct CatalogLoadOutcome {
19    migrated: bool,
20    collisions: Vec<SystemCollectionCollision>,
21}
22
23fn merge_shared_alias_resume(
24    legacy: &reddb_file::native_resume::CollectionResume,
25    canonical: &reddb_file::native_resume::CollectionResume,
26) -> Option<reddb_file::native_resume::CollectionResume> {
27    let (data_root_match, key_index_root_match, secondary_index_state_match) =
28        shared_alias_resume_matches(legacy, canonical);
29    if !data_root_match || !key_index_root_match || !secondary_index_state_match {
30        return None;
31    }
32    let mut merged = legacy.clone();
33    // The shared roots bind the exact durable data and derived indexes. Resume
34    // counters are advisory allocation state; retaining each high-water mark
35    // prevents identity reuse after an interrupted name migration. The exact
36    // entity count is read from the shared data tree while loading.
37    merged.entities = legacy.entities.max(canonical.entities);
38    merged.entity_id_next = legacy.entity_id_next.max(canonical.entity_id_next);
39    merged.row_id_next = legacy.row_id_next.max(canonical.row_id_next);
40    merged.sequence_id_next = legacy.sequence_id_next.max(canonical.sequence_id_next);
41    Some(merged)
42}
43
44fn shared_alias_resume_matches(
45    legacy: &reddb_file::native_resume::CollectionResume,
46    canonical: &reddb_file::native_resume::CollectionResume,
47) -> (bool, bool, bool) {
48    (
49        legacy.root_page == canonical.root_page,
50        legacy.key_index_root == canonical.key_index_root,
51        legacy.secondary_indices == canonical.secondary_indices
52            && legacy.metadata_expiries == canonical.metadata_expiries,
53    )
54}
55
56/// Keep encoded payload memory proportional to a small batch, not a collection.
57/// A single large value retains the existing B-tree value-size contract.
58struct CheckpointTreeBuilder {
59    tree: Arc<BTree>,
60    records: Vec<(Vec<u8>, Vec<u8>)>,
61    bytes: usize,
62}
63
64impl CheckpointTreeBuilder {
65    fn new(pager: &Arc<Pager>) -> Self {
66        Self {
67            tree: Arc::new(BTree::new(Arc::clone(pager))),
68            records: Vec::new(),
69            bytes: 0,
70        }
71    }
72
73    fn push(&mut self, record: (Vec<u8>, Vec<u8>)) -> Result<(), StoreError> {
74        let bytes = record.0.len() + record.1.len();
75        if !self.records.is_empty()
76            && (self.bytes.saturating_add(bytes) > CHECKPOINT_BATCH_BYTES
77                || self.records.len() >= CHECKPOINT_BATCH_ROWS)
78        {
79            self.flush()?;
80        }
81        self.bytes += bytes;
82        self.records.push(record);
83        #[cfg(test)]
84        CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(peak.get().max(self.bytes)));
85        Ok(())
86    }
87
88    fn flush(&mut self) -> Result<(), StoreError> {
89        self.tree
90            .bulk_insert_sorted(&self.records)
91            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
92        self.records.clear();
93        self.bytes = 0;
94        Ok(())
95    }
96
97    fn copy_prefix(&mut self, previous: &BTree, count: usize) -> Result<(), StoreError> {
98        let mut cursor = previous
99            .cursor_first()
100            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
101        for _ in 0..count {
102            let record = cursor
103                .next()
104                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
105                .ok_or_else(|| StoreError::Serialization("checkpoint prefix disappeared".into()))?;
106            self.push(record)?;
107        }
108        Ok(())
109    }
110}
111
112#[cfg(test)]
113thread_local! {
114    static CHECKPOINT_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
115    static REPLAY_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
116    static CHECKPOINT_BUFFERED_BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
117}
118
119fn checkpoint_visibility_matches(
120    previous: &CheckpointVersion,
121    current: &CheckpointVersion,
122) -> bool {
123    (previous.visibility_revision == current.visibility_revision
124        || previous.excluded_xids == current.excluded_xids)
125        && previous.pending_vault == current.pending_vault
126}
127
128// ── Pager-meta overflow chain (gh-477) ──────────────────────────────────────
129// When the serialized collection registry + cross-refs exceed a single page,
130// page 1 carries a native metadata-overflow header pointing at an overflow chain of
131// `PageType::Overflow` pages. Single-page metadata keeps the historical
132// bit-identical layout (`METADATA_MAGIC = "RDM2"` written directly at the
133// content offset).
134//
135// Page 1 (overflow form), starting at `HEADER_SIZE`:
136//   [0..4]   native metadata-overflow magic
137//   [4..8]   format_version (u32, mirrors inner payload version for debug)
138//   [8..12]  total_payload_bytes (u32)
139//   [12..16] next_overflow_page_id (u32, > 0)
140//   [16..]   first payload chunk (up to META_V3_FIRST_PAYLOAD_CAP bytes)
141//
142// Overflow continuation page, starting at `HEADER_SIZE`:
143//   [0..4]   next_overflow_page_id (u32, 0 if last)
144//   [4..8]   chunk_bytes (u32)
145//   [8..]    chunk payload (up to META_V3_OVERFLOW_PAYLOAD_CAP bytes)
146const META_PAGE_CONTENT_CAP: usize =
147    crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
148const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
149const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
150const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
151const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
152
153fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
154    let cs = crate::storage::engine::HEADER_SIZE;
155    let page = pager.read_page(1)?;
156    let bytes = page.as_bytes();
157    if bytes.len() < cs + META_V3_PAGE1_HEADER {
158        return Ok(Vec::new());
159    }
160    let Some(header) =
161        reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
162            PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
163        })?
164    else {
165        return Ok(Vec::new());
166    };
167    let mut next = header.next_overflow_page_id;
168    let mut pages = Vec::new();
169    let mut seen = std::collections::HashSet::new();
170    while next != 0 {
171        if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
172            return Err(PagerError::InvalidDatabase(
173                "cyclic metadata overflow chain".into(),
174            ));
175        }
176        let ov = pager.read_page(next)?;
177        if ov.page_type()? != crate::storage::engine::PageType::Overflow {
178            return Err(PagerError::InvalidDatabase(
179                "invalid metadata overflow page type".into(),
180            ));
181        }
182        let ob = ov.as_bytes();
183        let nn = reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
184            .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?
185            .next_overflow_page_id;
186        pages.push(next);
187        next = nn;
188    }
189    Ok(pages)
190}
191
192fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
193    let retired = existing_overflow_chain(pager)?;
194    if !retired.is_empty() {
195        pager.record_retirement_intent(&retired)?;
196    }
197    let page = build_meta_page1_with_overflow(pager, meta_data)?;
198    // Cache eviction order must not publish a root before its children.
199    pager.sync()?;
200    pager.write_page(1, page)?;
201    pager.sync()?;
202    for id in retired {
203        pager.free_page(id)?;
204    }
205    Ok(())
206}
207
208fn build_meta_page1_with_overflow(
209    pager: &Pager,
210    meta_data: &[u8],
211) -> Result<crate::storage::engine::Page, PagerError> {
212    use crate::storage::engine::{Page, PageType, HEADER_SIZE};
213
214    let mut page1 = Page::new(PageType::Header, 1);
215    let cs = HEADER_SIZE;
216
217    if meta_data.len() <= META_PAGE_CONTENT_CAP {
218        // Single-page: bit-identical to the historical layout.
219        let buf = page1.as_bytes_mut();
220        buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
221        return Ok(page1);
222    }
223
224    // Multi-page overflow form. Split the inner payload into the first chunk
225    // (held on page 1) followed by zero-or-more continuation chunks chained
226    // through `PageType::Overflow` pages.
227    let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
228    let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
229    let mut chunks: Vec<&[u8]> = Vec::new();
230    while !tail.is_empty() {
231        let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
232        chunks.push(&tail[..take]);
233        tail = &tail[take..];
234    }
235
236    let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
237    let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
238    for _ in 0..chunks.len() {
239        let pg = pager.allocate_page(PageType::Overflow)?;
240        overflow_ids.push(pg.page_id());
241        overflow_pages.push(pg);
242    }
243
244    for i in 0..chunks.len() {
245        let next = if i + 1 < chunks.len() {
246            overflow_ids[i + 1]
247        } else {
248            0u32
249        };
250        let len = chunks[i].len() as u32;
251        let buf = overflow_pages[i].as_bytes_mut();
252        reddb_file::encode_native_metadata_overflow_continuation_header(
253            &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
254            reddb_file::NativeMetadataOverflowContinuationHeader {
255                next_overflow_page_id: next,
256                chunk_bytes: len,
257            },
258        )
259        .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
260        buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
261    }
262    for (idx, page) in overflow_pages.into_iter().enumerate() {
263        let id = overflow_ids[idx];
264        pager.write_page(id, page)?;
265    }
266
267    // Mirror the inner format_version for debug-friendly hex dumps.
268    let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
269        .ok()
270        .flatten()
271        .map_or(0, |header| header.format_version);
272
273    let buf = page1.as_bytes_mut();
274    reddb_file::encode_native_metadata_overflow_header(
275        &mut buf[cs..cs + META_V3_PAGE1_HEADER],
276        reddb_file::NativeMetadataOverflowHeader {
277            format_version,
278            total_payload_bytes: meta_data.len() as u32,
279            next_overflow_page_id: overflow_ids[0],
280        },
281    )
282    .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
283    buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
284        .copy_from_slice(first_chunk);
285
286    Ok(page1)
287}
288
289fn storage_integrity(
290    zone: &str,
291    id: impl Into<String>,
292    collection: Option<String>,
293    detail: impl Into<String>,
294) -> StoreError {
295    StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
296        zone, id, collection, detail,
297    ))
298}
299
300fn page_integrity(
301    page_id: u32,
302    collection: Option<String>,
303    detail: impl Into<String>,
304) -> StoreError {
305    storage_integrity("page", page_id.to_string(), collection, detail)
306}
307
308/// Assemble the full metadata payload from the internal manifest zone: page 1
309/// plus its overflow chain when the native overflow wrapper is present. Returns
310/// the bytes that the metadata parser would see starting from the content
311/// offset of page 1. Single-page metadata returns the raw page content
312/// (including trailing zero-pad), so the legacy parser sees the same bytes it
313/// always saw.
314///
315/// The zone is rooted by the superblock and checksummed per ADR 0074 §2: a
316/// failing checksum surfaces the didactic manifest-zone integrity error
317/// directly — there is no sidecar copy to fall back to since ADR 0038 §4
318/// phase 1 retired the shadows.
319fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
320    let cs = crate::storage::engine::HEADER_SIZE;
321    let meta_page = pager.read_manifest_page().map_err(|err| {
322        storage_integrity(
323            "manifest",
324            "manifest",
325            None,
326            format!("internal manifest zone read failed ({err})"),
327        )
328    })?;
329    let bytes = meta_page.as_bytes();
330    if bytes.len() < cs + 4 {
331        return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
332    }
333    let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
334        .map_err(|err| page_integrity(1, None, err.to_string()))?
335    {
336        Some(header) => header,
337        None => {
338            return Ok(Some(bytes[cs..].to_vec()));
339        }
340    };
341    if bytes.len() < cs + META_V3_PAGE1_HEADER {
342        return Err(page_integrity(
343            1,
344            None,
345            "metadata overflow header is truncated",
346        ));
347    }
348    let total = header.total_payload_bytes as usize;
349    let mut next = header.next_overflow_page_id;
350    let mut payload: Vec<u8> = Vec::with_capacity(total);
351    let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
352    payload.extend_from_slice(
353        &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
354    );
355    let mut seen = std::collections::HashSet::new();
356    while next != 0 && payload.len() < total {
357        if !seen.insert(next)
358            || seen.len()
359                >= pager
360                    .page_count()
361                    .map_err(|err| page_integrity(next, None, err.to_string()))?
362                    as usize
363        {
364            return Err(page_integrity(next, None, "cyclic metadata overflow chain"));
365        }
366        let ov = pager
367            .read_page(next)
368            .map_err(|err| page_integrity(next, None, err.to_string()))?;
369        if ov
370            .page_type()
371            .map_err(|err| page_integrity(next, None, err.to_string()))?
372            != crate::storage::engine::PageType::Overflow
373        {
374            return Err(page_integrity(
375                next,
376                None,
377                "invalid metadata overflow page type",
378            ));
379        }
380        let ob = ov.as_bytes();
381        if ob.len() < cs + META_V3_OVERFLOW_HEADER {
382            return Err(page_integrity(
383                next,
384                None,
385                "metadata overflow continuation is truncated",
386            ));
387        }
388        let continuation =
389            reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
390                .map_err(|err| page_integrity(next, None, err.to_string()))?;
391        let nn = continuation.next_overflow_page_id;
392        let len = continuation.chunk_bytes as usize;
393        let remaining = total - payload.len();
394        let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
395        payload.extend_from_slice(
396            &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
397        );
398        next = nn;
399    }
400    if payload.len() != total {
401        return Err(page_integrity(
402            1,
403            None,
404            "metadata overflow chain is incomplete",
405        ));
406    }
407    Ok(Some(payload))
408}
409
410impl UnifiedStore {
411    /// Compare and encode one borrowed row at a time. Begin a new tree only at
412    /// the first difference, copying its already-verified prefix in bounded
413    /// batches. Unchanged trees require no replacement allocation.
414    fn checkpoint_tree(
415        &self,
416        pager: &Arc<Pager>,
417        manager: &SegmentManager,
418        previous: Option<&Arc<BTree>>,
419        mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
420    ) -> Result<Arc<BTree>, StoreError> {
421        if let Some((base, overlay)) = manager.backing_overlay(&mut encode) {
422            use reddb_engine::storage::collection_read::{CollectionRead, MergedCollection};
423            let merged = MergedCollection::new(base.as_ref(), overlay);
424            let mut cursor = merged.cursor()?;
425            let mut builder = CheckpointTreeBuilder::new(pager);
426            while let Some(record) = cursor.next_record(
427                usize::try_from(self.config.resident_memory_bytes_max.unwrap_or(u64::MAX))
428                    .unwrap_or(usize::MAX),
429            )? {
430                builder.push((record.id.raw().to_be_bytes().to_vec(), record.bytes))?;
431            }
432            builder.flush()?;
433            return Ok(builder.tree);
434        }
435        let mut cursor = previous
436            .map(|tree| tree.cursor_first())
437            .transpose()
438            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
439        let mut matched = 0;
440        let mut builder = None;
441        let mut failure = None;
442        manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
443            let Some(value) = encode(entity, metadata) else {
444                return true;
445            };
446            let record = (entity.id.raw().to_be_bytes().to_vec(), value);
447            let result = (|| {
448                if builder.is_none() {
449                    if let Some(cursor) = cursor.as_mut() {
450                        if cursor
451                            .next()
452                            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
453                            .as_ref()
454                            == Some(&record)
455                        {
456                            matched += 1;
457                            return Ok(());
458                        }
459                    }
460                    let mut replacement = CheckpointTreeBuilder::new(pager);
461                    if let Some(previous) = previous {
462                        replacement.copy_prefix(previous, matched)?;
463                    }
464                    builder = Some(replacement);
465                }
466                builder
467                    .as_mut()
468                    .expect("replacement initialized")
469                    .push(record)
470            })();
471            if let Err(error) = result {
472                failure = Some(error);
473                return false;
474            }
475            true
476        });
477        if let Some(error) = failure {
478            return Err(error);
479        }
480        if builder.is_none() {
481            if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
482                if cursor
483                    .next()
484                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
485                    .is_none()
486                {
487                    return Ok(Arc::clone(previous));
488                }
489            }
490            let mut replacement = CheckpointTreeBuilder::new(pager);
491            if let Some(previous) = previous {
492                replacement.copy_prefix(previous, matched)?;
493            }
494            builder = Some(replacement);
495        }
496        let mut builder = builder.expect("replacement or reused tree");
497        builder.flush()?;
498        Ok(builder.tree)
499    }
500
501    /// Publication first, reuse second. The immediately preceding generation
502    /// stays in the durable ledger for one more publication. Cursors and cloned
503    /// tree handles defer retirement further without holding the writer fence.
504    fn publish_reclaimable_trees(
505        &self,
506        pager: &Pager,
507        metadata: &mut Vec<u8>,
508        live: &HashMap<String, Arc<BTree>>,
509        keys: &HashMap<String, PublishedKeyIndex>,
510    ) -> Result<(), StoreError> {
511        let mut next_xid_summary = None;
512        let mut retired = self.retired_btrees.lock();
513        let mut roots = Vec::new();
514        let mut reclaim = Vec::new();
515        let mut remove = std::collections::HashSet::new();
516        for (index, entry) in retired.iter().enumerate() {
517            if live
518                .values()
519                .chain(keys.values().flat_map(PublishedKeyIndex::trees))
520                .any(|tree| Arc::ptr_eq(tree, &entry.tree))
521            {
522                remove.insert(index);
523                continue;
524            }
525            if entry.published
526                && Arc::strong_count(&entry.tree) == 1
527                && !entry.tree.has_retirement_readers()
528            {
529                // Deliberately unsafe experimental control: keep the old
530                // root in the catalog while releasing its pages. Never enable
531                // this outside the isolated recovery test.
532                #[cfg(test)]
533                if std::env::var_os("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT").is_some() {
534                    roots.push(entry.tree.root_page_id());
535                }
536                reclaim.extend(
537                    entry
538                        .tree
539                        .retired_pages()
540                        .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
541                );
542                remove.insert(index);
543            } else if entry.tree.root_page_id() != 0 {
544                roots.push(entry.tree.root_page_id());
545            }
546        }
547        roots.sort_unstable();
548        roots.dedup();
549        reddb_file::encode_native_paged_retired_trees(metadata, &roots)
550            .map_err(|e| StoreError::Serialization(e.to_string()))?;
551        let (pending_pages, published_pages) = pager
552            .deferred_reclamation_pages()
553            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
554        let readers = live
555            .values()
556            .chain(keys.values().flat_map(PublishedKeyIndex::trees))
557            .any(|tree| tree.has_retirement_readers())
558            || retired
559                .iter()
560                .any(|entry| entry.tree.has_retirement_readers());
561        let released_pages = if readers { Vec::new() } else { published_pages };
562        let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
563        let retained_pages: Vec<_> = pending_pages
564            .into_iter()
565            .filter(|id| !released.contains(id))
566            .collect();
567        reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
568            .map_err(|e| StoreError::Serialization(e.to_string()))?;
569        let active_epoch = self.ownership_active_epoch.load(Ordering::Acquire);
570        let catalog_epoch = if active_epoch == 0 {
571            self.ownership_catalog_epoch.load(Ordering::Acquire)
572        } else {
573            active_epoch
574        };
575        if catalog_epoch != 0 {
576            reddb_file::native_store::encode_native_paged_ownership_epoch(metadata, catalog_epoch)
577                .map_err(|e| StoreError::Serialization(e.to_string()))?;
578        }
579        if self.config.resident_memory_bytes_max.is_some() {
580            use reddb_file::native_resume::{
581                encode_catalog_resume, CatalogResume, CollectionResume,
582            };
583            let mut states = std::collections::BTreeMap::new();
584            let mut entity_id_next = self.resident.next_entity_id.load(Ordering::SeqCst);
585            let collections = self.resident.collections.read();
586            let metadata_expiry_prepared = collections
587                .values()
588                .all(|manager| manager.metadata_expiry_projection_ready());
589            let publishes_rows = {
590                let summary = self.recovery_xid_summary.read();
591                live.iter().any(|(name, tree)| {
592                    let root = tree.root_page_id();
593                    root != 0
594                        && summary.as_ref().and_then(|state| state.roots.get(name)) != Some(&root)
595                })
596            };
597            let xid_high_water_mark =
598                self.base_xid_high_water_mark(&collections).map(|mut high| {
599                    // Ordinary metadata flushes retain the old immutable-base
600                    // bound; WAL overlays are observed at boot. Only replacement
601                    // data roots require a borrowed visit over resident headers.
602                    // The publication fence excludes concurrent header mutations.
603                    if publishes_rows {
604                        for manager in collections.values() {
605                            manager.for_each_entity(|entity| {
606                                high = high.max(entity.xmin).max(entity.xmax);
607                                true
608                            });
609                        }
610                    }
611                    if let Some(snapshots) = self.snapshot_manager.get() {
612                        high = high.max(snapshots.peek_next_xid().saturating_sub(1));
613                    }
614                    high
615                });
616            for (name, manager) in collections.iter() {
617                let tree = live.get(name);
618                let (entity_next, row_id_next) = manager.identity_counters();
619                entity_id_next = entity_id_next.max(entity_next);
620                let root_page = tree.map_or(0, |tree| tree.root_page_id());
621                let entities = if root_page == 0 {
622                    0
623                } else if let Some(count) = manager.backing_count_at_root(root_page) {
624                    count
625                } else {
626                    // Changed projections may exclude pending/aborted rows. Count
627                    // published physical keys, not the live manager's cardinality.
628                    let mut cursor = tree
629                        .expect("invariant: nonzero root has a tree")
630                        .cursor_first()
631                        .map_err(|error| {
632                            StoreError::Io(std::io::Error::other(error.to_string()))
633                        })?;
634                    let mut count = 0_u64;
635                    while cursor
636                        .skip_next()
637                        .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
638                    {
639                        count += 1;
640                    }
641                    count
642                };
643                let metadata_expiries = if metadata_expiry_prepared {
644                    let mut expiries = Vec::new();
645                    if let Some(tree) = tree {
646                        for (deadline_ms, entity_id) in manager.metadata_expiry_entries() {
647                            if tree
648                                .get(&entity_id.raw().to_be_bytes())
649                                .map_err(|error| {
650                                    StoreError::Io(std::io::Error::other(error.to_string()))
651                                })?
652                                .is_some()
653                            {
654                                expiries.push(reddb_file::native_resume::MetadataExpiryResume {
655                                    deadline_ms,
656                                    entity_id: entity_id.raw(),
657                                });
658                            }
659                        }
660                    }
661                    expiries
662                } else {
663                    Vec::new()
664                };
665                states.insert(
666                    name.clone(),
667                    CollectionResume {
668                        root_page,
669                        entities,
670                        entity_id_next: entity_next,
671                        row_id_next,
672                        sequence_id_next: manager.sequence_id_next(),
673                        key_index_root: tree.and_then(|tree| {
674                            keys.get(name)
675                                .filter(|index| {
676                                    index.indexed && index.source.ptr_eq(&Arc::downgrade(tree))
677                                })
678                                .map(|index| index.tree.root_page_id())
679                        }),
680                        secondary_indices: tree
681                            .and_then(|tree| {
682                                keys.get(name)
683                                    .filter(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
684                            })
685                            .map(|index| {
686                                index
687                                    .secondary
688                                    .iter()
689                                    .map(|(name, index)| (name.clone(), index.resume()))
690                                    .collect()
691                            })
692                            .unwrap_or_default(),
693                        metadata_expiries,
694                    },
695                );
696            }
697            let resume = CatalogResume {
698                entity_id_next,
699                xid_high_water_mark,
700                metadata_expiry_prepared,
701                collections: states,
702            };
703            encode_catalog_resume(metadata, &resume)
704                .map_err(|error| StoreError::Serialization(error.to_string()))?;
705            next_xid_summary = xid_high_water_mark.map(|high_water_mark| {
706                super::recovery_xid::RecoveryXidSummary {
707                    high_water_mark,
708                    roots: resume
709                        .collections
710                        .into_iter()
711                        .map(|(name, state)| (name, state.root_page))
712                        .collect(),
713                }
714            });
715        }
716        // Record the exact pages before the catalog can forget their owners.
717        // Recovery resolves this intent against the durable catalog epoch.
718        let mut retirement_intent = reclaim.clone();
719        retirement_intent.extend_from_slice(&released_pages);
720        if !retirement_intent.is_empty() {
721            pager
722                .record_retirement_intent(&retirement_intent)
723                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
724        }
725        // Do not mutate retirement state or the free list before both the new
726        // children and the manifest that forgets old roots have been synced.
727        publish_paged_metadata(pager, metadata)
728            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
729        *self.recovery_xid_summary.write() = next_xid_summary;
730        self.ownership_catalog_epoch
731            .store(catalog_epoch, Ordering::Release);
732        #[cfg(test)]
733        if !reclaim.is_empty()
734            && std::env::var_os("REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE").is_some()
735        {
736            eprintln!(
737                "experimental exit before retired free pages={}",
738                reclaim.len()
739            );
740            std::process::exit(184);
741        }
742        let mut index = 0;
743        retired.retain_mut(|entry| {
744            let keep = !remove.contains(&index);
745            entry.published = true;
746            index += 1;
747            keep
748        });
749        if !reclaim.is_empty() {
750            pager
751                .free_retired_pages(&reclaim)
752                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
753            #[cfg(test)]
754            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE").is_some() {
755                eprintln!(
756                    "experimental exit after retired enqueue pages={}",
757                    reclaim.len()
758                );
759                std::process::exit(186);
760            }
761            pager
762                .sync()
763                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
764            #[cfg(test)]
765            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE").is_some() {
766                eprintln!(
767                    "experimental exit after retired free pages={}",
768                    reclaim.len()
769                );
770                std::process::exit(185);
771            }
772        }
773        pager
774            .complete_deferred_publication(&retained_pages, &released_pages)
775            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
776        if !released_pages.is_empty() {
777            pager
778                .sync()
779                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
780        }
781        Ok(())
782    }
783
784    pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
785        let mut retired = self.retired_btrees.lock();
786        if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
787        {
788            retired.push(RetiredBTree {
789                tree,
790                published: false,
791            });
792        }
793    }
794
795    /// A completed inline insert already made this tree match its manager.
796    /// Advance the reuse proof only for that single, fenced mutation. Any
797    /// untracked edit or segment publication keeps the full comparison path.
798    pub(crate) fn note_paged_insert(
799        &self,
800        collection: &str,
801        manager: &Arc<SegmentManager>,
802        before: &[(u64, u64)],
803        id: EntityId,
804    ) {
805        let after = manager.checkpoint_revisions();
806        if before.len() != after.len() {
807            return;
808        }
809        let mut changed = 0;
810        for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
811            if old_id != new_id {
812                return;
813            }
814            if old_revision != new_revision {
815                if old_revision.checked_add(1) != Some(*new_revision) {
816                    return;
817                }
818                changed += 1;
819            }
820        }
821        if changed != 1 {
822            return;
823        }
824        let mut versions = self.checkpoint_versions.write();
825        let Some(previous) = versions.get_mut(collection) else {
826            return;
827        };
828        if previous.manager.ptr_eq(&Arc::downgrade(manager))
829            && previous.segments == before
830            && previous.format_version == self.format_version()
831            && !previous.pending_vault.contains(&id)
832            && !previous.excluded_xids.contains(&0)
833        {
834            previous.segments = after;
835        }
836    }
837
838    /// Successful physical deletes already changed both the manager and tree.
839    /// Reuse remains valid only when their exact revision delta accounts for
840    /// this batch, with no intervening edits or segment replacement.
841    pub(crate) fn note_paged_delete(
842        &self,
843        collection: &str,
844        manager: &Arc<SegmentManager>,
845        before: &[(u64, u64)],
846        deleted: &[EntityId],
847    ) {
848        let after = manager.checkpoint_revisions();
849        if before.len() != after.len() {
850            return;
851        }
852        let mut changes = 0_u64;
853        for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
854            if old_id != new_id {
855                return;
856            }
857            let Some(delta) = new_revision.checked_sub(*old_revision) else {
858                return;
859            };
860            let Some(total) = changes.checked_add(delta) else {
861                return;
862            };
863            changes = total;
864        }
865        let Ok(expected) = u64::try_from(deleted.len()) else {
866            return;
867        };
868        if changes != expected {
869            return;
870        }
871        let mut versions = self.checkpoint_versions.write();
872        let Some(previous) = versions.get_mut(collection) else {
873            return;
874        };
875        if !previous.manager.ptr_eq(&Arc::downgrade(manager))
876            || previous.format_version != self.format_version()
877            || deleted.iter().any(|id| previous.pending_vault.contains(id))
878            || previous.excluded_xids.contains(&0)
879        {
880            return;
881        }
882        if previous.segments != before {
883            let Some(deferred) = previous.deferred_rows.as_ref() else {
884                return;
885            };
886            let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
887                return;
888            };
889            let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
890            let pending: std::collections::BTreeSet<_> = deferred
891                .iter()
892                .copied()
893                .chain(changed)
894                .filter(|id| !removed.contains(id))
895                .collect();
896            if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
897                return;
898            }
899            // GC already applied its deletes to the tree. Carry earlier dirty
900            // IDs forward instead of spending their journal on these deletes.
901            // Sparse projection still requires exact WAL proof for every
902            // differing row, so an unlogged manager edit cannot be concealed.
903            previous.has_unprojected_rows = !pending.is_empty();
904            previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
905        }
906        previous.segments = after;
907    }
908
909    pub(crate) fn mark_paged_registry_dirty(&self) {
910        self.resident.mark_paged_registry_dirty()
911    }
912
913    /// Get (or lazily create) the per-collection B-tree under a *read*
914    /// lock whenever possible. Returns a cloned `Arc<BTree>` so callers
915    /// can mutate the tree without holding the outer map's RwLock —
916    /// previously every insert serialised on `btree_indices.write()`,
917    /// costing ~60% of the concurrent-insert throughput ceiling.
918    pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
919        let collection = Self::canonical_collection(collection);
920        let pager = self.pager.as_ref()?;
921        if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
922            return Some(btree);
923        }
924        let mut write = self.btree_indices.write();
925        let btree = write
926            .entry(collection.to_string())
927            .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
928            .clone();
929        Some(btree)
930    }
931
932    pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
933        if self.is_read_only() {
934            return Ok(());
935        }
936        let Some(pager) = &self.pager else {
937            return Ok(());
938        };
939
940        if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
941            self.flush_paged_registry()?;
942            self.resident
943                .paged_registry_dirty
944                .store(false, Ordering::Release);
945            return Ok(());
946        }
947
948        pager
949            .flush()
950            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
951    }
952
953    pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
954        if self.is_read_only() {
955            return Ok(());
956        }
957        let _vault_guard = self.vault_publication_lock.lock();
958        let Some(pager) = &self.pager else {
959            return Ok(());
960        };
961
962        match pager.read_page(1) {
963            Ok(_) => {}
964            Err(PagerError::PageNotFound(_)) => {
965                let meta_page = pager
966                    .allocate_page(crate::storage::engine::PageType::Header)
967                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
968                pager
969                    .write_page(meta_page.page_id(), meta_page)
970                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
971            }
972            Err(e) => {
973                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
974            }
975        }
976
977        let format_version = STORE_VERSION_V11;
978        self.set_format_version(format_version);
979
980        let collections = self.resident.collections.read();
981        let btree_indices = self.btree_indices.read();
982        let mut collection_roots = Vec::with_capacity(collections.len());
983        for (name, _) in collections.iter() {
984            let root_page = btree_indices
985                .get(name)
986                .map_or(0, |btree| btree.root_page_id());
987            collection_roots.push((name.clone(), root_page));
988        }
989        drop(btree_indices);
990        drop(collections);
991
992        let mut meta_data = Vec::with_capacity(4096);
993        reddb_file::encode_native_paged_metadata_header(
994            &mut meta_data,
995            reddb_file::NativePagedMetadataHeader {
996                format_version,
997                collection_count: collection_roots.len() as u32,
998            },
999        );
1000        for (name, root_page) in &collection_roots {
1001            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1002        }
1003
1004        let cross_refs = self.resident.cross_refs.read();
1005        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1006        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1007        for (source_id, refs) in cross_refs.iter() {
1008            for (target_id, ref_type, collection) in refs {
1009                reddb_file::encode_native_paged_cross_ref(
1010                    &mut meta_data,
1011                    source_id.raw(),
1012                    target_id.raw(),
1013                    ref_type.to_byte(),
1014                    reddb_engine::storage::system_collection_names::canonical_or_self(collection),
1015                );
1016            }
1017        }
1018
1019        self.publish_reclaimable_trees(
1020            pager,
1021            &mut meta_data,
1022            &self.btree_indices.read(),
1023            &self.key_indices.read(),
1024        )?;
1025
1026        Ok(())
1027    }
1028
1029    /// Get a reference to the underlying pager (if in paged mode).
1030    pub fn pager(&self) -> Option<&Arc<Pager>> {
1031        self.pager.as_ref()
1032    }
1033
1034    pub(crate) fn is_read_only(&self) -> bool {
1035        self.pager
1036            .as_ref()
1037            .is_some_and(|pager| pager.is_read_only())
1038    }
1039
1040    /// Bytes of page-cache memory currently occupied by resident pages
1041    /// (ADR 0073 §2). Slots are fixed size, so occupancy times the page size
1042    /// *is* the footprint; a store without a pager occupies none.
1043    pub fn page_cache_bytes_in_use(&self) -> u64 {
1044        self.pager.as_ref().map_or(0, |pager| {
1045            pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
1046        })
1047    }
1048
1049    /// Bytes of WAL buffer memory held by this store's group-commit
1050    /// coordinator: queued records plus the writer's fixed append buffer.
1051    pub fn wal_buffer_bytes_in_use(&self) -> u64 {
1052        self.commit
1053            .as_ref()
1054            .map_or(0, |commit| commit.buffered_bytes())
1055    }
1056
1057    /// Approximate resident bytes across every collection's segment arena
1058    /// (ADR 0073 §2). Same number `stats().total_memory_bytes` reports, without
1059    /// building the per-collection stats map a sampler would throw away.
1060    pub fn segment_memory_bytes(&self) -> u64 {
1061        let managers: Vec<Arc<SegmentManager>> = self
1062            .resident
1063            .collections
1064            .read()
1065            .values()
1066            .map(Arc::clone)
1067            .collect();
1068
1069        managers
1070            .iter()
1071            .map(|manager| manager.resident_bytes())
1072            .fold(0, u64::saturating_add)
1073    }
1074
1075    /// Borrow the immutable store configuration. Runtime hooks (e.g. the
1076    /// `auto_index_id` first-insert hook in `MutationEngine`) read knobs
1077    /// off this struct without going through the legacy global config tree.
1078    pub fn config(&self) -> &UnifiedStoreConfig {
1079        &self.config
1080    }
1081
1082    pub fn with_config(config: UnifiedStoreConfig) -> Self {
1083        Self {
1084            config,
1085            resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1086            format_version: AtomicU32::new(STORE_VERSION_V11),
1087            pager: None,
1088            db_path: None,
1089            operational_manifest: parking_lot::Mutex::new(None),
1090            btree_indices: RwLock::new(HashMap::new()),
1091            key_indices: RwLock::new(HashMap::new()),
1092            secondary_index_specs: RwLock::new(HashMap::new()),
1093            count_builds: RwLock::new(HashMap::new()),
1094            count_projection_seeds: parking_lot::Mutex::new(HashMap::new()),
1095            queue_dedup_cursors: parking_lot::Mutex::new(HashMap::new()),
1096            recovery_xid_summary: RwLock::new(None),
1097            checkpoint_versions: RwLock::new(HashMap::new()),
1098            checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1099            retired_btrees: parking_lot::Mutex::new(Vec::new()),
1100            ownership_catalog_epoch: AtomicU64::new(0),
1101            ownership_active_epoch: AtomicU64::new(0),
1102            detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1103            commit: None,
1104            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1105            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1106            result_cache_epoch: Arc::new(result_cache_epoch::ResultCacheEpochState::default()),
1107            snapshot_manager: std::sync::OnceLock::new(),
1108            embedded_commit_session: std::sync::OnceLock::new(),
1109            #[cfg(test)]
1110            test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1111            #[cfg(test)]
1112            test_versioned_update_after_old: parking_lot::Mutex::new(None),
1113            #[cfg(test)]
1114            test_fail_versioned_update_phase: std::sync::atomic::AtomicU8::new(0),
1115            #[cfg(test)]
1116            test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1117            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1118            row_publications: Default::default(),
1119            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1120        }
1121    }
1122
1123    /// Open or create a page-based database
1124    ///
1125    /// This uses the page engine for ACID durability with B-tree indices.
1126    /// The database file uses 16 KiB pages with checksums and efficient caching.
1127    ///
1128    /// # Arguments
1129    ///
1130    /// * `path` - Path to the database file (e.g., "data.rdb")
1131    ///
1132    /// # Example
1133    ///
1134    /// ```rust,ignore
1135    /// let store = UnifiedStore::open("security.rdb")?;
1136    /// store.create_collection("hosts")?;
1137    /// // ... operations ...
1138    /// store.persist()?; // Flush to disk
1139    /// ```
1140    pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
1141        Self::open_with_config(path, UnifiedStoreConfig::default())
1142    }
1143
1144    pub fn open_with_config(
1145        path: impl AsRef<Path>,
1146        config: UnifiedStoreConfig,
1147    ) -> Result<Self, StoreError> {
1148        Self::open_with_config_for_recovery(path, config, false, None, None)
1149    }
1150
1151    /// Recover a native database into a private read view. No WAL writer,
1152    /// checkpoint, manifest publication or allocator reclamation is attached.
1153    pub fn open_read_only_with_config(
1154        path: impl AsRef<Path>,
1155        mut config: UnifiedStoreConfig,
1156    ) -> Result<Self, StoreError> {
1157        let path = path.as_ref();
1158        config.embedded_wal_path = None;
1159        let pager = Pager::open(
1160            path,
1161            PagerConfig {
1162                read_only: true,
1163                create: false,
1164                disk_residency: config.resident_memory_bytes_max.is_some(),
1165                cache_size: config
1166                    .page_cache_slots
1167                    .unwrap_or_else(|| PagerConfig::default().cache_size),
1168                ..PagerConfig::default()
1169            },
1170        )
1171        .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1172        pager
1173            .enable_deferred_reclamation()
1174            .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1175        let mut store = Self::with_config(config);
1176        store.pager = Some(Arc::new(pager));
1177        store.db_path = Some(path.to_path_buf());
1178        let wal_path = reddb_file::layout::unified_wal_path(path);
1179        StoreCommitCoordinator::restore_checkpoint_page_images_from_path(&store, &wal_path)
1180            .map_err(StoreError::Io)?;
1181        let catalog = store.load_from_pages(false, None)?;
1182        StoreCommitCoordinator::replay_read_only(&store, &wal_path).map_err(StoreError::Io)?;
1183        store.load_result_cache_epoch()?;
1184        let pending_drops = store.legacy_pending_collection_drops()?.unwrap_or_default();
1185        for name in pending_drops {
1186            if !catalog
1187                .collisions
1188                .iter()
1189                .any(|collision| collision.loser == name)
1190                && store.get_collection(&name).is_some()
1191            {
1192                store.drop_collection_in_memory(&name)?;
1193            }
1194        }
1195        Ok(store)
1196    }
1197
1198    /// Only for an isolated, operator-selected recovery copy. The caller must
1199    /// first validate every WAL action addressing the discarded identity.
1200    pub(crate) fn open_with_config_for_recovery(
1201        path: impl AsRef<Path>,
1202        config: UnifiedStoreConfig,
1203        retain_canonical_system_config: bool,
1204        boot_wal: Option<&crate::storage::compact::boot_system_wal::BootSystemWalPlan>,
1205        catalog_plan: Option<&crate::storage::compact::catalog_aliases::CanonicalCatalogPlan>,
1206    ) -> Result<Self, StoreError> {
1207        let path = path.as_ref();
1208        let mut pager_config = PagerConfig {
1209            disk_residency: config.resident_memory_bytes_max.is_some(),
1210            ..PagerConfig::default()
1211        };
1212        // ADR 0073 §2 — the page cache is pre-sized from its budget share.
1213        // The pager's own `cache_size` default survives only for direct
1214        // library callers who never resolved a budget.
1215        if let Some(slots) = config.page_cache_slots {
1216            pager_config.cache_size = slots;
1217        }
1218        // Tunables via env — experimental, used by the benchmark harness
1219        // to compare durability profiles head-to-head with Postgres.
1220        // REDDB_DOUBLE_WRITE=0 requests skipping the double-write buffer,
1221        // which otherwise adds two fsyncs per pager flush (one on DWB, one
1222        // on the main file). The pager honors this only when the actual
1223        // data file is proven to live on a CoW filesystem with atomic page
1224        // writes; otherwise it fails closed and keeps DWB enabled.
1225        if matches!(
1226            std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
1227            Some("0") | Some("false") | Some("off")
1228        ) {
1229            pager_config.double_write = false;
1230        }
1231        let pager_open_started = std::time::Instant::now();
1232        let pager = Pager::open(path, pager_config)
1233            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1234        pager
1235            .enable_deferred_reclamation()
1236            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1237        trace_startup_phase(path, "paged_pager_open", pager_open_started);
1238
1239        if config.resident_memory_bytes_max.is_none()
1240            && reddb_file::file_format::page_format_is_disk_resident(
1241                pager
1242                    .header()
1243                    .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1244                    .version,
1245            )
1246        {
1247            return Err(StoreError::Io(std::io::Error::new(std::io::ErrorKind::Unsupported,
1248                "this database was prepared for disk residency; reopen with the disk-resident storage profile")));
1249        }
1250
1251        let wal_path = reddb_file::layout::unified_wal_path(path);
1252        // Explicit transactions and WAL-only vault versions need committed
1253        // batches in every durability mode, including Strict's inline fsync.
1254        let commit = Some(Arc::new(
1255            StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
1256                .map_err(StoreError::Io)?,
1257        ));
1258
1259        let store = Self {
1260            config,
1261            resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1262            format_version: AtomicU32::new(STORE_VERSION_V11),
1263            pager: Some(Arc::new(pager)),
1264            db_path: Some(path.to_path_buf()),
1265            operational_manifest: parking_lot::Mutex::new(None),
1266            btree_indices: RwLock::new(HashMap::new()),
1267            key_indices: RwLock::new(HashMap::new()),
1268            secondary_index_specs: RwLock::new(HashMap::new()),
1269            count_builds: RwLock::new(HashMap::new()),
1270            count_projection_seeds: parking_lot::Mutex::new(HashMap::new()),
1271            queue_dedup_cursors: parking_lot::Mutex::new(HashMap::new()),
1272            recovery_xid_summary: RwLock::new(None),
1273            checkpoint_versions: RwLock::new(HashMap::new()),
1274            checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1275            retired_btrees: parking_lot::Mutex::new(Vec::new()),
1276            ownership_catalog_epoch: AtomicU64::new(0),
1277            ownership_active_epoch: AtomicU64::new(0),
1278            detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1279            commit,
1280            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1281            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1282            result_cache_epoch: Arc::new(result_cache_epoch::ResultCacheEpochState::default()),
1283            snapshot_manager: std::sync::OnceLock::new(),
1284            embedded_commit_session: std::sync::OnceLock::new(),
1285            #[cfg(test)]
1286            test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1287            #[cfg(test)]
1288            test_versioned_update_after_old: parking_lot::Mutex::new(None),
1289            #[cfg(test)]
1290            test_fail_versioned_update_phase: std::sync::atomic::AtomicU8::new(0),
1291            #[cfg(test)]
1292            test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1293            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1294            row_publications: Default::default(),
1295            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1296        };
1297
1298        // An interrupted structural edit may have reached home pages without
1299        // publishing its catalog. Restore those pages before following roots.
1300        let checkpoint_recovery_started = std::time::Instant::now();
1301        if let Some(commit) = &store.commit {
1302            commit
1303                .restore_checkpoint_page_images(&store)
1304                .map_err(StoreError::Io)?;
1305        }
1306        trace_startup_phase(
1307            path,
1308            "paged_checkpoint_recovery",
1309            checkpoint_recovery_started,
1310        );
1311        // Load existing data from pages if database exists
1312        let page_load_started = std::time::Instant::now();
1313        let catalog_load = store.load_from_pages(retain_canonical_system_config, catalog_plan)?;
1314        trace_startup_phase(path, "paged_catalog_and_roots", page_load_started);
1315        let wal_replay_started = std::time::Instant::now();
1316        if let Some(commit) = &store.commit {
1317            commit
1318                .replay_into(&store, boot_wal)
1319                .map_err(StoreError::Io)?;
1320        }
1321        store.load_result_cache_epoch()?;
1322        trace_startup_phase(path, "paged_wal_replay", wal_replay_started);
1323        if catalog_load.migrated {
1324            // Publish only the renamed catalog keys. Existing row, key and
1325            // secondary-index roots remain attached and are not rebuilt.
1326            store.mark_paged_registry_dirty();
1327            store.flush_paged_state()?;
1328        }
1329        let manifest_recovery_started = std::time::Instant::now();
1330        store.recover_operational_manifest(&catalog_load.collisions)?;
1331        if store.config.resident_memory_bytes_max.is_some() {
1332            let missing_key_index = {
1333                let trees = store.btree_indices.read();
1334                let keys = store.key_indices.read();
1335                trees.iter().any(|(name, tree)| {
1336                    !keys
1337                        .get(name)
1338                        .is_some_and(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
1339                })
1340            };
1341            if missing_key_index {
1342                store.persist()?;
1343            }
1344        }
1345        trace_startup_phase(
1346            path,
1347            "paged_manifest_and_derived_roots",
1348            manifest_recovery_started,
1349        );
1350
1351        Ok(store)
1352    }
1353
1354    /// Replay changes the authoritative managers first. Build replacement trees
1355    /// only for changed collections, leaving every published tree untouched until
1356    /// all replacement pages are durable. Retain the WAL for auxiliary recovery.
1357    pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
1358        let Some(pager) = &self.pager else {
1359            return Ok(());
1360        };
1361        let collections = self.resident.collections.read();
1362        let mut indices = self.btree_indices.write();
1363        let previous_versions = self.checkpoint_versions.read();
1364        let raw_replay =
1365            self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
1366        for (name, manager) in collections.iter() {
1367            let mut reusable = None;
1368            if raw_replay {
1369                if let (Some(previous), Some(tree)) =
1370                    (previous_versions.get(name), indices.get(name))
1371                {
1372                    let mut current = CheckpointVersion {
1373                        manager: Arc::downgrade(manager),
1374                        segments: manager.checkpoint_revisions(),
1375                        visibility_revision: 0,
1376                        untracked_visibility_revision: 0,
1377                        excluded_xids: Arc::default(),
1378                        pending_vault: Arc::default(),
1379                        format_version: self.format_version(),
1380                        deferred_rows: Some(Arc::default()),
1381                        has_unprojected_rows: false,
1382                    };
1383                    if previous.manager.ptr_eq(&current.manager)
1384                        && previous.format_version == current.format_version
1385                    {
1386                        if previous.segments == current.segments
1387                            && !previous.has_unprojected_rows
1388                            && checkpoint_visibility_matches(previous, &current)
1389                        {
1390                            reusable = Some(Arc::clone(tree));
1391                        } else {
1392                            // The replay ownership epoch protects in-place
1393                            // changes with the same undo/publication protocol
1394                            // used by normal sparse checkpoints. Missing WAL
1395                            // proof, old formats and exhausted journals retain
1396                            // the full comparison/rebuild path below.
1397                            reusable = self.sparse_checkpoint_tree(
1398                                name,
1399                                manager,
1400                                tree,
1401                                previous,
1402                                &mut current,
1403                            )?;
1404                        }
1405                    }
1406                }
1407            }
1408            let tree = match reusable {
1409                Some(tree) => tree,
1410                None => {
1411                    self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1412                        #[cfg(test)]
1413                        REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1414                        Some(Self::serialize_entity_record(
1415                            entity,
1416                            metadata.as_ref(),
1417                            self.format_version(),
1418                        ))
1419                    })?
1420                }
1421            };
1422            if indices
1423                .get(name)
1424                .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1425            {
1426                continue;
1427            }
1428            if let Some(previous) = indices.insert(name.clone(), tree) {
1429                self.retire_btree(previous);
1430            }
1431            self.mark_paged_registry_dirty();
1432            #[cfg(test)]
1433            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1434                if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1435                    pager.sync().map_err(|error| {
1436                        StoreError::Io(std::io::Error::other(error.to_string()))
1437                    })?;
1438                }
1439                eprintln!("experimental replay rebuild exit before manifest");
1440                std::process::exit(182);
1441            }
1442        }
1443        drop(previous_versions);
1444        drop(indices);
1445        drop(collections);
1446        self.flush_paged_state()?;
1447        #[cfg(test)]
1448        if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1449            eprintln!("experimental replay exit after manifest");
1450            std::process::exit(182);
1451        }
1452        // Opening has no concurrent writers. Replay has now durably reconciled
1453        // these raw trees; do not immediately rebuild them at the first flush.
1454        if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1455            let collections = self.resident.collections.read();
1456            let mut versions = self.checkpoint_versions.write();
1457            for (name, manager) in collections.iter() {
1458                versions.insert(
1459                    name.clone(),
1460                    CheckpointVersion {
1461                        manager: Arc::downgrade(manager),
1462                        segments: manager.checkpoint_revisions(),
1463                        visibility_revision: 0,
1464                        untracked_visibility_revision: 0,
1465                        excluded_xids: Arc::default(),
1466                        pending_vault: Arc::default(),
1467                        format_version: self.format_version(),
1468                        deferred_rows: Some(Arc::default()),
1469                        has_unprojected_rows: false,
1470                    },
1471                );
1472            }
1473        }
1474        Ok(())
1475    }
1476
1477    fn legacy_pending_collection_drops(&self) -> Result<Option<Vec<String>>, StoreError> {
1478        match self.try_get_config("red.storage.legacy_manifest_imported")? {
1479            Some(Value::Boolean(true)) => return Ok(None),
1480            None => {}
1481            Some(_) => {
1482                return Err(StoreError::Serialization(
1483                    "invalid legacy manifest import marker".into(),
1484                ))
1485            }
1486        }
1487        let Some(path) = &self.db_path else {
1488            return Ok(None);
1489        };
1490        self.operational_manifest_for_path(path)
1491            .read_only_pending_drops_if_present()
1492            .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))
1493    }
1494
1495    fn recover_operational_manifest(
1496        &self,
1497        collisions: &[SystemCollectionCollision],
1498    ) -> Result<(), StoreError> {
1499        let Some(pending) = self.legacy_pending_collection_drops()? else {
1500            return Ok(());
1501        };
1502        // Consume legacy drop intents once, without rewriting their manifest.
1503        // WAL replay restores both the drops and this marker after a crash.
1504        for name in pending {
1505            if !collisions.iter().any(|collision| collision.loser == name)
1506                && self.get_collection(&name).is_some()
1507            {
1508                self.drop_collection(&name)?;
1509            }
1510        }
1511        self.try_set_config_tree(
1512            "red.storage.legacy_manifest_imported",
1513            &crate::serde_json::Value::Bool(true),
1514        )?;
1515        Ok(())
1516    }
1517
1518    /// Read the commit boundary before following potentially interrupted trees.
1519    pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1520        let Some(pager) = &self.pager else {
1521            return Ok(0);
1522        };
1523        let Some(content) = read_meta_payload(pager)? else {
1524            return Ok(0);
1525        };
1526        reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1527            .map(|epoch| epoch.unwrap_or(0))
1528            .map_err(|error| StoreError::Serialization(error.to_string()))
1529    }
1530
1531    /// Read the B-tree indices and reconstruct collections from pages.
1532    fn load_from_pages(
1533        &self,
1534        retain_canonical_system_config: bool,
1535        catalog_plan: Option<&crate::storage::compact::catalog_aliases::CanonicalCatalogPlan>,
1536    ) -> Result<CatalogLoadOutcome, StoreError> {
1537        let pager = match &self.pager {
1538            Some(p) => p,
1539            None => return Ok(CatalogLoadOutcome::default()), // No pager, nothing to load
1540        };
1541
1542        // Get page count
1543        let page_count = pager.page_count().map_err(|e| {
1544            StoreError::Io(std::io::Error::other(format!(
1545                "failed to read page count: {}",
1546                e
1547            )))
1548        })?;
1549        if page_count <= 1 {
1550            // Empty database (only header page)
1551            return Ok(CatalogLoadOutcome::default());
1552        }
1553
1554        // Read metadata starting from page 1 (collections registry). The
1555        // helper transparently follows the `RDM3` overflow chain when the
1556        // metadata blob spans multiple pages and falls back to the legacy
1557        // `<data>-meta` shadow when page 1 itself is corrupted.
1558        if let Some(content_vec) = read_meta_payload(pager)? {
1559            let content: &[u8] = &content_vec;
1560            let file_version = pager
1561                .header()
1562                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1563                .version;
1564            let prepared = reddb_file::file_format::page_format_is_disk_resident(file_version);
1565            let resume = if prepared {
1566                reddb_file::native_resume::decode_catalog_resume(content)
1567                    .map_err(|error| StoreError::Serialization(error.to_string()))?
1568            } else {
1569                None
1570            };
1571            let mut outcome = CatalogLoadOutcome::default();
1572            let mut ignored_collections = std::collections::HashSet::new();
1573            if retain_canonical_system_config
1574                && !resume.as_ref().is_some_and(|resume| {
1575                    resume.collections.contains_key("red_config")
1576                        && resume.collections.contains_key("red.config")
1577                })
1578            {
1579                return Err(page_integrity(
1580                    1,
1581                    None,
1582                    "canonical config recovery requires both resume identities",
1583                ));
1584            }
1585            if let Some(resume) = &resume {
1586                for &(legacy, canonical) in
1587                    reddb_engine::storage::system_collection_names::LEGACY_NAMES
1588                {
1589                    let (Some(legacy_state), Some(canonical_state)) = (
1590                        resume.collections.get(legacy),
1591                        resume.collections.get(canonical),
1592                    ) else {
1593                        continue;
1594                    };
1595                    let merged_resume = merge_shared_alias_resume(legacy_state, canonical_state);
1596                    let shared_physical_state = merged_resume.is_some();
1597                    let resume_counter_drift = legacy_state != canonical_state;
1598                    let (survivor, loser) = match (
1599                        legacy_state.entities == 0,
1600                        canonical_state.entities == 0,
1601                    ) {
1602                        _ if retain_canonical_system_config && legacy == "red_config" => {
1603                            (canonical, legacy)
1604                        }
1605                        _ if catalog_plan.is_some_and(|plan| plan.retires(legacy, canonical)) => {
1606                            (canonical, legacy)
1607                        }
1608                        (false, true) => (legacy, canonical),
1609                        (true, false) | (true, true) => (canonical, legacy),
1610                        (false, false) if shared_physical_state => (canonical, legacy),
1611                        (false, false) => {
1612                            let (
1613                                data_root_match,
1614                                key_index_root_match,
1615                                secondary_index_state_match,
1616                            ) = shared_alias_resume_matches(legacy_state, canonical_state);
1617                            return Err(page_integrity(
1618                                1,
1619                                None,
1620                                format!(
1621                                    "catalog contains both populated legacy collection '{legacy}' and canonical collection '{canonical}' (data_root_match={data_root_match}, key_index_root_match={key_index_root_match}, secondary_index_state_match={secondary_index_state_match})"
1622                                ),
1623                            ));
1624                        }
1625                    };
1626                    ignored_collections.insert(loser.to_string());
1627                    outcome.collisions.push(SystemCollectionCollision {
1628                        survivor: survivor.to_string(),
1629                        loser: loser.to_string(),
1630                        canonical: canonical.to_string(),
1631                        shared_physical_state,
1632                        resume_counter_drift,
1633                        merged_resume,
1634                    });
1635                }
1636                let live_roots = resume
1637                    .collections
1638                    .iter()
1639                    .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1640                    .flat_map(|(_, state)| {
1641                        std::iter::once(state.root_page)
1642                            .chain(state.key_index_root)
1643                            .chain(
1644                                state
1645                                    .secondary_indices
1646                                    .values()
1647                                    .map(|index| index.root_page),
1648                            )
1649                    })
1650                    .filter(|root| *root != 0)
1651                    .collect::<std::collections::HashSet<_>>();
1652                for collision in &outcome.collisions {
1653                    if collision.shared_physical_state {
1654                        continue;
1655                    }
1656                    let loser_state = resume
1657                        .collections
1658                        .get(&collision.loser)
1659                        .expect("collision loser is present");
1660                    if std::iter::once(loser_state.root_page)
1661                        .chain(loser_state.key_index_root)
1662                        .chain(
1663                            loser_state
1664                                .secondary_indices
1665                                .values()
1666                                .map(|index| index.root_page),
1667                        )
1668                        .filter(|root| *root != 0)
1669                        .any(|root| live_roots.contains(&root))
1670                    {
1671                        return Err(page_integrity(
1672                            1,
1673                            None,
1674                            format!(
1675                                "empty collection '{}' aliases a live collection root",
1676                                collision.loser
1677                            ),
1678                        ));
1679                    }
1680                }
1681                reddb_engine::storage::system_collection_names::migration_plan(
1682                    resume
1683                        .collections
1684                        .keys()
1685                        .map(String::as_str)
1686                        .filter(|name| !ignored_collections.contains(*name)),
1687                )
1688                .map_err(|(legacy, canonical)| {
1689                    page_integrity(
1690                        1,
1691                        None,
1692                        format!(
1693                            "catalog contains both legacy collection '{legacy}' and canonical collection '{canonical}'"
1694                        ),
1695                    )
1696                })?;
1697                for state in resume.collections.values() {
1698                    if !state.secondary_indices.is_empty()
1699                        && file_version < reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
1700                    {
1701                        return Err(page_integrity(
1702                            1,
1703                            None,
1704                            "secondary roots require the file format fence",
1705                        ));
1706                    }
1707                    for root in std::iter::once(state.root_page)
1708                        .chain(state.key_index_root)
1709                        .chain(
1710                            state
1711                                .secondary_indices
1712                                .values()
1713                                .map(|index| index.root_page),
1714                        )
1715                    {
1716                        if root >= page_count {
1717                            return Err(page_integrity(
1718                                root,
1719                                None,
1720                                "published root exceeds the database page count",
1721                            ));
1722                        }
1723                    }
1724                }
1725                self.resident
1726                    .next_entity_id
1727                    .fetch_max(resume.entity_id_next, Ordering::SeqCst);
1728            }
1729            *self.recovery_xid_summary.write() = resume.as_ref().and_then(|resume| {
1730                resume.xid_high_water_mark.map(|high_water_mark| {
1731                    super::recovery_xid::RecoveryXidSummary {
1732                        high_water_mark,
1733                        roots: resume
1734                            .collections
1735                            .iter()
1736                            .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1737                            .map(|(name, state)| {
1738                                (
1739                                    reddb_engine::storage::system_collection_names::canonical_or_self(name)
1740                                        .to_owned(),
1741                                    state.root_page,
1742                                )
1743                            })
1744                            .collect(),
1745                    }
1746                })
1747            });
1748            if content.len() >= 4 {
1749                let mut pos = 0;
1750                let mut format_version = STORE_VERSION_V1;
1751
1752                let collection_count = if let Some(header) =
1753                    reddb_file::decode_native_paged_metadata_header(content)
1754                        .map_err(|err| StoreError::Serialization(err.to_string()))?
1755                {
1756                    format_version = header.format_version;
1757                    pos += reddb_file::METADATA_HEADER_BYTES;
1758                    header.collection_count as usize
1759                } else {
1760                    let count = u32::from_le_bytes([
1761                        content[pos],
1762                        content[pos + 1],
1763                        content[pos + 2],
1764                        content[pos + 3],
1765                    ]) as usize;
1766                    pos += 4;
1767                    count
1768                };
1769
1770                self.set_format_version(format_version);
1771
1772                if pos > content.len() {
1773                    return Ok(CatalogLoadOutcome::default());
1774                }
1775
1776                if resume.is_none() {
1777                    // Old native catalogs have no advisory entity counters. A
1778                    // zero root is the only proof that an alias owns no rows;
1779                    // two nonzero roots still require explicit reconciliation.
1780                    let mut roots = std::collections::HashMap::new();
1781                    let mut root_pos = pos;
1782                    for _ in 0..collection_count {
1783                        let root =
1784                            reddb_file::decode_native_paged_collection_root(content, &mut root_pos)
1785                                .map_err(|err| page_integrity(1, None, err.to_string()))?;
1786                        if roots.insert(root.collection, root.root_page).is_some() {
1787                            return Err(page_integrity(
1788                                1,
1789                                None,
1790                                "duplicate raw collection identity",
1791                            ));
1792                        }
1793                    }
1794                    for &(legacy, canonical) in
1795                        reddb_engine::storage::system_collection_names::LEGACY_NAMES
1796                    {
1797                        let (Some(&legacy_root), Some(&canonical_root)) =
1798                            (roots.get(legacy), roots.get(canonical))
1799                        else {
1800                            continue;
1801                        };
1802                        let (survivor, loser) = match (legacy_root == 0, canonical_root == 0) {
1803                            (false, true) => (legacy, canonical),
1804                            (true, _) => (canonical, legacy),
1805                            (false, false) => continue,
1806                        };
1807                        ignored_collections.insert(loser.to_owned());
1808                        outcome.collisions.push(SystemCollectionCollision {
1809                            survivor: survivor.to_owned(),
1810                            loser: loser.to_owned(),
1811                            canonical: canonical.to_owned(),
1812                            shared_physical_state: false,
1813                            resume_counter_drift: false,
1814                            merged_resume: None,
1815                        });
1816                    }
1817                }
1818                let mut source_names = std::collections::HashMap::<String, String>::new();
1819                let mut migrated_catalog = !outcome.collisions.is_empty();
1820                let mut empty_duplicate_roots = std::collections::HashSet::new();
1821                let mut recovery_config_roots = [0u8; 2];
1822                // Older catalogs have no resume state, so the bounded loader
1823                // must already decode every physical entity to restore IDs.
1824                // Reuse that complete pass for the transaction floor too.
1825                let mut scanned_xid_high_water_mark = 0u64;
1826                let mut count_seed_bytes_remaining = self
1827                    .config
1828                    .resident_memory_bytes_max
1829                    .and_then(|limit| usize::try_from(limit).ok())
1830                    .unwrap_or(0)
1831                    / 2;
1832
1833                // Read collection names and their B-tree root pages
1834                for _ in 0..collection_count {
1835                    let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1836                        .map_err(|err| page_integrity(1, None, err.to_string()))?;
1837                    // Root page ID for this collection's B-tree
1838                    let root_page = root.root_page;
1839                    let stored_name = root.collection;
1840                    if retain_canonical_system_config {
1841                        let config_index = match stored_name.as_str() {
1842                            "red_config" => Some(0),
1843                            "red.config" => Some(1),
1844                            _ => None,
1845                        };
1846                        if let Some(index) = config_index {
1847                            recovery_config_roots[index] =
1848                                recovery_config_roots[index].saturating_add(1);
1849                            let state = resume
1850                                .as_ref()
1851                                .and_then(|resume| resume.collections.get(&stored_name))
1852                                .expect("both recovery identities were checked");
1853                            if state.root_page != root_page {
1854                                return Err(page_integrity(
1855                                    1,
1856                                    None,
1857                                    "config collection root differs from resume catalog",
1858                                ));
1859                            }
1860                        }
1861                    }
1862                    if ignored_collections.contains(&stored_name) {
1863                        if resume.is_none() && root_page == 0 {
1864                            continue;
1865                        }
1866                        let shared_physical_state = outcome
1867                            .collisions
1868                            .iter()
1869                            .find(|collision| collision.loser == stored_name)
1870                            .is_some_and(|collision| collision.shared_physical_state);
1871                        if shared_physical_state {
1872                            continue;
1873                        }
1874                        let state = resume
1875                            .as_ref()
1876                            .and_then(|resume| resume.collections.get(&stored_name))
1877                            .expect("ignored collection was proven by catalog resume");
1878                        for retired_root in std::iter::once(state.root_page)
1879                            .chain(state.key_index_root)
1880                            .chain(
1881                                state
1882                                    .secondary_indices
1883                                    .values()
1884                                    .map(|index| index.root_page),
1885                            )
1886                            .filter(|root| *root != 0)
1887                        {
1888                            empty_duplicate_roots.insert(retired_root);
1889                        }
1890                        continue;
1891                    }
1892                    let name = reddb_engine::storage::system_collection_names::canonical_or_self(
1893                        &stored_name,
1894                    )
1895                    .to_owned();
1896                    if let Some(previous) = source_names.insert(name.clone(), stored_name.clone()) {
1897                        if previous != stored_name {
1898                            return Err(page_integrity(
1899                                1,
1900                                None,
1901                                format!(
1902                                    "catalog contains both legacy collection '{previous}' and canonical collection '{name}'"
1903                                ),
1904                            ));
1905                        }
1906                    }
1907                    migrated_catalog |= name != stored_name;
1908
1909                    // Hydrate the collection in memory only. Loading must
1910                    // not emit WAL entries or rewrite the on-disk registry
1911                    // before the existing B-tree roots are attached.
1912                    let _ = self.create_collection_in_memory(&name);
1913
1914                    let shared_alias = outcome.collisions.iter().find(|collision| {
1915                        collision.survivor == stored_name && collision.shared_physical_state
1916                    });
1917                    let counters = shared_alias
1918                        .and_then(|collision| collision.merged_resume.as_ref())
1919                        .or_else(|| {
1920                            resume
1921                                .as_ref()
1922                                .and_then(|resume| resume.collections.get(&stored_name))
1923                        });
1924                    let manager = self.get_collection(&name);
1925                    let mut count_seed =
1926                        if counters.is_none() && self.commit.is_some() && !self.is_read_only() {
1927                            let schema =
1928                                manager.as_ref().and_then(|manager| manager.column_schema());
1929                            super::count_index::CountProjectionSeedCollector::new(
1930                                schema,
1931                                count_seed_bytes_remaining,
1932                            )
1933                        } else {
1934                            None
1935                        };
1936                    if let (Some(counters), Some(manager)) = (counters, self.get_collection(&name))
1937                    {
1938                        manager.restore_identity_counters(
1939                            counters.entity_id_next,
1940                            counters.row_id_next,
1941                        );
1942                        manager.restore_sequence_id_next(counters.sequence_id_next);
1943                        let expiry_entries = counters
1944                            .metadata_expiries
1945                            .iter()
1946                            .map(|entry| (entry.deadline_ms, EntityId::new(entry.entity_id)))
1947                            .collect::<Vec<_>>();
1948                        manager.restore_metadata_expiry_projection(
1949                            &expiry_entries,
1950                            resume
1951                                .as_ref()
1952                                .is_some_and(|resume| resume.metadata_expiry_prepared),
1953                        );
1954                    }
1955                    // Load B-tree with root page if it exists
1956                    if root_page > 0 || counters.is_some() {
1957                        let btree = BTree::with_root(Arc::clone(pager), root_page);
1958
1959                        // Load all entities from B-tree into the collection
1960                        let mut cursor = btree.cursor_first().map_err(|err| {
1961                            page_integrity(root_page, Some(name.clone()), err.to_string())
1962                        })?;
1963                        let hydrate_read_only =
1964                            self.is_read_only() && self.config.resident_memory_bytes_max.is_none();
1965                        let mut persisted_entities = if hydrate_read_only {
1966                            0
1967                        } else {
1968                            counters.map_or(0, |state| state.entities)
1969                        };
1970                        while counters.is_none() || hydrate_read_only {
1971                            let next = cursor
1972                                .next_with_budget(
1973                                    self.config
1974                                        .resident_memory_bytes_max
1975                                        .and_then(|limit| usize::try_from(limit).ok())
1976                                        .unwrap_or(usize::MAX),
1977                                )
1978                                .map_err(|err| {
1979                                    page_integrity(root_page, Some(name.clone()), err.to_string())
1980                                })?;
1981                            let Some((key, value)) = next else {
1982                                break;
1983                            };
1984                            persisted_entities += 1;
1985                            // Deserialize entity from value bytes
1986                            let key_bytes: [u8; 8] = key.as_slice().try_into().map_err(|_| {
1987                                page_integrity(
1988                                    root_page,
1989                                    Some(name.clone()),
1990                                    "physical entity key must contain eight bytes",
1991                                )
1992                            })?;
1993                            let record = reddb_engine::storage::collection_read::EncodedRecord {
1994                                id: EntityId::new(u64::from_be_bytes(key_bytes)),
1995                                bytes: value,
1996                            };
1997                            let decoded = if let Some(limit) = self.config.resident_memory_bytes_max
1998                            {
1999                                record.decode_with_budget(
2000                                    self.format_version(),
2001                                    usize::try_from(limit).unwrap_or(usize::MAX),
2002                                )
2003                            } else {
2004                                record.decode(self.format_version())
2005                            };
2006                            let (entity, metadata) = decoded.map_err(|err| {
2007                                page_integrity(root_page, Some(name.clone()), err.to_string())
2008                            })?;
2009                            scanned_xid_high_water_mark = scanned_xid_high_water_mark
2010                                .max(entity.xmin)
2011                                .max(entity.xmax);
2012                            let discard_count_seed = if let Some(seed) = count_seed.as_mut() {
2013                                match seed.push(&entity) {
2014                                    Ok(complete) => !complete,
2015                                    Err(error) => {
2016                                        return Err(page_integrity(
2017                                            root_page,
2018                                            Some(name.clone()),
2019                                            error.to_string(),
2020                                        ));
2021                                    }
2022                                }
2023                            } else {
2024                                false
2025                            };
2026                            if discard_count_seed {
2027                                count_seed = None;
2028                            }
2029                            if let Some(m) = &manager {
2030                                let id = entity.id;
2031                                if let EntityKind::TableRow { row_id, .. } = &entity.kind {
2032                                    m.register_row_id(*row_id);
2033                                }
2034                                if self.config.resident_memory_bytes_max.is_some() {
2035                                    m.restore_metadata_expiry(&entity, metadata.as_ref());
2036                                    self.register_entity_id(id);
2037                                    m.restore_sequence_id_next(
2038                                        entity.sequence_id.saturating_add(1),
2039                                    );
2040                                    m.restore_identity_counters(
2041                                        id.raw().saturating_add(1),
2042                                        m.identity_counters().1,
2043                                    );
2044                                    continue;
2045                                }
2046                                self.resident.context_index.index_entity(&name, &entity);
2047                                m.restore_snapshot_entity(entity.clone())?;
2048                                if let Some(metadata) = metadata {
2049                                    let _ = m.set_metadata(id, metadata);
2050                                }
2051                                self.register_entity_id(id);
2052                                if self.config.auto_index_refs {
2053                                    self.index_cross_refs(&entity, &name)?;
2054                                }
2055                            }
2056                        }
2057
2058                        if let Some(seed) = count_seed.take() {
2059                            count_seed_bytes_remaining =
2060                                count_seed_bytes_remaining.saturating_sub(seed.bytes());
2061                            self.remember_count_projection_seed(&name, root_page, seed);
2062                        }
2063
2064                        // Store the B-tree for future lookups
2065                        let btree = Arc::new(btree);
2066                        if shared_alias.is_some_and(|collision| collision.resume_counter_drift) {
2067                            persisted_entities = u64::try_from(btree.count().map_err(|err| {
2068                                page_integrity(root_page, Some(name.clone()), err.to_string())
2069                            })?)
2070                            .map_err(|_| {
2071                                page_integrity(
2072                                    root_page,
2073                                    Some(name.clone()),
2074                                    "shared collection entity count exceeds u64",
2075                                )
2076                            })?;
2077                        }
2078                        let key_tree = counters
2079                            .and_then(|state| state.key_index_root)
2080                            .map(|root| Arc::new(BTree::with_root(Arc::clone(pager), root)));
2081                        if let Some(counters) = counters {
2082                            self.key_indices.write().insert(
2083                                name.clone(),
2084                                PublishedKeyIndex {
2085                                    indexed: key_tree.is_some(),
2086                                    secondary: counters.secondary_indices.iter().map(|(name, index)| {
2087                                        (name.clone(), super::secondary_index::PublishedSecondaryIndex {
2088                                            identity: super::secondary_index::SecondaryIndexIdentity {
2089                                                codec_version: index.codec_version,
2090                                                definition: index.definition,
2091                                            },
2092                                            tree: Arc::new(BTree::with_root(Arc::clone(pager), index.root_page)),
2093                                        })
2094                                    }).collect(),
2095                                    source: Arc::downgrade(&btree),
2096                                    tree: key_tree.clone().unwrap_or_else(|| {
2097                                        Arc::new(BTree::with_root(Arc::clone(pager), 0))
2098                                    }),
2099                                },
2100                            );
2101                        }
2102                        if let (Some(manager), Some(limit)) =
2103                            (&manager, self.config.resident_memory_bytes_max)
2104                        {
2105                            manager.install_backing(
2106                                Arc::new(
2107                                    match key_tree {
2108                                        Some(keys) => reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(&btree), keys),
2109                                        None => reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(&btree)),
2110                                    },
2111                                ),
2112                                btree.root_page_id(),
2113                                format_version,
2114                                usize::try_from(limit).unwrap_or(usize::MAX),
2115                                persisted_entities,
2116                            );
2117                        }
2118                        self.btree_indices.write().insert(name.clone(), btree);
2119                        if format_version == STORE_VERSION_V11 {
2120                            if let Some(manager) = &manager {
2121                                self.checkpoint_versions.write().insert(
2122                                    name,
2123                                    CheckpointVersion {
2124                                        manager: Arc::downgrade(manager),
2125                                        segments: manager.checkpoint_revisions(),
2126                                        visibility_revision: 0,
2127                                        untracked_visibility_revision: 0,
2128                                        excluded_xids: Arc::default(),
2129                                        pending_vault: Arc::default(),
2130                                        format_version,
2131                                        deferred_rows: Some(Arc::default()),
2132                                        has_unprojected_rows: false,
2133                                    },
2134                                );
2135                            }
2136                        }
2137                    }
2138                }
2139
2140                if retain_canonical_system_config && recovery_config_roots != [1, 1] {
2141                    return Err(page_integrity(
2142                        1,
2143                        None,
2144                        "canonical config recovery requires exactly one root for each identity",
2145                    ));
2146                }
2147                if retain_canonical_system_config
2148                    && (format_version < STORE_VERSION_V2 || pos + 4 > content.len())
2149                {
2150                    return Err(page_integrity(
2151                        1,
2152                        None,
2153                        "canonical config recovery requires a complete cross-reference catalog",
2154                    ));
2155                }
2156                if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
2157                    let cross_ref_count = u32::from_le_bytes([
2158                        content[pos],
2159                        content[pos + 1],
2160                        content[pos + 2],
2161                        content[pos + 3],
2162                    ]) as usize;
2163                    pos += 4;
2164
2165                    for _ in 0..cross_ref_count {
2166                        let cross_ref =
2167                            match reddb_file::decode_native_paged_cross_ref(content, &mut pos) {
2168                                Ok(cross_ref) => cross_ref,
2169                                Err(error) if retain_canonical_system_config => {
2170                                    return Err(page_integrity(1, None, error.to_string()));
2171                                }
2172                                Err(_) => break,
2173                            };
2174                        if retain_canonical_system_config
2175                            && cross_ref.target_collection == "red_config"
2176                        {
2177                            return Err(page_integrity(
2178                                1,
2179                                None,
2180                                "legacy red_config has cross-references and cannot be retired",
2181                            ));
2182                        }
2183                        let source_id = EntityId::new(cross_ref.source_id);
2184                        let target_id = EntityId::new(cross_ref.target_id);
2185                        let ref_type = RefType::from_byte(cross_ref.ref_type);
2186                        let target_collection =
2187                            reddb_engine::storage::system_collection_names::canonical_owned(
2188                                cross_ref.target_collection,
2189                            );
2190
2191                        self.resident
2192                            .cross_refs
2193                            .write()
2194                            .entry(source_id)
2195                            .or_default()
2196                            .push((target_id, ref_type, target_collection.clone()));
2197
2198                        if let Some((collection, mut entity)) = self.get_any(source_id) {
2199                            let exists = entity.cross_refs().iter().any(|xref| {
2200                                xref.target == target_id
2201                                    && xref.ref_type == ref_type
2202                                    && xref.target_collection == target_collection
2203                            });
2204                            if !exists {
2205                                entity.cross_refs_mut().push(CrossRef::new(
2206                                    source_id,
2207                                    target_id,
2208                                    target_collection.clone(),
2209                                    ref_type,
2210                                ));
2211                                if let Some(manager) = self.get_collection(&collection) {
2212                                    let _ = manager.update(entity);
2213                                }
2214                            }
2215                        }
2216                    }
2217                }
2218                let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
2219                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
2220                let already_retired = retired
2221                    .iter()
2222                    .copied()
2223                    .collect::<std::collections::HashSet<_>>();
2224                let mut live_roots: std::collections::HashSet<_> = self
2225                    .btree_indices
2226                    .read()
2227                    .values()
2228                    .map(|tree| tree.root_page_id())
2229                    .collect();
2230                live_roots.extend(
2231                    self.key_indices
2232                        .read()
2233                        .values()
2234                        .flat_map(PublishedKeyIndex::trees)
2235                        .map(|tree| tree.root_page_id()),
2236                );
2237                for root in retired {
2238                    if live_roots.contains(&root)
2239                        || root
2240                            >= pager
2241                                .page_count()
2242                                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
2243                    {
2244                        return Err(page_integrity(
2245                            root,
2246                            None,
2247                            "retired root aliases a live or missing page",
2248                        ));
2249                    }
2250                    self.retired_btrees.lock().push(RetiredBTree {
2251                        tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
2252                        published: true,
2253                    });
2254                }
2255                for root in empty_duplicate_roots
2256                    .iter()
2257                    .copied()
2258                    .filter(|root| !already_retired.contains(root))
2259                {
2260                    self.retire_btree(Arc::new(BTree::with_root(Arc::clone(pager), root)));
2261                }
2262                let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
2263                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
2264                if deferred
2265                    .iter()
2266                    .any(|id| live_roots.contains(id) || empty_duplicate_roots.contains(id))
2267                {
2268                    return Err(page_integrity(1, None, "deferred page aliases a live root"));
2269                }
2270                pager
2271                    .restore_deferred_reclamation(&deferred)
2272                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2273                let ownership_epoch =
2274                    reddb_file::native_store::decode_native_paged_ownership_epoch(
2275                        content, &mut pos,
2276                    )
2277                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
2278                self.ownership_catalog_epoch
2279                    .store(ownership_epoch.unwrap_or(0), Ordering::Release);
2280                if migrated_catalog {
2281                    self.mark_paged_registry_dirty();
2282                }
2283                if self.format_version() < STORE_VERSION_V11 {
2284                    self.set_format_version(STORE_VERSION_V11);
2285                }
2286                if self.config.resident_memory_bytes_max.is_some()
2287                    && resume.is_none()
2288                    && !migrated_catalog
2289                    && !retain_canonical_system_config
2290                {
2291                    self.remember_startup_xid_high_water_mark(scanned_xid_high_water_mark);
2292                }
2293                outcome.migrated = migrated_catalog;
2294                return Ok(outcome);
2295            }
2296        }
2297
2298        if retain_canonical_system_config {
2299            return Err(page_integrity(
2300                1,
2301                None,
2302                "canonical config recovery requires a complete catalog",
2303            ));
2304        }
2305        if self.format_version() < STORE_VERSION_V11 {
2306            self.set_format_version(STORE_VERSION_V11);
2307        }
2308
2309        Ok(CatalogLoadOutcome::default())
2310    }
2311
2312    /// Deserialize an entity from binary bytes
2313    pub(crate) fn deserialize_entity(
2314        data: &[u8],
2315        format_version: u32,
2316    ) -> Result<UnifiedEntity, StoreError> {
2317        let mut pos = 0;
2318        Self::read_entity_binary(data, &mut pos, format_version)
2319            .map_err(|e| StoreError::Serialization(e.to_string()))
2320    }
2321
2322    /// Serialize an entity to binary bytes
2323    pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
2324        // Pre-allocate ~256 bytes to cover the typical 15-column
2325        // typed row without any Vec growth. Bulk insert calls this
2326        // millions of times per bench run; saving 2-3 reallocs per
2327        // entity amortises.
2328        let mut buf = Vec::with_capacity(256);
2329        Self::write_entity_binary(&mut buf, entity, format_version);
2330        buf
2331    }
2332
2333    pub(crate) fn serialize_entity_record(
2334        entity: &UnifiedEntity,
2335        metadata: Option<&Metadata>,
2336        format_version: u32,
2337    ) -> Vec<u8> {
2338        reddb_engine::storage::metadata_codec::serialize_entity_record(
2339            entity,
2340            metadata,
2341            format_version,
2342        )
2343    }
2344
2345    pub(crate) fn deserialize_entity_record(
2346        data: &[u8],
2347        format_version: u32,
2348    ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
2349        reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
2350            .map_err(StoreError::Serialization)
2351    }
2352
2353    /// Persist all data to page-based storage
2354    ///
2355    /// Writes all entities to B-tree pages and flushes to disk.
2356    /// This provides ACID durability guarantees.
2357    pub fn persist(&self) -> Result<(), StoreError> {
2358        self.persist_inner(false)
2359    }
2360
2361    pub(crate) fn persist_after_runtime_snapshots(&self) -> Result<(), StoreError> {
2362        self.persist_inner(true)
2363    }
2364
2365    fn persist_inner(&self, runtime_snapshots_durable: bool) -> Result<(), StoreError> {
2366        if self.is_read_only() {
2367            return Ok(());
2368        }
2369        let _vault_guard = self.vault_publication_lock.lock();
2370        let pending_vault_versions = self.pending_vault_versions.read();
2371        let pager = match &self.pager {
2372            Some(p) => p,
2373            None => {
2374                // No pager attached - use binary file fallback if path available
2375                if let Some(path) = &self.db_path {
2376                    return self
2377                        .save_to_file(path)
2378                        .map_err(|e| StoreError::Serialization(e.to_string()));
2379                }
2380                return Err(StoreError::Io(std::io::Error::other(
2381                    "No pager or path configured for persistence",
2382                )));
2383            }
2384        };
2385        // Prevent unrelated allocations or frees from being attributed to
2386        // this checkpoint's ownership epoch.
2387        let _ownership_guard = pager.lock_ownership_mutations();
2388
2389        match pager.read_page(1) {
2390            Ok(_) => {}
2391            Err(PagerError::PageNotFound(_)) => {
2392                let meta_page = pager
2393                    .allocate_page(crate::storage::engine::PageType::Header)
2394                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2395                pager
2396                    .write_page(meta_page.page_id(), meta_page)
2397                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2398            }
2399            Err(e) => {
2400                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
2401            }
2402        }
2403
2404        if let Some(commit) = &self.commit {
2405            commit.force_sync().map_err(StoreError::Io)?;
2406        }
2407
2408        struct InterruptedOwnership<'a> {
2409            pager: &'a Pager,
2410            active: bool,
2411        }
2412        impl Drop for InterruptedOwnership<'_> {
2413            fn drop(&mut self) {
2414                if self.active {
2415                    self.pager.fence_interrupted_ownership();
2416                }
2417            }
2418        }
2419        let mut ownership = InterruptedOwnership {
2420            pager,
2421            active: self.commit.is_some(),
2422        };
2423        let epoch = if let Some(commit) = &self.commit {
2424            let epoch = commit
2425                .next_page_ownership_epoch(self)
2426                .map_err(StoreError::Io)?;
2427            commit
2428                .begin_page_ownership_epoch(self, epoch)
2429                .map_err(StoreError::Io)?;
2430            Some(epoch)
2431        } else {
2432            None
2433        };
2434
2435        let collections = self.resident.collections.read();
2436        let mut btree_indices = self.btree_indices.write();
2437
2438        // Collect collection names and their B-tree root pages
2439        let mut collection_roots: Vec<(String, u32)> = Vec::new();
2440
2441        // Build unpublished replacements. Deleting from the current trees here
2442        // would destroy the last checkpoint if a write fails before publication.
2443        let mut rebuilt_indices = HashMap::with_capacity(collections.len());
2444        let previous_versions = self.checkpoint_versions.read();
2445        let mut checkpoint_versions = HashMap::with_capacity(collections.len());
2446        let pending_vault = Arc::new(pending_vault_versions.clone());
2447        let (visibility_revision, excluded, untracked_visibility_revision) =
2448            self.snapshot_manager.get().map_or_else(
2449                || (0, Default::default(), 0),
2450                |manager| manager.checkpoint_projection_state(),
2451            );
2452        let excluded = Arc::new(excluded);
2453        for (name, manager) in collections.iter() {
2454            let mut version = CheckpointVersion {
2455                manager: Arc::downgrade(manager),
2456                segments: manager.checkpoint_revisions(),
2457                visibility_revision,
2458                untracked_visibility_revision,
2459                excluded_xids: Arc::clone(&excluded),
2460                pending_vault: Arc::clone(&pending_vault),
2461                format_version: self.format_version(),
2462                deferred_rows: Some(Arc::default()),
2463                has_unprojected_rows: false,
2464            };
2465            if let (Some(previous), Some(tree)) =
2466                (previous_versions.get(name), btree_indices.get(name))
2467            {
2468                if previous.manager.ptr_eq(&version.manager)
2469                    && previous.segments == version.segments
2470                    && previous.format_version == version.format_version
2471                    && !previous.has_unprojected_rows
2472                    && checkpoint_visibility_matches(previous, &version)
2473                {
2474                    version.deferred_rows = previous.deferred_rows.clone();
2475                    collection_roots.push((name.clone(), tree.root_page_id()));
2476                    rebuilt_indices.insert(name.clone(), Arc::clone(tree));
2477                    checkpoint_versions.insert(name.clone(), version);
2478                    continue;
2479                }
2480            }
2481            if let (Some(previous), Some(tree)) =
2482                (previous_versions.get(name), btree_indices.get(name))
2483            {
2484                if previous.manager.ptr_eq(&version.manager)
2485                    && previous.format_version == version.format_version
2486                {
2487                    if let Some(tree) =
2488                        self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
2489                    {
2490                        collection_roots.push((name.clone(), tree.root_page_id()));
2491                        rebuilt_indices.insert(name.clone(), tree);
2492                        checkpoint_versions.insert(name.clone(), version);
2493                        continue;
2494                    }
2495                }
2496            }
2497            let btree = self.checkpoint_tree(
2498                pager,
2499                manager,
2500                btree_indices.get(name),
2501                |entity, metadata| {
2502                    #[cfg(test)]
2503                    CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
2504                    let hidden = pending_vault_versions.contains(&entity.id)
2505                        || (matches!(
2506                            entity.data,
2507                            EntityData::Row(_)
2508                                | EntityData::Vector(_)
2509                                | EntityData::QueueMessage(_)
2510                        ) && excluded.contains(&entity.xmin));
2511                    let normalize_xmax = matches!(
2512                        entity.data,
2513                        EntityData::Row(_) | EntityData::QueueMessage(_)
2514                    ) && excluded.contains(&entity.xmax);
2515                    if hidden || normalize_xmax {
2516                        version.remember_deferred_row(entity.id);
2517                    }
2518                    if hidden {
2519                        return None;
2520                    }
2521                    let mut normalized = std::borrow::Cow::Borrowed(entity);
2522                    if normalize_xmax {
2523                        normalized.to_mut().xmax = 0;
2524                    }
2525                    Some(Self::serialize_entity_record(
2526                        &normalized,
2527                        metadata.as_ref(),
2528                        self.format_version(),
2529                    ))
2530                },
2531            )?;
2532            checkpoint_versions.insert(name.clone(), version);
2533
2534            collection_roots.push((name.clone(), btree.root_page_id()));
2535            rebuilt_indices.insert(name.clone(), btree);
2536        }
2537
2538        // Write collection metadata to page 1
2539        let mut meta_data = Vec::with_capacity(4096);
2540
2541        let format_version = STORE_VERSION_V11;
2542        self.set_format_version(format_version);
2543
2544        reddb_file::encode_native_paged_metadata_header(
2545            &mut meta_data,
2546            reddb_file::NativePagedMetadataHeader {
2547                format_version,
2548                collection_count: collection_roots.len() as u32,
2549            },
2550        );
2551
2552        // Write each collection's name and B-tree root page
2553        for (name, root_page) in &collection_roots {
2554            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
2555        }
2556
2557        // Write cross-reference metadata
2558        let cross_refs = self.resident.cross_refs.read();
2559        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
2560        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
2561        for (source_id, refs) in cross_refs.iter() {
2562            for (target_id, ref_type, collection) in refs {
2563                reddb_file::encode_native_paged_cross_ref(
2564                    &mut meta_data,
2565                    source_id.raw(),
2566                    target_id.raw(),
2567                    ref_type.to_byte(),
2568                    collection,
2569                );
2570            }
2571        }
2572
2573        for (name, previous) in btree_indices.iter() {
2574            if !rebuilt_indices
2575                .get(name)
2576                .is_some_and(|tree| Arc::ptr_eq(tree, previous))
2577            {
2578                self.retire_btree(Arc::clone(previous));
2579            }
2580        }
2581        #[cfg(test)]
2582        if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
2583            pager
2584                .sync()
2585                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2586            eprintln!("experimental checkpoint exit before manifest");
2587            std::process::exit(183);
2588        }
2589        let rebuilt_keys = self.checkpoint_key_indices(pager, &rebuilt_indices)?;
2590        #[cfg(test)]
2591        if std::env::var_os("REDDB_TEST_EXIT_AFTER_SECONDARY_INDEX_BUILD").is_some() {
2592            pager
2593                .sync()
2594                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2595            std::process::exit(196);
2596        }
2597        self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices, &rebuilt_keys)?;
2598
2599        #[cfg(test)]
2600        if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
2601            std::process::exit(195);
2602        }
2603
2604        // Only the now-durable catalog may replace the live tree handles. The
2605        // previous generation is retained in the manifest retirement ledger.
2606        *btree_indices = rebuilt_indices;
2607        *self.key_indices.write() = rebuilt_keys;
2608        drop(previous_versions);
2609        *self.checkpoint_versions.write() = checkpoint_versions;
2610
2611        if let Some(commit) = &self.commit {
2612            commit
2613                .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
2614                .map_err(StoreError::Io)?;
2615            ownership.active = false;
2616            if runtime_snapshots_durable {
2617                commit
2618                    .truncate_after_runtime_checkpoint()
2619                    .map_err(StoreError::Io)?;
2620            } else {
2621                commit.truncate().map_err(StoreError::Io)?;
2622            }
2623        }
2624        *self.checkpoint_durable_rows.write() = Default::default();
2625
2626        if let Some(limit) = self.config.resident_memory_bytes_max {
2627            let mut versions = self.checkpoint_versions.write();
2628            for (name, manager) in collections.iter() {
2629                if versions
2630                    .get(name)
2631                    .is_some_and(|version| !version.has_unprojected_rows)
2632                {
2633                    if let Some(tree) = btree_indices.get(name) {
2634                        manager.install_backing(
2635                            Arc::new(
2636                                {
2637                                    let keys = self.key_indices.read();
2638                                    let index = keys.get(name).expect("invariant: prepared checkpoint classifies its key index");
2639                                    if index.indexed {
2640                                        reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(tree), Arc::clone(&index.tree))
2641                                    } else {
2642                                        reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(tree))
2643                                    }
2644                                },
2645                            ),
2646                            tree.root_page_id(),
2647                            self.format_version(),
2648                            usize::try_from(limit).unwrap_or(usize::MAX),
2649                            manager.count() as u64,
2650                        );
2651                        if let Some(version) = versions.get_mut(name) {
2652                            version.segments = manager.checkpoint_revisions();
2653                        }
2654                    }
2655                }
2656            }
2657        }
2658
2659        Ok(())
2660    }
2661
2662    /// Check if the store is using page-based persistence
2663    pub fn is_paged(&self) -> bool {
2664        self.pager.is_some()
2665    }
2666
2667    /// Current root page for a collection's primary B-tree, if one has
2668    /// been materialized in this store.
2669    pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
2670        self.btree_indices
2671            .read()
2672            .get(Self::canonical_collection(collection))
2673            .map(|btree| btree.root_page_id())
2674            .filter(|root| *root != 0)
2675    }
2676
2677    /// Keep the journal replay baseline for the lifetime of this store, including
2678    /// persistent resident layouts whose paged `db_path` is absent.
2679    pub(crate) fn operational_manifest_for_path(
2680        &self,
2681        path: &Path,
2682    ) -> reddb_file::OperationalManifest {
2683        let mut cached = self.operational_manifest.lock();
2684        if let Some((current, manifest)) = cached.as_ref() {
2685            if current == path {
2686                return manifest.clone();
2687            }
2688        }
2689        let manifest = reddb_file::OperationalManifest::for_db_path(path);
2690        *cached = Some((path.to_owned(), manifest.clone()));
2691        manifest
2692    }
2693
2694    /// Get the database file path (if using paged mode)
2695    pub fn db_path(&self) -> Option<&Path> {
2696        self.db_path.as_deref()
2697    }
2698}
2699
2700#[cfg(test)]
2701mod checkpoint_work_tests {
2702    use super::*;
2703    use reddb_file::PAGED_PAGE_SIZE as PAGE_SIZE;
2704
2705    struct SystemNameProjection;
2706
2707    impl super::super::secondary_index::SecondaryIndexProjector for SystemNameProjection {
2708        fn project(
2709            &self,
2710            entity: &UnifiedEntity,
2711            key: &mut [u8; super::super::secondary_index::SECONDARY_KEY_BYTES_MAX],
2712        ) -> Result<Option<usize>, StoreError> {
2713            key[..8].copy_from_slice(&entity.id.raw().to_be_bytes());
2714            Ok(Some(8))
2715        }
2716    }
2717
2718    #[cfg(target_os = "linux")]
2719    #[test]
2720    fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
2721        const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
2722        if let Some(path) = std::env::var_os(CHILD) {
2723            let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
2724            let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
2725            let store = UnifiedStore::open(path).expect("open store");
2726            let initial_count = if multi { 600 } else { 1 };
2727            for id in 1..=initial_count {
2728                store.insert_auto("rows", row(id, 1)).expect("seed");
2729            }
2730            store.persist().expect("baseline");
2731            let manager = store.get_collection("rows").expect("collection");
2732            let ids: Vec<u64> = if multi {
2733                (1..=initial_count).collect()
2734            } else {
2735                vec![if insert { 2 } else { 1 }]
2736            };
2737            let mut actions = Vec::new();
2738            for id in ids {
2739                if insert {
2740                    manager.insert(row(id, 9)).expect("insert version");
2741                } else {
2742                    manager.update(row(id, 9)).expect("update version");
2743                }
2744                let entity = manager.get(EntityId::new(id)).expect("changed entity");
2745                actions.push(StoreWalAction::upsert_entity(
2746                    "rows",
2747                    &entity,
2748                    None,
2749                    store.format_version(),
2750                ));
2751            }
2752            let references = store
2753                .commit
2754                .as_ref()
2755                .expect("coordinator")
2756                .append_checkpoint_actions(&actions)
2757                .expect("durable WAL");
2758            store.remember_checkpoint_rows(actions, references);
2759            store.persist().expect("exit after sparse write");
2760            panic!("inline sparse crash hook was not reached");
2761        }
2762        for (insert, multi) in [(false, false), (true, false), (false, true)] {
2763            let directory = tempfile::tempdir().expect("temporary store");
2764            let path = directory.path().join("data.rdb");
2765            let mut command =
2766                std::process::Command::new(std::env::current_exe().expect("test executable"));
2767            command
2768                .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
2769                .env(CHILD, &path)
2770                .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
2771            if insert {
2772                command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
2773            }
2774            if multi {
2775                command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
2776            }
2777            let output = command.output().expect("crash child");
2778            assert_eq!(
2779                output.status.code(),
2780                Some(190),
2781                "insert={insert} multi={multi}: {}",
2782                String::from_utf8_lossy(&output.stderr)
2783            );
2784            let recovered = UnifiedStore::open(&path).expect("recover");
2785            let ids: Vec<u64> = if multi {
2786                (1..=600).collect()
2787            } else {
2788                vec![if insert { 2 } else { 1 }]
2789            };
2790            for id in ids {
2791                assert_eq!(
2792                    recovered
2793                        .get("rows", EntityId::new(id))
2794                        .expect("recovered row")
2795                        .data
2796                        .as_row()
2797                        .expect("row")
2798                        .columns,
2799                    vec![Value::Integer(9)]
2800                );
2801            }
2802            assert_eq!(
2803                recovered.get_collection("rows").expect("rows").count(),
2804                if multi {
2805                    600
2806                } else if insert {
2807                    2
2808                } else {
2809                    1
2810                }
2811            );
2812        }
2813    }
2814
2815    #[test]
2816    fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
2817        let directory = tempfile::tempdir().expect("temporary store");
2818        let path = directory.path().join("data.rdb");
2819        {
2820            let store = UnifiedStore::open(&path).expect("open store");
2821            store.create_collection("rows").expect("create collection");
2822            store.persist().expect("publish ownership epoch");
2823            assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
2824            store.persist().expect("publish next ownership epoch");
2825        }
2826        let reopened = UnifiedStore::open(&path).expect("reopen store");
2827        assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
2828    }
2829
2830    #[test]
2831    fn metadata_overflow_with_missing_continuation_fails_closed() {
2832        use crate::storage::engine::{Page, PageType, HEADER_SIZE};
2833
2834        let directory = tempfile::tempdir().expect("temporary store");
2835        let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
2836        let pager = store.pager.as_ref().unwrap();
2837        let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
2838        let valid_manifest = build_meta_page1_with_overflow(pager, &valid_payload).unwrap();
2839        pager.write_page(1, valid_manifest).unwrap();
2840        assert!(!existing_overflow_chain(pager).unwrap().is_empty());
2841        assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
2842
2843        let missing = pager.page_count().unwrap() + 100;
2844        let mut manifest = Page::new(PageType::Header, 1);
2845        reddb_file::encode_native_metadata_overflow_header(
2846            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2847            reddb_file::NativeMetadataOverflowHeader {
2848                format_version: STORE_VERSION_V11,
2849                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2850                next_overflow_page_id: missing,
2851            },
2852        )
2853        .unwrap();
2854        pager.write_page(1, manifest).unwrap();
2855
2856        assert!(
2857            existing_overflow_chain(pager).is_err(),
2858            "a missing continuation must not be treated as an empty chain"
2859        );
2860
2861        let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
2862        let mut manifest = Page::new(PageType::Header, 1);
2863        reddb_file::encode_native_metadata_overflow_header(
2864            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2865            reddb_file::NativeMetadataOverflowHeader {
2866                format_version: STORE_VERSION_V11,
2867                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2868                next_overflow_page_id: wrong_type.page_id(),
2869            },
2870        )
2871        .unwrap();
2872        pager.write_page(1, manifest).unwrap();
2873        assert!(
2874            existing_overflow_chain(pager).is_err(),
2875            "a live page must not be mistaken for metadata overflow"
2876        );
2877        assert!(
2878            read_meta_payload(pager).is_err(),
2879            "a live page must not be read as metadata overflow"
2880        );
2881
2882        let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
2883        let cycle_id = cycle.page_id();
2884        reddb_file::encode_native_metadata_overflow_continuation_header(
2885            &mut cycle.as_bytes_mut()[HEADER_SIZE..],
2886            reddb_file::NativeMetadataOverflowContinuationHeader {
2887                next_overflow_page_id: cycle_id,
2888                chunk_bytes: 1,
2889            },
2890        )
2891        .unwrap();
2892        pager.write_page(cycle_id, cycle).unwrap();
2893        let mut manifest = Page::new(PageType::Header, 1);
2894        reddb_file::encode_native_metadata_overflow_header(
2895            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2896            reddb_file::NativeMetadataOverflowHeader {
2897                format_version: STORE_VERSION_V11,
2898                total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
2899                next_overflow_page_id: cycle_id,
2900            },
2901        )
2902        .unwrap();
2903        pager.write_page(1, manifest).unwrap();
2904        assert!(
2905            read_meta_payload(pager).is_err(),
2906            "cyclic overflow must not be accepted"
2907        );
2908
2909        let mut manifest = Page::new(PageType::Header, 1);
2910        reddb_file::encode_native_metadata_overflow_header(
2911            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2912            reddb_file::NativeMetadataOverflowHeader {
2913                format_version: STORE_VERSION_V11,
2914                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2915                next_overflow_page_id: 0,
2916            },
2917        )
2918        .unwrap();
2919        pager.write_page(1, manifest).unwrap();
2920        assert!(
2921            read_meta_payload(pager).is_err(),
2922            "a truncated metadata payload must not be accepted"
2923        );
2924    }
2925
2926    #[test]
2927    fn retired_tree_pages_remain_pinned_by_a_cursor() {
2928        let directory = tempfile::tempdir().expect("temporary store");
2929        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2930        for id in 1..=100 {
2931            store.insert_auto("rows", row(id, 1)).expect("seed");
2932        }
2933        store.persist().expect("initial checkpoint");
2934        let tree = store
2935            .btree_indices
2936            .read()
2937            .get("rows")
2938            .cloned()
2939            .expect("tree");
2940        let mut cursor = tree.cursor_first().expect("cursor");
2941        drop(tree);
2942        let manager = store.get_collection("rows").expect("rows");
2943        for value in 2..=6 {
2944            manager.update(row(1, value)).expect("change");
2945            store.persist().expect("checkpoint with old reader");
2946        }
2947        let mut count = 0;
2948        while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
2949            let (entity, _) =
2950                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2951                    .expect("decode");
2952            assert_eq!(
2953                entity.data.as_row().expect("row").columns,
2954                [Value::Integer(1)]
2955            );
2956            count += 1;
2957        }
2958        assert_eq!(count, 100);
2959        drop(cursor);
2960        store.persist().expect("reclaim after reader release");
2961        assert!(store.retired_btrees.lock().is_empty());
2962    }
2963
2964    #[test]
2965    fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
2966        let directory = tempfile::tempdir().expect("temporary store");
2967        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2968        store.create_collection("rows").expect("collection");
2969        let manager = store.get_collection("rows").expect("rows");
2970        for id in (1..=1200).rev() {
2971            manager
2972                .insert(UnifiedEntity::table_row(
2973                    EntityId::new(id),
2974                    "rows",
2975                    id,
2976                    vec![Value::text("x".repeat(4096))],
2977                ))
2978                .expect("seed");
2979        }
2980        CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
2981        store.persist().expect("bounded checkpoint");
2982        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
2983        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
2984        assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
2985    }
2986
2987    #[test]
2988    fn unchanged_checkpoint_does_not_serialize_collection_rows() {
2989        let directory = tempfile::tempdir().expect("temporary store");
2990        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2991        store.create_collection("rows").expect("collection");
2992        for id in 1..=40 {
2993            store
2994                .insert(
2995                    "rows",
2996                    UnifiedEntity::table_row(
2997                        EntityId::new(id),
2998                        "rows",
2999                        id,
3000                        vec![Value::Integer(id as i64)],
3001                    ),
3002                )
3003                .expect("seed row");
3004        }
3005        store.persist().expect("initial checkpoint");
3006        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3007        store.persist().expect("unchanged checkpoint");
3008        assert_eq!(
3009            CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
3010            0,
3011            "unchanged collections must not hold the writer fence while serializing every row"
3012        );
3013        drop(store);
3014        let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
3015        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3016        reopened.persist().expect("unchanged recovered generation");
3017        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3018    }
3019
3020    #[test]
3021    fn sparse_wal_replay_does_not_reencode_complete_collections() {
3022        let directory = tempfile::tempdir().expect("temporary store");
3023        let path = directory.path().join("data.rdb");
3024        let store = UnifiedStore::open(&path).expect("paged store");
3025        for (name, first) in [("rows", 1), ("cold", 129)] {
3026            store.create_collection(name).expect("collection");
3027            for id in first..first + 128 {
3028                store
3029                    .insert(
3030                        name,
3031                        UnifiedEntity::table_row(
3032                            EntityId::new(id),
3033                            name,
3034                            id,
3035                            vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
3036                        ),
3037                    )
3038                    .expect("seed");
3039            }
3040        }
3041        store.persist().expect("durable baseline");
3042        let action = super::super::commit::StoreWalAction::upsert_entity(
3043            "rows",
3044            &row(1, 42),
3045            None,
3046            STORE_VERSION_V11,
3047        );
3048        // The acknowledged WAL image has not reached its home B-tree yet.
3049        store
3050            .commit
3051            .as_ref()
3052            .unwrap()
3053            .append_actions(&[action])
3054            .unwrap();
3055        drop(store);
3056
3057        REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
3058        let recovered = UnifiedStore::open(&path).expect("replay committed change");
3059        assert_eq!(
3060            recovered
3061                .get("rows", EntityId::new(1))
3062                .unwrap()
3063                .data
3064                .as_row()
3065                .unwrap()
3066                .columns,
3067            vec![Value::Integer(42)]
3068        );
3069        for name in ["rows", "cold"] {
3070            assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
3071        }
3072        assert_eq!(
3073            REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
3074            0,
3075            "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
3076        );
3077        for (name, first) in [("rows", 2), ("cold", 129)] {
3078            let end = if name == "rows" { 129 } else { 257 };
3079            for id in first..end {
3080                assert_eq!(
3081                    recovered
3082                        .get(name, EntityId::new(id))
3083                        .unwrap()
3084                        .data
3085                        .as_row()
3086                        .unwrap()
3087                        .columns,
3088                    vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
3089                );
3090            }
3091        }
3092
3093        drop(recovered);
3094        let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
3095        assert_eq!(
3096            reopened
3097                .get("rows", EntityId::new(1))
3098                .unwrap()
3099                .data
3100                .as_row()
3101                .unwrap()
3102                .columns,
3103            vec![Value::Integer(42)]
3104        );
3105    }
3106
3107    fn replay_test_payload(bytes: usize) -> String {
3108        let mut state = 0x1234_5678_u32;
3109        (0..bytes)
3110            .map(|_| {
3111                state ^= state << 13;
3112                state ^= state >> 17;
3113                state ^= state << 5;
3114                char::from(b'!' + (state % 90) as u8)
3115            })
3116            .collect()
3117    }
3118
3119    #[test]
3120    fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
3121        let directory = tempfile::tempdir().unwrap();
3122        let path = directory.path().join("data.rdb");
3123        let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
3124        let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3125        for id in 1..=16 {
3126            store.insert_auto("rows", row(id, 1)).unwrap();
3127        }
3128        store.persist().unwrap();
3129        // Larger than the retained-image allowance, so replay must fall back.
3130        let payload = replay_test_payload(64 * 1024);
3131        let changed =
3132            UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
3133        let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
3134        store
3135            .commit
3136            .as_ref()
3137            .unwrap()
3138            .append_actions(&[action])
3139            .unwrap();
3140        drop(store);
3141        REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
3142        let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3143        assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
3144        assert_eq!(
3145            recovered
3146                .get("rows", EntityId::new(1))
3147                .unwrap()
3148                .data
3149                .as_row()
3150                .unwrap()
3151                .columns,
3152            changed.data.as_row().unwrap().columns
3153        );
3154        drop(recovered);
3155        let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3156        assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
3157        assert_eq!(
3158            reopened
3159                .get("rows", EntityId::new(1))
3160                .unwrap()
3161                .data
3162                .as_row()
3163                .unwrap()
3164                .columns,
3165            changed.data.as_row().unwrap().columns
3166        );
3167    }
3168
3169    #[cfg(target_os = "linux")]
3170    #[test]
3171    fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
3172        const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
3173        if let Some(path) = std::env::var_os(CHILD) {
3174            let _store = UnifiedStore::open(path).unwrap();
3175            panic!("replay crash hook was not reached");
3176        }
3177        for overflow in [false, true] {
3178            for (hook, code) in [
3179                ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
3180                ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
3181            ] {
3182                let directory = tempfile::tempdir().unwrap();
3183                let path = directory.path().join("data.rdb");
3184                let store = UnifiedStore::open(&path).unwrap();
3185                for id in 1..=128 {
3186                    store.insert_auto("rows", row(id, 1)).unwrap();
3187                }
3188                store.persist().unwrap();
3189                let changed = if overflow {
3190                    UnifiedEntity::table_row(
3191                        EntityId::new(1),
3192                        "rows",
3193                        1,
3194                        vec![Value::text(replay_test_payload(16 * 1024))],
3195                    )
3196                } else {
3197                    row(1, 42)
3198                };
3199                let action =
3200                    StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
3201                store
3202                    .commit
3203                    .as_ref()
3204                    .unwrap()
3205                    .append_actions(&[action])
3206                    .unwrap();
3207                drop(store);
3208                let mut first_size = 0;
3209                for cycle in 0..12 {
3210                    let output = std::process::Command::new(std::env::current_exe().unwrap())
3211                        .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
3212                        .env(CHILD, &path).env(hook, "1").output().unwrap();
3213                    assert_eq!(
3214                        output.status.code(),
3215                        Some(code),
3216                        "overflow={overflow} hook={hook} cycle={cycle}: {}",
3217                        String::from_utf8_lossy(&output.stderr)
3218                    );
3219                    let size = std::fs::metadata(&path).unwrap().len();
3220                    if cycle == 0 {
3221                        first_size = size;
3222                    }
3223                    assert!(
3224                        size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
3225                        "repeated recovery must reuse reservations: {first_size} -> {size}"
3226                    );
3227                }
3228                for _ in 0..2 {
3229                    let recovered = UnifiedStore::open(&path).unwrap();
3230                    assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
3231                    for id in 1..=128 {
3232                        let expected = if id == 1 { changed.clone() } else { row(id, 1) };
3233                        assert_eq!(
3234                            recovered
3235                                .get("rows", EntityId::new(id))
3236                                .unwrap()
3237                                .data
3238                                .as_row()
3239                                .unwrap()
3240                                .columns,
3241                            expected.data.as_row().unwrap().columns
3242                        );
3243                    }
3244                }
3245            }
3246        }
3247    }
3248
3249    #[test]
3250    fn paged_reopen_preserves_stored_sequence_identity() {
3251        let directory = tempfile::tempdir().expect("temporary store");
3252        let path = directory.path().join("data.rdb");
3253        let store = UnifiedStore::open(&path).expect("paged store");
3254        store.create_collection("rows").expect("collection");
3255        for id in [30, 10, 20] {
3256            store.insert("rows", row(id, id as i64)).expect("insert");
3257        }
3258        store.persist().expect("checkpoint");
3259        let sequences = [10, 20, 30].map(|id| {
3260            store
3261                .get("rows", EntityId::new(id))
3262                .expect("row")
3263                .sequence_id
3264        });
3265        drop(store);
3266        let reopened = UnifiedStore::open(&path).expect("reopen");
3267        let recovered = [10, 20, 30].map(|id| {
3268            reopened
3269                .get("rows", EntityId::new(id))
3270                .expect("row")
3271                .sequence_id
3272        });
3273        assert_eq!(
3274            recovered, sequences,
3275            "opening a checkpoint must not rewrite stored row identities"
3276        );
3277    }
3278
3279    #[test]
3280    fn auto_insert_persists_the_assigned_sequence_identity() {
3281        let directory = tempfile::tempdir().expect("temporary store");
3282        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3283        for id in 1u64..=3 {
3284            store
3285                .insert_auto("rows", row(id, id as i64))
3286                .expect("insert");
3287        }
3288        let manager = store.get_collection("rows").expect("rows");
3289        let tree = store
3290            .btree_indices
3291            .read()
3292            .get("rows")
3293            .cloned()
3294            .expect("tree");
3295        for id in 1u64..=3 {
3296            let bytes = tree
3297                .get(&id.to_be_bytes())
3298                .expect("lookup")
3299                .expect("stored row");
3300            let (persisted, _) =
3301                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
3302                    .expect("decode");
3303            assert_eq!(
3304                persisted.sequence_id,
3305                manager
3306                    .get(EntityId::new(id))
3307                    .expect("live row")
3308                    .sequence_id
3309            );
3310        }
3311    }
3312
3313    #[test]
3314    fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
3315        let directory = tempfile::tempdir().expect("temporary store");
3316        let path = directory.path().join("data.rdb");
3317        let store = UnifiedStore::open(&path).expect("paged store");
3318        store.insert_auto("rows", row(1, 1)).expect("seed");
3319        store.persist().expect("checkpoint");
3320        store.insert_auto("rows", row(2, 2)).expect("append");
3321        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3322        store.persist().expect("inline append checkpoint");
3323        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3324        store
3325            .get_collection("rows")
3326            .expect("rows")
3327            .update(row(1, 99))
3328            .expect("untracked edit");
3329        store.insert_auto("rows", row(3, 3)).expect("next append");
3330        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3331        store.persist().expect("untracked edit checkpoint");
3332        assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
3333        drop(store);
3334        let reopened = UnifiedStore::open(&path).expect("reopen");
3335        assert_eq!(
3336            reopened
3337                .get("rows", EntityId::new(1))
3338                .expect("row")
3339                .data
3340                .as_row()
3341                .expect("table")
3342                .columns,
3343            vec![Value::Integer(99)]
3344        );
3345    }
3346
3347    #[test]
3348    fn replayed_trees_are_reusable_after_durable_reconciliation() {
3349        let directory = tempfile::tempdir().expect("temporary store");
3350        let path = directory.path().join("data.rdb");
3351        let store = UnifiedStore::open(&path).expect("paged store");
3352        store.insert_auto("rows", row(1, 1)).expect("seed");
3353        store.persist().expect("checkpoint");
3354        store
3355            .insert_auto("rows", row(2, 2))
3356            .expect("WAL-backed append");
3357        drop(store);
3358        let reopened = UnifiedStore::open(&path).expect("replay");
3359        assert!(reopened.get("rows", EntityId::new(2)).is_some());
3360        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3361        reopened.persist().expect("checkpoint reconciled tree");
3362        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3363    }
3364
3365    #[test]
3366    fn legacy_system_catalog_and_wal_migrate_without_cold_boot_rebuild() {
3367        use crate::storage::wal::{WalRecord, WalWriter};
3368
3369        let directory = tempfile::tempdir().expect("temporary store");
3370        let path = directory.path().join("data.rdb");
3371        let config = UnifiedStoreConfig {
3372            page_cache_slots: Some(32),
3373            resident_memory_bytes_max: Some(2 * 1024 * 1024),
3374            ..UnifiedStoreConfig::default()
3375        };
3376        let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3377            codec_version: 1,
3378            definition: [91; 32],
3379        };
3380        let baseline_index_root;
3381        {
3382            let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3383            store
3384                .create_collection("red_config")
3385                .expect("system collection");
3386            store
3387                .insert_auto(
3388                    "red_config",
3389                    UnifiedEntity::table_row(
3390                        EntityId::new(1),
3391                        "red_config",
3392                        1,
3393                        vec![Value::text("checkpoint")],
3394                    ),
3395                )
3396                .expect("legacy-compatible insert");
3397            store
3398                .register_secondary_index(
3399                    "red_config",
3400                    "by_id",
3401                    index_identity.clone(),
3402                    Arc::new(SystemNameProjection),
3403                )
3404                .expect("secondary index");
3405            store.persist().expect("baseline checkpoint");
3406            baseline_index_root = store
3407                .secondary_index("red.config", "by_id", &index_identity)
3408                .expect("published secondary index")
3409                .root_page_id();
3410        }
3411
3412        // Recreate a pre-migration catalog and operational manifest. The two
3413        // spellings are deliberately equal-length so the test can preserve
3414        // every root and checksum-protected page layout except the name.
3415        {
3416            let pager = Pager::open(
3417                &path,
3418                PagerConfig {
3419                    disk_residency: true,
3420                    ..PagerConfig::default()
3421                },
3422            )
3423            .expect("open pager");
3424            let mut page = pager.read_page(1).expect("catalog page");
3425            let bytes = page.as_bytes_mut();
3426            let canonical = b"red.config";
3427            let legacy = b"red_config";
3428            let mut replacements = 0;
3429            for offset in 0..=bytes.len() - canonical.len() {
3430                if &bytes[offset..offset + canonical.len()] == canonical {
3431                    bytes[offset..offset + legacy.len()].copy_from_slice(legacy);
3432                    replacements += 1;
3433                }
3434            }
3435            assert!(replacements >= 1, "catalog must contain the canonical name");
3436            pager.write_page(1, page).expect("write legacy catalog");
3437            pager.sync().expect("sync legacy catalog");
3438        }
3439        crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3440            .rename_collections(&[("red.config", "red_config")])
3441            .expect("restore legacy operational name");
3442
3443        // Append an acknowledged transaction written by a legacy binary. Its
3444        // payload must replay into the canonical manager during migration.
3445        let wal_upsert = reddb_file::encode_store_wal_action_frame(
3446            &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
3447                collection: "red_config".to_string(),
3448                record: UnifiedStore::serialize_entity_record(
3449                    &UnifiedEntity::table_row(
3450                        EntityId::new(2),
3451                        "red_config",
3452                        2,
3453                        vec![Value::text("legacy WAL")],
3454                    ),
3455                    None,
3456                    STORE_VERSION_CURRENT,
3457                ),
3458            },
3459        )
3460        .expect("legacy WAL action");
3461        let wal_delete = reddb_file::encode_store_wal_action_frame(
3462            &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
3463                collection: "red_config".to_string(),
3464                entity_id: EntityId::new(1).raw(),
3465            },
3466        )
3467        .expect("legacy WAL delete");
3468        let wal_path = reddb_file::layout::unified_wal_path(&path);
3469        {
3470            let mut writer = WalWriter::open(&wal_path).expect("open WAL");
3471            writer
3472                .append(&WalRecord::TxCommitBatch {
3473                    tx_id: 77,
3474                    actions: vec![wal_upsert, wal_delete],
3475                })
3476                .expect("append legacy transaction");
3477            writer.sync().expect("sync legacy transaction");
3478        }
3479
3480        let (root_after_recovery, pages_after_recovery) = {
3481            let recovered =
3482                UnifiedStore::open_with_config(&path, config.clone()).expect("migrate and recover");
3483            assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3484            assert!(
3485                recovered.get("red.config", EntityId::new(1)).is_none(),
3486                "legacy WAL deletes must target the canonical collection"
3487            );
3488            assert_eq!(
3489                recovered
3490                    .get("red.config", EntityId::new(2))
3491                    .expect("WAL row")
3492                    .kind
3493                    .collection(),
3494                "red.config"
3495            );
3496            let root = recovered
3497                .collection_root_page("red.config")
3498                .expect("published canonical root");
3499            assert_eq!(
3500                recovered
3501                    .secondary_index("red.config", "by_id", &index_identity)
3502                    .expect("recovered secondary index")
3503                    .root_page_id(),
3504                baseline_index_root,
3505                "name migration must reuse the secondary-index root"
3506            );
3507            let pages = recovered
3508                .pager()
3509                .expect("pager")
3510                .page_count()
3511                .expect("page count");
3512            (root, pages)
3513        };
3514
3515        // Simulate a crash/import boundary where the catalog rename is
3516        // durable but the operational manifest still has the legacy key.
3517        crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3518            .rename_collections(&[("red.config", "red_config")])
3519            .expect("restore stale manifest identity");
3520
3521        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3522        let cold = UnifiedStore::open_with_config(&path, config).expect("second cold boot");
3523        assert_eq!(
3524            cold.collection_root_page("red.config"),
3525            Some(root_after_recovery),
3526            "an idempotent cold boot must reuse the published data root"
3527        );
3528        assert_eq!(
3529            cold.pager()
3530                .expect("pager")
3531                .page_count()
3532                .expect("page count"),
3533            pages_after_recovery,
3534            "cold boot must not allocate replacement index trees"
3535        );
3536        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3537        assert_eq!(
3538            cold.secondary_index("red.config", "by_id", &index_identity)
3539                .expect("secondary index after cold boot")
3540                .root_page_id(),
3541            baseline_index_root
3542        );
3543        assert!(
3544            crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3545                .recover_or_bootstrap(&["red.config".to_string()])
3546                .expect("canonical manifest")
3547                .is_empty()
3548        );
3549    }
3550
3551    struct DuplicateSystemCatalogFixture {
3552        _directory: tempfile::TempDir,
3553        path: std::path::PathBuf,
3554        config: UnifiedStoreConfig,
3555        pages_before: u32,
3556        legacy_root: Option<u32>,
3557        legacy_key_root: Option<u32>,
3558        legacy_secondary_root: Option<u32>,
3559        index_identity: super::super::secondary_index::SecondaryIndexIdentity,
3560    }
3561
3562    fn duplicate_system_catalog_fixture(
3563        legacy_populated: bool,
3564        canonical_populated: bool,
3565        index_legacy: bool,
3566    ) -> DuplicateSystemCatalogFixture {
3567        duplicate_system_catalog_fixture_with_ref(
3568            legacy_populated,
3569            canonical_populated,
3570            index_legacy,
3571            false,
3572        )
3573    }
3574
3575    fn duplicate_system_catalog_fixture_with_ref(
3576        legacy_populated: bool,
3577        canonical_populated: bool,
3578        index_legacy: bool,
3579        cross_reference_legacy: bool,
3580    ) -> DuplicateSystemCatalogFixture {
3581        duplicate_system_catalog_fixture_with_key(
3582            legacy_populated,
3583            canonical_populated,
3584            index_legacy,
3585            cross_reference_legacy,
3586            None,
3587        )
3588    }
3589
3590    fn config_recovery_row(id: u64, key: &str, value: i64) -> UnifiedEntity {
3591        UnifiedEntity::new(
3592            EntityId::new(id),
3593            EntityKind::TableRow {
3594                table: Arc::from("red.config"),
3595                row_id: id,
3596            },
3597            EntityData::Row(crate::storage::RowData::with_names(
3598                vec![Value::text(key), Value::Integer(value)],
3599                vec!["key".into(), "value".into()],
3600            )),
3601        )
3602    }
3603
3604    fn duplicate_system_catalog_fixture_with_key(
3605        legacy_populated: bool,
3606        canonical_populated: bool,
3607        index_legacy: bool,
3608        cross_reference_legacy: bool,
3609        config_key: Option<&str>,
3610    ) -> DuplicateSystemCatalogFixture {
3611        duplicate_system_catalog_fixture_with_aliases(
3612            legacy_populated,
3613            canonical_populated,
3614            index_legacy,
3615            cross_reference_legacy,
3616            config_key,
3617            None,
3618        )
3619    }
3620
3621    fn duplicate_system_catalog_fixture_with_aliases(
3622        legacy_populated: bool,
3623        canonical_populated: bool,
3624        index_legacy: bool,
3625        cross_reference_legacy: bool,
3626        config_key: Option<&str>,
3627        alias_case: Option<&str>,
3628    ) -> DuplicateSystemCatalogFixture {
3629        let directory = tempfile::tempdir().expect("temporary store");
3630        let path = directory.path().join("data.rdb");
3631        let config = UnifiedStoreConfig {
3632            page_cache_slots: Some(32),
3633            resident_memory_bytes_max: Some(2 * 1024 * 1024),
3634            ..UnifiedStoreConfig::default()
3635        };
3636        let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3637            codec_version: 1,
3638            definition: [73; 32],
3639        };
3640        let (legacy_root, legacy_key_root, legacy_secondary_root);
3641        {
3642            let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3643            store
3644                .create_collection("cfg_legacy")
3645                .expect("legacy source collection");
3646            if legacy_populated {
3647                store
3648                    .insert_auto(
3649                        "cfg_legacy",
3650                        config_key.map_or_else(
3651                            || {
3652                                UnifiedEntity::table_row(
3653                                    EntityId::new(1),
3654                                    "red_config",
3655                                    1,
3656                                    vec![Value::text("legacy")],
3657                                )
3658                            },
3659                            |key| config_recovery_row(1, key, 1),
3660                        ),
3661                    )
3662                    .expect("legacy row");
3663            }
3664            store
3665                .create_collection("cfg.canon_")
3666                .expect("canonical source collection");
3667            if canonical_populated {
3668                store
3669                    .insert_auto(
3670                        "cfg.canon_",
3671                        config_key.map_or_else(
3672                            || {
3673                                UnifiedEntity::table_row(
3674                                    EntityId::new(2),
3675                                    "red.config",
3676                                    2,
3677                                    vec![Value::text("canonical")],
3678                                )
3679                            },
3680                            |key| config_recovery_row(2, key, 2),
3681                        ),
3682                    )
3683                    .expect("canonical row");
3684            }
3685            if cross_reference_legacy {
3686                store
3687                    .add_cross_ref(
3688                        "cfg.canon_",
3689                        EntityId::new(2),
3690                        "cfg_legacy",
3691                        EntityId::new(1),
3692                        RefType::RowToNode,
3693                        1.0,
3694                    )
3695                    .expect("reference to legacy config");
3696            }
3697            if index_legacy {
3698                store
3699                    .register_secondary_index(
3700                        "cfg_legacy",
3701                        "by_id",
3702                        index_identity.clone(),
3703                        Arc::new(SystemNameProjection),
3704                    )
3705                    .expect("legacy secondary index");
3706            }
3707
3708            if let Some(case) = alias_case {
3709                use crate::storage::{EntityData, RowData};
3710                let pairs = [
3711                    (
3712                        "old_stats",
3713                        "new.query.stats_catalog",
3714                        "red_stats",
3715                        "red.query.stats_catalog",
3716                    ),
3717                    (
3718                        "old_index_registry",
3719                        "new.index.registry",
3720                        "red_index_registry",
3721                        "red.index.registry",
3722                    ),
3723                    (
3724                        "old_queue_meta",
3725                        "new.queue.meta",
3726                        "red_queue_meta",
3727                        "red.queue.meta",
3728                    ),
3729                    (
3730                        "old_materialized_view_defs",
3731                        "new.materialized_view.definitions",
3732                        "red_materialized_view_defs",
3733                        "red.materialized_view.definitions",
3734                    ),
3735                ];
3736                for (pair_index, (old, new, legacy, canonical)) in pairs.iter().enumerate() {
3737                    for (is_old, name, table) in [(true, *old, *legacy), (false, *new, *canonical)]
3738                    {
3739                        store.create_collection(name).unwrap();
3740                        let text = |s: &str| Value::text(s);
3741                        let mut fields = match pair_index {
3742                            0 => vec![
3743                                ("kind", text("table")),
3744                                ("table", text("users")),
3745                                ("row_count", Value::Integer(if is_old { 1 } else { 2 })),
3746                                ("avg_row_size", Value::Integer(20)),
3747                                ("page_count", Value::Integer(1)),
3748                            ],
3749                            1 => vec![
3750                                ("collection", text("users")),
3751                                (
3752                                    "name",
3753                                    text(if is_old {
3754                                        "legacy_only"
3755                                    } else {
3756                                        "canonical_only"
3757                                    }),
3758                                ),
3759                                ("columns", text("id")),
3760                                ("method", text("hash")),
3761                                ("resolution", Value::Integer(0)),
3762                                ("unique", Value::Boolean(false)),
3763                                ("dropped", Value::Boolean(false)),
3764                            ],
3765                            2 => vec![
3766                                ("kind", text("queue_group")),
3767                                ("queue", text("jobs")),
3768                                ("group", text("workers")),
3769                                (
3770                                    "created_at_ns",
3771                                    Value::UnsignedInteger(if is_old { 1 } else { 2 }),
3772                                ),
3773                            ],
3774                            _ => vec![
3775                                ("name", text("view")),
3776                                ("refresh_every_ms", Value::UnsignedInteger(1000)),
3777                                ("retention_duration_ms", Value::UnsignedInteger(0)),
3778                                ("source_collections", text("users")),
3779                                (
3780                                    "source_sql",
3781                                    text(if case == "view-conflict" && !is_old {
3782                                        "SELECT 2"
3783                                    } else {
3784                                        "SELECT 1"
3785                                    }),
3786                                ),
3787                            ],
3788                        };
3789                        if case == "queue-delivery" && pair_index == 2 && is_old {
3790                            fields[0].1 = text("queue_pending");
3791                        }
3792                        let id = if is_old {
3793                            10000 + pair_index as u64
3794                        } else {
3795                            100 + pair_index as u64
3796                        };
3797                        let entity = UnifiedEntity::new(
3798                            EntityId::new(id),
3799                            EntityKind::TableRow {
3800                                table: Arc::from(table),
3801                                row_id: id,
3802                            },
3803                            EntityData::Row(RowData {
3804                                columns: vec![],
3805                                named: Some(
3806                                    fields.into_iter().map(|(k, v)| (k.to_owned(), v)).collect(),
3807                                ),
3808                                schema: None,
3809                            }),
3810                        );
3811                        store.insert_auto(name, entity).unwrap();
3812                    }
3813                }
3814            }
3815            store.persist().expect("checkpoint duplicate catalog");
3816            legacy_root = store.collection_root_page("cfg_legacy");
3817            legacy_key_root = store
3818                .key_indices
3819                .read()
3820                .get("cfg_legacy")
3821                .map(|index| index.tree.root_page_id());
3822            legacy_secondary_root = index_legacy.then(|| {
3823                store
3824                    .secondary_index("cfg_legacy", "by_id", &index_identity)
3825                    .expect("legacy secondary root")
3826                    .root_page_id()
3827            });
3828        }
3829
3830        // Recreate the production shape: a populated legacy collection and an
3831        // empty canonical collection published by an intermediate binary.
3832        // Every spelling has the same byte length, so roots and checksummed
3833        // page layouts stay unchanged while only the catalog identities move.
3834        {
3835            let pager = Pager::open(
3836                &path,
3837                PagerConfig {
3838                    disk_residency: true,
3839                    ..PagerConfig::default()
3840                },
3841            )
3842            .expect("open pager");
3843            let mut page = pager.read_page(1).expect("catalog page");
3844            let bytes = page.as_bytes_mut();
3845            let mut names = vec![("cfg_legacy", "red_config"), ("cfg.canon_", "red.config")];
3846            if alias_case.is_some() {
3847                names.extend([
3848                    ("old_stats", "red_stats"),
3849                    ("new.query.stats_catalog", "red.query.stats_catalog"),
3850                    ("old_index_registry", "red_index_registry"),
3851                    ("new.index.registry", "red.index.registry"),
3852                    ("old_queue_meta", "red_queue_meta"),
3853                    ("new.queue.meta", "red.queue.meta"),
3854                    ("old_materialized_view_defs", "red_materialized_view_defs"),
3855                    (
3856                        "new.materialized_view.definitions",
3857                        "red.materialized_view.definitions",
3858                    ),
3859                ]);
3860            }
3861            for (source, destination) in names {
3862                let (source, destination) = (source.as_bytes(), destination.as_bytes());
3863                let mut replacements = 0;
3864                for offset in 0..=bytes.len() - source.len() {
3865                    if &bytes[offset..offset + source.len()] == source {
3866                        bytes[offset..offset + destination.len()].copy_from_slice(destination);
3867                        replacements += 1;
3868                    }
3869                }
3870                assert!(replacements >= 1, "catalog must contain the source name");
3871            }
3872            pager.write_page(1, page).expect("write duplicate catalog");
3873            pager.sync().expect("sync duplicate catalog");
3874            if alias_case.is_some() {
3875                let mut metadata = read_meta_payload(&pager).unwrap().unwrap();
3876                let resume = reddb_file::native_resume::decode_catalog_resume(&metadata)
3877                    .unwrap()
3878                    .unwrap();
3879                let trailer = metadata
3880                    .windows(4)
3881                    .rposition(|bytes| bytes == b"RDRS")
3882                    .unwrap();
3883                metadata.truncate(trailer);
3884                reddb_file::native_resume::encode_catalog_resume(&mut metadata, &resume).unwrap();
3885                publish_paged_metadata(&pager, &metadata).unwrap();
3886                pager.sync().unwrap();
3887            }
3888        }
3889        crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3890            .rename_collections(&[("cfg_legacy", "red_config"), ("cfg.canon_", "red.config")])
3891            .expect("publish duplicate operational names");
3892
3893        if alias_case.is_some() {
3894            crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3895                .rename_collections(&[
3896                    ("old_stats", "red_stats"),
3897                    ("new.query.stats_catalog", "red.query.stats_catalog"),
3898                    ("old_index_registry", "red_index_registry"),
3899                    ("new.index.registry", "red.index.registry"),
3900                    ("old_queue_meta", "red_queue_meta"),
3901                    ("new.queue.meta", "red.queue.meta"),
3902                    ("old_materialized_view_defs", "red_materialized_view_defs"),
3903                    (
3904                        "new.materialized_view.definitions",
3905                        "red.materialized_view.definitions",
3906                    ),
3907                ])
3908                .unwrap();
3909        }
3910
3911        let pages_before = Pager::open(
3912            &path,
3913            PagerConfig {
3914                disk_residency: true,
3915                ..PagerConfig::default()
3916            },
3917        )
3918        .expect("inspect pager")
3919        .page_count()
3920        .expect("page count");
3921        DuplicateSystemCatalogFixture {
3922            _directory: directory,
3923            path,
3924            config,
3925            pages_before,
3926            legacy_root,
3927            legacy_key_root,
3928            legacy_secondary_root,
3929            index_identity,
3930        }
3931    }
3932
3933    fn remove_fixture_catalog_resume(fixture: &DuplicateSystemCatalogFixture) {
3934        let pager = Pager::open(
3935            &fixture.path,
3936            PagerConfig {
3937                disk_residency: true,
3938                ..PagerConfig::default()
3939            },
3940        )
3941        .expect("fixture pager");
3942        let mut metadata = read_meta_payload(&pager).unwrap().unwrap();
3943        assert!(reddb_file::native_resume::decode_catalog_resume(&metadata)
3944            .unwrap()
3945            .is_some());
3946        let trailer = metadata
3947            .windows(4)
3948            .rposition(|bytes| bytes == b"RDRS")
3949            .unwrap();
3950        metadata.truncate(trailer);
3951        publish_paged_metadata(&pager, &metadata).unwrap();
3952        pager.sync().unwrap();
3953        assert!(reddb_file::native_resume::decode_catalog_resume(
3954            &read_meta_payload(&pager).unwrap().unwrap()
3955        )
3956        .unwrap()
3957        .is_none());
3958    }
3959
3960    #[test]
3961    fn no_resume_catalog_keeps_populated_config_beside_zero_root_alias() {
3962        for (legacy_populated, canonical_populated) in
3963            [(true, false), (false, true), (false, false)]
3964        {
3965            let fixture =
3966                duplicate_system_catalog_fixture(legacy_populated, canonical_populated, false);
3967            remove_fixture_catalog_resume(&fixture);
3968            let store = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
3969                .expect("resolve physically empty alias without resume counters");
3970            assert_eq!(store.list_collections(), vec!["red.config".to_owned()]);
3971            assert_eq!(
3972                store.get("red.config", EntityId::new(1)).is_some(),
3973                legacy_populated
3974            );
3975            assert_eq!(
3976                store.get("red.config", EntityId::new(2)).is_some(),
3977                canonical_populated
3978            );
3979            if legacy_populated {
3980                assert_eq!(
3981                    store.collection_root_page("red.config"),
3982                    fixture.legacy_root
3983                );
3984            }
3985            store.persist().expect("checkpoint canonical catalog");
3986            drop(store);
3987            let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3988                .expect("cold reopen canonical catalog");
3989            assert_eq!(cold.list_collections(), vec!["red.config".to_owned()]);
3990            assert_eq!(
3991                cold.get("red.config", EntityId::new(1)).is_some(),
3992                legacy_populated
3993            );
3994            assert_eq!(
3995                cold.get("red.config", EntityId::new(2)).is_some(),
3996                canonical_populated
3997            );
3998        }
3999    }
4000
4001    #[test]
4002    fn no_resume_catalog_refuses_two_populated_config_roots() {
4003        let fixture = duplicate_system_catalog_fixture(true, true, false);
4004        remove_fixture_catalog_resume(&fixture);
4005        let error = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4006            .err()
4007            .expect("two populated roots require reconciliation");
4008        assert!(error.to_string().contains("both legacy collection"));
4009    }
4010
4011    fn alias_duplicate_system_catalog_state_with(
4012        fixture: &DuplicateSystemCatalogFixture,
4013        mutate_canonical: impl FnOnce(&mut reddb_file::native_resume::CollectionResume),
4014    ) {
4015        let pager = Pager::open(
4016            &fixture.path,
4017            PagerConfig {
4018                disk_residency: true,
4019                ..PagerConfig::default()
4020            },
4021        )
4022        .expect("open duplicate catalog pager");
4023        let mut metadata = read_meta_payload(&pager)
4024            .expect("read duplicate catalog")
4025            .expect("published duplicate catalog");
4026        let mut resume = reddb_file::native_resume::decode_catalog_resume(&metadata)
4027            .expect("decode duplicate resume")
4028            .expect("prepared duplicate resume");
4029        let legacy_state = resume
4030            .collections
4031            .get("red_config")
4032            .expect("legacy resume state")
4033            .clone();
4034        let mut canonical_state = legacy_state.clone();
4035        mutate_canonical(&mut canonical_state);
4036        resume
4037            .collections
4038            .insert("red.config".to_string(), canonical_state);
4039
4040        let header = reddb_file::decode_native_paged_metadata_header(&metadata)
4041            .expect("decode metadata header")
4042            .expect("native metadata header");
4043        let mut position = reddb_file::METADATA_HEADER_BYTES;
4044        let mut replaced_root = false;
4045        for _ in 0..header.collection_count {
4046            let root = reddb_file::decode_native_paged_collection_root(&metadata, &mut position)
4047                .expect("decode collection root");
4048            if root.collection == "red.config" {
4049                metadata[position - 4..position]
4050                    .copy_from_slice(&legacy_state.root_page.to_le_bytes());
4051                replaced_root = true;
4052            }
4053        }
4054        assert!(replaced_root, "canonical catalog root must be present");
4055
4056        let trailer = metadata
4057            .windows(4)
4058            .rposition(|bytes| bytes == b"RDRS")
4059            .expect("catalog resume trailer");
4060        metadata.truncate(trailer);
4061        reddb_file::native_resume::encode_catalog_resume(&mut metadata, &resume)
4062            .expect("encode aliased resume");
4063        publish_paged_metadata(&pager, &metadata).expect("publish aliased catalog");
4064    }
4065
4066    fn alias_duplicate_system_catalog_state(fixture: &DuplicateSystemCatalogFixture) {
4067        alias_duplicate_system_catalog_state_with(fixture, |_| {});
4068    }
4069
4070    #[test]
4071    fn populated_legacy_system_collection_replaces_empty_canonical_duplicate() {
4072        let fixture = duplicate_system_catalog_fixture(true, false, true);
4073        let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
4074            .expect("resolve empty duplicate");
4075        assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4076        assert_eq!(
4077            recovered
4078                .get("red.config", EntityId::new(1))
4079                .expect("preserved legacy row")
4080                .kind
4081                .collection(),
4082            "red.config"
4083        );
4084        assert_eq!(
4085            recovered.collection_root_page("red.config"),
4086            fixture.legacy_root,
4087            "the populated data root must be reused"
4088        );
4089        assert_eq!(
4090            recovered
4091                .key_indices
4092                .read()
4093                .get("red.config")
4094                .map(|index| index.tree.root_page_id()),
4095            fixture.legacy_key_root,
4096            "the populated key-index root must be reused"
4097        );
4098        assert_eq!(
4099            recovered
4100                .secondary_index("red.config", "by_id", &fixture.index_identity)
4101                .expect("surviving secondary index")
4102                .root_page_id(),
4103            fixture
4104                .legacy_secondary_root
4105                .expect("fixture secondary root")
4106        );
4107        drop(recovered);
4108
4109        let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4110            .expect("idempotent cold boot");
4111        assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
4112        assert_eq!(
4113            cold.pager()
4114                .expect("pager")
4115                .page_count()
4116                .expect("page count"),
4117            fixture.pages_before,
4118            "resolving an empty duplicate must not extend the file"
4119        );
4120    }
4121
4122    #[test]
4123    fn populated_canonical_system_collection_replaces_empty_legacy_duplicate() {
4124        let fixture = duplicate_system_catalog_fixture(false, true, false);
4125        let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4126            .expect("retain populated canonical duplicate");
4127        assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4128        assert_eq!(
4129            recovered
4130                .get("red.config", EntityId::new(2))
4131                .expect("preserved canonical row")
4132                .kind
4133                .collection(),
4134            "red.config"
4135        );
4136        assert!(recovered.get("red.config", EntityId::new(1)).is_none());
4137    }
4138
4139    #[test]
4140    fn empty_canonical_system_collection_replaces_empty_legacy_duplicate() {
4141        let fixture = duplicate_system_catalog_fixture(false, false, false);
4142        let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4143            .expect("prefer canonical empty duplicate");
4144        assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4145        assert_eq!(
4146            recovered
4147                .get_collection("red.config")
4148                .expect("canonical collection")
4149                .count(),
4150            0
4151        );
4152    }
4153
4154    #[test]
4155    fn populated_system_collection_duplicates_still_fail_closed() {
4156        let fixture = duplicate_system_catalog_fixture(true, true, false);
4157        let error = match UnifiedStore::open_with_config(&fixture.path, fixture.config) {
4158            Ok(_) => panic!("two populated identities are ambiguous"),
4159            Err(error) => error,
4160        };
4161        assert!(
4162            error
4163                .to_string()
4164                .contains("both populated legacy collection 'red_config' and canonical collection 'red.config'"),
4165            "unexpected error: {error}"
4166        );
4167        assert!(
4168            error.to_string().contains(
4169                "data_root_match=false, key_index_root_match=true, secondary_index_state_match=true"
4170            ),
4171            "the error must identify which physical state diverged without exposing page IDs: {error}"
4172        );
4173    }
4174
4175    #[test]
4176    fn explicit_compaction_retires_populated_legacy_system_config() {
4177        let fixture = duplicate_system_catalog_fixture(true, true, false);
4178        let destination = fixture._directory.path().join("canonical-system-config");
4179
4180        let report = crate::storage::compact::compact_paged_canonical_system_config(
4181            &fixture.path,
4182            &destination,
4183        )
4184        .expect("rewrite duplicate system config");
4185        assert!(report.records > 0);
4186
4187        let rewritten_path = destination.join("data.rdb");
4188        let rewritten = UnifiedStore::open_with_config(&rewritten_path, fixture.config.clone())
4189            .expect("open canonical rewrite");
4190        assert_eq!(rewritten.list_collections(), vec!["red.config".to_string()]);
4191        assert!(
4192            rewritten.get("red.config", EntityId::new(1)).is_none(),
4193            "the obsolete legacy tree must not leak into the canonical collection"
4194        );
4195        assert_eq!(
4196            rewritten
4197                .get("red.config", EntityId::new(2))
4198                .expect("canonical row survives")
4199                .kind
4200                .collection(),
4201            "red.config"
4202        );
4203        drop(rewritten);
4204
4205        let cold = UnifiedStore::open_with_config(&rewritten_path, fixture.config)
4206            .expect("cold reopen canonical rewrite");
4207        assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
4208        assert_eq!(
4209            cold.get_collection("red.config")
4210                .expect("canonical collection")
4211                .count(),
4212            1
4213        );
4214        assert!(
4215            crate::storage::operational_manifest::OperationalManifest::for_db_path(&rewritten_path)
4216                .recover_or_bootstrap(&["red.config".to_string()])
4217                .expect("canonical operational manifest")
4218                .is_empty()
4219        );
4220    }
4221
4222    #[test]
4223    fn canonical_system_config_recovery_replays_pending_unrelated_wal() {
4224        use crate::storage::wal::{WalRecord, WalWriter};
4225
4226        let fixture = duplicate_system_catalog_fixture(true, true, true);
4227        let wal = reddb_file::unified_wal_path(&fixture.path);
4228        let action = reddb_file::encode_store_wal_action_frame(
4229            &reddb_file::StoreWalActionFrame::CreateCollection {
4230                name: "pending_collection".into(),
4231            },
4232        )
4233        .expect("encode pending action");
4234        let canonical_update = reddb_file::encode_store_wal_action_frame(
4235            &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
4236                collection: "red.config".into(),
4237                record: UnifiedStore::serialize_entity_record(
4238                    &UnifiedEntity::table_row(
4239                        EntityId::new(3),
4240                        "red.config",
4241                        3,
4242                        vec![Value::text("canonical WAL")],
4243                    ),
4244                    None,
4245                    STORE_VERSION_CURRENT,
4246                ),
4247            },
4248        )
4249        .expect("encode canonical update");
4250        {
4251            let mut writer = WalWriter::open(&wal).expect("open WAL");
4252            writer
4253                .append(&WalRecord::TxCommitBatch {
4254                    tx_id: 999,
4255                    actions: vec![action, canonical_update],
4256                })
4257                .expect("append committed WAL");
4258            writer.sync().expect("sync WAL");
4259        }
4260        let original = std::fs::read(&fixture.path).expect("source bytes");
4261        let original_wal = std::fs::read(&wal).expect("WAL bytes");
4262        let destination = fixture._directory.path().join("recovered-canonical-config");
4263
4264        let default_destination = fixture._directory.path().join("default-recovery");
4265        let error = crate::storage::compact::recover_paged_wal(&fixture.path, &default_destination)
4266            .expect_err("ordinary recovery must reject divergent identities");
4267        assert!(error
4268            .to_string()
4269            .contains("both populated legacy collection"));
4270
4271        crate::storage::compact::recover_paged_wal_canonical_system_config(
4272            &fixture.path,
4273            &destination,
4274        )
4275        .expect("recover pending WAL and retain canonical config");
4276
4277        assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4278        assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4279        for _ in 0..2 {
4280            let recovered = UnifiedStore::open_with_config(
4281                destination.join("data.rdb"),
4282                fixture.config.clone(),
4283            )
4284            .expect("open recovered store");
4285            assert!(recovered.get_collection("pending_collection").is_some());
4286            assert!(recovered.get("red.config", EntityId::new(1)).is_none());
4287            assert!(recovered.get("red.config", EntityId::new(2)).is_some());
4288            assert!(recovered.get("red.config", EntityId::new(3)).is_some());
4289        }
4290    }
4291
4292    #[test]
4293    fn canonical_system_catalog_recovery_reconciles_all_metadata_and_keeps_missing_index() {
4294        use crate::storage::wal::{WalRecord, WalWriter};
4295        for case in [
4296            "valid",
4297            "view-conflict",
4298            "queue-delivery",
4299            "legacy-wal",
4300            "canonical-wal",
4301        ] {
4302            let fixture = duplicate_system_catalog_fixture_with_aliases(
4303                true,
4304                true,
4305                true,
4306                false,
4307                Some("red.system.pid"),
4308                Some(case),
4309            );
4310            let wal = reddb_file::unified_wal_path(&fixture.path);
4311            {
4312                let mut writer = WalWriter::open(&wal).unwrap();
4313                let name = match case {
4314                    "legacy-wal" => "red_index_registry",
4315                    "canonical-wal" => "red.index.registry",
4316                    _ => "durable_rows",
4317                };
4318                writer
4319                    .append(&WalRecord::TxCommitBatch {
4320                        tx_id: 1001,
4321                        actions: vec![reddb_file::encode_store_wal_action_frame(
4322                            &reddb_file::StoreWalActionFrame::CreateCollection {
4323                                name: name.into(),
4324                            },
4325                        )
4326                        .unwrap()],
4327                    })
4328                    .unwrap();
4329                writer.sync().unwrap();
4330            }
4331            let original = std::fs::read(&fixture.path).unwrap();
4332            let original_wal = std::fs::read(&wal).unwrap();
4333            let baseline = UnifiedStore::open_with_config_for_recovery(
4334                &fixture.path,
4335                fixture.config.clone(),
4336                true,
4337                None,
4338                None,
4339            )
4340            .err()
4341            .expect("unproved aliases must still fail");
4342            assert!(baseline.to_string().contains("red_stats"), "{baseline}");
4343            assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4344            let destination = fixture._directory.path().join("catalog-recovered");
4345            let result = crate::storage::compact::recover_paged_wal_canonical_system_config(
4346                &fixture.path,
4347                &destination,
4348            );
4349            assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4350            assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4351            if case != "valid" {
4352                let error = result.expect_err(case).to_string();
4353                let expected = match case {
4354                    "view-conflict" => "conflicting canonical definition",
4355                    "queue-delivery" => "unsupported legacy metadata",
4356                    _ => "WAL changes metadata",
4357                };
4358                assert!(error.contains(expected), "{case}: {error}");
4359                assert!(!destination.join("data.rdb").exists());
4360                continue;
4361            }
4362            let report = result.expect("recover all supported aliases together");
4363            assert_eq!(report.reconciled_system_collections, 4);
4364            assert_eq!(report.imported_system_records, 1);
4365            for _ in 0..2 {
4366                let store = UnifiedStore::open_with_config(
4367                    destination.join("data.rdb"),
4368                    fixture.config.clone(),
4369                )
4370                .unwrap();
4371                assert!(store.get_collection("durable_rows").is_some());
4372                let mut imported = Vec::new();
4373                store
4374                    .get_collection("red.index.registry")
4375                    .unwrap()
4376                    .try_for_each_entity(4 * 1024 * 1024, |entity| {
4377                        if entity.data.as_row().and_then(|r| r.get_field("name"))
4378                            == Some(&Value::text("legacy_only"))
4379                        {
4380                            imported.push(entity.clone());
4381                        }
4382                        true
4383                    })
4384                    .unwrap();
4385                assert_eq!(imported.len(), 1, "legacy-only index must survive");
4386                assert!(
4387                    imported[0].id.raw() > 10003,
4388                    "all retired identities must be reserved first"
4389                );
4390                assert!(
4391                    matches!(imported[0].kind, EntityKind::TableRow{row_id,..} if row_id>10001)
4392                );
4393                assert_eq!(imported[0].logical_id(), imported[0].id);
4394                assert!(store
4395                    .get("red.index.registry", EntityId::new(10001))
4396                    .is_none());
4397                assert!(store
4398                    .get("red.index.registry", EntityId::new(101))
4399                    .is_some());
4400                assert!(store
4401                    .get("red.query.stats_catalog", EntityId::new(100))
4402                    .is_some());
4403                assert!(store
4404                    .get("red.query.stats_catalog", EntityId::new(10000))
4405                    .is_none());
4406                assert_eq!(store.get_collection("red.queue.meta").unwrap().count(), 1);
4407                assert_eq!(
4408                    store
4409                        .get_collection("red.materialized_view.definitions")
4410                        .unwrap()
4411                        .count(),
4412                    1
4413                );
4414                assert!(
4415                    store
4416                        .get_collection("red.index.registry")
4417                        .unwrap()
4418                        .identity_counters()
4419                        .0
4420                        > 10001
4421                );
4422            }
4423        }
4424    }
4425
4426    #[test]
4427    fn canonical_system_config_recovery_retires_boot_wal_without_losing_other_actions() {
4428        use crate::storage::wal::{WalRecord, WalWriter};
4429        use reddb_file::StoreWalActionFrame as Action;
4430        for pending_dwb in [false, true] {
4431            let fixture = duplicate_system_catalog_fixture_with_key(
4432                true,
4433                true,
4434                true,
4435                false,
4436                Some("red.system.pid"),
4437            );
4438            let encode = |action| reddb_file::encode_store_wal_action_frame(&action).unwrap();
4439            let upsert = |id, value| {
4440                encode(Action::UpsertEntityRecord {
4441                    collection: "red_config".into(),
4442                    record: UnifiedStore::serialize_entity_record(
4443                        &config_recovery_row(id, "red.system.pid", value),
4444                        None,
4445                        STORE_VERSION_CURRENT,
4446                    ),
4447                })
4448            };
4449            let delete = |id| {
4450                encode(Action::DeleteEntityRecord {
4451                    collection: "red_config".into(),
4452                    entity_id: id,
4453                })
4454            };
4455            let wal = reddb_file::unified_wal_path(&fixture.path);
4456            {
4457                let mut writer = WalWriter::open(&wal).unwrap();
4458                writer
4459                    .append(&WalRecord::TxCommitBatch {
4460                        tx_id: 1001,
4461                        actions: vec![
4462                            upsert(10000, 3),
4463                            delete(1),
4464                            encode(Action::CreateCollection {
4465                                name: "durable_rows".into(),
4466                            }),
4467                        ],
4468                    })
4469                    .unwrap();
4470                writer
4471                    .append(&WalRecord::TxCommitBatch {
4472                        tx_id: 1002,
4473                        actions: vec![
4474                            upsert(10001, 4),
4475                            delete(10000),
4476                            encode(Action::UpsertEntityRecord {
4477                                collection: "durable_rows".into(),
4478                                record: UnifiedStore::serialize_entity_record(
4479                                    &UnifiedEntity::table_row(
4480                                        EntityId::new(3),
4481                                        "durable_rows",
4482                                        1,
4483                                        vec![Value::text("must survive")],
4484                                    ),
4485                                    None,
4486                                    STORE_VERSION_CURRENT,
4487                                ),
4488                            }),
4489                        ],
4490                    })
4491                    .unwrap();
4492                writer.sync().unwrap();
4493            }
4494            if pending_dwb {
4495                use std::io::{Seek, SeekFrom, Write};
4496                let bytes = std::fs::read(&fixture.path).unwrap();
4497                let root = fixture.legacy_root.unwrap();
4498                let offset = root as usize * PAGE_SIZE;
4499                let page: &[u8; PAGE_SIZE] = bytes[offset..offset + PAGE_SIZE].try_into().unwrap();
4500                let frame = reddb_file::encode_paged_dwb_frame([(root, page)]);
4501                let mut file = std::fs::OpenOptions::new()
4502                    .write(true)
4503                    .open(&fixture.path)
4504                    .unwrap();
4505                file.seek(SeekFrom::Start(3 * PAGE_SIZE as u64)).unwrap();
4506                file.write_all(&frame).unwrap();
4507                // Make raw-source identity inspection impossible: only the staged
4508                // DWB replay can restore the legacy configuration leaf.
4509                file.seek(SeekFrom::Start(offset as u64)).unwrap();
4510                file.write_all(&[0u8; PAGE_SIZE]).unwrap();
4511                file.sync_all().unwrap();
4512            }
4513            let original = std::fs::read(&fixture.path).unwrap();
4514            let original_wal = std::fs::read(&wal).unwrap();
4515            let destination = fixture._directory.path().join("boot-config-recovered");
4516            let report = crate::storage::compact::recover_paged_wal_canonical_system_config(
4517                &fixture.path,
4518                &destination,
4519            )
4520            .expect("retire only proved boot metadata actions on the isolated copy");
4521            assert_eq!(report.retired_boot_system_wal_actions, 4);
4522            assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4523            assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4524            for _ in 0..2 {
4525                let recovered = UnifiedStore::open_with_config(
4526                    destination.join("data.rdb"),
4527                    fixture.config.clone(),
4528                )
4529                .unwrap();
4530                let manager = recovered.get_collection("red.config").unwrap();
4531                assert!(manager.identity_counters().0 > 10001);
4532                assert!(manager.identity_counters().1 > 10001);
4533                assert_eq!(
4534                    recovered.get_config("red.system.pid"),
4535                    Some(Value::Integer(2))
4536                );
4537                assert!(recovered.get("red.config", EntityId::new(1)).is_none());
4538                assert!(recovered.get("red.config", EntityId::new(10000)).is_none());
4539                assert!(recovered.get("red.config", EntityId::new(10001)).is_none());
4540                assert_eq!(
4541                    recovered
4542                        .get("durable_rows", EntityId::new(3))
4543                        .unwrap()
4544                        .data
4545                        .as_row()
4546                        .unwrap()
4547                        .columns,
4548                    vec![Value::text("must survive")]
4549                );
4550            }
4551        }
4552    }
4553
4554    #[test]
4555    fn canonical_system_config_recovery_requires_stable_boot_identity_evidence() {
4556        use crate::storage::wal::{WalRecord, WalWriter};
4557        let fixture = duplicate_system_catalog_fixture_with_key(
4558            true,
4559            true,
4560            false,
4561            false,
4562            Some("red.system.pid"),
4563        );
4564        let original = std::fs::read(&fixture.path).unwrap();
4565        let wal = reddb_file::unified_wal_path(&fixture.path);
4566        {
4567            let mut writer = WalWriter::open(&wal).unwrap();
4568            writer
4569                .append(&WalRecord::TxCommitBatch {
4570                    tx_id: 1004,
4571                    actions: vec![reddb_file::encode_store_wal_action_frame(
4572                        &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
4573                            collection: "red_config".into(),
4574                            entity_id: 1,
4575                        },
4576                    )
4577                    .unwrap()],
4578                })
4579                .unwrap();
4580            writer
4581                .append(&WalRecord::FullPageImage {
4582                    tx_id: 1004,
4583                    page_id: 1,
4584                    ckpt_epoch: 1,
4585                    data: original[PAGE_SIZE..2 * PAGE_SIZE].to_vec(),
4586                })
4587                .unwrap();
4588            writer.sync().unwrap();
4589        }
4590        let original_wal = std::fs::read(&wal).unwrap();
4591        let destination = fixture._directory.path().join("unproved-checkpoint");
4592        let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
4593            &fixture.path,
4594            &destination,
4595        )
4596        .expect_err("undo images must be recovered before proving legacy identities");
4597        assert!(error
4598            .to_string()
4599            .contains("requires a stable checkpoint image"));
4600        assert!(!destination.exists());
4601        assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4602        assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4603    }
4604
4605    #[test]
4606    fn canonical_system_config_recovery_rejects_unproved_boot_wal() {
4607        use crate::storage::wal::{WalRecord, WalWriter};
4608        use reddb_file::StoreWalActionFrame as Action;
4609        for (case, checkpoint_key, wal_key, delete_id) in [
4610            (
4611                "unknown-delete",
4612                "red.system.pid",
4613                "red.system.pid",
4614                Some(90000),
4615            ),
4616            (
4617                "user-key",
4618                "red.system.customer_setting",
4619                "red.system.customer_setting",
4620                None,
4621            ),
4622            (
4623                "identity-reuse",
4624                "red.config.customer_setting",
4625                "red.system.pid",
4626                None,
4627            ),
4628        ] {
4629            let fixture = duplicate_system_catalog_fixture_with_key(
4630                true,
4631                true,
4632                false,
4633                false,
4634                Some(checkpoint_key),
4635            );
4636            let action = if let Some(entity_id) = delete_id {
4637                Action::DeleteEntityRecord {
4638                    collection: "red_config".into(),
4639                    entity_id,
4640                }
4641            } else {
4642                Action::UpsertEntityRecord {
4643                    collection: "red_config".into(),
4644                    record: UnifiedStore::serialize_entity_record(
4645                        &config_recovery_row(1, wal_key, 99),
4646                        None,
4647                        STORE_VERSION_CURRENT,
4648                    ),
4649                }
4650            };
4651            let wal = reddb_file::unified_wal_path(&fixture.path);
4652            {
4653                let mut writer = WalWriter::open(&wal).unwrap();
4654                writer
4655                    .append(&WalRecord::TxCommitBatch {
4656                        tx_id: 1003,
4657                        actions: vec![reddb_file::encode_store_wal_action_frame(&action).unwrap()],
4658                    })
4659                    .unwrap();
4660                writer.sync().unwrap();
4661            }
4662            let original = std::fs::read(&fixture.path).unwrap();
4663            let original_wal = std::fs::read(&wal).unwrap();
4664            let destination = fixture._directory.path().join(case);
4665            let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
4666                &fixture.path,
4667                &destination,
4668            )
4669            .expect_err(case);
4670            assert!(
4671                error
4672                    .to_string()
4673                    .contains("WAL references divergent legacy red_config"),
4674                "{case}: {error}"
4675            );
4676            assert!(!destination.exists());
4677            assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4678            assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4679        }
4680    }
4681
4682    #[test]
4683    fn canonical_system_config_recovery_rejects_legacy_wal_actions() {
4684        use crate::storage::wal::{WalRecord, WalWriter};
4685
4686        let fixture = duplicate_system_catalog_fixture(true, true, false);
4687        let wal = reddb_file::unified_wal_path(&fixture.path);
4688        let action = reddb_file::encode_store_wal_action_frame(
4689            &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
4690                collection: "red_config".into(),
4691                entity_id: 1,
4692            },
4693        )
4694        .unwrap();
4695        {
4696            let mut writer = WalWriter::open(&wal).unwrap();
4697            writer
4698                .append(&WalRecord::TxCommitBatch {
4699                    tx_id: 999,
4700                    actions: vec![action],
4701                })
4702                .unwrap();
4703            writer.sync().unwrap();
4704        }
4705        let original = std::fs::read(&fixture.path).unwrap();
4706        let original_wal = std::fs::read(&wal).unwrap();
4707        let destination = fixture._directory.path().join("rejected-legacy-wal");
4708        let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
4709            &fixture.path,
4710            &destination,
4711        )
4712        .expect_err("legacy WAL must not be replayed against the canonical tree");
4713        assert!(error
4714            .to_string()
4715            .contains("WAL references divergent legacy red_config"));
4716        assert!(!destination.exists());
4717        assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4718        assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4719    }
4720
4721    #[test]
4722    fn canonical_system_config_recovery_rejects_legacy_cross_references() {
4723        use crate::storage::wal::{WalRecord, WalWriter};
4724
4725        let fixture = duplicate_system_catalog_fixture_with_ref(true, true, false, true);
4726        let wal = reddb_file::unified_wal_path(&fixture.path);
4727        {
4728            let mut writer = WalWriter::open(&wal).unwrap();
4729            writer.append(&WalRecord::Begin { tx_id: 999 }).unwrap();
4730            writer.sync().unwrap();
4731        }
4732        let original = std::fs::read(&fixture.path).unwrap();
4733        let original_wal = std::fs::read(&wal).unwrap();
4734        let destination = fixture._directory.path().join("rejected-legacy-reference");
4735        let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
4736            &fixture.path,
4737            &destination,
4738        )
4739        .expect_err("legacy references must not be retargeted silently");
4740        assert!(error
4741            .to_string()
4742            .contains("legacy red_config has cross-references"));
4743        assert!(!destination.join("data.rdb").exists());
4744        assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4745        assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4746    }
4747
4748    #[test]
4749    fn populated_system_collection_aliases_reconcile_without_rebuild() {
4750        let fixture = duplicate_system_catalog_fixture(true, false, true);
4751        alias_duplicate_system_catalog_state(&fixture);
4752
4753        let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
4754            .expect("reconcile identical populated aliases");
4755        assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4756        assert_eq!(
4757            recovered
4758                .get("red.config", EntityId::new(1))
4759                .expect("preserved aliased row")
4760                .kind
4761                .collection(),
4762            "red.config"
4763        );
4764        assert_eq!(
4765            recovered.collection_root_page("red.config"),
4766            fixture.legacy_root,
4767            "the shared data root must be reused"
4768        );
4769        assert_eq!(
4770            recovered
4771                .key_indices
4772                .read()
4773                .get("red.config")
4774                .map(|index| index.tree.root_page_id()),
4775            fixture.legacy_key_root,
4776            "the shared key-index root must be reused"
4777        );
4778        assert_eq!(
4779            recovered
4780                .secondary_index("red.config", "by_id", &fixture.index_identity)
4781                .expect("shared secondary index")
4782                .root_page_id(),
4783            fixture
4784                .legacy_secondary_root
4785                .expect("fixture secondary root")
4786        );
4787        drop(recovered);
4788
4789        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4790        let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4791            .expect("idempotent cold boot");
4792        assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
4793        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
4794        assert_eq!(
4795            cold.pager()
4796                .expect("pager")
4797                .page_count()
4798                .expect("page count"),
4799            fixture.pages_before,
4800            "reconciling a physical alias must not extend the file"
4801        );
4802    }
4803
4804    #[test]
4805    fn populated_system_collection_aliases_merge_resume_counter_drift() {
4806        let fixture = duplicate_system_catalog_fixture(true, false, false);
4807        alias_duplicate_system_catalog_state_with(&fixture, |canonical| {
4808            canonical.entities += 7;
4809            canonical.entity_id_next += 100;
4810            canonical.row_id_next += 200;
4811            canonical.sequence_id_next += 300;
4812        });
4813
4814        let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
4815            .expect("reconcile shared roots with conservative counters");
4816        let manager = recovered
4817            .get_collection("red.config")
4818            .expect("canonical collection");
4819        assert_eq!(manager.count(), 1, "the shared tree owns the exact count");
4820        assert_eq!(manager.identity_counters(), (102, 202));
4821        assert_eq!(manager.sequence_id_next(), 301);
4822        assert_eq!(
4823            recovered.collection_root_page("red.config"),
4824            fixture.legacy_root,
4825            "counter reconciliation must keep the shared data root"
4826        );
4827        drop(manager);
4828        drop(recovered);
4829
4830        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4831        let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4832            .expect("idempotent cold boot after counter reconciliation");
4833        assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
4834        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
4835        assert_eq!(
4836            cold.pager()
4837                .expect("pager")
4838                .page_count()
4839                .expect("page count"),
4840            fixture.pages_before,
4841            "counter reconciliation must not extend the file"
4842        );
4843    }
4844
4845    fn row(id: u64, value: i64) -> UnifiedEntity {
4846        UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
4847    }
4848
4849    #[test]
4850    fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
4851        let directory = tempfile::tempdir().expect("temporary store");
4852        let path = directory.path().join("data.rdb");
4853        let store = UnifiedStore::open(&path).expect("paged store");
4854        store.create_collection("rows").expect("collection");
4855        store.insert("rows", row(1, 1)).expect("seed");
4856        store.persist().expect("initial checkpoint");
4857        let manager = store.get_collection("rows").expect("rows");
4858        manager.update(row(1, 2)).expect("direct update");
4859        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4860        store.persist().expect("updated row checkpoint");
4861        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
4862        let mut metadata = Metadata::new();
4863        metadata.set("verified", MetadataValue::Bool(true));
4864        manager
4865            .set_metadata(EntityId::new(1), metadata)
4866            .expect("metadata");
4867        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4868        store.persist().expect("metadata-only checkpoint");
4869        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
4870        manager
4871            .bulk_insert(vec![row(2, 2), row(3, 3)])
4872            .expect("bulk insert");
4873        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4874        store.persist().expect("bulk insert checkpoint");
4875        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
4876        manager.delete(EntityId::new(2)).expect("delete");
4877        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4878        store.persist().expect("changed checkpoint");
4879        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
4880        drop(store);
4881        let recovered = UnifiedStore::open(&path).expect("recover changed rows");
4882        let manager = recovered.get_collection("rows").expect("rows");
4883        assert_eq!(manager.count(), 2);
4884        assert_eq!(
4885            manager
4886                .get(EntityId::new(1))
4887                .expect("row")
4888                .data
4889                .as_row()
4890                .expect("table")
4891                .columns,
4892            vec![Value::Integer(2)]
4893        );
4894        assert!(manager.get(EntityId::new(2)).is_none());
4895        assert!(manager
4896            .get_metadata(EntityId::new(1))
4897            .expect("metadata")
4898            .get("verified")
4899            .is_some());
4900    }
4901
4902    #[test]
4903    fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
4904        let directory = tempfile::tempdir().expect("temporary store");
4905        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
4906        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
4907        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
4908        store.create_collection("rows").expect("collection");
4909        for id in 1..=40 {
4910            store.insert("rows", row(id, id as i64)).expect("seed");
4911        }
4912        store.persist().expect("initial checkpoint");
4913        let unrelated = snapshots.begin();
4914        snapshots.rollback(unrelated);
4915        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4916        store.persist().expect("unrelated rollback checkpoint");
4917        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
4918
4919        store
4920            .pending_vault_versions
4921            .write()
4922            .insert(EntityId::new(100));
4923        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4924        store.persist().expect("unrelated vault checkpoint");
4925        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
4926        store.pending_vault_versions.write().clear();
4927        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4928        store
4929            .persist()
4930            .expect("unrelated vault publication checkpoint");
4931        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
4932    }
4933
4934    #[test]
4935    fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
4936        let directory = tempfile::tempdir().expect("temporary store");
4937        let path = directory.path().join("data.rdb");
4938        let store = UnifiedStore::open(&path).expect("paged store");
4939        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
4940        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
4941        store.create_collection("rows").expect("collection");
4942        let xid = snapshots.begin();
4943        let mut entity = row(1, 1);
4944        entity.xmin = xid;
4945        store.insert("rows", entity).expect("committed row");
4946        snapshots.commit(xid);
4947        store.persist().expect("committed baseline");
4948        snapshots.rollback(xid);
4949        store
4950            .persist()
4951            .expect("untracked outcome requires validation");
4952        drop(store);
4953        let recovered = UnifiedStore::open(&path).expect("recover");
4954        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
4955    }
4956
4957    #[test]
4958    fn exhausted_deferred_row_cache_keeps_full_validation() {
4959        let directory = tempfile::tempdir().expect("temporary store");
4960        let path = directory.path().join("data.rdb");
4961        let store = UnifiedStore::open(&path).expect("paged store");
4962        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
4963        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
4964        store.create_collection("rows").expect("collection");
4965        let xid = snapshots.begin();
4966        for id in 1..=1025 {
4967            let mut entity = row(id, id as i64);
4968            entity.xmin = xid;
4969            store.insert("rows", entity).expect("pending row");
4970        }
4971        store.persist().expect("exclude pending rows");
4972        assert!(store.checkpoint_versions.read()["rows"]
4973            .deferred_rows
4974            .is_none());
4975        snapshots.commit(xid);
4976        store
4977            .persist()
4978            .expect("publish committed rows conservatively");
4979        drop(store);
4980        let recovered = UnifiedStore::open(&path).expect("recover");
4981        assert_eq!(
4982            recovered.get_collection("rows").expect("rows").count(),
4983            1025
4984        );
4985    }
4986
4987    #[test]
4988    fn rollback_invalidates_reuse_without_a_segment_mutation() {
4989        let directory = tempfile::tempdir().expect("temporary store");
4990        let path = directory.path().join("data.rdb");
4991        let store = UnifiedStore::open(&path).expect("paged store");
4992        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
4993        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
4994        store.create_collection("rows").expect("collection");
4995        let xid = snapshots.begin();
4996        let mut pending = row(1, 1);
4997        pending.xmin = xid;
4998        store.insert("rows", pending).expect("version");
4999        store.persist().expect("before rollback");
5000        snapshots.rollback(xid);
5001        store.persist().expect("after rollback");
5002        drop(store);
5003        let recovered = UnifiedStore::open(&path).expect("recover");
5004        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
5005    }
5006
5007    #[test]
5008    fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
5009        let directory = tempfile::tempdir().expect("temporary store");
5010        let path = directory.path().join("data.rdb");
5011        let store = UnifiedStore::open(&path).expect("paged store");
5012        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5013        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5014        store.create_collection("rows").expect("collection");
5015        let xid = snapshots.begin();
5016        let mut deleted = row(1, 1);
5017        deleted.xmax = xid;
5018        store.insert("rows", deleted).expect("version");
5019        store.persist().expect("before rollback");
5020        snapshots.rollback(xid);
5021        store.persist().expect("after rollback");
5022        drop(store);
5023        let recovered = UnifiedStore::open(&path).expect("recover");
5024        let manager = recovered.get_collection("rows").expect("rows");
5025        assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
5026    }
5027
5028    #[test]
5029    fn publishing_a_pending_vault_version_invalidates_reuse() {
5030        let directory = tempfile::tempdir().expect("temporary store");
5031        let path = directory.path().join("data.rdb");
5032        let store = UnifiedStore::open(&path).expect("paged store");
5033        store.create_collection("rows").expect("collection");
5034        store.insert("rows", row(1, 1)).expect("row");
5035        store
5036            .pending_vault_versions
5037            .write()
5038            .insert(EntityId::new(1));
5039        store.persist().expect("exclude pending vault version");
5040        store
5041            .pending_vault_versions
5042            .write()
5043            .remove(&EntityId::new(1));
5044        store.persist().expect("include published vault version");
5045        drop(store);
5046        let recovered = UnifiedStore::open(&path).expect("recover");
5047        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
5048    }
5049}