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