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