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