Skip to main content

reddb_server/storage/unified/store/
impl_pages.rs

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