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