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