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}
13
14#[derive(Debug, Default)]
15struct CatalogLoadOutcome {
16    migrated: bool,
17    collisions: Vec<SystemCollectionCollision>,
18}
19
20/// Keep encoded payload memory proportional to a small batch, not a collection.
21/// A single large value retains the existing B-tree value-size contract.
22struct CheckpointTreeBuilder {
23    tree: Arc<BTree>,
24    records: Vec<(Vec<u8>, Vec<u8>)>,
25    bytes: usize,
26}
27
28impl CheckpointTreeBuilder {
29    fn new(pager: &Arc<Pager>) -> Self {
30        Self {
31            tree: Arc::new(BTree::new(Arc::clone(pager))),
32            records: Vec::new(),
33            bytes: 0,
34        }
35    }
36
37    fn push(&mut self, record: (Vec<u8>, Vec<u8>)) -> Result<(), StoreError> {
38        let bytes = record.0.len() + record.1.len();
39        if !self.records.is_empty()
40            && (self.bytes.saturating_add(bytes) > CHECKPOINT_BATCH_BYTES
41                || self.records.len() >= CHECKPOINT_BATCH_ROWS)
42        {
43            self.flush()?;
44        }
45        self.bytes += bytes;
46        self.records.push(record);
47        #[cfg(test)]
48        CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(peak.get().max(self.bytes)));
49        Ok(())
50    }
51
52    fn flush(&mut self) -> Result<(), StoreError> {
53        self.tree
54            .bulk_insert_sorted(&self.records)
55            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
56        self.records.clear();
57        self.bytes = 0;
58        Ok(())
59    }
60
61    fn copy_prefix(&mut self, previous: &BTree, count: usize) -> Result<(), StoreError> {
62        let mut cursor = previous
63            .cursor_first()
64            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
65        for _ in 0..count {
66            let record = cursor
67                .next()
68                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
69                .ok_or_else(|| StoreError::Serialization("checkpoint prefix disappeared".into()))?;
70            self.push(record)?;
71        }
72        Ok(())
73    }
74}
75
76#[cfg(test)]
77thread_local! {
78    static CHECKPOINT_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
79    static REPLAY_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
80    static CHECKPOINT_BUFFERED_BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
81}
82
83fn checkpoint_visibility_matches(
84    previous: &CheckpointVersion,
85    current: &CheckpointVersion,
86) -> bool {
87    (previous.visibility_revision == current.visibility_revision
88        || previous.excluded_xids == current.excluded_xids)
89        && previous.pending_vault == current.pending_vault
90}
91
92// ── Pager-meta overflow chain (gh-477) ──────────────────────────────────────
93// When the serialized collection registry + cross-refs exceed a single page,
94// page 1 carries a native metadata-overflow header pointing at an overflow chain of
95// `PageType::Overflow` pages. Single-page metadata keeps the historical
96// bit-identical layout (`METADATA_MAGIC = "RDM2"` written directly at the
97// content offset).
98//
99// Page 1 (overflow form), starting at `HEADER_SIZE`:
100//   [0..4]   native metadata-overflow magic
101//   [4..8]   format_version (u32, mirrors inner payload version for debug)
102//   [8..12]  total_payload_bytes (u32)
103//   [12..16] next_overflow_page_id (u32, > 0)
104//   [16..]   first payload chunk (up to META_V3_FIRST_PAYLOAD_CAP bytes)
105//
106// Overflow continuation page, starting at `HEADER_SIZE`:
107//   [0..4]   next_overflow_page_id (u32, 0 if last)
108//   [4..8]   chunk_bytes (u32)
109//   [8..]    chunk payload (up to META_V3_OVERFLOW_PAYLOAD_CAP bytes)
110const META_PAGE_CONTENT_CAP: usize =
111    crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
112const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
113const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
114const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
115const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
116
117fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
118    let cs = crate::storage::engine::HEADER_SIZE;
119    let page = pager.read_page(1)?;
120    let bytes = page.as_bytes();
121    if bytes.len() < cs + META_V3_PAGE1_HEADER {
122        return Ok(Vec::new());
123    }
124    let Some(header) =
125        reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
126            PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
127        })?
128    else {
129        return Ok(Vec::new());
130    };
131    let mut next = header.next_overflow_page_id;
132    let mut pages = Vec::new();
133    let mut seen = std::collections::HashSet::new();
134    while next != 0 {
135        if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
136            return Err(PagerError::InvalidDatabase(
137                "cyclic metadata overflow chain".into(),
138            ));
139        }
140        let ov = pager.read_page(next)?;
141        if ov.page_type()? != crate::storage::engine::PageType::Overflow {
142            return Err(PagerError::InvalidDatabase(
143                "invalid metadata overflow page type".into(),
144            ));
145        }
146        let ob = ov.as_bytes();
147        let nn = reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
148            .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?
149            .next_overflow_page_id;
150        pages.push(next);
151        next = nn;
152    }
153    Ok(pages)
154}
155
156fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
157    let retired = existing_overflow_chain(pager)?;
158    if !retired.is_empty() {
159        pager.record_retirement_intent(&retired)?;
160    }
161    let page = build_meta_page1_with_overflow(pager, meta_data)?;
162    // Cache eviction order must not publish a root before its children.
163    pager.sync()?;
164    pager.write_page(1, page)?;
165    pager.sync()?;
166    for id in retired {
167        pager.free_page(id)?;
168    }
169    Ok(())
170}
171
172fn build_meta_page1_with_overflow(
173    pager: &Pager,
174    meta_data: &[u8],
175) -> Result<crate::storage::engine::Page, PagerError> {
176    use crate::storage::engine::{Page, PageType, HEADER_SIZE};
177
178    let mut page1 = Page::new(PageType::Header, 1);
179    let cs = HEADER_SIZE;
180
181    if meta_data.len() <= META_PAGE_CONTENT_CAP {
182        // Single-page: bit-identical to the historical layout.
183        let buf = page1.as_bytes_mut();
184        buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
185        return Ok(page1);
186    }
187
188    // Multi-page overflow form. Split the inner payload into the first chunk
189    // (held on page 1) followed by zero-or-more continuation chunks chained
190    // through `PageType::Overflow` pages.
191    let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
192    let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
193    let mut chunks: Vec<&[u8]> = Vec::new();
194    while !tail.is_empty() {
195        let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
196        chunks.push(&tail[..take]);
197        tail = &tail[take..];
198    }
199
200    let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
201    let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
202    for _ in 0..chunks.len() {
203        let pg = pager.allocate_page(PageType::Overflow)?;
204        overflow_ids.push(pg.page_id());
205        overflow_pages.push(pg);
206    }
207
208    for i in 0..chunks.len() {
209        let next = if i + 1 < chunks.len() {
210            overflow_ids[i + 1]
211        } else {
212            0u32
213        };
214        let len = chunks[i].len() as u32;
215        let buf = overflow_pages[i].as_bytes_mut();
216        reddb_file::encode_native_metadata_overflow_continuation_header(
217            &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
218            reddb_file::NativeMetadataOverflowContinuationHeader {
219                next_overflow_page_id: next,
220                chunk_bytes: len,
221            },
222        )
223        .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
224        buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
225    }
226    for (idx, page) in overflow_pages.into_iter().enumerate() {
227        let id = overflow_ids[idx];
228        pager.write_page(id, page)?;
229    }
230
231    // Mirror the inner format_version for debug-friendly hex dumps.
232    let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
233        .ok()
234        .flatten()
235        .map_or(0, |header| header.format_version);
236
237    let buf = page1.as_bytes_mut();
238    reddb_file::encode_native_metadata_overflow_header(
239        &mut buf[cs..cs + META_V3_PAGE1_HEADER],
240        reddb_file::NativeMetadataOverflowHeader {
241            format_version,
242            total_payload_bytes: meta_data.len() as u32,
243            next_overflow_page_id: overflow_ids[0],
244        },
245    )
246    .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
247    buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
248        .copy_from_slice(first_chunk);
249
250    Ok(page1)
251}
252
253fn storage_integrity(
254    zone: &str,
255    id: impl Into<String>,
256    collection: Option<String>,
257    detail: impl Into<String>,
258) -> StoreError {
259    StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
260        zone, id, collection, detail,
261    ))
262}
263
264fn page_integrity(
265    page_id: u32,
266    collection: Option<String>,
267    detail: impl Into<String>,
268) -> StoreError {
269    storage_integrity("page", page_id.to_string(), collection, detail)
270}
271
272/// Assemble the full metadata payload from the internal manifest zone: page 1
273/// plus its overflow chain when the native overflow wrapper is present. Returns
274/// the bytes that the metadata parser would see starting from the content
275/// offset of page 1. Single-page metadata returns the raw page content
276/// (including trailing zero-pad), so the legacy parser sees the same bytes it
277/// always saw.
278///
279/// The zone is rooted by the superblock and checksummed per ADR 0074 §2: a
280/// failing checksum surfaces the didactic manifest-zone integrity error
281/// directly — there is no sidecar copy to fall back to since ADR 0038 §4
282/// phase 1 retired the shadows.
283fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
284    let cs = crate::storage::engine::HEADER_SIZE;
285    let meta_page = pager.read_manifest_page().map_err(|err| {
286        storage_integrity(
287            "manifest",
288            "manifest",
289            None,
290            format!("internal manifest zone read failed ({err})"),
291        )
292    })?;
293    let bytes = meta_page.as_bytes();
294    if bytes.len() < cs + 4 {
295        return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
296    }
297    let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
298        .map_err(|err| page_integrity(1, None, err.to_string()))?
299    {
300        Some(header) => header,
301        None => {
302            return Ok(Some(bytes[cs..].to_vec()));
303        }
304    };
305    if bytes.len() < cs + META_V3_PAGE1_HEADER {
306        return Err(page_integrity(
307            1,
308            None,
309            "metadata overflow header is truncated",
310        ));
311    }
312    let total = header.total_payload_bytes as usize;
313    let mut next = header.next_overflow_page_id;
314    let mut payload: Vec<u8> = Vec::with_capacity(total);
315    let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
316    payload.extend_from_slice(
317        &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
318    );
319    let mut seen = std::collections::HashSet::new();
320    while next != 0 && payload.len() < total {
321        if !seen.insert(next)
322            || seen.len()
323                >= pager
324                    .page_count()
325                    .map_err(|err| page_integrity(next, None, err.to_string()))?
326                    as usize
327        {
328            return Err(page_integrity(next, None, "cyclic metadata overflow chain"));
329        }
330        let ov = pager
331            .read_page(next)
332            .map_err(|err| page_integrity(next, None, err.to_string()))?;
333        if ov
334            .page_type()
335            .map_err(|err| page_integrity(next, None, err.to_string()))?
336            != crate::storage::engine::PageType::Overflow
337        {
338            return Err(page_integrity(
339                next,
340                None,
341                "invalid metadata overflow page type",
342            ));
343        }
344        let ob = ov.as_bytes();
345        if ob.len() < cs + META_V3_OVERFLOW_HEADER {
346            return Err(page_integrity(
347                next,
348                None,
349                "metadata overflow continuation is truncated",
350            ));
351        }
352        let continuation =
353            reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
354                .map_err(|err| page_integrity(next, None, err.to_string()))?;
355        let nn = continuation.next_overflow_page_id;
356        let len = continuation.chunk_bytes as usize;
357        let remaining = total - payload.len();
358        let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
359        payload.extend_from_slice(
360            &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
361        );
362        next = nn;
363    }
364    if payload.len() != total {
365        return Err(page_integrity(
366            1,
367            None,
368            "metadata overflow chain is incomplete",
369        ));
370    }
371    Ok(Some(payload))
372}
373
374impl UnifiedStore {
375    /// Compare and encode one borrowed row at a time. Begin a new tree only at
376    /// the first difference, copying its already-verified prefix in bounded
377    /// batches. Unchanged trees require no replacement allocation.
378    fn checkpoint_tree(
379        &self,
380        pager: &Arc<Pager>,
381        manager: &SegmentManager,
382        previous: Option<&Arc<BTree>>,
383        mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
384    ) -> Result<Arc<BTree>, StoreError> {
385        if let Some((base, overlay)) = manager.backing_overlay(&mut encode) {
386            use reddb_engine::storage::collection_read::{CollectionRead, MergedCollection};
387            let merged = MergedCollection::new(base.as_ref(), overlay);
388            let mut cursor = merged.cursor()?;
389            let mut builder = CheckpointTreeBuilder::new(pager);
390            while let Some(record) = cursor.next_record(
391                usize::try_from(self.config.resident_memory_bytes_max.unwrap_or(u64::MAX))
392                    .unwrap_or(usize::MAX),
393            )? {
394                builder.push((record.id.raw().to_be_bytes().to_vec(), record.bytes))?;
395            }
396            builder.flush()?;
397            return Ok(builder.tree);
398        }
399        let mut cursor = previous
400            .map(|tree| tree.cursor_first())
401            .transpose()
402            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
403        let mut matched = 0;
404        let mut builder = None;
405        let mut failure = None;
406        manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
407            let Some(value) = encode(entity, metadata) else {
408                return true;
409            };
410            let record = (entity.id.raw().to_be_bytes().to_vec(), value);
411            let result = (|| {
412                if builder.is_none() {
413                    if let Some(cursor) = cursor.as_mut() {
414                        if cursor
415                            .next()
416                            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
417                            .as_ref()
418                            == Some(&record)
419                        {
420                            matched += 1;
421                            return Ok(());
422                        }
423                    }
424                    let mut replacement = CheckpointTreeBuilder::new(pager);
425                    if let Some(previous) = previous {
426                        replacement.copy_prefix(previous, matched)?;
427                    }
428                    builder = Some(replacement);
429                }
430                builder
431                    .as_mut()
432                    .expect("replacement initialized")
433                    .push(record)
434            })();
435            if let Err(error) = result {
436                failure = Some(error);
437                return false;
438            }
439            true
440        });
441        if let Some(error) = failure {
442            return Err(error);
443        }
444        if builder.is_none() {
445            if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
446                if cursor
447                    .next()
448                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
449                    .is_none()
450                {
451                    return Ok(Arc::clone(previous));
452                }
453            }
454            let mut replacement = CheckpointTreeBuilder::new(pager);
455            if let Some(previous) = previous {
456                replacement.copy_prefix(previous, matched)?;
457            }
458            builder = Some(replacement);
459        }
460        let mut builder = builder.expect("replacement or reused tree");
461        builder.flush()?;
462        Ok(builder.tree)
463    }
464
465    /// Publication first, reuse second. The immediately preceding generation
466    /// stays in the durable ledger for one more publication. Cursors and cloned
467    /// tree handles defer retirement further without holding the writer fence.
468    fn publish_reclaimable_trees(
469        &self,
470        pager: &Pager,
471        metadata: &mut Vec<u8>,
472        live: &HashMap<String, Arc<BTree>>,
473        keys: &HashMap<String, PublishedKeyIndex>,
474    ) -> Result<(), StoreError> {
475        let mut next_xid_summary = None;
476        let mut retired = self.retired_btrees.lock();
477        let mut roots = Vec::new();
478        let mut reclaim = Vec::new();
479        let mut remove = std::collections::HashSet::new();
480        for (index, entry) in retired.iter().enumerate() {
481            if live
482                .values()
483                .chain(keys.values().flat_map(PublishedKeyIndex::trees))
484                .any(|tree| Arc::ptr_eq(tree, &entry.tree))
485            {
486                remove.insert(index);
487                continue;
488            }
489            if entry.published
490                && Arc::strong_count(&entry.tree) == 1
491                && !entry.tree.has_retirement_readers()
492            {
493                // Deliberately unsafe experimental control: keep the old
494                // root in the catalog while releasing its pages. Never enable
495                // this outside the isolated recovery test.
496                #[cfg(test)]
497                if std::env::var_os("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT").is_some() {
498                    roots.push(entry.tree.root_page_id());
499                }
500                reclaim.extend(
501                    entry
502                        .tree
503                        .retired_pages()
504                        .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
505                );
506                remove.insert(index);
507            } else if entry.tree.root_page_id() != 0 {
508                roots.push(entry.tree.root_page_id());
509            }
510        }
511        roots.sort_unstable();
512        roots.dedup();
513        reddb_file::encode_native_paged_retired_trees(metadata, &roots)
514            .map_err(|e| StoreError::Serialization(e.to_string()))?;
515        let (pending_pages, published_pages) = pager
516            .deferred_reclamation_pages()
517            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
518        let readers = live
519            .values()
520            .chain(keys.values().flat_map(PublishedKeyIndex::trees))
521            .any(|tree| tree.has_retirement_readers())
522            || retired
523                .iter()
524                .any(|entry| entry.tree.has_retirement_readers());
525        let released_pages = if readers { Vec::new() } else { published_pages };
526        let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
527        let retained_pages: Vec<_> = pending_pages
528            .into_iter()
529            .filter(|id| !released.contains(id))
530            .collect();
531        reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
532            .map_err(|e| StoreError::Serialization(e.to_string()))?;
533        let active_epoch = self.ownership_active_epoch.load(Ordering::Acquire);
534        let catalog_epoch = if active_epoch == 0 {
535            self.ownership_catalog_epoch.load(Ordering::Acquire)
536        } else {
537            active_epoch
538        };
539        if catalog_epoch != 0 {
540            reddb_file::native_store::encode_native_paged_ownership_epoch(metadata, catalog_epoch)
541                .map_err(|e| StoreError::Serialization(e.to_string()))?;
542        }
543        if self.config.resident_memory_bytes_max.is_some() {
544            use reddb_file::native_resume::{
545                encode_catalog_resume, CatalogResume, CollectionResume,
546            };
547            let mut states = std::collections::BTreeMap::new();
548            let mut entity_id_next = self.resident.next_entity_id.load(Ordering::SeqCst);
549            let collections = self.resident.collections.read();
550            let publishes_rows = {
551                let summary = self.recovery_xid_summary.read();
552                live.iter().any(|(name, tree)| {
553                    let root = tree.root_page_id();
554                    root != 0
555                        && summary.as_ref().and_then(|state| state.roots.get(name)) != Some(&root)
556                })
557            };
558            let xid_high_water_mark =
559                self.base_xid_high_water_mark(&collections).map(|mut high| {
560                    // Ordinary metadata flushes retain the old immutable-base
561                    // bound; WAL overlays are observed at boot. Only replacement
562                    // data roots require a borrowed visit over resident headers.
563                    // The publication fence excludes concurrent header mutations.
564                    if publishes_rows {
565                        for manager in collections.values() {
566                            manager.for_each_entity(|entity| {
567                                high = high.max(entity.xmin).max(entity.xmax);
568                                true
569                            });
570                        }
571                    }
572                    if let Some(snapshots) = self.snapshot_manager.get() {
573                        high = high.max(snapshots.peek_next_xid().saturating_sub(1));
574                    }
575                    high
576                });
577            for (name, manager) in collections.iter() {
578                let tree = live.get(name);
579                let (entity_next, row_id_next) = manager.identity_counters();
580                entity_id_next = entity_id_next.max(entity_next);
581                let root_page = tree.map_or(0, |tree| tree.root_page_id());
582                let entities = if root_page == 0 {
583                    0
584                } else if let Some(count) = manager.backing_count_at_root(root_page) {
585                    count
586                } else {
587                    // Changed projections may exclude pending/aborted rows. Count
588                    // published physical keys, not the live manager's cardinality.
589                    let mut cursor = tree
590                        .expect("invariant: nonzero root has a tree")
591                        .cursor_first()
592                        .map_err(|error| {
593                            StoreError::Io(std::io::Error::other(error.to_string()))
594                        })?;
595                    let mut count = 0_u64;
596                    while cursor
597                        .skip_next()
598                        .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
599                    {
600                        count += 1;
601                    }
602                    count
603                };
604                states.insert(
605                    name.clone(),
606                    CollectionResume {
607                        root_page,
608                        entities,
609                        entity_id_next: entity_next,
610                        row_id_next,
611                        sequence_id_next: manager.sequence_id_next(),
612                        key_index_root: tree.and_then(|tree| {
613                            keys.get(name)
614                                .filter(|index| {
615                                    index.indexed && index.source.ptr_eq(&Arc::downgrade(tree))
616                                })
617                                .map(|index| index.tree.root_page_id())
618                        }),
619                        secondary_indices: tree
620                            .and_then(|tree| {
621                                keys.get(name)
622                                    .filter(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
623                            })
624                            .map(|index| {
625                                index
626                                    .secondary
627                                    .iter()
628                                    .map(|(name, index)| (name.clone(), index.resume()))
629                                    .collect()
630                            })
631                            .unwrap_or_default(),
632                    },
633                );
634            }
635            let resume = CatalogResume {
636                entity_id_next,
637                xid_high_water_mark,
638                collections: states,
639            };
640            encode_catalog_resume(metadata, &resume)
641                .map_err(|error| StoreError::Serialization(error.to_string()))?;
642            next_xid_summary = xid_high_water_mark.map(|high_water_mark| {
643                super::recovery_xid::RecoveryXidSummary {
644                    high_water_mark,
645                    roots: resume
646                        .collections
647                        .into_iter()
648                        .map(|(name, state)| (name, state.root_page))
649                        .collect(),
650                }
651            });
652        }
653        // Record the exact pages before the catalog can forget their owners.
654        // Recovery resolves this intent against the durable catalog epoch.
655        let mut retirement_intent = reclaim.clone();
656        retirement_intent.extend_from_slice(&released_pages);
657        if !retirement_intent.is_empty() {
658            pager
659                .record_retirement_intent(&retirement_intent)
660                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
661        }
662        // Do not mutate retirement state or the free list before both the new
663        // children and the manifest that forgets old roots have been synced.
664        publish_paged_metadata(pager, metadata)
665            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
666        *self.recovery_xid_summary.write() = next_xid_summary;
667        self.ownership_catalog_epoch
668            .store(catalog_epoch, Ordering::Release);
669        #[cfg(test)]
670        if !reclaim.is_empty()
671            && std::env::var_os("REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE").is_some()
672        {
673            eprintln!(
674                "experimental exit before retired free pages={}",
675                reclaim.len()
676            );
677            std::process::exit(184);
678        }
679        let mut index = 0;
680        retired.retain_mut(|entry| {
681            let keep = !remove.contains(&index);
682            entry.published = true;
683            index += 1;
684            keep
685        });
686        if !reclaim.is_empty() {
687            pager
688                .free_retired_pages(&reclaim)
689                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
690            #[cfg(test)]
691            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE").is_some() {
692                eprintln!(
693                    "experimental exit after retired enqueue pages={}",
694                    reclaim.len()
695                );
696                std::process::exit(186);
697            }
698            pager
699                .sync()
700                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
701            #[cfg(test)]
702            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE").is_some() {
703                eprintln!(
704                    "experimental exit after retired free pages={}",
705                    reclaim.len()
706                );
707                std::process::exit(185);
708            }
709        }
710        pager
711            .complete_deferred_publication(&retained_pages, &released_pages)
712            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
713        if !released_pages.is_empty() {
714            pager
715                .sync()
716                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
717        }
718        Ok(())
719    }
720
721    pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
722        let mut retired = self.retired_btrees.lock();
723        if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
724        {
725            retired.push(RetiredBTree {
726                tree,
727                published: false,
728            });
729        }
730    }
731
732    /// A completed inline insert already made this tree match its manager.
733    /// Advance the reuse proof only for that single, fenced mutation. Any
734    /// untracked edit or segment publication keeps the full comparison path.
735    pub(crate) fn note_paged_insert(
736        &self,
737        collection: &str,
738        manager: &Arc<SegmentManager>,
739        before: &[(u64, u64)],
740        id: EntityId,
741    ) {
742        let after = manager.checkpoint_revisions();
743        if before.len() != after.len() {
744            return;
745        }
746        let mut changed = 0;
747        for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
748            if old_id != new_id {
749                return;
750            }
751            if old_revision != new_revision {
752                if old_revision.checked_add(1) != Some(*new_revision) {
753                    return;
754                }
755                changed += 1;
756            }
757        }
758        if changed != 1 {
759            return;
760        }
761        let mut versions = self.checkpoint_versions.write();
762        let Some(previous) = versions.get_mut(collection) else {
763            return;
764        };
765        if previous.manager.ptr_eq(&Arc::downgrade(manager))
766            && previous.segments == before
767            && previous.format_version == self.format_version()
768            && !previous.pending_vault.contains(&id)
769            && !previous.excluded_xids.contains(&0)
770        {
771            previous.segments = after;
772        }
773    }
774
775    /// Successful physical deletes already changed both the manager and tree.
776    /// Reuse remains valid only when their exact revision delta accounts for
777    /// this batch, with no intervening edits or segment replacement.
778    pub(crate) fn note_paged_delete(
779        &self,
780        collection: &str,
781        manager: &Arc<SegmentManager>,
782        before: &[(u64, u64)],
783        deleted: &[EntityId],
784    ) {
785        let after = manager.checkpoint_revisions();
786        if before.len() != after.len() {
787            return;
788        }
789        let mut changes = 0_u64;
790        for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
791            if old_id != new_id {
792                return;
793            }
794            let Some(delta) = new_revision.checked_sub(*old_revision) else {
795                return;
796            };
797            let Some(total) = changes.checked_add(delta) else {
798                return;
799            };
800            changes = total;
801        }
802        let Ok(expected) = u64::try_from(deleted.len()) else {
803            return;
804        };
805        if changes != expected {
806            return;
807        }
808        let mut versions = self.checkpoint_versions.write();
809        let Some(previous) = versions.get_mut(collection) else {
810            return;
811        };
812        if !previous.manager.ptr_eq(&Arc::downgrade(manager))
813            || previous.format_version != self.format_version()
814            || deleted.iter().any(|id| previous.pending_vault.contains(id))
815            || previous.excluded_xids.contains(&0)
816        {
817            return;
818        }
819        if previous.segments != before {
820            let Some(deferred) = previous.deferred_rows.as_ref() else {
821                return;
822            };
823            let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
824                return;
825            };
826            let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
827            let pending: std::collections::BTreeSet<_> = deferred
828                .iter()
829                .copied()
830                .chain(changed)
831                .filter(|id| !removed.contains(id))
832                .collect();
833            if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
834                return;
835            }
836            // GC already applied its deletes to the tree. Carry earlier dirty
837            // IDs forward instead of spending their journal on these deletes.
838            // Sparse projection still requires exact WAL proof for every
839            // differing row, so an unlogged manager edit cannot be concealed.
840            previous.has_unprojected_rows = !pending.is_empty();
841            previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
842        }
843        previous.segments = after;
844    }
845
846    pub(crate) fn mark_paged_registry_dirty(&self) {
847        self.resident.mark_paged_registry_dirty()
848    }
849
850    /// Get (or lazily create) the per-collection B-tree under a *read*
851    /// lock whenever possible. Returns a cloned `Arc<BTree>` so callers
852    /// can mutate the tree without holding the outer map's RwLock —
853    /// previously every insert serialised on `btree_indices.write()`,
854    /// costing ~60% of the concurrent-insert throughput ceiling.
855    pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
856        let collection = Self::canonical_collection(collection);
857        let pager = self.pager.as_ref()?;
858        if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
859            return Some(btree);
860        }
861        let mut write = self.btree_indices.write();
862        let btree = write
863            .entry(collection.to_string())
864            .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
865            .clone();
866        Some(btree)
867    }
868
869    pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
870        let Some(pager) = &self.pager else {
871            return Ok(());
872        };
873
874        if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
875            self.flush_paged_registry()?;
876            self.resident
877                .paged_registry_dirty
878                .store(false, Ordering::Release);
879            return Ok(());
880        }
881
882        pager
883            .flush()
884            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
885    }
886
887    pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
888        let _vault_guard = self.vault_publication_lock.lock();
889        let Some(pager) = &self.pager else {
890            return Ok(());
891        };
892
893        match pager.read_page(1) {
894            Ok(_) => {}
895            Err(PagerError::PageNotFound(_)) => {
896                let meta_page = pager
897                    .allocate_page(crate::storage::engine::PageType::Header)
898                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
899                pager
900                    .write_page(meta_page.page_id(), meta_page)
901                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
902            }
903            Err(e) => {
904                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
905            }
906        }
907
908        let format_version = STORE_VERSION_V11;
909        self.set_format_version(format_version);
910
911        let collections = self.resident.collections.read();
912        let btree_indices = self.btree_indices.read();
913        let mut collection_roots = Vec::with_capacity(collections.len());
914        for (name, _) in collections.iter() {
915            let root_page = btree_indices
916                .get(name)
917                .map_or(0, |btree| btree.root_page_id());
918            collection_roots.push((name.clone(), root_page));
919        }
920        drop(btree_indices);
921        drop(collections);
922
923        let mut meta_data = Vec::with_capacity(4096);
924        reddb_file::encode_native_paged_metadata_header(
925            &mut meta_data,
926            reddb_file::NativePagedMetadataHeader {
927                format_version,
928                collection_count: collection_roots.len() as u32,
929            },
930        );
931        for (name, root_page) in &collection_roots {
932            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
933        }
934
935        let cross_refs = self.resident.cross_refs.read();
936        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
937        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
938        for (source_id, refs) in cross_refs.iter() {
939            for (target_id, ref_type, collection) in refs {
940                reddb_file::encode_native_paged_cross_ref(
941                    &mut meta_data,
942                    source_id.raw(),
943                    target_id.raw(),
944                    ref_type.to_byte(),
945                    reddb_engine::storage::system_collection_names::canonical_or_self(collection),
946                );
947            }
948        }
949
950        self.publish_reclaimable_trees(
951            pager,
952            &mut meta_data,
953            &self.btree_indices.read(),
954            &self.key_indices.read(),
955        )?;
956
957        Ok(())
958    }
959
960    /// Get a reference to the underlying pager (if in paged mode).
961    pub fn pager(&self) -> Option<&Arc<Pager>> {
962        self.pager.as_ref()
963    }
964
965    /// Bytes of page-cache memory currently occupied by resident pages
966    /// (ADR 0073 §2). Slots are fixed size, so occupancy times the page size
967    /// *is* the footprint; a store without a pager occupies none.
968    pub fn page_cache_bytes_in_use(&self) -> u64 {
969        self.pager.as_ref().map_or(0, |pager| {
970            pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
971        })
972    }
973
974    /// Bytes of WAL buffer memory held by this store's group-commit
975    /// coordinator: queued records plus the writer's fixed append buffer.
976    pub fn wal_buffer_bytes_in_use(&self) -> u64 {
977        self.commit
978            .as_ref()
979            .map_or(0, |commit| commit.buffered_bytes())
980    }
981
982    /// Approximate resident bytes across every collection's segment arena
983    /// (ADR 0073 §2). Same number `stats().total_memory_bytes` reports, without
984    /// building the per-collection stats map a sampler would throw away.
985    pub fn segment_memory_bytes(&self) -> u64 {
986        let managers: Vec<Arc<SegmentManager>> = self
987            .resident
988            .collections
989            .read()
990            .values()
991            .map(Arc::clone)
992            .collect();
993
994        managers
995            .iter()
996            .map(|manager| manager.resident_bytes())
997            .fold(0, u64::saturating_add)
998    }
999
1000    /// Borrow the immutable store configuration. Runtime hooks (e.g. the
1001    /// `auto_index_id` first-insert hook in `MutationEngine`) read knobs
1002    /// off this struct without going through the legacy global config tree.
1003    pub fn config(&self) -> &UnifiedStoreConfig {
1004        &self.config
1005    }
1006
1007    pub fn with_config(config: UnifiedStoreConfig) -> Self {
1008        Self {
1009            config,
1010            resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1011            format_version: AtomicU32::new(STORE_VERSION_V11),
1012            pager: None,
1013            db_path: None,
1014            btree_indices: RwLock::new(HashMap::new()),
1015            key_indices: RwLock::new(HashMap::new()),
1016            secondary_index_specs: RwLock::new(HashMap::new()),
1017            recovery_xid_summary: RwLock::new(None),
1018            checkpoint_versions: RwLock::new(HashMap::new()),
1019            checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1020            retired_btrees: parking_lot::Mutex::new(Vec::new()),
1021            ownership_catalog_epoch: AtomicU64::new(0),
1022            ownership_active_epoch: AtomicU64::new(0),
1023            detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1024            commit: None,
1025            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1026            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1027            snapshot_manager: std::sync::OnceLock::new(),
1028            embedded_commit_session: std::sync::OnceLock::new(),
1029            #[cfg(test)]
1030            test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1031            #[cfg(test)]
1032            test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1033            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1034            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1035        }
1036    }
1037
1038    /// Open or create a page-based database
1039    ///
1040    /// This uses the page engine for ACID durability with B-tree indices.
1041    /// The database file uses 16 KiB pages with checksums and efficient caching.
1042    ///
1043    /// # Arguments
1044    ///
1045    /// * `path` - Path to the database file (e.g., "data.rdb")
1046    ///
1047    /// # Example
1048    ///
1049    /// ```rust,ignore
1050    /// let store = UnifiedStore::open("security.rdb")?;
1051    /// store.create_collection("hosts")?;
1052    /// // ... operations ...
1053    /// store.persist()?; // Flush to disk
1054    /// ```
1055    pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
1056        Self::open_with_config(path, UnifiedStoreConfig::default())
1057    }
1058
1059    pub fn open_with_config(
1060        path: impl AsRef<Path>,
1061        config: UnifiedStoreConfig,
1062    ) -> Result<Self, StoreError> {
1063        let path = path.as_ref();
1064        let mut pager_config = PagerConfig {
1065            disk_residency: config.resident_memory_bytes_max.is_some(),
1066            ..PagerConfig::default()
1067        };
1068        // ADR 0073 §2 — the page cache is pre-sized from its budget share.
1069        // The pager's own `cache_size` default survives only for direct
1070        // library callers who never resolved a budget.
1071        if let Some(slots) = config.page_cache_slots {
1072            pager_config.cache_size = slots;
1073        }
1074        // Tunables via env — experimental, used by the benchmark harness
1075        // to compare durability profiles head-to-head with Postgres.
1076        // REDDB_DOUBLE_WRITE=0 requests skipping the double-write buffer,
1077        // which otherwise adds two fsyncs per pager flush (one on DWB, one
1078        // on the main file). The pager honors this only when the actual
1079        // data file is proven to live on a CoW filesystem with atomic page
1080        // writes; otherwise it fails closed and keeps DWB enabled.
1081        if matches!(
1082            std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
1083            Some("0") | Some("false") | Some("off")
1084        ) {
1085            pager_config.double_write = false;
1086        }
1087        let pager_open_started = std::time::Instant::now();
1088        let pager = Pager::open(path, pager_config)
1089            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1090        pager
1091            .enable_deferred_reclamation()
1092            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1093        trace_startup_phase(path, "paged_pager_open", pager_open_started);
1094
1095        if config.resident_memory_bytes_max.is_none()
1096            && pager
1097                .header()
1098                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1099                .version
1100                >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
1101        {
1102            return Err(StoreError::Io(std::io::Error::new(std::io::ErrorKind::Unsupported,
1103                "this database was prepared for disk residency; reopen with the disk-resident storage profile")));
1104        }
1105
1106        let wal_path = reddb_file::layout::unified_wal_path(path);
1107        // Explicit transactions and WAL-only vault versions need committed
1108        // batches in every durability mode, including Strict's inline fsync.
1109        let commit = Some(Arc::new(
1110            StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
1111                .map_err(StoreError::Io)?,
1112        ));
1113
1114        let store = Self {
1115            config,
1116            resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1117            format_version: AtomicU32::new(STORE_VERSION_V11),
1118            pager: Some(Arc::new(pager)),
1119            db_path: Some(path.to_path_buf()),
1120            btree_indices: RwLock::new(HashMap::new()),
1121            key_indices: RwLock::new(HashMap::new()),
1122            secondary_index_specs: RwLock::new(HashMap::new()),
1123            recovery_xid_summary: RwLock::new(None),
1124            checkpoint_versions: RwLock::new(HashMap::new()),
1125            checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1126            retired_btrees: parking_lot::Mutex::new(Vec::new()),
1127            ownership_catalog_epoch: AtomicU64::new(0),
1128            ownership_active_epoch: AtomicU64::new(0),
1129            detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1130            commit,
1131            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1132            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1133            snapshot_manager: std::sync::OnceLock::new(),
1134            embedded_commit_session: std::sync::OnceLock::new(),
1135            #[cfg(test)]
1136            test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1137            #[cfg(test)]
1138            test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1139            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1140            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1141        };
1142
1143        // An interrupted structural edit may have reached home pages without
1144        // publishing its catalog. Restore those pages before following roots.
1145        let checkpoint_recovery_started = std::time::Instant::now();
1146        if let Some(commit) = &store.commit {
1147            commit
1148                .restore_checkpoint_page_images(&store)
1149                .map_err(StoreError::Io)?;
1150        }
1151        trace_startup_phase(
1152            path,
1153            "paged_checkpoint_recovery",
1154            checkpoint_recovery_started,
1155        );
1156        // Load existing data from pages if database exists
1157        let page_load_started = std::time::Instant::now();
1158        let catalog_load = store.load_from_pages()?;
1159        trace_startup_phase(path, "paged_catalog_and_roots", page_load_started);
1160        // Reconcile independently from the catalog flag. A prior process may
1161        // have published one side of the rename before crashing; the manifest
1162        // operation is idempotent and fails closed on old/new collisions.
1163        store.migrate_operational_manifest(&catalog_load.collisions)?;
1164        let wal_replay_started = std::time::Instant::now();
1165        if let Some(commit) = &store.commit {
1166            commit.replay_into(&store).map_err(StoreError::Io)?;
1167        }
1168        trace_startup_phase(path, "paged_wal_replay", wal_replay_started);
1169        if catalog_load.migrated {
1170            // Publish only the renamed catalog keys. Existing row, key and
1171            // secondary-index roots remain attached and are not rebuilt.
1172            store.mark_paged_registry_dirty();
1173            store.flush_paged_state()?;
1174        }
1175        let manifest_recovery_started = std::time::Instant::now();
1176        store.recover_operational_manifest()?;
1177        if store.config.resident_memory_bytes_max.is_some() {
1178            let missing_key_index = {
1179                let trees = store.btree_indices.read();
1180                let keys = store.key_indices.read();
1181                trees.iter().any(|(name, tree)| {
1182                    !keys
1183                        .get(name)
1184                        .is_some_and(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
1185                })
1186            };
1187            if missing_key_index {
1188                store.persist()?;
1189            }
1190        }
1191        trace_startup_phase(
1192            path,
1193            "paged_manifest_and_derived_roots",
1194            manifest_recovery_started,
1195        );
1196
1197        Ok(store)
1198    }
1199
1200    /// Replay changes the authoritative managers first. Build replacement trees
1201    /// only for changed collections, leaving every published tree untouched until
1202    /// all replacement pages are durable. Retain the WAL for auxiliary recovery.
1203    pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
1204        let Some(pager) = &self.pager else {
1205            return Ok(());
1206        };
1207        let collections = self.resident.collections.read();
1208        let mut indices = self.btree_indices.write();
1209        let previous_versions = self.checkpoint_versions.read();
1210        let raw_replay =
1211            self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
1212        for (name, manager) in collections.iter() {
1213            let mut reusable = None;
1214            if raw_replay {
1215                if let (Some(previous), Some(tree)) =
1216                    (previous_versions.get(name), indices.get(name))
1217                {
1218                    let mut current = CheckpointVersion {
1219                        manager: Arc::downgrade(manager),
1220                        segments: manager.checkpoint_revisions(),
1221                        visibility_revision: 0,
1222                        untracked_visibility_revision: 0,
1223                        excluded_xids: Arc::default(),
1224                        pending_vault: Arc::default(),
1225                        format_version: self.format_version(),
1226                        deferred_rows: Some(Arc::default()),
1227                        has_unprojected_rows: false,
1228                    };
1229                    if previous.manager.ptr_eq(&current.manager)
1230                        && previous.format_version == current.format_version
1231                    {
1232                        if previous.segments == current.segments
1233                            && !previous.has_unprojected_rows
1234                            && checkpoint_visibility_matches(previous, &current)
1235                        {
1236                            reusable = Some(Arc::clone(tree));
1237                        } else {
1238                            // The replay ownership epoch protects in-place
1239                            // changes with the same undo/publication protocol
1240                            // used by normal sparse checkpoints. Missing WAL
1241                            // proof, old formats and exhausted journals retain
1242                            // the full comparison/rebuild path below.
1243                            reusable = self.sparse_checkpoint_tree(
1244                                name,
1245                                manager,
1246                                tree,
1247                                previous,
1248                                &mut current,
1249                            )?;
1250                        }
1251                    }
1252                }
1253            }
1254            let tree = match reusable {
1255                Some(tree) => tree,
1256                None => {
1257                    self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1258                        #[cfg(test)]
1259                        REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1260                        Some(Self::serialize_entity_record(
1261                            entity,
1262                            metadata.as_ref(),
1263                            self.format_version(),
1264                        ))
1265                    })?
1266                }
1267            };
1268            if indices
1269                .get(name)
1270                .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1271            {
1272                continue;
1273            }
1274            if let Some(previous) = indices.insert(name.clone(), tree) {
1275                self.retire_btree(previous);
1276            }
1277            self.mark_paged_registry_dirty();
1278            #[cfg(test)]
1279            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1280                if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1281                    pager.sync().map_err(|error| {
1282                        StoreError::Io(std::io::Error::other(error.to_string()))
1283                    })?;
1284                }
1285                eprintln!("experimental replay rebuild exit before manifest");
1286                std::process::exit(182);
1287            }
1288        }
1289        drop(previous_versions);
1290        drop(indices);
1291        drop(collections);
1292        self.flush_paged_state()?;
1293        #[cfg(test)]
1294        if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1295            eprintln!("experimental replay exit after manifest");
1296            std::process::exit(182);
1297        }
1298        // Opening has no concurrent writers. Replay has now durably reconciled
1299        // these raw trees; do not immediately rebuild them at the first flush.
1300        if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1301            let collections = self.resident.collections.read();
1302            let mut versions = self.checkpoint_versions.write();
1303            for (name, manager) in collections.iter() {
1304                versions.insert(
1305                    name.clone(),
1306                    CheckpointVersion {
1307                        manager: Arc::downgrade(manager),
1308                        segments: manager.checkpoint_revisions(),
1309                        visibility_revision: 0,
1310                        untracked_visibility_revision: 0,
1311                        excluded_xids: Arc::default(),
1312                        pending_vault: Arc::default(),
1313                        format_version: self.format_version(),
1314                        deferred_rows: Some(Arc::default()),
1315                        has_unprojected_rows: false,
1316                    },
1317                );
1318            }
1319        }
1320        Ok(())
1321    }
1322
1323    pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
1324        let Some(path) = &self.db_path else {
1325            return Ok(());
1326        };
1327        let mut collections = self.list_collections();
1328        collections.sort();
1329        let pending_drops =
1330            crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1331                .recover_or_bootstrap(&collections)
1332                .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1333        for name in pending_drops {
1334            if self.get_collection(&name).is_some() {
1335                self.drop_collection(&name)?;
1336            }
1337        }
1338        Ok(())
1339    }
1340
1341    fn migrate_operational_manifest(
1342        &self,
1343        collisions: &[SystemCollectionCollision],
1344    ) -> Result<(), StoreError> {
1345        let Some(path) = &self.db_path else {
1346            return Ok(());
1347        };
1348        let manifest = crate::storage::operational_manifest::OperationalManifest::for_db_path(path);
1349        let collision_refs = collisions
1350            .iter()
1351            .map(|collision| {
1352                (
1353                    collision.survivor.as_str(),
1354                    collision.loser.as_str(),
1355                    collision.canonical.as_str(),
1356                )
1357            })
1358            .collect::<Vec<_>>();
1359        manifest
1360            .reconcile_collection_collisions(&collision_refs)
1361            .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1362        manifest
1363            .rename_collections(reddb_engine::storage::system_collection_names::LEGACY_NAMES)
1364            .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))
1365    }
1366
1367    pub(crate) fn prepare_operational_collection_create(
1368        &self,
1369        name: &str,
1370    ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
1371        let Some(path) = &self.db_path else {
1372            return Ok(None);
1373        };
1374        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1375            .prepare_collection_create(name)
1376            .map(Some)
1377            .map_err(StoreError::Io)
1378    }
1379
1380    pub(crate) fn prepare_operational_collection_drop(
1381        &self,
1382        name: &str,
1383    ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
1384        let Some(path) = &self.db_path else {
1385            return Ok(None);
1386        };
1387        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1388            .prepare_collection_drop(name)
1389            .map(Some)
1390            .map_err(StoreError::Io)
1391    }
1392
1393    /// Read the commit boundary before following potentially interrupted trees.
1394    pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1395        let Some(pager) = &self.pager else {
1396            return Ok(0);
1397        };
1398        let Some(content) = read_meta_payload(pager)? else {
1399            return Ok(0);
1400        };
1401        reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1402            .map(|epoch| epoch.unwrap_or(0))
1403            .map_err(|error| StoreError::Serialization(error.to_string()))
1404    }
1405
1406    /// Read the B-tree indices and reconstruct collections from pages.
1407    fn load_from_pages(&self) -> Result<CatalogLoadOutcome, StoreError> {
1408        let pager = match &self.pager {
1409            Some(p) => p,
1410            None => return Ok(CatalogLoadOutcome::default()), // No pager, nothing to load
1411        };
1412
1413        // Get page count
1414        let page_count = pager.page_count().map_err(|e| {
1415            StoreError::Io(std::io::Error::other(format!(
1416                "failed to read page count: {}",
1417                e
1418            )))
1419        })?;
1420        if page_count <= 1 {
1421            // Empty database (only header page)
1422            return Ok(CatalogLoadOutcome::default());
1423        }
1424
1425        // Read metadata starting from page 1 (collections registry). The
1426        // helper transparently follows the `RDM3` overflow chain when the
1427        // metadata blob spans multiple pages and falls back to the legacy
1428        // `<data>-meta` shadow when page 1 itself is corrupted.
1429        if let Some(content_vec) = read_meta_payload(pager)? {
1430            let content: &[u8] = &content_vec;
1431            let file_version = pager
1432                .header()
1433                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1434                .version;
1435            let prepared =
1436                file_version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION;
1437            let resume = if prepared {
1438                reddb_file::native_resume::decode_catalog_resume(content)
1439                    .map_err(|error| StoreError::Serialization(error.to_string()))?
1440            } else {
1441                None
1442            };
1443            let mut outcome = CatalogLoadOutcome::default();
1444            let mut ignored_collections = std::collections::HashSet::new();
1445            if let Some(resume) = &resume {
1446                for &(legacy, canonical) in
1447                    reddb_engine::storage::system_collection_names::LEGACY_NAMES
1448                {
1449                    let (Some(legacy_state), Some(canonical_state)) = (
1450                        resume.collections.get(legacy),
1451                        resume.collections.get(canonical),
1452                    ) else {
1453                        continue;
1454                    };
1455                    let (survivor, loser) = match (
1456                        legacy_state.entities == 0,
1457                        canonical_state.entities == 0,
1458                    ) {
1459                        (false, true) => (legacy, canonical),
1460                        (true, false) | (true, true) => (canonical, legacy),
1461                        (false, false) => {
1462                            return Err(page_integrity(
1463                                1,
1464                                None,
1465                                format!(
1466                                    "catalog contains both populated legacy collection '{legacy}' and canonical collection '{canonical}'"
1467                                ),
1468                            ));
1469                        }
1470                    };
1471                    ignored_collections.insert(loser.to_string());
1472                    outcome.collisions.push(SystemCollectionCollision {
1473                        survivor: survivor.to_string(),
1474                        loser: loser.to_string(),
1475                        canonical: canonical.to_string(),
1476                    });
1477                }
1478                let live_roots = resume
1479                    .collections
1480                    .iter()
1481                    .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1482                    .flat_map(|(_, state)| {
1483                        std::iter::once(state.root_page)
1484                            .chain(state.key_index_root)
1485                            .chain(
1486                                state
1487                                    .secondary_indices
1488                                    .values()
1489                                    .map(|index| index.root_page),
1490                            )
1491                    })
1492                    .filter(|root| *root != 0)
1493                    .collect::<std::collections::HashSet<_>>();
1494                for collision in &outcome.collisions {
1495                    let loser_state = resume
1496                        .collections
1497                        .get(&collision.loser)
1498                        .expect("collision loser is present");
1499                    if std::iter::once(loser_state.root_page)
1500                        .chain(loser_state.key_index_root)
1501                        .chain(
1502                            loser_state
1503                                .secondary_indices
1504                                .values()
1505                                .map(|index| index.root_page),
1506                        )
1507                        .filter(|root| *root != 0)
1508                        .any(|root| live_roots.contains(&root))
1509                    {
1510                        return Err(page_integrity(
1511                            1,
1512                            None,
1513                            format!(
1514                                "empty collection '{}' aliases a live collection root",
1515                                collision.loser
1516                            ),
1517                        ));
1518                    }
1519                }
1520                reddb_engine::storage::system_collection_names::migration_plan(
1521                    resume
1522                        .collections
1523                        .keys()
1524                        .map(String::as_str)
1525                        .filter(|name| !ignored_collections.contains(*name)),
1526                )
1527                .map_err(|(legacy, canonical)| {
1528                    page_integrity(
1529                        1,
1530                        None,
1531                        format!(
1532                            "catalog contains both legacy collection '{legacy}' and canonical collection '{canonical}'"
1533                        ),
1534                    )
1535                })?;
1536                for state in resume.collections.values() {
1537                    if !state.secondary_indices.is_empty()
1538                        && file_version < reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
1539                    {
1540                        return Err(page_integrity(
1541                            1,
1542                            None,
1543                            "secondary roots require the file format fence",
1544                        ));
1545                    }
1546                    for root in std::iter::once(state.root_page)
1547                        .chain(state.key_index_root)
1548                        .chain(
1549                            state
1550                                .secondary_indices
1551                                .values()
1552                                .map(|index| index.root_page),
1553                        )
1554                    {
1555                        if root >= page_count {
1556                            return Err(page_integrity(
1557                                root,
1558                                None,
1559                                "published root exceeds the database page count",
1560                            ));
1561                        }
1562                    }
1563                }
1564                self.resident
1565                    .next_entity_id
1566                    .fetch_max(resume.entity_id_next, Ordering::SeqCst);
1567            }
1568            *self.recovery_xid_summary.write() = resume.as_ref().and_then(|resume| {
1569                resume.xid_high_water_mark.map(|high_water_mark| {
1570                    super::recovery_xid::RecoveryXidSummary {
1571                        high_water_mark,
1572                        roots: resume
1573                            .collections
1574                            .iter()
1575                            .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1576                            .map(|(name, state)| {
1577                                (
1578                                    reddb_engine::storage::system_collection_names::canonical_or_self(name)
1579                                        .to_owned(),
1580                                    state.root_page,
1581                                )
1582                            })
1583                            .collect(),
1584                    }
1585                })
1586            });
1587            if content.len() >= 4 {
1588                let mut pos = 0;
1589                let mut format_version = STORE_VERSION_V1;
1590
1591                let collection_count = if let Some(header) =
1592                    reddb_file::decode_native_paged_metadata_header(content)
1593                        .map_err(|err| StoreError::Serialization(err.to_string()))?
1594                {
1595                    format_version = header.format_version;
1596                    pos += reddb_file::METADATA_HEADER_BYTES;
1597                    header.collection_count as usize
1598                } else {
1599                    let count = u32::from_le_bytes([
1600                        content[pos],
1601                        content[pos + 1],
1602                        content[pos + 2],
1603                        content[pos + 3],
1604                    ]) as usize;
1605                    pos += 4;
1606                    count
1607                };
1608
1609                self.set_format_version(format_version);
1610
1611                if pos > content.len() {
1612                    return Ok(CatalogLoadOutcome::default());
1613                }
1614
1615                let mut source_names = std::collections::HashMap::<String, String>::new();
1616                let mut migrated_catalog = !outcome.collisions.is_empty();
1617                let mut empty_duplicate_roots = std::collections::HashSet::new();
1618
1619                // Read collection names and their B-tree root pages
1620                for _ in 0..collection_count {
1621                    let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1622                        .map_err(|err| page_integrity(1, None, err.to_string()))?;
1623                    // Root page ID for this collection's B-tree
1624                    let root_page = root.root_page;
1625                    let stored_name = root.collection;
1626                    if ignored_collections.contains(&stored_name) {
1627                        let state = resume
1628                            .as_ref()
1629                            .and_then(|resume| resume.collections.get(&stored_name))
1630                            .expect("ignored collection was proven by catalog resume");
1631                        for retired_root in std::iter::once(state.root_page)
1632                            .chain(state.key_index_root)
1633                            .chain(
1634                                state
1635                                    .secondary_indices
1636                                    .values()
1637                                    .map(|index| index.root_page),
1638                            )
1639                            .filter(|root| *root != 0)
1640                        {
1641                            empty_duplicate_roots.insert(retired_root);
1642                        }
1643                        continue;
1644                    }
1645                    let name = reddb_engine::storage::system_collection_names::canonical_or_self(
1646                        &stored_name,
1647                    )
1648                    .to_owned();
1649                    if let Some(previous) = source_names.insert(name.clone(), stored_name.clone()) {
1650                        if previous != stored_name {
1651                            return Err(page_integrity(
1652                                1,
1653                                None,
1654                                format!(
1655                                    "catalog contains both legacy collection '{previous}' and canonical collection '{name}'"
1656                                ),
1657                            ));
1658                        }
1659                    }
1660                    migrated_catalog |= name != stored_name;
1661
1662                    // Hydrate the collection in memory only. Loading must
1663                    // not emit WAL entries or rewrite the on-disk registry
1664                    // before the existing B-tree roots are attached.
1665                    let _ = self.create_collection_in_memory(&name);
1666
1667                    let counters = resume
1668                        .as_ref()
1669                        .and_then(|resume| resume.collections.get(&stored_name));
1670                    if let (Some(counters), Some(manager)) = (counters, self.get_collection(&name))
1671                    {
1672                        manager.restore_identity_counters(
1673                            counters.entity_id_next,
1674                            counters.row_id_next,
1675                        );
1676                        manager.restore_sequence_id_next(counters.sequence_id_next);
1677                    }
1678                    // Load B-tree with root page if it exists
1679                    if root_page > 0 || counters.is_some() {
1680                        let btree = BTree::with_root(Arc::clone(pager), root_page);
1681
1682                        // Load all entities from B-tree into the collection
1683                        let mut cursor = btree.cursor_first().map_err(|err| {
1684                            page_integrity(root_page, Some(name.clone()), err.to_string())
1685                        })?;
1686                        let manager = self.get_collection(&name);
1687                        let mut persisted_entities = counters.map_or(0, |state| state.entities);
1688                        while counters.is_none() {
1689                            let next = cursor
1690                                .next_with_budget(
1691                                    self.config
1692                                        .resident_memory_bytes_max
1693                                        .and_then(|limit| usize::try_from(limit).ok())
1694                                        .unwrap_or(usize::MAX),
1695                                )
1696                                .map_err(|err| {
1697                                    page_integrity(root_page, Some(name.clone()), err.to_string())
1698                                })?;
1699                            let Some((key, value)) = next else {
1700                                break;
1701                            };
1702                            persisted_entities += 1;
1703                            // Deserialize entity from value bytes
1704                            let key_bytes: [u8; 8] = key.as_slice().try_into().map_err(|_| {
1705                                page_integrity(
1706                                    root_page,
1707                                    Some(name.clone()),
1708                                    "physical entity key must contain eight bytes",
1709                                )
1710                            })?;
1711                            let record = reddb_engine::storage::collection_read::EncodedRecord {
1712                                id: EntityId::new(u64::from_be_bytes(key_bytes)),
1713                                bytes: value,
1714                            };
1715                            let decoded = if let Some(limit) = self.config.resident_memory_bytes_max
1716                            {
1717                                record.decode_with_budget(
1718                                    self.format_version(),
1719                                    usize::try_from(limit).unwrap_or(usize::MAX),
1720                                )
1721                            } else {
1722                                record.decode(self.format_version())
1723                            };
1724                            let (entity, metadata) = decoded.map_err(|err| {
1725                                page_integrity(root_page, Some(name.clone()), err.to_string())
1726                            })?;
1727                            if let Some(m) = &manager {
1728                                let id = entity.id;
1729                                if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1730                                    m.register_row_id(*row_id);
1731                                }
1732                                if self.config.resident_memory_bytes_max.is_some() {
1733                                    self.register_entity_id(id);
1734                                    m.restore_sequence_id_next(
1735                                        entity.sequence_id.saturating_add(1),
1736                                    );
1737                                    m.restore_identity_counters(
1738                                        id.raw().saturating_add(1),
1739                                        m.identity_counters().1,
1740                                    );
1741                                    continue;
1742                                }
1743                                self.resident.context_index.index_entity(&name, &entity);
1744                                m.restore_snapshot_entity(entity.clone())?;
1745                                if let Some(metadata) = metadata {
1746                                    let _ = m.set_metadata(id, metadata);
1747                                }
1748                                self.register_entity_id(id);
1749                                if self.config.auto_index_refs {
1750                                    self.index_cross_refs(&entity, &name)?;
1751                                }
1752                            }
1753                        }
1754
1755                        // Store the B-tree for future lookups
1756                        let btree = Arc::new(btree);
1757                        let key_tree = counters
1758                            .and_then(|state| state.key_index_root)
1759                            .map(|root| Arc::new(BTree::with_root(Arc::clone(pager), root)));
1760                        if let Some(counters) = counters {
1761                            self.key_indices.write().insert(
1762                                name.clone(),
1763                                PublishedKeyIndex {
1764                                    indexed: key_tree.is_some(),
1765                                    secondary: counters.secondary_indices.iter().map(|(name, index)| {
1766                                        (name.clone(), super::secondary_index::PublishedSecondaryIndex {
1767                                            identity: super::secondary_index::SecondaryIndexIdentity {
1768                                                codec_version: index.codec_version,
1769                                                definition: index.definition,
1770                                            },
1771                                            tree: Arc::new(BTree::with_root(Arc::clone(pager), index.root_page)),
1772                                        })
1773                                    }).collect(),
1774                                    source: Arc::downgrade(&btree),
1775                                    tree: key_tree.clone().unwrap_or_else(|| {
1776                                        Arc::new(BTree::with_root(Arc::clone(pager), 0))
1777                                    }),
1778                                },
1779                            );
1780                        }
1781                        if let (Some(manager), Some(limit)) =
1782                            (&manager, self.config.resident_memory_bytes_max)
1783                        {
1784                            manager.install_backing(
1785                                Arc::new(
1786                                    match key_tree {
1787                                        Some(keys) => reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(&btree), keys),
1788                                        None => reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(&btree)),
1789                                    },
1790                                ),
1791                                btree.root_page_id(),
1792                                format_version,
1793                                usize::try_from(limit).unwrap_or(usize::MAX),
1794                                persisted_entities,
1795                            );
1796                        }
1797                        self.btree_indices.write().insert(name.clone(), btree);
1798                        if format_version == STORE_VERSION_V11 {
1799                            if let Some(manager) = &manager {
1800                                self.checkpoint_versions.write().insert(
1801                                    name,
1802                                    CheckpointVersion {
1803                                        manager: Arc::downgrade(manager),
1804                                        segments: manager.checkpoint_revisions(),
1805                                        visibility_revision: 0,
1806                                        untracked_visibility_revision: 0,
1807                                        excluded_xids: Arc::default(),
1808                                        pending_vault: Arc::default(),
1809                                        format_version,
1810                                        deferred_rows: Some(Arc::default()),
1811                                        has_unprojected_rows: false,
1812                                    },
1813                                );
1814                            }
1815                        }
1816                    }
1817                }
1818
1819                if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1820                    let cross_ref_count = u32::from_le_bytes([
1821                        content[pos],
1822                        content[pos + 1],
1823                        content[pos + 2],
1824                        content[pos + 3],
1825                    ]) as usize;
1826                    pos += 4;
1827
1828                    for _ in 0..cross_ref_count {
1829                        let Ok(cross_ref) =
1830                            reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1831                        else {
1832                            break;
1833                        };
1834                        let source_id = EntityId::new(cross_ref.source_id);
1835                        let target_id = EntityId::new(cross_ref.target_id);
1836                        let ref_type = RefType::from_byte(cross_ref.ref_type);
1837                        let target_collection =
1838                            reddb_engine::storage::system_collection_names::canonical_owned(
1839                                cross_ref.target_collection,
1840                            );
1841
1842                        self.resident
1843                            .cross_refs
1844                            .write()
1845                            .entry(source_id)
1846                            .or_default()
1847                            .push((target_id, ref_type, target_collection.clone()));
1848
1849                        if let Some((collection, mut entity)) = self.get_any(source_id) {
1850                            let exists = entity.cross_refs().iter().any(|xref| {
1851                                xref.target == target_id
1852                                    && xref.ref_type == ref_type
1853                                    && xref.target_collection == target_collection
1854                            });
1855                            if !exists {
1856                                entity.cross_refs_mut().push(CrossRef::new(
1857                                    source_id,
1858                                    target_id,
1859                                    target_collection.clone(),
1860                                    ref_type,
1861                                ));
1862                                if let Some(manager) = self.get_collection(&collection) {
1863                                    let _ = manager.update(entity);
1864                                }
1865                            }
1866                        }
1867                    }
1868                }
1869                let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1870                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1871                let already_retired = retired
1872                    .iter()
1873                    .copied()
1874                    .collect::<std::collections::HashSet<_>>();
1875                let mut live_roots: std::collections::HashSet<_> = self
1876                    .btree_indices
1877                    .read()
1878                    .values()
1879                    .map(|tree| tree.root_page_id())
1880                    .collect();
1881                live_roots.extend(
1882                    self.key_indices
1883                        .read()
1884                        .values()
1885                        .flat_map(PublishedKeyIndex::trees)
1886                        .map(|tree| tree.root_page_id()),
1887                );
1888                for root in retired {
1889                    if live_roots.contains(&root)
1890                        || root
1891                            >= pager
1892                                .page_count()
1893                                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1894                    {
1895                        return Err(page_integrity(
1896                            root,
1897                            None,
1898                            "retired root aliases a live or missing page",
1899                        ));
1900                    }
1901                    self.retired_btrees.lock().push(RetiredBTree {
1902                        tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1903                        published: true,
1904                    });
1905                }
1906                for root in empty_duplicate_roots
1907                    .iter()
1908                    .copied()
1909                    .filter(|root| !already_retired.contains(root))
1910                {
1911                    self.retire_btree(Arc::new(BTree::with_root(Arc::clone(pager), root)));
1912                }
1913                let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1914                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1915                if deferred
1916                    .iter()
1917                    .any(|id| live_roots.contains(id) || empty_duplicate_roots.contains(id))
1918                {
1919                    return Err(page_integrity(1, None, "deferred page aliases a live root"));
1920                }
1921                pager
1922                    .restore_deferred_reclamation(&deferred)
1923                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1924                let ownership_epoch =
1925                    reddb_file::native_store::decode_native_paged_ownership_epoch(
1926                        content, &mut pos,
1927                    )
1928                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1929                self.ownership_catalog_epoch
1930                    .store(ownership_epoch.unwrap_or(0), Ordering::Release);
1931                if migrated_catalog {
1932                    self.mark_paged_registry_dirty();
1933                }
1934                if self.format_version() < STORE_VERSION_V11 {
1935                    self.set_format_version(STORE_VERSION_V11);
1936                }
1937                outcome.migrated = migrated_catalog;
1938                return Ok(outcome);
1939            }
1940        }
1941
1942        if self.format_version() < STORE_VERSION_V11 {
1943            self.set_format_version(STORE_VERSION_V11);
1944        }
1945
1946        Ok(CatalogLoadOutcome::default())
1947    }
1948
1949    /// Deserialize an entity from binary bytes
1950    pub(crate) fn deserialize_entity(
1951        data: &[u8],
1952        format_version: u32,
1953    ) -> Result<UnifiedEntity, StoreError> {
1954        let mut pos = 0;
1955        Self::read_entity_binary(data, &mut pos, format_version)
1956            .map_err(|e| StoreError::Serialization(e.to_string()))
1957    }
1958
1959    /// Serialize an entity to binary bytes
1960    pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1961        // Pre-allocate ~256 bytes to cover the typical 15-column
1962        // typed row without any Vec growth. Bulk insert calls this
1963        // millions of times per bench run; saving 2-3 reallocs per
1964        // entity amortises.
1965        let mut buf = Vec::with_capacity(256);
1966        Self::write_entity_binary(&mut buf, entity, format_version);
1967        buf
1968    }
1969
1970    pub(crate) fn serialize_entity_record(
1971        entity: &UnifiedEntity,
1972        metadata: Option<&Metadata>,
1973        format_version: u32,
1974    ) -> Vec<u8> {
1975        reddb_engine::storage::metadata_codec::serialize_entity_record(
1976            entity,
1977            metadata,
1978            format_version,
1979        )
1980    }
1981
1982    pub(crate) fn deserialize_entity_record(
1983        data: &[u8],
1984        format_version: u32,
1985    ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1986        reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
1987            .map_err(StoreError::Serialization)
1988    }
1989
1990    /// Persist all data to page-based storage
1991    ///
1992    /// Writes all entities to B-tree pages and flushes to disk.
1993    /// This provides ACID durability guarantees.
1994    pub fn persist(&self) -> Result<(), StoreError> {
1995        let _vault_guard = self.vault_publication_lock.lock();
1996        let pending_vault_versions = self.pending_vault_versions.read();
1997        let pager = match &self.pager {
1998            Some(p) => p,
1999            None => {
2000                // No pager attached - use binary file fallback if path available
2001                if let Some(path) = &self.db_path {
2002                    return self
2003                        .save_to_file(path)
2004                        .map_err(|e| StoreError::Serialization(e.to_string()));
2005                }
2006                return Err(StoreError::Io(std::io::Error::other(
2007                    "No pager or path configured for persistence",
2008                )));
2009            }
2010        };
2011        // Prevent unrelated allocations or frees from being attributed to
2012        // this checkpoint's ownership epoch.
2013        let _ownership_guard = pager.lock_ownership_mutations();
2014
2015        match pager.read_page(1) {
2016            Ok(_) => {}
2017            Err(PagerError::PageNotFound(_)) => {
2018                let meta_page = pager
2019                    .allocate_page(crate::storage::engine::PageType::Header)
2020                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2021                pager
2022                    .write_page(meta_page.page_id(), meta_page)
2023                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2024            }
2025            Err(e) => {
2026                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
2027            }
2028        }
2029
2030        if let Some(commit) = &self.commit {
2031            commit.force_sync().map_err(StoreError::Io)?;
2032        }
2033
2034        struct InterruptedOwnership<'a> {
2035            pager: &'a Pager,
2036            active: bool,
2037        }
2038        impl Drop for InterruptedOwnership<'_> {
2039            fn drop(&mut self) {
2040                if self.active {
2041                    self.pager.fence_interrupted_ownership();
2042                }
2043            }
2044        }
2045        let mut ownership = InterruptedOwnership {
2046            pager,
2047            active: self.commit.is_some(),
2048        };
2049        let epoch = if let Some(commit) = &self.commit {
2050            let epoch = commit
2051                .next_page_ownership_epoch(self)
2052                .map_err(StoreError::Io)?;
2053            commit
2054                .begin_page_ownership_epoch(self, epoch)
2055                .map_err(StoreError::Io)?;
2056            Some(epoch)
2057        } else {
2058            None
2059        };
2060
2061        let collections = self.resident.collections.read();
2062        let mut btree_indices = self.btree_indices.write();
2063
2064        // Collect collection names and their B-tree root pages
2065        let mut collection_roots: Vec<(String, u32)> = Vec::new();
2066
2067        // Build unpublished replacements. Deleting from the current trees here
2068        // would destroy the last checkpoint if a write fails before publication.
2069        let mut rebuilt_indices = HashMap::with_capacity(collections.len());
2070        let previous_versions = self.checkpoint_versions.read();
2071        let mut checkpoint_versions = HashMap::with_capacity(collections.len());
2072        let pending_vault = Arc::new(pending_vault_versions.clone());
2073        let (visibility_revision, excluded, untracked_visibility_revision) =
2074            self.snapshot_manager.get().map_or_else(
2075                || (0, Default::default(), 0),
2076                |manager| manager.checkpoint_projection_state(),
2077            );
2078        let excluded = Arc::new(excluded);
2079        for (name, manager) in collections.iter() {
2080            let mut version = CheckpointVersion {
2081                manager: Arc::downgrade(manager),
2082                segments: manager.checkpoint_revisions(),
2083                visibility_revision,
2084                untracked_visibility_revision,
2085                excluded_xids: Arc::clone(&excluded),
2086                pending_vault: Arc::clone(&pending_vault),
2087                format_version: self.format_version(),
2088                deferred_rows: Some(Arc::default()),
2089                has_unprojected_rows: false,
2090            };
2091            if let (Some(previous), Some(tree)) =
2092                (previous_versions.get(name), btree_indices.get(name))
2093            {
2094                if previous.manager.ptr_eq(&version.manager)
2095                    && previous.segments == version.segments
2096                    && previous.format_version == version.format_version
2097                    && !previous.has_unprojected_rows
2098                    && checkpoint_visibility_matches(previous, &version)
2099                {
2100                    version.deferred_rows = previous.deferred_rows.clone();
2101                    collection_roots.push((name.clone(), tree.root_page_id()));
2102                    rebuilt_indices.insert(name.clone(), Arc::clone(tree));
2103                    checkpoint_versions.insert(name.clone(), version);
2104                    continue;
2105                }
2106            }
2107            if let (Some(previous), Some(tree)) =
2108                (previous_versions.get(name), btree_indices.get(name))
2109            {
2110                if previous.manager.ptr_eq(&version.manager)
2111                    && previous.format_version == version.format_version
2112                {
2113                    if let Some(tree) =
2114                        self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
2115                    {
2116                        collection_roots.push((name.clone(), tree.root_page_id()));
2117                        rebuilt_indices.insert(name.clone(), tree);
2118                        checkpoint_versions.insert(name.clone(), version);
2119                        continue;
2120                    }
2121                }
2122            }
2123            let btree = self.checkpoint_tree(
2124                pager,
2125                manager,
2126                btree_indices.get(name),
2127                |entity, metadata| {
2128                    #[cfg(test)]
2129                    CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
2130                    let hidden = pending_vault_versions.contains(&entity.id)
2131                        || (matches!(
2132                            entity.data,
2133                            EntityData::Row(_)
2134                                | EntityData::Vector(_)
2135                                | EntityData::QueueMessage(_)
2136                        ) && excluded.contains(&entity.xmin));
2137                    let normalize_xmax = matches!(
2138                        entity.data,
2139                        EntityData::Row(_) | EntityData::QueueMessage(_)
2140                    ) && excluded.contains(&entity.xmax);
2141                    if hidden || normalize_xmax {
2142                        version.remember_deferred_row(entity.id);
2143                    }
2144                    if hidden {
2145                        return None;
2146                    }
2147                    let mut normalized = std::borrow::Cow::Borrowed(entity);
2148                    if normalize_xmax {
2149                        normalized.to_mut().xmax = 0;
2150                    }
2151                    Some(Self::serialize_entity_record(
2152                        &normalized,
2153                        metadata.as_ref(),
2154                        self.format_version(),
2155                    ))
2156                },
2157            )?;
2158            checkpoint_versions.insert(name.clone(), version);
2159
2160            collection_roots.push((name.clone(), btree.root_page_id()));
2161            rebuilt_indices.insert(name.clone(), btree);
2162        }
2163
2164        // Write collection metadata to page 1
2165        let mut meta_data = Vec::with_capacity(4096);
2166
2167        let format_version = STORE_VERSION_V11;
2168        self.set_format_version(format_version);
2169
2170        reddb_file::encode_native_paged_metadata_header(
2171            &mut meta_data,
2172            reddb_file::NativePagedMetadataHeader {
2173                format_version,
2174                collection_count: collection_roots.len() as u32,
2175            },
2176        );
2177
2178        // Write each collection's name and B-tree root page
2179        for (name, root_page) in &collection_roots {
2180            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
2181        }
2182
2183        // Write cross-reference metadata
2184        let cross_refs = self.resident.cross_refs.read();
2185        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
2186        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
2187        for (source_id, refs) in cross_refs.iter() {
2188            for (target_id, ref_type, collection) in refs {
2189                reddb_file::encode_native_paged_cross_ref(
2190                    &mut meta_data,
2191                    source_id.raw(),
2192                    target_id.raw(),
2193                    ref_type.to_byte(),
2194                    collection,
2195                );
2196            }
2197        }
2198
2199        for (name, previous) in btree_indices.iter() {
2200            if !rebuilt_indices
2201                .get(name)
2202                .is_some_and(|tree| Arc::ptr_eq(tree, previous))
2203            {
2204                self.retire_btree(Arc::clone(previous));
2205            }
2206        }
2207        #[cfg(test)]
2208        if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
2209            pager
2210                .sync()
2211                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2212            eprintln!("experimental checkpoint exit before manifest");
2213            std::process::exit(183);
2214        }
2215        let rebuilt_keys = self.checkpoint_key_indices(pager, &rebuilt_indices)?;
2216        #[cfg(test)]
2217        if std::env::var_os("REDDB_TEST_EXIT_AFTER_SECONDARY_INDEX_BUILD").is_some() {
2218            pager
2219                .sync()
2220                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2221            std::process::exit(196);
2222        }
2223        self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices, &rebuilt_keys)?;
2224
2225        #[cfg(test)]
2226        if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
2227            std::process::exit(195);
2228        }
2229
2230        // Only the now-durable catalog may replace the live tree handles. The
2231        // previous generation is retained in the manifest retirement ledger.
2232        *btree_indices = rebuilt_indices;
2233        *self.key_indices.write() = rebuilt_keys;
2234        drop(previous_versions);
2235        *self.checkpoint_versions.write() = checkpoint_versions;
2236
2237        if let Some(commit) = &self.commit {
2238            commit
2239                .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
2240                .map_err(StoreError::Io)?;
2241            ownership.active = false;
2242            commit.truncate().map_err(StoreError::Io)?;
2243        }
2244        *self.checkpoint_durable_rows.write() = Default::default();
2245
2246        if let Some(limit) = self.config.resident_memory_bytes_max {
2247            let mut versions = self.checkpoint_versions.write();
2248            for (name, manager) in collections.iter() {
2249                if versions
2250                    .get(name)
2251                    .is_some_and(|version| !version.has_unprojected_rows)
2252                {
2253                    if let Some(tree) = btree_indices.get(name) {
2254                        manager.install_backing(
2255                            Arc::new(
2256                                {
2257                                    let keys = self.key_indices.read();
2258                                    let index = keys.get(name).expect("invariant: prepared checkpoint classifies its key index");
2259                                    if index.indexed {
2260                                        reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(tree), Arc::clone(&index.tree))
2261                                    } else {
2262                                        reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(tree))
2263                                    }
2264                                },
2265                            ),
2266                            tree.root_page_id(),
2267                            self.format_version(),
2268                            usize::try_from(limit).unwrap_or(usize::MAX),
2269                            manager.count() as u64,
2270                        );
2271                        if let Some(version) = versions.get_mut(name) {
2272                            version.segments = manager.checkpoint_revisions();
2273                        }
2274                    }
2275                }
2276            }
2277        }
2278
2279        Ok(())
2280    }
2281
2282    /// Check if the store is using page-based persistence
2283    pub fn is_paged(&self) -> bool {
2284        self.pager.is_some()
2285    }
2286
2287    /// Current root page for a collection's primary B-tree, if one has
2288    /// been materialized in this store.
2289    pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
2290        self.btree_indices
2291            .read()
2292            .get(Self::canonical_collection(collection))
2293            .map(|btree| btree.root_page_id())
2294            .filter(|root| *root != 0)
2295    }
2296
2297    /// Get the database file path (if using paged mode)
2298    pub fn db_path(&self) -> Option<&Path> {
2299        self.db_path.as_deref()
2300    }
2301}
2302
2303#[cfg(test)]
2304mod checkpoint_work_tests {
2305    use super::*;
2306
2307    struct SystemNameProjection;
2308
2309    impl super::super::secondary_index::SecondaryIndexProjector for SystemNameProjection {
2310        fn project(
2311            &self,
2312            entity: &UnifiedEntity,
2313            key: &mut [u8; super::super::secondary_index::SECONDARY_KEY_BYTES_MAX],
2314        ) -> Result<Option<usize>, StoreError> {
2315            key[..8].copy_from_slice(&entity.id.raw().to_be_bytes());
2316            Ok(Some(8))
2317        }
2318    }
2319
2320    #[cfg(target_os = "linux")]
2321    #[test]
2322    fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
2323        const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
2324        if let Some(path) = std::env::var_os(CHILD) {
2325            let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
2326            let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
2327            let store = UnifiedStore::open(path).expect("open store");
2328            let initial_count = if multi { 600 } else { 1 };
2329            for id in 1..=initial_count {
2330                store.insert_auto("rows", row(id, 1)).expect("seed");
2331            }
2332            store.persist().expect("baseline");
2333            let manager = store.get_collection("rows").expect("collection");
2334            let ids: Vec<u64> = if multi {
2335                (1..=initial_count).collect()
2336            } else {
2337                vec![if insert { 2 } else { 1 }]
2338            };
2339            let mut actions = Vec::new();
2340            for id in ids {
2341                if insert {
2342                    manager.insert(row(id, 9)).expect("insert version");
2343                } else {
2344                    manager.update(row(id, 9)).expect("update version");
2345                }
2346                let entity = manager.get(EntityId::new(id)).expect("changed entity");
2347                actions.push(StoreWalAction::upsert_entity(
2348                    "rows",
2349                    &entity,
2350                    None,
2351                    store.format_version(),
2352                ));
2353            }
2354            let references = store
2355                .commit
2356                .as_ref()
2357                .expect("coordinator")
2358                .append_checkpoint_actions(&actions)
2359                .expect("durable WAL");
2360            store.remember_checkpoint_rows(actions, references);
2361            store.persist().expect("exit after sparse write");
2362            panic!("inline sparse crash hook was not reached");
2363        }
2364        for (insert, multi) in [(false, false), (true, false), (false, true)] {
2365            let directory = tempfile::tempdir().expect("temporary store");
2366            let path = directory.path().join("data.rdb");
2367            let mut command =
2368                std::process::Command::new(std::env::current_exe().expect("test executable"));
2369            command
2370                .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
2371                .env(CHILD, &path)
2372                .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
2373            if insert {
2374                command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
2375            }
2376            if multi {
2377                command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
2378            }
2379            let output = command.output().expect("crash child");
2380            assert_eq!(
2381                output.status.code(),
2382                Some(190),
2383                "insert={insert} multi={multi}: {}",
2384                String::from_utf8_lossy(&output.stderr)
2385            );
2386            let recovered = UnifiedStore::open(&path).expect("recover");
2387            let ids: Vec<u64> = if multi {
2388                (1..=600).collect()
2389            } else {
2390                vec![if insert { 2 } else { 1 }]
2391            };
2392            for id in ids {
2393                assert_eq!(
2394                    recovered
2395                        .get("rows", EntityId::new(id))
2396                        .expect("recovered row")
2397                        .data
2398                        .as_row()
2399                        .expect("row")
2400                        .columns,
2401                    vec![Value::Integer(9)]
2402                );
2403            }
2404            assert_eq!(
2405                recovered.get_collection("rows").expect("rows").count(),
2406                if multi {
2407                    600
2408                } else if insert {
2409                    2
2410                } else {
2411                    1
2412                }
2413            );
2414        }
2415    }
2416
2417    #[test]
2418    fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
2419        let directory = tempfile::tempdir().expect("temporary store");
2420        let path = directory.path().join("data.rdb");
2421        {
2422            let store = UnifiedStore::open(&path).expect("open store");
2423            store.create_collection("rows").expect("create collection");
2424            store.persist().expect("publish ownership epoch");
2425            assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
2426            store.persist().expect("publish next ownership epoch");
2427        }
2428        let reopened = UnifiedStore::open(&path).expect("reopen store");
2429        assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
2430    }
2431
2432    #[test]
2433    fn metadata_overflow_with_missing_continuation_fails_closed() {
2434        use crate::storage::engine::{Page, PageType, HEADER_SIZE};
2435
2436        let directory = tempfile::tempdir().expect("temporary store");
2437        let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
2438        let pager = store.pager.as_ref().unwrap();
2439        let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
2440        let valid_manifest = build_meta_page1_with_overflow(pager, &valid_payload).unwrap();
2441        pager.write_page(1, valid_manifest).unwrap();
2442        assert!(!existing_overflow_chain(pager).unwrap().is_empty());
2443        assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
2444
2445        let missing = pager.page_count().unwrap() + 100;
2446        let mut manifest = Page::new(PageType::Header, 1);
2447        reddb_file::encode_native_metadata_overflow_header(
2448            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2449            reddb_file::NativeMetadataOverflowHeader {
2450                format_version: STORE_VERSION_V11,
2451                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2452                next_overflow_page_id: missing,
2453            },
2454        )
2455        .unwrap();
2456        pager.write_page(1, manifest).unwrap();
2457
2458        assert!(
2459            existing_overflow_chain(pager).is_err(),
2460            "a missing continuation must not be treated as an empty chain"
2461        );
2462
2463        let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
2464        let mut manifest = Page::new(PageType::Header, 1);
2465        reddb_file::encode_native_metadata_overflow_header(
2466            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2467            reddb_file::NativeMetadataOverflowHeader {
2468                format_version: STORE_VERSION_V11,
2469                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2470                next_overflow_page_id: wrong_type.page_id(),
2471            },
2472        )
2473        .unwrap();
2474        pager.write_page(1, manifest).unwrap();
2475        assert!(
2476            existing_overflow_chain(pager).is_err(),
2477            "a live page must not be mistaken for metadata overflow"
2478        );
2479        assert!(
2480            read_meta_payload(pager).is_err(),
2481            "a live page must not be read as metadata overflow"
2482        );
2483
2484        let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
2485        let cycle_id = cycle.page_id();
2486        reddb_file::encode_native_metadata_overflow_continuation_header(
2487            &mut cycle.as_bytes_mut()[HEADER_SIZE..],
2488            reddb_file::NativeMetadataOverflowContinuationHeader {
2489                next_overflow_page_id: cycle_id,
2490                chunk_bytes: 1,
2491            },
2492        )
2493        .unwrap();
2494        pager.write_page(cycle_id, cycle).unwrap();
2495        let mut manifest = Page::new(PageType::Header, 1);
2496        reddb_file::encode_native_metadata_overflow_header(
2497            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2498            reddb_file::NativeMetadataOverflowHeader {
2499                format_version: STORE_VERSION_V11,
2500                total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
2501                next_overflow_page_id: cycle_id,
2502            },
2503        )
2504        .unwrap();
2505        pager.write_page(1, manifest).unwrap();
2506        assert!(
2507            read_meta_payload(pager).is_err(),
2508            "cyclic overflow must not be accepted"
2509        );
2510
2511        let mut manifest = Page::new(PageType::Header, 1);
2512        reddb_file::encode_native_metadata_overflow_header(
2513            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2514            reddb_file::NativeMetadataOverflowHeader {
2515                format_version: STORE_VERSION_V11,
2516                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2517                next_overflow_page_id: 0,
2518            },
2519        )
2520        .unwrap();
2521        pager.write_page(1, manifest).unwrap();
2522        assert!(
2523            read_meta_payload(pager).is_err(),
2524            "a truncated metadata payload must not be accepted"
2525        );
2526    }
2527
2528    #[test]
2529    fn retired_tree_pages_remain_pinned_by_a_cursor() {
2530        let directory = tempfile::tempdir().expect("temporary store");
2531        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2532        for id in 1..=100 {
2533            store.insert_auto("rows", row(id, 1)).expect("seed");
2534        }
2535        store.persist().expect("initial checkpoint");
2536        let tree = store
2537            .btree_indices
2538            .read()
2539            .get("rows")
2540            .cloned()
2541            .expect("tree");
2542        let mut cursor = tree.cursor_first().expect("cursor");
2543        drop(tree);
2544        let manager = store.get_collection("rows").expect("rows");
2545        for value in 2..=6 {
2546            manager.update(row(1, value)).expect("change");
2547            store.persist().expect("checkpoint with old reader");
2548        }
2549        let mut count = 0;
2550        while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
2551            let (entity, _) =
2552                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2553                    .expect("decode");
2554            assert_eq!(
2555                entity.data.as_row().expect("row").columns,
2556                [Value::Integer(1)]
2557            );
2558            count += 1;
2559        }
2560        assert_eq!(count, 100);
2561        drop(cursor);
2562        store.persist().expect("reclaim after reader release");
2563        assert!(store.retired_btrees.lock().is_empty());
2564    }
2565
2566    #[test]
2567    fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
2568        let directory = tempfile::tempdir().expect("temporary store");
2569        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2570        store.create_collection("rows").expect("collection");
2571        let manager = store.get_collection("rows").expect("rows");
2572        for id in (1..=1200).rev() {
2573            manager
2574                .insert(UnifiedEntity::table_row(
2575                    EntityId::new(id),
2576                    "rows",
2577                    id,
2578                    vec![Value::text("x".repeat(4096))],
2579                ))
2580                .expect("seed");
2581        }
2582        CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
2583        store.persist().expect("bounded checkpoint");
2584        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
2585        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
2586        assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
2587    }
2588
2589    #[test]
2590    fn unchanged_checkpoint_does_not_serialize_collection_rows() {
2591        let directory = tempfile::tempdir().expect("temporary store");
2592        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2593        store.create_collection("rows").expect("collection");
2594        for id in 1..=40 {
2595            store
2596                .insert(
2597                    "rows",
2598                    UnifiedEntity::table_row(
2599                        EntityId::new(id),
2600                        "rows",
2601                        id,
2602                        vec![Value::Integer(id as i64)],
2603                    ),
2604                )
2605                .expect("seed row");
2606        }
2607        store.persist().expect("initial checkpoint");
2608        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2609        store.persist().expect("unchanged checkpoint");
2610        assert_eq!(
2611            CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
2612            0,
2613            "unchanged collections must not hold the writer fence while serializing every row"
2614        );
2615        drop(store);
2616        let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
2617        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2618        reopened.persist().expect("unchanged recovered generation");
2619        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2620    }
2621
2622    #[test]
2623    fn sparse_wal_replay_does_not_reencode_complete_collections() {
2624        let directory = tempfile::tempdir().expect("temporary store");
2625        let path = directory.path().join("data.rdb");
2626        let store = UnifiedStore::open(&path).expect("paged store");
2627        for (name, first) in [("rows", 1), ("cold", 129)] {
2628            store.create_collection(name).expect("collection");
2629            for id in first..first + 128 {
2630                store
2631                    .insert(
2632                        name,
2633                        UnifiedEntity::table_row(
2634                            EntityId::new(id),
2635                            name,
2636                            id,
2637                            vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2638                        ),
2639                    )
2640                    .expect("seed");
2641            }
2642        }
2643        store.persist().expect("durable baseline");
2644        let action = super::super::commit::StoreWalAction::upsert_entity(
2645            "rows",
2646            &row(1, 42),
2647            None,
2648            STORE_VERSION_V11,
2649        );
2650        // The acknowledged WAL image has not reached its home B-tree yet.
2651        store
2652            .commit
2653            .as_ref()
2654            .unwrap()
2655            .append_actions(&[action])
2656            .unwrap();
2657        drop(store);
2658
2659        REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2660        let recovered = UnifiedStore::open(&path).expect("replay committed change");
2661        assert_eq!(
2662            recovered
2663                .get("rows", EntityId::new(1))
2664                .unwrap()
2665                .data
2666                .as_row()
2667                .unwrap()
2668                .columns,
2669            vec![Value::Integer(42)]
2670        );
2671        for name in ["rows", "cold"] {
2672            assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
2673        }
2674        assert_eq!(
2675            REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
2676            0,
2677            "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
2678        );
2679        for (name, first) in [("rows", 2), ("cold", 129)] {
2680            let end = if name == "rows" { 129 } else { 257 };
2681            for id in first..end {
2682                assert_eq!(
2683                    recovered
2684                        .get(name, EntityId::new(id))
2685                        .unwrap()
2686                        .data
2687                        .as_row()
2688                        .unwrap()
2689                        .columns,
2690                    vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2691                );
2692            }
2693        }
2694
2695        drop(recovered);
2696        let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
2697        assert_eq!(
2698            reopened
2699                .get("rows", EntityId::new(1))
2700                .unwrap()
2701                .data
2702                .as_row()
2703                .unwrap()
2704                .columns,
2705            vec![Value::Integer(42)]
2706        );
2707    }
2708
2709    fn replay_test_payload(bytes: usize) -> String {
2710        let mut state = 0x1234_5678_u32;
2711        (0..bytes)
2712            .map(|_| {
2713                state ^= state << 13;
2714                state ^= state >> 17;
2715                state ^= state << 5;
2716                char::from(b'!' + (state % 90) as u8)
2717            })
2718            .collect()
2719    }
2720
2721    #[test]
2722    fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
2723        let directory = tempfile::tempdir().unwrap();
2724        let path = directory.path().join("data.rdb");
2725        let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
2726        let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2727        for id in 1..=16 {
2728            store.insert_auto("rows", row(id, 1)).unwrap();
2729        }
2730        store.persist().unwrap();
2731        // Larger than the retained-image allowance, so replay must fall back.
2732        let payload = replay_test_payload(64 * 1024);
2733        let changed =
2734            UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
2735        let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2736        store
2737            .commit
2738            .as_ref()
2739            .unwrap()
2740            .append_actions(&[action])
2741            .unwrap();
2742        drop(store);
2743        REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2744        let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
2745        assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
2746        assert_eq!(
2747            recovered
2748                .get("rows", EntityId::new(1))
2749                .unwrap()
2750                .data
2751                .as_row()
2752                .unwrap()
2753                .columns,
2754            changed.data.as_row().unwrap().columns
2755        );
2756        drop(recovered);
2757        let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
2758        assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
2759        assert_eq!(
2760            reopened
2761                .get("rows", EntityId::new(1))
2762                .unwrap()
2763                .data
2764                .as_row()
2765                .unwrap()
2766                .columns,
2767            changed.data.as_row().unwrap().columns
2768        );
2769    }
2770
2771    #[cfg(target_os = "linux")]
2772    #[test]
2773    fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
2774        const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
2775        if let Some(path) = std::env::var_os(CHILD) {
2776            let _store = UnifiedStore::open(path).unwrap();
2777            panic!("replay crash hook was not reached");
2778        }
2779        for overflow in [false, true] {
2780            for (hook, code) in [
2781                ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
2782                ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
2783            ] {
2784                let directory = tempfile::tempdir().unwrap();
2785                let path = directory.path().join("data.rdb");
2786                let store = UnifiedStore::open(&path).unwrap();
2787                for id in 1..=128 {
2788                    store.insert_auto("rows", row(id, 1)).unwrap();
2789                }
2790                store.persist().unwrap();
2791                let changed = if overflow {
2792                    UnifiedEntity::table_row(
2793                        EntityId::new(1),
2794                        "rows",
2795                        1,
2796                        vec![Value::text(replay_test_payload(16 * 1024))],
2797                    )
2798                } else {
2799                    row(1, 42)
2800                };
2801                let action =
2802                    StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2803                store
2804                    .commit
2805                    .as_ref()
2806                    .unwrap()
2807                    .append_actions(&[action])
2808                    .unwrap();
2809                drop(store);
2810                let mut first_size = 0;
2811                for cycle in 0..12 {
2812                    let output = std::process::Command::new(std::env::current_exe().unwrap())
2813                        .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
2814                        .env(CHILD, &path).env(hook, "1").output().unwrap();
2815                    assert_eq!(
2816                        output.status.code(),
2817                        Some(code),
2818                        "overflow={overflow} hook={hook} cycle={cycle}: {}",
2819                        String::from_utf8_lossy(&output.stderr)
2820                    );
2821                    let size = std::fs::metadata(&path).unwrap().len();
2822                    if cycle == 0 {
2823                        first_size = size;
2824                    }
2825                    assert!(
2826                        size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
2827                        "repeated recovery must reuse reservations: {first_size} -> {size}"
2828                    );
2829                }
2830                for _ in 0..2 {
2831                    let recovered = UnifiedStore::open(&path).unwrap();
2832                    assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
2833                    for id in 1..=128 {
2834                        let expected = if id == 1 { changed.clone() } else { row(id, 1) };
2835                        assert_eq!(
2836                            recovered
2837                                .get("rows", EntityId::new(id))
2838                                .unwrap()
2839                                .data
2840                                .as_row()
2841                                .unwrap()
2842                                .columns,
2843                            expected.data.as_row().unwrap().columns
2844                        );
2845                    }
2846                }
2847            }
2848        }
2849    }
2850
2851    #[test]
2852    fn paged_reopen_preserves_stored_sequence_identity() {
2853        let directory = tempfile::tempdir().expect("temporary store");
2854        let path = directory.path().join("data.rdb");
2855        let store = UnifiedStore::open(&path).expect("paged store");
2856        store.create_collection("rows").expect("collection");
2857        for id in [30, 10, 20] {
2858            store.insert("rows", row(id, id as i64)).expect("insert");
2859        }
2860        store.persist().expect("checkpoint");
2861        let sequences = [10, 20, 30].map(|id| {
2862            store
2863                .get("rows", EntityId::new(id))
2864                .expect("row")
2865                .sequence_id
2866        });
2867        drop(store);
2868        let reopened = UnifiedStore::open(&path).expect("reopen");
2869        let recovered = [10, 20, 30].map(|id| {
2870            reopened
2871                .get("rows", EntityId::new(id))
2872                .expect("row")
2873                .sequence_id
2874        });
2875        assert_eq!(
2876            recovered, sequences,
2877            "opening a checkpoint must not rewrite stored row identities"
2878        );
2879    }
2880
2881    #[test]
2882    fn auto_insert_persists_the_assigned_sequence_identity() {
2883        let directory = tempfile::tempdir().expect("temporary store");
2884        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2885        for id in 1u64..=3 {
2886            store
2887                .insert_auto("rows", row(id, id as i64))
2888                .expect("insert");
2889        }
2890        let manager = store.get_collection("rows").expect("rows");
2891        let tree = store
2892            .btree_indices
2893            .read()
2894            .get("rows")
2895            .cloned()
2896            .expect("tree");
2897        for id in 1u64..=3 {
2898            let bytes = tree
2899                .get(&id.to_be_bytes())
2900                .expect("lookup")
2901                .expect("stored row");
2902            let (persisted, _) =
2903                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2904                    .expect("decode");
2905            assert_eq!(
2906                persisted.sequence_id,
2907                manager
2908                    .get(EntityId::new(id))
2909                    .expect("live row")
2910                    .sequence_id
2911            );
2912        }
2913    }
2914
2915    #[test]
2916    fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
2917        let directory = tempfile::tempdir().expect("temporary store");
2918        let path = directory.path().join("data.rdb");
2919        let store = UnifiedStore::open(&path).expect("paged store");
2920        store.insert_auto("rows", row(1, 1)).expect("seed");
2921        store.persist().expect("checkpoint");
2922        store.insert_auto("rows", row(2, 2)).expect("append");
2923        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2924        store.persist().expect("inline append checkpoint");
2925        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2926        store
2927            .get_collection("rows")
2928            .expect("rows")
2929            .update(row(1, 99))
2930            .expect("untracked edit");
2931        store.insert_auto("rows", row(3, 3)).expect("next append");
2932        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2933        store.persist().expect("untracked edit checkpoint");
2934        assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
2935        drop(store);
2936        let reopened = UnifiedStore::open(&path).expect("reopen");
2937        assert_eq!(
2938            reopened
2939                .get("rows", EntityId::new(1))
2940                .expect("row")
2941                .data
2942                .as_row()
2943                .expect("table")
2944                .columns,
2945            vec![Value::Integer(99)]
2946        );
2947    }
2948
2949    #[test]
2950    fn replayed_trees_are_reusable_after_durable_reconciliation() {
2951        let directory = tempfile::tempdir().expect("temporary store");
2952        let path = directory.path().join("data.rdb");
2953        let store = UnifiedStore::open(&path).expect("paged store");
2954        store.insert_auto("rows", row(1, 1)).expect("seed");
2955        store.persist().expect("checkpoint");
2956        store
2957            .insert_auto("rows", row(2, 2))
2958            .expect("WAL-backed append");
2959        drop(store);
2960        let reopened = UnifiedStore::open(&path).expect("replay");
2961        assert!(reopened.get("rows", EntityId::new(2)).is_some());
2962        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2963        reopened.persist().expect("checkpoint reconciled tree");
2964        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2965    }
2966
2967    #[test]
2968    fn legacy_system_catalog_and_wal_migrate_without_cold_boot_rebuild() {
2969        use crate::storage::wal::{WalRecord, WalWriter};
2970
2971        let directory = tempfile::tempdir().expect("temporary store");
2972        let path = directory.path().join("data.rdb");
2973        let config = UnifiedStoreConfig {
2974            page_cache_slots: Some(32),
2975            resident_memory_bytes_max: Some(2 * 1024 * 1024),
2976            ..UnifiedStoreConfig::default()
2977        };
2978        let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
2979            codec_version: 1,
2980            definition: [91; 32],
2981        };
2982        let baseline_index_root;
2983        {
2984            let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
2985            store
2986                .create_collection("red_config")
2987                .expect("system collection");
2988            store
2989                .insert_auto(
2990                    "red_config",
2991                    UnifiedEntity::table_row(
2992                        EntityId::new(1),
2993                        "red_config",
2994                        1,
2995                        vec![Value::text("checkpoint")],
2996                    ),
2997                )
2998                .expect("legacy-compatible insert");
2999            store
3000                .register_secondary_index(
3001                    "red_config",
3002                    "by_id",
3003                    index_identity.clone(),
3004                    Arc::new(SystemNameProjection),
3005                )
3006                .expect("secondary index");
3007            store.persist().expect("baseline checkpoint");
3008            baseline_index_root = store
3009                .secondary_index("red.config", "by_id", &index_identity)
3010                .expect("published secondary index")
3011                .root_page_id();
3012        }
3013
3014        // Recreate a pre-migration catalog and operational manifest. The two
3015        // spellings are deliberately equal-length so the test can preserve
3016        // every root and checksum-protected page layout except the name.
3017        {
3018            let pager = Pager::open(
3019                &path,
3020                PagerConfig {
3021                    disk_residency: true,
3022                    ..PagerConfig::default()
3023                },
3024            )
3025            .expect("open pager");
3026            let mut page = pager.read_page(1).expect("catalog page");
3027            let bytes = page.as_bytes_mut();
3028            let canonical = b"red.config";
3029            let legacy = b"red_config";
3030            let mut replacements = 0;
3031            for offset in 0..=bytes.len() - canonical.len() {
3032                if &bytes[offset..offset + canonical.len()] == canonical {
3033                    bytes[offset..offset + legacy.len()].copy_from_slice(legacy);
3034                    replacements += 1;
3035                }
3036            }
3037            assert!(replacements >= 1, "catalog must contain the canonical name");
3038            pager.write_page(1, page).expect("write legacy catalog");
3039            pager.sync().expect("sync legacy catalog");
3040        }
3041        crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3042            .rename_collections(&[("red.config", "red_config")])
3043            .expect("restore legacy operational name");
3044
3045        // Append an acknowledged transaction written by a legacy binary. Its
3046        // payload must replay into the canonical manager during migration.
3047        let wal_upsert = reddb_file::encode_store_wal_action_frame(
3048            &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
3049                collection: "red_config".to_string(),
3050                record: UnifiedStore::serialize_entity_record(
3051                    &UnifiedEntity::table_row(
3052                        EntityId::new(2),
3053                        "red_config",
3054                        2,
3055                        vec![Value::text("legacy WAL")],
3056                    ),
3057                    None,
3058                    STORE_VERSION_CURRENT,
3059                ),
3060            },
3061        )
3062        .expect("legacy WAL action");
3063        let wal_delete = reddb_file::encode_store_wal_action_frame(
3064            &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
3065                collection: "red_config".to_string(),
3066                entity_id: EntityId::new(1).raw(),
3067            },
3068        )
3069        .expect("legacy WAL delete");
3070        let wal_path = reddb_file::layout::unified_wal_path(&path);
3071        {
3072            let mut writer = WalWriter::open(&wal_path).expect("open WAL");
3073            writer
3074                .append(&WalRecord::TxCommitBatch {
3075                    tx_id: 77,
3076                    actions: vec![wal_upsert, wal_delete],
3077                })
3078                .expect("append legacy transaction");
3079            writer.sync().expect("sync legacy transaction");
3080        }
3081
3082        let (root_after_recovery, pages_after_recovery) = {
3083            let recovered =
3084                UnifiedStore::open_with_config(&path, config.clone()).expect("migrate and recover");
3085            assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3086            assert!(
3087                recovered.get("red.config", EntityId::new(1)).is_none(),
3088                "legacy WAL deletes must target the canonical collection"
3089            );
3090            assert_eq!(
3091                recovered
3092                    .get("red.config", EntityId::new(2))
3093                    .expect("WAL row")
3094                    .kind
3095                    .collection(),
3096                "red.config"
3097            );
3098            let root = recovered
3099                .collection_root_page("red.config")
3100                .expect("published canonical root");
3101            assert_eq!(
3102                recovered
3103                    .secondary_index("red.config", "by_id", &index_identity)
3104                    .expect("recovered secondary index")
3105                    .root_page_id(),
3106                baseline_index_root,
3107                "name migration must reuse the secondary-index root"
3108            );
3109            let pages = recovered
3110                .pager()
3111                .expect("pager")
3112                .page_count()
3113                .expect("page count");
3114            (root, pages)
3115        };
3116
3117        // Simulate a crash/import boundary where the catalog rename is
3118        // durable but the operational manifest still has the legacy key.
3119        crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3120            .rename_collections(&[("red.config", "red_config")])
3121            .expect("restore stale manifest identity");
3122
3123        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3124        let cold = UnifiedStore::open_with_config(&path, config).expect("second cold boot");
3125        assert_eq!(
3126            cold.collection_root_page("red.config"),
3127            Some(root_after_recovery),
3128            "an idempotent cold boot must reuse the published data root"
3129        );
3130        assert_eq!(
3131            cold.pager()
3132                .expect("pager")
3133                .page_count()
3134                .expect("page count"),
3135            pages_after_recovery,
3136            "cold boot must not allocate replacement index trees"
3137        );
3138        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3139        assert_eq!(
3140            cold.secondary_index("red.config", "by_id", &index_identity)
3141                .expect("secondary index after cold boot")
3142                .root_page_id(),
3143            baseline_index_root
3144        );
3145        assert!(
3146            crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3147                .recover_or_bootstrap(&["red.config".to_string()])
3148                .expect("canonical manifest")
3149                .is_empty()
3150        );
3151    }
3152
3153    struct DuplicateSystemCatalogFixture {
3154        _directory: tempfile::TempDir,
3155        path: std::path::PathBuf,
3156        config: UnifiedStoreConfig,
3157        pages_before: u32,
3158        legacy_root: Option<u32>,
3159        legacy_key_root: Option<u32>,
3160        legacy_secondary_root: Option<u32>,
3161        index_identity: super::super::secondary_index::SecondaryIndexIdentity,
3162    }
3163
3164    fn duplicate_system_catalog_fixture(
3165        legacy_populated: bool,
3166        canonical_populated: bool,
3167        index_legacy: bool,
3168    ) -> DuplicateSystemCatalogFixture {
3169        let directory = tempfile::tempdir().expect("temporary store");
3170        let path = directory.path().join("data.rdb");
3171        let config = UnifiedStoreConfig {
3172            page_cache_slots: Some(32),
3173            resident_memory_bytes_max: Some(2 * 1024 * 1024),
3174            ..UnifiedStoreConfig::default()
3175        };
3176        let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3177            codec_version: 1,
3178            definition: [73; 32],
3179        };
3180        let (legacy_root, legacy_key_root, legacy_secondary_root);
3181        {
3182            let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3183            store
3184                .create_collection("cfg_legacy")
3185                .expect("legacy source collection");
3186            if legacy_populated {
3187                store
3188                    .insert_auto(
3189                        "cfg_legacy",
3190                        UnifiedEntity::table_row(
3191                            EntityId::new(1),
3192                            "red_config",
3193                            1,
3194                            vec![Value::text("legacy")],
3195                        ),
3196                    )
3197                    .expect("legacy row");
3198            }
3199            store
3200                .create_collection("cfg.canon_")
3201                .expect("canonical source collection");
3202            if canonical_populated {
3203                store
3204                    .insert_auto(
3205                        "cfg.canon_",
3206                        UnifiedEntity::table_row(
3207                            EntityId::new(2),
3208                            "red.config",
3209                            2,
3210                            vec![Value::text("canonical")],
3211                        ),
3212                    )
3213                    .expect("canonical row");
3214            }
3215            if index_legacy {
3216                store
3217                    .register_secondary_index(
3218                        "cfg_legacy",
3219                        "by_id",
3220                        index_identity.clone(),
3221                        Arc::new(SystemNameProjection),
3222                    )
3223                    .expect("legacy secondary index");
3224            }
3225            store.persist().expect("checkpoint duplicate catalog");
3226            legacy_root = store.collection_root_page("cfg_legacy");
3227            legacy_key_root = store
3228                .key_indices
3229                .read()
3230                .get("cfg_legacy")
3231                .map(|index| index.tree.root_page_id());
3232            legacy_secondary_root = index_legacy.then(|| {
3233                store
3234                    .secondary_index("cfg_legacy", "by_id", &index_identity)
3235                    .expect("legacy secondary root")
3236                    .root_page_id()
3237            });
3238        }
3239
3240        // Recreate the production shape: a populated legacy collection and an
3241        // empty canonical collection published by an intermediate binary.
3242        // Every spelling has the same byte length, so roots and checksummed
3243        // page layouts stay unchanged while only the catalog identities move.
3244        {
3245            let pager = Pager::open(
3246                &path,
3247                PagerConfig {
3248                    disk_residency: true,
3249                    ..PagerConfig::default()
3250                },
3251            )
3252            .expect("open pager");
3253            let mut page = pager.read_page(1).expect("catalog page");
3254            let bytes = page.as_bytes_mut();
3255            for (source, destination) in [
3256                (b"cfg_legacy".as_slice(), b"red_config".as_slice()),
3257                (b"cfg.canon_".as_slice(), b"red.config".as_slice()),
3258            ] {
3259                let mut replacements = 0;
3260                for offset in 0..=bytes.len() - source.len() {
3261                    if &bytes[offset..offset + source.len()] == source {
3262                        bytes[offset..offset + destination.len()].copy_from_slice(destination);
3263                        replacements += 1;
3264                    }
3265                }
3266                assert!(replacements >= 1, "catalog must contain the source name");
3267            }
3268            pager.write_page(1, page).expect("write duplicate catalog");
3269            pager.sync().expect("sync duplicate catalog");
3270        }
3271        crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3272            .rename_collections(&[("cfg_legacy", "red_config"), ("cfg.canon_", "red.config")])
3273            .expect("publish duplicate operational names");
3274
3275        let pages_before = Pager::open(
3276            &path,
3277            PagerConfig {
3278                disk_residency: true,
3279                ..PagerConfig::default()
3280            },
3281        )
3282        .expect("inspect pager")
3283        .page_count()
3284        .expect("page count");
3285        DuplicateSystemCatalogFixture {
3286            _directory: directory,
3287            path,
3288            config,
3289            pages_before,
3290            legacy_root,
3291            legacy_key_root,
3292            legacy_secondary_root,
3293            index_identity,
3294        }
3295    }
3296
3297    #[test]
3298    fn populated_legacy_system_collection_replaces_empty_canonical_duplicate() {
3299        let fixture = duplicate_system_catalog_fixture(true, false, true);
3300        let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
3301            .expect("resolve empty duplicate");
3302        assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3303        assert_eq!(
3304            recovered
3305                .get("red.config", EntityId::new(1))
3306                .expect("preserved legacy row")
3307                .kind
3308                .collection(),
3309            "red.config"
3310        );
3311        assert_eq!(
3312            recovered.collection_root_page("red.config"),
3313            fixture.legacy_root,
3314            "the populated data root must be reused"
3315        );
3316        assert_eq!(
3317            recovered
3318                .key_indices
3319                .read()
3320                .get("red.config")
3321                .map(|index| index.tree.root_page_id()),
3322            fixture.legacy_key_root,
3323            "the populated key-index root must be reused"
3324        );
3325        assert_eq!(
3326            recovered
3327                .secondary_index("red.config", "by_id", &fixture.index_identity)
3328                .expect("surviving secondary index")
3329                .root_page_id(),
3330            fixture
3331                .legacy_secondary_root
3332                .expect("fixture secondary root")
3333        );
3334        drop(recovered);
3335
3336        let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3337            .expect("idempotent cold boot");
3338        assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
3339        assert_eq!(
3340            cold.pager()
3341                .expect("pager")
3342                .page_count()
3343                .expect("page count"),
3344            fixture.pages_before,
3345            "resolving an empty duplicate must not extend the file"
3346        );
3347    }
3348
3349    #[test]
3350    fn populated_canonical_system_collection_replaces_empty_legacy_duplicate() {
3351        let fixture = duplicate_system_catalog_fixture(false, true, false);
3352        let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3353            .expect("retain populated canonical duplicate");
3354        assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3355        assert_eq!(
3356            recovered
3357                .get("red.config", EntityId::new(2))
3358                .expect("preserved canonical row")
3359                .kind
3360                .collection(),
3361            "red.config"
3362        );
3363        assert!(recovered.get("red.config", EntityId::new(1)).is_none());
3364    }
3365
3366    #[test]
3367    fn empty_canonical_system_collection_replaces_empty_legacy_duplicate() {
3368        let fixture = duplicate_system_catalog_fixture(false, false, false);
3369        let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3370            .expect("prefer canonical empty duplicate");
3371        assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3372        assert_eq!(
3373            recovered
3374                .get_collection("red.config")
3375                .expect("canonical collection")
3376                .count(),
3377            0
3378        );
3379    }
3380
3381    #[test]
3382    fn populated_system_collection_duplicates_still_fail_closed() {
3383        let fixture = duplicate_system_catalog_fixture(true, true, false);
3384        let error = match UnifiedStore::open_with_config(&fixture.path, fixture.config) {
3385            Ok(_) => panic!("two populated identities are ambiguous"),
3386            Err(error) => error,
3387        };
3388        assert!(
3389            error
3390                .to_string()
3391                .contains("both populated legacy collection 'red_config' and canonical collection 'red.config'"),
3392            "unexpected error: {error}"
3393        );
3394    }
3395
3396    fn row(id: u64, value: i64) -> UnifiedEntity {
3397        UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
3398    }
3399
3400    #[test]
3401    fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
3402        let directory = tempfile::tempdir().expect("temporary store");
3403        let path = directory.path().join("data.rdb");
3404        let store = UnifiedStore::open(&path).expect("paged store");
3405        store.create_collection("rows").expect("collection");
3406        store.insert("rows", row(1, 1)).expect("seed");
3407        store.persist().expect("initial checkpoint");
3408        let manager = store.get_collection("rows").expect("rows");
3409        manager.update(row(1, 2)).expect("direct update");
3410        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3411        store.persist().expect("updated row checkpoint");
3412        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
3413        let mut metadata = Metadata::new();
3414        metadata.set("verified", MetadataValue::Bool(true));
3415        manager
3416            .set_metadata(EntityId::new(1), metadata)
3417            .expect("metadata");
3418        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3419        store.persist().expect("metadata-only checkpoint");
3420        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
3421        manager
3422            .bulk_insert(vec![row(2, 2), row(3, 3)])
3423            .expect("bulk insert");
3424        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3425        store.persist().expect("bulk insert checkpoint");
3426        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
3427        manager.delete(EntityId::new(2)).expect("delete");
3428        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3429        store.persist().expect("changed checkpoint");
3430        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
3431        drop(store);
3432        let recovered = UnifiedStore::open(&path).expect("recover changed rows");
3433        let manager = recovered.get_collection("rows").expect("rows");
3434        assert_eq!(manager.count(), 2);
3435        assert_eq!(
3436            manager
3437                .get(EntityId::new(1))
3438                .expect("row")
3439                .data
3440                .as_row()
3441                .expect("table")
3442                .columns,
3443            vec![Value::Integer(2)]
3444        );
3445        assert!(manager.get(EntityId::new(2)).is_none());
3446        assert!(manager
3447            .get_metadata(EntityId::new(1))
3448            .expect("metadata")
3449            .get("verified")
3450            .is_some());
3451    }
3452
3453    #[test]
3454    fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
3455        let directory = tempfile::tempdir().expect("temporary store");
3456        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3457        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3458        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3459        store.create_collection("rows").expect("collection");
3460        for id in 1..=40 {
3461            store.insert("rows", row(id, id as i64)).expect("seed");
3462        }
3463        store.persist().expect("initial checkpoint");
3464        let unrelated = snapshots.begin();
3465        snapshots.rollback(unrelated);
3466        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3467        store.persist().expect("unrelated rollback checkpoint");
3468        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3469
3470        store
3471            .pending_vault_versions
3472            .write()
3473            .insert(EntityId::new(100));
3474        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3475        store.persist().expect("unrelated vault checkpoint");
3476        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3477        store.pending_vault_versions.write().clear();
3478        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3479        store
3480            .persist()
3481            .expect("unrelated vault publication checkpoint");
3482        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3483    }
3484
3485    #[test]
3486    fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
3487        let directory = tempfile::tempdir().expect("temporary store");
3488        let path = directory.path().join("data.rdb");
3489        let store = UnifiedStore::open(&path).expect("paged store");
3490        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3491        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3492        store.create_collection("rows").expect("collection");
3493        let xid = snapshots.begin();
3494        let mut entity = row(1, 1);
3495        entity.xmin = xid;
3496        store.insert("rows", entity).expect("committed row");
3497        snapshots.commit(xid);
3498        store.persist().expect("committed baseline");
3499        snapshots.rollback(xid);
3500        store
3501            .persist()
3502            .expect("untracked outcome requires validation");
3503        drop(store);
3504        let recovered = UnifiedStore::open(&path).expect("recover");
3505        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
3506    }
3507
3508    #[test]
3509    fn exhausted_deferred_row_cache_keeps_full_validation() {
3510        let directory = tempfile::tempdir().expect("temporary store");
3511        let path = directory.path().join("data.rdb");
3512        let store = UnifiedStore::open(&path).expect("paged store");
3513        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3514        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3515        store.create_collection("rows").expect("collection");
3516        let xid = snapshots.begin();
3517        for id in 1..=1025 {
3518            let mut entity = row(id, id as i64);
3519            entity.xmin = xid;
3520            store.insert("rows", entity).expect("pending row");
3521        }
3522        store.persist().expect("exclude pending rows");
3523        assert!(store.checkpoint_versions.read()["rows"]
3524            .deferred_rows
3525            .is_none());
3526        snapshots.commit(xid);
3527        store
3528            .persist()
3529            .expect("publish committed rows conservatively");
3530        drop(store);
3531        let recovered = UnifiedStore::open(&path).expect("recover");
3532        assert_eq!(
3533            recovered.get_collection("rows").expect("rows").count(),
3534            1025
3535        );
3536    }
3537
3538    #[test]
3539    fn rollback_invalidates_reuse_without_a_segment_mutation() {
3540        let directory = tempfile::tempdir().expect("temporary store");
3541        let path = directory.path().join("data.rdb");
3542        let store = UnifiedStore::open(&path).expect("paged store");
3543        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3544        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3545        store.create_collection("rows").expect("collection");
3546        let xid = snapshots.begin();
3547        let mut pending = row(1, 1);
3548        pending.xmin = xid;
3549        store.insert("rows", pending).expect("version");
3550        store.persist().expect("before rollback");
3551        snapshots.rollback(xid);
3552        store.persist().expect("after rollback");
3553        drop(store);
3554        let recovered = UnifiedStore::open(&path).expect("recover");
3555        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
3556    }
3557
3558    #[test]
3559    fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
3560        let directory = tempfile::tempdir().expect("temporary store");
3561        let path = directory.path().join("data.rdb");
3562        let store = UnifiedStore::open(&path).expect("paged store");
3563        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3564        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3565        store.create_collection("rows").expect("collection");
3566        let xid = snapshots.begin();
3567        let mut deleted = row(1, 1);
3568        deleted.xmax = xid;
3569        store.insert("rows", deleted).expect("version");
3570        store.persist().expect("before rollback");
3571        snapshots.rollback(xid);
3572        store.persist().expect("after rollback");
3573        drop(store);
3574        let recovered = UnifiedStore::open(&path).expect("recover");
3575        let manager = recovered.get_collection("rows").expect("rows");
3576        assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
3577    }
3578
3579    #[test]
3580    fn publishing_a_pending_vault_version_invalidates_reuse() {
3581        let directory = tempfile::tempdir().expect("temporary store");
3582        let path = directory.path().join("data.rdb");
3583        let store = UnifiedStore::open(&path).expect("paged store");
3584        store.create_collection("rows").expect("collection");
3585        store.insert("rows", row(1, 1)).expect("row");
3586        store
3587            .pending_vault_versions
3588            .write()
3589            .insert(EntityId::new(1));
3590        store.persist().expect("exclude pending vault version");
3591        store
3592            .pending_vault_versions
3593            .write()
3594            .remove(&EntityId::new(1));
3595        store.persist().expect("include published vault version");
3596        drop(store);
3597        let recovered = UnifiedStore::open(&path).expect("recover");
3598        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
3599    }
3600}