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_open_started = std::time::Instant::now();
1074        let pager = Pager::open(path, pager_config)
1075            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1076        pager
1077            .enable_deferred_reclamation()
1078            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1079        trace_startup_phase(path, "paged_pager_open", pager_open_started);
1080
1081        if config.resident_memory_bytes_max.is_none()
1082            && pager
1083                .header()
1084                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1085                .version
1086                >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
1087        {
1088            return Err(StoreError::Io(std::io::Error::new(std::io::ErrorKind::Unsupported,
1089                "this database was prepared for disk residency; reopen with the disk-resident storage profile")));
1090        }
1091
1092        let wal_path = reddb_file::layout::unified_wal_path(path);
1093        // Explicit transactions and WAL-only vault versions need committed
1094        // batches in every durability mode, including Strict's inline fsync.
1095        let commit = Some(Arc::new(
1096            StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
1097                .map_err(StoreError::Io)?,
1098        ));
1099
1100        let store = Self {
1101            config,
1102            resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1103            format_version: AtomicU32::new(STORE_VERSION_V11),
1104            pager: Some(Arc::new(pager)),
1105            db_path: Some(path.to_path_buf()),
1106            btree_indices: RwLock::new(HashMap::new()),
1107            key_indices: RwLock::new(HashMap::new()),
1108            secondary_index_specs: RwLock::new(HashMap::new()),
1109            recovery_xid_summary: RwLock::new(None),
1110            checkpoint_versions: RwLock::new(HashMap::new()),
1111            checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1112            retired_btrees: parking_lot::Mutex::new(Vec::new()),
1113            ownership_catalog_epoch: AtomicU64::new(0),
1114            ownership_active_epoch: AtomicU64::new(0),
1115            detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1116            commit,
1117            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1118            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1119            snapshot_manager: std::sync::OnceLock::new(),
1120            embedded_commit_session: std::sync::OnceLock::new(),
1121            #[cfg(test)]
1122            test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1123            #[cfg(test)]
1124            test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1125            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1126            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1127        };
1128
1129        // An interrupted structural edit may have reached home pages without
1130        // publishing its catalog. Restore those pages before following roots.
1131        let checkpoint_recovery_started = std::time::Instant::now();
1132        if let Some(commit) = &store.commit {
1133            commit
1134                .restore_checkpoint_page_images(&store)
1135                .map_err(StoreError::Io)?;
1136        }
1137        trace_startup_phase(
1138            path,
1139            "paged_checkpoint_recovery",
1140            checkpoint_recovery_started,
1141        );
1142        // Load existing data from pages if database exists
1143        let page_load_started = std::time::Instant::now();
1144        store.load_from_pages()?;
1145        trace_startup_phase(path, "paged_catalog_and_roots", page_load_started);
1146        let wal_replay_started = std::time::Instant::now();
1147        if let Some(commit) = &store.commit {
1148            commit.replay_into(&store).map_err(StoreError::Io)?;
1149        }
1150        trace_startup_phase(path, "paged_wal_replay", wal_replay_started);
1151        let manifest_recovery_started = std::time::Instant::now();
1152        store.recover_operational_manifest()?;
1153        if store.config.resident_memory_bytes_max.is_some() {
1154            let missing_key_index = {
1155                let trees = store.btree_indices.read();
1156                let keys = store.key_indices.read();
1157                trees.iter().any(|(name, tree)| {
1158                    !keys
1159                        .get(name)
1160                        .is_some_and(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
1161                })
1162            };
1163            if missing_key_index {
1164                store.persist()?;
1165            }
1166        }
1167        trace_startup_phase(
1168            path,
1169            "paged_manifest_and_derived_roots",
1170            manifest_recovery_started,
1171        );
1172
1173        Ok(store)
1174    }
1175
1176    /// Replay changes the authoritative managers first. Build replacement trees
1177    /// only for changed collections, leaving every published tree untouched until
1178    /// all replacement pages are durable. Retain the WAL for auxiliary recovery.
1179    pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
1180        let Some(pager) = &self.pager else {
1181            return Ok(());
1182        };
1183        let collections = self.resident.collections.read();
1184        let mut indices = self.btree_indices.write();
1185        let previous_versions = self.checkpoint_versions.read();
1186        let raw_replay =
1187            self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
1188        for (name, manager) in collections.iter() {
1189            let mut reusable = None;
1190            if raw_replay {
1191                if let (Some(previous), Some(tree)) =
1192                    (previous_versions.get(name), indices.get(name))
1193                {
1194                    let mut current = CheckpointVersion {
1195                        manager: Arc::downgrade(manager),
1196                        segments: manager.checkpoint_revisions(),
1197                        visibility_revision: 0,
1198                        untracked_visibility_revision: 0,
1199                        excluded_xids: Arc::default(),
1200                        pending_vault: Arc::default(),
1201                        format_version: self.format_version(),
1202                        deferred_rows: Some(Arc::default()),
1203                        has_unprojected_rows: false,
1204                    };
1205                    if previous.manager.ptr_eq(&current.manager)
1206                        && previous.format_version == current.format_version
1207                    {
1208                        if previous.segments == current.segments
1209                            && !previous.has_unprojected_rows
1210                            && checkpoint_visibility_matches(previous, &current)
1211                        {
1212                            reusable = Some(Arc::clone(tree));
1213                        } else {
1214                            // The replay ownership epoch protects in-place
1215                            // changes with the same undo/publication protocol
1216                            // used by normal sparse checkpoints. Missing WAL
1217                            // proof, old formats and exhausted journals retain
1218                            // the full comparison/rebuild path below.
1219                            reusable = self.sparse_checkpoint_tree(
1220                                name,
1221                                manager,
1222                                tree,
1223                                previous,
1224                                &mut current,
1225                            )?;
1226                        }
1227                    }
1228                }
1229            }
1230            let tree = match reusable {
1231                Some(tree) => tree,
1232                None => {
1233                    self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1234                        #[cfg(test)]
1235                        REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1236                        Some(Self::serialize_entity_record(
1237                            entity,
1238                            metadata.as_ref(),
1239                            self.format_version(),
1240                        ))
1241                    })?
1242                }
1243            };
1244            if indices
1245                .get(name)
1246                .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1247            {
1248                continue;
1249            }
1250            if let Some(previous) = indices.insert(name.clone(), tree) {
1251                self.retire_btree(previous);
1252            }
1253            self.mark_paged_registry_dirty();
1254            #[cfg(test)]
1255            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1256                if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1257                    pager.sync().map_err(|error| {
1258                        StoreError::Io(std::io::Error::other(error.to_string()))
1259                    })?;
1260                }
1261                eprintln!("experimental replay rebuild exit before manifest");
1262                std::process::exit(182);
1263            }
1264        }
1265        drop(previous_versions);
1266        drop(indices);
1267        drop(collections);
1268        self.flush_paged_state()?;
1269        #[cfg(test)]
1270        if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1271            eprintln!("experimental replay exit after manifest");
1272            std::process::exit(182);
1273        }
1274        // Opening has no concurrent writers. Replay has now durably reconciled
1275        // these raw trees; do not immediately rebuild them at the first flush.
1276        if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1277            let collections = self.resident.collections.read();
1278            let mut versions = self.checkpoint_versions.write();
1279            for (name, manager) in collections.iter() {
1280                versions.insert(
1281                    name.clone(),
1282                    CheckpointVersion {
1283                        manager: Arc::downgrade(manager),
1284                        segments: manager.checkpoint_revisions(),
1285                        visibility_revision: 0,
1286                        untracked_visibility_revision: 0,
1287                        excluded_xids: Arc::default(),
1288                        pending_vault: Arc::default(),
1289                        format_version: self.format_version(),
1290                        deferred_rows: Some(Arc::default()),
1291                        has_unprojected_rows: false,
1292                    },
1293                );
1294            }
1295        }
1296        Ok(())
1297    }
1298
1299    pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
1300        let Some(path) = &self.db_path else {
1301            return Ok(());
1302        };
1303        let mut collections = self.list_collections();
1304        collections.sort();
1305        let pending_drops =
1306            crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1307                .recover_or_bootstrap(&collections)
1308                .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1309        for name in pending_drops {
1310            if self.get_collection(&name).is_some() {
1311                self.drop_collection(&name)?;
1312            }
1313        }
1314        Ok(())
1315    }
1316
1317    pub(crate) fn prepare_operational_collection_create(
1318        &self,
1319        name: &str,
1320    ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
1321        let Some(path) = &self.db_path else {
1322            return Ok(None);
1323        };
1324        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1325            .prepare_collection_create(name)
1326            .map(Some)
1327            .map_err(StoreError::Io)
1328    }
1329
1330    pub(crate) fn prepare_operational_collection_drop(
1331        &self,
1332        name: &str,
1333    ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
1334        let Some(path) = &self.db_path else {
1335            return Ok(None);
1336        };
1337        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1338            .prepare_collection_drop(name)
1339            .map(Some)
1340            .map_err(StoreError::Io)
1341    }
1342
1343    /// Read the commit boundary before following potentially interrupted trees.
1344    pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1345        let Some(pager) = &self.pager else {
1346            return Ok(0);
1347        };
1348        let Some(content) = read_meta_payload(pager)? else {
1349            return Ok(0);
1350        };
1351        reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1352            .map(|epoch| epoch.unwrap_or(0))
1353            .map_err(|error| StoreError::Serialization(error.to_string()))
1354    }
1355
1356    /// Read the B-tree indices and reconstruct collections from pages.
1357    fn load_from_pages(&self) -> Result<(), StoreError> {
1358        let pager = match &self.pager {
1359            Some(p) => p,
1360            None => return Ok(()), // No pager, nothing to load
1361        };
1362
1363        // Get page count
1364        let page_count = pager.page_count().map_err(|e| {
1365            StoreError::Io(std::io::Error::other(format!(
1366                "failed to read page count: {}",
1367                e
1368            )))
1369        })?;
1370        if page_count <= 1 {
1371            // Empty database (only header page)
1372            return Ok(());
1373        }
1374
1375        // Read metadata starting from page 1 (collections registry). The
1376        // helper transparently follows the `RDM3` overflow chain when the
1377        // metadata blob spans multiple pages and falls back to the legacy
1378        // `<data>-meta` shadow when page 1 itself is corrupted.
1379        if let Some(content_vec) = read_meta_payload(pager)? {
1380            let content: &[u8] = &content_vec;
1381            let file_version = pager
1382                .header()
1383                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1384                .version;
1385            let prepared =
1386                file_version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION;
1387            let resume = if prepared {
1388                reddb_file::native_resume::decode_catalog_resume(content)
1389                    .map_err(|error| StoreError::Serialization(error.to_string()))?
1390            } else {
1391                None
1392            };
1393            if let Some(resume) = &resume {
1394                for state in resume.collections.values() {
1395                    if !state.secondary_indices.is_empty()
1396                        && file_version < reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
1397                    {
1398                        return Err(page_integrity(
1399                            1,
1400                            None,
1401                            "secondary roots require the file format fence",
1402                        ));
1403                    }
1404                    for root in std::iter::once(state.root_page)
1405                        .chain(state.key_index_root)
1406                        .chain(
1407                            state
1408                                .secondary_indices
1409                                .values()
1410                                .map(|index| index.root_page),
1411                        )
1412                    {
1413                        if root >= page_count {
1414                            return Err(page_integrity(
1415                                root,
1416                                None,
1417                                "published root exceeds the database page count",
1418                            ));
1419                        }
1420                    }
1421                }
1422                self.resident
1423                    .next_entity_id
1424                    .fetch_max(resume.entity_id_next, Ordering::SeqCst);
1425            }
1426            *self.recovery_xid_summary.write() = resume.as_ref().and_then(|resume| {
1427                resume.xid_high_water_mark.map(|high_water_mark| {
1428                    super::recovery_xid::RecoveryXidSummary {
1429                        high_water_mark,
1430                        roots: resume
1431                            .collections
1432                            .iter()
1433                            .map(|(name, state)| (name.clone(), state.root_page))
1434                            .collect(),
1435                    }
1436                })
1437            });
1438            if content.len() >= 4 {
1439                let mut pos = 0;
1440                let mut format_version = STORE_VERSION_V1;
1441
1442                let collection_count = if let Some(header) =
1443                    reddb_file::decode_native_paged_metadata_header(content)
1444                        .map_err(|err| StoreError::Serialization(err.to_string()))?
1445                {
1446                    format_version = header.format_version;
1447                    pos += reddb_file::METADATA_HEADER_BYTES;
1448                    header.collection_count as usize
1449                } else {
1450                    let count = u32::from_le_bytes([
1451                        content[pos],
1452                        content[pos + 1],
1453                        content[pos + 2],
1454                        content[pos + 3],
1455                    ]) as usize;
1456                    pos += 4;
1457                    count
1458                };
1459
1460                self.set_format_version(format_version);
1461
1462                if pos > content.len() {
1463                    return Ok(());
1464                }
1465
1466                // Read collection names and their B-tree root pages
1467                for _ in 0..collection_count {
1468                    let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1469                        .map_err(|err| page_integrity(1, None, err.to_string()))?;
1470                    // Root page ID for this collection's B-tree
1471                    let root_page = root.root_page;
1472                    let name = root.collection;
1473
1474                    // Hydrate the collection in memory only. Loading must
1475                    // not emit WAL entries or rewrite the on-disk registry
1476                    // before the existing B-tree roots are attached.
1477                    let _ = self.create_collection_in_memory(&name);
1478
1479                    let counters = resume
1480                        .as_ref()
1481                        .and_then(|resume| resume.collections.get(&name));
1482                    if let (Some(counters), Some(manager)) = (counters, self.get_collection(&name))
1483                    {
1484                        manager.restore_identity_counters(
1485                            counters.entity_id_next,
1486                            counters.row_id_next,
1487                        );
1488                        manager.restore_sequence_id_next(counters.sequence_id_next);
1489                    }
1490                    // Load B-tree with root page if it exists
1491                    if root_page > 0 || counters.is_some() {
1492                        let btree = BTree::with_root(Arc::clone(pager), root_page);
1493
1494                        // Load all entities from B-tree into the collection
1495                        let mut cursor = btree.cursor_first().map_err(|err| {
1496                            page_integrity(root_page, Some(name.clone()), err.to_string())
1497                        })?;
1498                        let manager = self.get_collection(&name);
1499                        let mut persisted_entities = counters.map_or(0, |state| state.entities);
1500                        while counters.is_none() {
1501                            let next = cursor
1502                                .next_with_budget(
1503                                    self.config
1504                                        .resident_memory_bytes_max
1505                                        .and_then(|limit| usize::try_from(limit).ok())
1506                                        .unwrap_or(usize::MAX),
1507                                )
1508                                .map_err(|err| {
1509                                    page_integrity(root_page, Some(name.clone()), err.to_string())
1510                                })?;
1511                            let Some((key, value)) = next else {
1512                                break;
1513                            };
1514                            persisted_entities += 1;
1515                            // Deserialize entity from value bytes
1516                            let key_bytes: [u8; 8] = key.as_slice().try_into().map_err(|_| {
1517                                page_integrity(
1518                                    root_page,
1519                                    Some(name.clone()),
1520                                    "physical entity key must contain eight bytes",
1521                                )
1522                            })?;
1523                            let record = reddb_engine::storage::collection_read::EncodedRecord {
1524                                id: EntityId::new(u64::from_be_bytes(key_bytes)),
1525                                bytes: value,
1526                            };
1527                            let decoded = if let Some(limit) = self.config.resident_memory_bytes_max
1528                            {
1529                                record.decode_with_budget(
1530                                    self.format_version(),
1531                                    usize::try_from(limit).unwrap_or(usize::MAX),
1532                                )
1533                            } else {
1534                                record.decode(self.format_version())
1535                            };
1536                            let (entity, metadata) = decoded.map_err(|err| {
1537                                page_integrity(root_page, Some(name.clone()), err.to_string())
1538                            })?;
1539                            if let Some(m) = &manager {
1540                                let id = entity.id;
1541                                if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1542                                    m.register_row_id(*row_id);
1543                                }
1544                                if self.config.resident_memory_bytes_max.is_some() {
1545                                    self.register_entity_id(id);
1546                                    m.restore_sequence_id_next(
1547                                        entity.sequence_id.saturating_add(1),
1548                                    );
1549                                    m.restore_identity_counters(
1550                                        id.raw().saturating_add(1),
1551                                        m.identity_counters().1,
1552                                    );
1553                                    continue;
1554                                }
1555                                self.resident.context_index.index_entity(&name, &entity);
1556                                m.restore_snapshot_entity(entity.clone())?;
1557                                if let Some(metadata) = metadata {
1558                                    let _ = m.set_metadata(id, metadata);
1559                                }
1560                                self.register_entity_id(id);
1561                                if self.config.auto_index_refs {
1562                                    self.index_cross_refs(&entity, &name)?;
1563                                }
1564                            }
1565                        }
1566
1567                        // Store the B-tree for future lookups
1568                        let btree = Arc::new(btree);
1569                        let key_tree = counters
1570                            .and_then(|state| state.key_index_root)
1571                            .map(|root| Arc::new(BTree::with_root(Arc::clone(pager), root)));
1572                        if let Some(counters) = counters {
1573                            self.key_indices.write().insert(
1574                                name.clone(),
1575                                PublishedKeyIndex {
1576                                    indexed: key_tree.is_some(),
1577                                    secondary: counters.secondary_indices.iter().map(|(name, index)| {
1578                                        (name.clone(), super::secondary_index::PublishedSecondaryIndex {
1579                                            identity: super::secondary_index::SecondaryIndexIdentity {
1580                                                codec_version: index.codec_version,
1581                                                definition: index.definition,
1582                                            },
1583                                            tree: Arc::new(BTree::with_root(Arc::clone(pager), index.root_page)),
1584                                        })
1585                                    }).collect(),
1586                                    source: Arc::downgrade(&btree),
1587                                    tree: key_tree.clone().unwrap_or_else(|| {
1588                                        Arc::new(BTree::with_root(Arc::clone(pager), 0))
1589                                    }),
1590                                },
1591                            );
1592                        }
1593                        if let (Some(manager), Some(limit)) =
1594                            (&manager, self.config.resident_memory_bytes_max)
1595                        {
1596                            manager.install_backing(
1597                                Arc::new(
1598                                    match key_tree {
1599                                        Some(keys) => reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(&btree), keys),
1600                                        None => reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(&btree)),
1601                                    },
1602                                ),
1603                                btree.root_page_id(),
1604                                format_version,
1605                                usize::try_from(limit).unwrap_or(usize::MAX),
1606                                persisted_entities,
1607                            );
1608                        }
1609                        self.btree_indices.write().insert(name.clone(), btree);
1610                        if format_version == STORE_VERSION_V11 {
1611                            if let Some(manager) = &manager {
1612                                self.checkpoint_versions.write().insert(
1613                                    name,
1614                                    CheckpointVersion {
1615                                        manager: Arc::downgrade(manager),
1616                                        segments: manager.checkpoint_revisions(),
1617                                        visibility_revision: 0,
1618                                        untracked_visibility_revision: 0,
1619                                        excluded_xids: Arc::default(),
1620                                        pending_vault: Arc::default(),
1621                                        format_version,
1622                                        deferred_rows: Some(Arc::default()),
1623                                        has_unprojected_rows: false,
1624                                    },
1625                                );
1626                            }
1627                        }
1628                    }
1629                }
1630
1631                if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1632                    let cross_ref_count = u32::from_le_bytes([
1633                        content[pos],
1634                        content[pos + 1],
1635                        content[pos + 2],
1636                        content[pos + 3],
1637                    ]) as usize;
1638                    pos += 4;
1639
1640                    for _ in 0..cross_ref_count {
1641                        let Ok(cross_ref) =
1642                            reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1643                        else {
1644                            break;
1645                        };
1646                        let source_id = EntityId::new(cross_ref.source_id);
1647                        let target_id = EntityId::new(cross_ref.target_id);
1648                        let ref_type = RefType::from_byte(cross_ref.ref_type);
1649                        let target_collection = cross_ref.target_collection;
1650
1651                        self.resident
1652                            .cross_refs
1653                            .write()
1654                            .entry(source_id)
1655                            .or_default()
1656                            .push((target_id, ref_type, target_collection.clone()));
1657
1658                        if let Some((collection, mut entity)) = self.get_any(source_id) {
1659                            let exists = entity.cross_refs().iter().any(|xref| {
1660                                xref.target == target_id
1661                                    && xref.ref_type == ref_type
1662                                    && xref.target_collection == target_collection
1663                            });
1664                            if !exists {
1665                                entity.cross_refs_mut().push(CrossRef::new(
1666                                    source_id,
1667                                    target_id,
1668                                    target_collection.clone(),
1669                                    ref_type,
1670                                ));
1671                                if let Some(manager) = self.get_collection(&collection) {
1672                                    let _ = manager.update(entity);
1673                                }
1674                            }
1675                        }
1676                    }
1677                }
1678                let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1679                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1680                let mut live_roots: std::collections::HashSet<_> = self
1681                    .btree_indices
1682                    .read()
1683                    .values()
1684                    .map(|tree| tree.root_page_id())
1685                    .collect();
1686                live_roots.extend(
1687                    self.key_indices
1688                        .read()
1689                        .values()
1690                        .flat_map(PublishedKeyIndex::trees)
1691                        .map(|tree| tree.root_page_id()),
1692                );
1693                for root in retired {
1694                    if live_roots.contains(&root)
1695                        || root
1696                            >= pager
1697                                .page_count()
1698                                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1699                    {
1700                        return Err(page_integrity(
1701                            root,
1702                            None,
1703                            "retired root aliases a live or missing page",
1704                        ));
1705                    }
1706                    self.retired_btrees.lock().push(RetiredBTree {
1707                        tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1708                        published: true,
1709                    });
1710                }
1711                let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1712                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1713                if deferred.iter().any(|id| live_roots.contains(id)) {
1714                    return Err(page_integrity(1, None, "deferred page aliases a live root"));
1715                }
1716                pager
1717                    .restore_deferred_reclamation(&deferred)
1718                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1719                let ownership_epoch =
1720                    reddb_file::native_store::decode_native_paged_ownership_epoch(
1721                        content, &mut pos,
1722                    )
1723                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1724                self.ownership_catalog_epoch
1725                    .store(ownership_epoch.unwrap_or(0), Ordering::Release);
1726            }
1727        }
1728
1729        if self.format_version() < STORE_VERSION_V11 {
1730            self.set_format_version(STORE_VERSION_V11);
1731        }
1732
1733        Ok(())
1734    }
1735
1736    /// Deserialize an entity from binary bytes
1737    pub(crate) fn deserialize_entity(
1738        data: &[u8],
1739        format_version: u32,
1740    ) -> Result<UnifiedEntity, StoreError> {
1741        let mut pos = 0;
1742        Self::read_entity_binary(data, &mut pos, format_version)
1743            .map_err(|e| StoreError::Serialization(e.to_string()))
1744    }
1745
1746    /// Serialize an entity to binary bytes
1747    pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1748        // Pre-allocate ~256 bytes to cover the typical 15-column
1749        // typed row without any Vec growth. Bulk insert calls this
1750        // millions of times per bench run; saving 2-3 reallocs per
1751        // entity amortises.
1752        let mut buf = Vec::with_capacity(256);
1753        Self::write_entity_binary(&mut buf, entity, format_version);
1754        buf
1755    }
1756
1757    pub(crate) fn serialize_entity_record(
1758        entity: &UnifiedEntity,
1759        metadata: Option<&Metadata>,
1760        format_version: u32,
1761    ) -> Vec<u8> {
1762        reddb_engine::storage::metadata_codec::serialize_entity_record(
1763            entity,
1764            metadata,
1765            format_version,
1766        )
1767    }
1768
1769    pub(crate) fn deserialize_entity_record(
1770        data: &[u8],
1771        format_version: u32,
1772    ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1773        reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
1774            .map_err(StoreError::Serialization)
1775    }
1776
1777    /// Persist all data to page-based storage
1778    ///
1779    /// Writes all entities to B-tree pages and flushes to disk.
1780    /// This provides ACID durability guarantees.
1781    pub fn persist(&self) -> Result<(), StoreError> {
1782        let _vault_guard = self.vault_publication_lock.lock();
1783        let pending_vault_versions = self.pending_vault_versions.read();
1784        let pager = match &self.pager {
1785            Some(p) => p,
1786            None => {
1787                // No pager attached - use binary file fallback if path available
1788                if let Some(path) = &self.db_path {
1789                    return self
1790                        .save_to_file(path)
1791                        .map_err(|e| StoreError::Serialization(e.to_string()));
1792                }
1793                return Err(StoreError::Io(std::io::Error::other(
1794                    "No pager or path configured for persistence",
1795                )));
1796            }
1797        };
1798        // Prevent unrelated allocations or frees from being attributed to
1799        // this checkpoint's ownership epoch.
1800        let _ownership_guard = pager.lock_ownership_mutations();
1801
1802        match pager.read_page(1) {
1803            Ok(_) => {}
1804            Err(PagerError::PageNotFound(_)) => {
1805                let meta_page = pager
1806                    .allocate_page(crate::storage::engine::PageType::Header)
1807                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1808                pager
1809                    .write_page(meta_page.page_id(), meta_page)
1810                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1811            }
1812            Err(e) => {
1813                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1814            }
1815        }
1816
1817        if let Some(commit) = &self.commit {
1818            commit.force_sync().map_err(StoreError::Io)?;
1819        }
1820
1821        struct InterruptedOwnership<'a> {
1822            pager: &'a Pager,
1823            active: bool,
1824        }
1825        impl Drop for InterruptedOwnership<'_> {
1826            fn drop(&mut self) {
1827                if self.active {
1828                    self.pager.fence_interrupted_ownership();
1829                }
1830            }
1831        }
1832        let mut ownership = InterruptedOwnership {
1833            pager,
1834            active: self.commit.is_some(),
1835        };
1836        let epoch = if let Some(commit) = &self.commit {
1837            let epoch = commit
1838                .next_page_ownership_epoch(self)
1839                .map_err(StoreError::Io)?;
1840            commit
1841                .begin_page_ownership_epoch(self, epoch)
1842                .map_err(StoreError::Io)?;
1843            Some(epoch)
1844        } else {
1845            None
1846        };
1847
1848        let collections = self.resident.collections.read();
1849        let mut btree_indices = self.btree_indices.write();
1850
1851        // Collect collection names and their B-tree root pages
1852        let mut collection_roots: Vec<(String, u32)> = Vec::new();
1853
1854        // Build unpublished replacements. Deleting from the current trees here
1855        // would destroy the last checkpoint if a write fails before publication.
1856        let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1857        let previous_versions = self.checkpoint_versions.read();
1858        let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1859        let pending_vault = Arc::new(pending_vault_versions.clone());
1860        let (visibility_revision, excluded, untracked_visibility_revision) =
1861            self.snapshot_manager.get().map_or_else(
1862                || (0, Default::default(), 0),
1863                |manager| manager.checkpoint_projection_state(),
1864            );
1865        let excluded = Arc::new(excluded);
1866        for (name, manager) in collections.iter() {
1867            let mut version = CheckpointVersion {
1868                manager: Arc::downgrade(manager),
1869                segments: manager.checkpoint_revisions(),
1870                visibility_revision,
1871                untracked_visibility_revision,
1872                excluded_xids: Arc::clone(&excluded),
1873                pending_vault: Arc::clone(&pending_vault),
1874                format_version: self.format_version(),
1875                deferred_rows: Some(Arc::default()),
1876                has_unprojected_rows: false,
1877            };
1878            if let (Some(previous), Some(tree)) =
1879                (previous_versions.get(name), btree_indices.get(name))
1880            {
1881                if previous.manager.ptr_eq(&version.manager)
1882                    && previous.segments == version.segments
1883                    && previous.format_version == version.format_version
1884                    && !previous.has_unprojected_rows
1885                    && checkpoint_visibility_matches(previous, &version)
1886                {
1887                    version.deferred_rows = previous.deferred_rows.clone();
1888                    collection_roots.push((name.clone(), tree.root_page_id()));
1889                    rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1890                    checkpoint_versions.insert(name.clone(), version);
1891                    continue;
1892                }
1893            }
1894            if let (Some(previous), Some(tree)) =
1895                (previous_versions.get(name), btree_indices.get(name))
1896            {
1897                if previous.manager.ptr_eq(&version.manager)
1898                    && previous.format_version == version.format_version
1899                {
1900                    if let Some(tree) =
1901                        self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
1902                    {
1903                        collection_roots.push((name.clone(), tree.root_page_id()));
1904                        rebuilt_indices.insert(name.clone(), tree);
1905                        checkpoint_versions.insert(name.clone(), version);
1906                        continue;
1907                    }
1908                }
1909            }
1910            let btree = self.checkpoint_tree(
1911                pager,
1912                manager,
1913                btree_indices.get(name),
1914                |entity, metadata| {
1915                    #[cfg(test)]
1916                    CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1917                    let hidden = pending_vault_versions.contains(&entity.id)
1918                        || (matches!(
1919                            entity.data,
1920                            EntityData::Row(_)
1921                                | EntityData::Vector(_)
1922                                | EntityData::QueueMessage(_)
1923                        ) && excluded.contains(&entity.xmin));
1924                    let normalize_xmax = matches!(
1925                        entity.data,
1926                        EntityData::Row(_) | EntityData::QueueMessage(_)
1927                    ) && excluded.contains(&entity.xmax);
1928                    if hidden || normalize_xmax {
1929                        version.remember_deferred_row(entity.id);
1930                    }
1931                    if hidden {
1932                        return None;
1933                    }
1934                    let mut normalized = std::borrow::Cow::Borrowed(entity);
1935                    if normalize_xmax {
1936                        normalized.to_mut().xmax = 0;
1937                    }
1938                    Some(Self::serialize_entity_record(
1939                        &normalized,
1940                        metadata.as_ref(),
1941                        self.format_version(),
1942                    ))
1943                },
1944            )?;
1945            checkpoint_versions.insert(name.clone(), version);
1946
1947            collection_roots.push((name.clone(), btree.root_page_id()));
1948            rebuilt_indices.insert(name.clone(), btree);
1949        }
1950
1951        // Write collection metadata to page 1
1952        let mut meta_data = Vec::with_capacity(4096);
1953
1954        let format_version = STORE_VERSION_V11;
1955        self.set_format_version(format_version);
1956
1957        reddb_file::encode_native_paged_metadata_header(
1958            &mut meta_data,
1959            reddb_file::NativePagedMetadataHeader {
1960                format_version,
1961                collection_count: collection_roots.len() as u32,
1962            },
1963        );
1964
1965        // Write each collection's name and B-tree root page
1966        for (name, root_page) in &collection_roots {
1967            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1968        }
1969
1970        // Write cross-reference metadata
1971        let cross_refs = self.resident.cross_refs.read();
1972        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1973        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1974        for (source_id, refs) in cross_refs.iter() {
1975            for (target_id, ref_type, collection) in refs {
1976                reddb_file::encode_native_paged_cross_ref(
1977                    &mut meta_data,
1978                    source_id.raw(),
1979                    target_id.raw(),
1980                    ref_type.to_byte(),
1981                    collection,
1982                );
1983            }
1984        }
1985
1986        for (name, previous) in btree_indices.iter() {
1987            if !rebuilt_indices
1988                .get(name)
1989                .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1990            {
1991                self.retire_btree(Arc::clone(previous));
1992            }
1993        }
1994        #[cfg(test)]
1995        if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
1996            pager
1997                .sync()
1998                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1999            eprintln!("experimental checkpoint exit before manifest");
2000            std::process::exit(183);
2001        }
2002        let rebuilt_keys = self.checkpoint_key_indices(pager, &rebuilt_indices)?;
2003        #[cfg(test)]
2004        if std::env::var_os("REDDB_TEST_EXIT_AFTER_SECONDARY_INDEX_BUILD").is_some() {
2005            pager
2006                .sync()
2007                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2008            std::process::exit(196);
2009        }
2010        self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices, &rebuilt_keys)?;
2011
2012        #[cfg(test)]
2013        if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
2014            std::process::exit(195);
2015        }
2016
2017        // Only the now-durable catalog may replace the live tree handles. The
2018        // previous generation is retained in the manifest retirement ledger.
2019        *btree_indices = rebuilt_indices;
2020        *self.key_indices.write() = rebuilt_keys;
2021        drop(previous_versions);
2022        *self.checkpoint_versions.write() = checkpoint_versions;
2023
2024        if let Some(commit) = &self.commit {
2025            commit
2026                .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
2027                .map_err(StoreError::Io)?;
2028            ownership.active = false;
2029            commit.truncate().map_err(StoreError::Io)?;
2030        }
2031        *self.checkpoint_durable_rows.write() = Default::default();
2032
2033        if let Some(limit) = self.config.resident_memory_bytes_max {
2034            let mut versions = self.checkpoint_versions.write();
2035            for (name, manager) in collections.iter() {
2036                if versions
2037                    .get(name)
2038                    .is_some_and(|version| !version.has_unprojected_rows)
2039                {
2040                    if let Some(tree) = btree_indices.get(name) {
2041                        manager.install_backing(
2042                            Arc::new(
2043                                {
2044                                    let keys = self.key_indices.read();
2045                                    let index = keys.get(name).expect("invariant: prepared checkpoint classifies its key index");
2046                                    if index.indexed {
2047                                        reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(tree), Arc::clone(&index.tree))
2048                                    } else {
2049                                        reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(tree))
2050                                    }
2051                                },
2052                            ),
2053                            tree.root_page_id(),
2054                            self.format_version(),
2055                            usize::try_from(limit).unwrap_or(usize::MAX),
2056                            manager.count() as u64,
2057                        );
2058                        if let Some(version) = versions.get_mut(name) {
2059                            version.segments = manager.checkpoint_revisions();
2060                        }
2061                    }
2062                }
2063            }
2064        }
2065
2066        Ok(())
2067    }
2068
2069    /// Check if the store is using page-based persistence
2070    pub fn is_paged(&self) -> bool {
2071        self.pager.is_some()
2072    }
2073
2074    /// Current root page for a collection's primary B-tree, if one has
2075    /// been materialized in this store.
2076    pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
2077        self.btree_indices
2078            .read()
2079            .get(collection)
2080            .map(|btree| btree.root_page_id())
2081            .filter(|root| *root != 0)
2082    }
2083
2084    /// Get the database file path (if using paged mode)
2085    pub fn db_path(&self) -> Option<&Path> {
2086        self.db_path.as_deref()
2087    }
2088}
2089
2090#[cfg(test)]
2091mod checkpoint_work_tests {
2092    use super::*;
2093
2094    #[cfg(target_os = "linux")]
2095    #[test]
2096    fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
2097        const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
2098        if let Some(path) = std::env::var_os(CHILD) {
2099            let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
2100            let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
2101            let store = UnifiedStore::open(path).expect("open store");
2102            let initial_count = if multi { 600 } else { 1 };
2103            for id in 1..=initial_count {
2104                store.insert_auto("rows", row(id, 1)).expect("seed");
2105            }
2106            store.persist().expect("baseline");
2107            let manager = store.get_collection("rows").expect("collection");
2108            let ids: Vec<u64> = if multi {
2109                (1..=initial_count).collect()
2110            } else {
2111                vec![if insert { 2 } else { 1 }]
2112            };
2113            let mut actions = Vec::new();
2114            for id in ids {
2115                if insert {
2116                    manager.insert(row(id, 9)).expect("insert version");
2117                } else {
2118                    manager.update(row(id, 9)).expect("update version");
2119                }
2120                let entity = manager.get(EntityId::new(id)).expect("changed entity");
2121                actions.push(StoreWalAction::upsert_entity(
2122                    "rows",
2123                    &entity,
2124                    None,
2125                    store.format_version(),
2126                ));
2127            }
2128            let references = store
2129                .commit
2130                .as_ref()
2131                .expect("coordinator")
2132                .append_checkpoint_actions(&actions)
2133                .expect("durable WAL");
2134            store.remember_checkpoint_rows(actions, references);
2135            store.persist().expect("exit after sparse write");
2136            panic!("inline sparse crash hook was not reached");
2137        }
2138        for (insert, multi) in [(false, false), (true, false), (false, true)] {
2139            let directory = tempfile::tempdir().expect("temporary store");
2140            let path = directory.path().join("data.rdb");
2141            let mut command =
2142                std::process::Command::new(std::env::current_exe().expect("test executable"));
2143            command
2144                .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
2145                .env(CHILD, &path)
2146                .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
2147            if insert {
2148                command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
2149            }
2150            if multi {
2151                command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
2152            }
2153            let output = command.output().expect("crash child");
2154            assert_eq!(
2155                output.status.code(),
2156                Some(190),
2157                "insert={insert} multi={multi}: {}",
2158                String::from_utf8_lossy(&output.stderr)
2159            );
2160            let recovered = UnifiedStore::open(&path).expect("recover");
2161            let ids: Vec<u64> = if multi {
2162                (1..=600).collect()
2163            } else {
2164                vec![if insert { 2 } else { 1 }]
2165            };
2166            for id in ids {
2167                assert_eq!(
2168                    recovered
2169                        .get("rows", EntityId::new(id))
2170                        .expect("recovered row")
2171                        .data
2172                        .as_row()
2173                        .expect("row")
2174                        .columns,
2175                    vec![Value::Integer(9)]
2176                );
2177            }
2178            assert_eq!(
2179                recovered.get_collection("rows").expect("rows").count(),
2180                if multi {
2181                    600
2182                } else if insert {
2183                    2
2184                } else {
2185                    1
2186                }
2187            );
2188        }
2189    }
2190
2191    #[test]
2192    fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
2193        let directory = tempfile::tempdir().expect("temporary store");
2194        let path = directory.path().join("data.rdb");
2195        {
2196            let store = UnifiedStore::open(&path).expect("open store");
2197            store.create_collection("rows").expect("create collection");
2198            store.persist().expect("publish ownership epoch");
2199            assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
2200            store.persist().expect("publish next ownership epoch");
2201        }
2202        let reopened = UnifiedStore::open(&path).expect("reopen store");
2203        assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
2204    }
2205
2206    #[test]
2207    fn metadata_overflow_with_missing_continuation_fails_closed() {
2208        use crate::storage::engine::{Page, PageType, HEADER_SIZE};
2209
2210        let directory = tempfile::tempdir().expect("temporary store");
2211        let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
2212        let pager = store.pager.as_ref().unwrap();
2213        let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
2214        let valid_manifest = build_meta_page1_with_overflow(pager, &valid_payload).unwrap();
2215        pager.write_page(1, valid_manifest).unwrap();
2216        assert!(!existing_overflow_chain(pager).unwrap().is_empty());
2217        assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
2218
2219        let missing = pager.page_count().unwrap() + 100;
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: missing,
2227            },
2228        )
2229        .unwrap();
2230        pager.write_page(1, manifest).unwrap();
2231
2232        assert!(
2233            existing_overflow_chain(pager).is_err(),
2234            "a missing continuation must not be treated as an empty chain"
2235        );
2236
2237        let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
2238        let mut manifest = Page::new(PageType::Header, 1);
2239        reddb_file::encode_native_metadata_overflow_header(
2240            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2241            reddb_file::NativeMetadataOverflowHeader {
2242                format_version: STORE_VERSION_V11,
2243                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2244                next_overflow_page_id: wrong_type.page_id(),
2245            },
2246        )
2247        .unwrap();
2248        pager.write_page(1, manifest).unwrap();
2249        assert!(
2250            existing_overflow_chain(pager).is_err(),
2251            "a live page must not be mistaken for metadata overflow"
2252        );
2253        assert!(
2254            read_meta_payload(pager).is_err(),
2255            "a live page must not be read as metadata overflow"
2256        );
2257
2258        let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
2259        let cycle_id = cycle.page_id();
2260        reddb_file::encode_native_metadata_overflow_continuation_header(
2261            &mut cycle.as_bytes_mut()[HEADER_SIZE..],
2262            reddb_file::NativeMetadataOverflowContinuationHeader {
2263                next_overflow_page_id: cycle_id,
2264                chunk_bytes: 1,
2265            },
2266        )
2267        .unwrap();
2268        pager.write_page(cycle_id, cycle).unwrap();
2269        let mut manifest = Page::new(PageType::Header, 1);
2270        reddb_file::encode_native_metadata_overflow_header(
2271            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2272            reddb_file::NativeMetadataOverflowHeader {
2273                format_version: STORE_VERSION_V11,
2274                total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
2275                next_overflow_page_id: cycle_id,
2276            },
2277        )
2278        .unwrap();
2279        pager.write_page(1, manifest).unwrap();
2280        assert!(
2281            read_meta_payload(pager).is_err(),
2282            "cyclic overflow must not be accepted"
2283        );
2284
2285        let mut manifest = Page::new(PageType::Header, 1);
2286        reddb_file::encode_native_metadata_overflow_header(
2287            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2288            reddb_file::NativeMetadataOverflowHeader {
2289                format_version: STORE_VERSION_V11,
2290                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2291                next_overflow_page_id: 0,
2292            },
2293        )
2294        .unwrap();
2295        pager.write_page(1, manifest).unwrap();
2296        assert!(
2297            read_meta_payload(pager).is_err(),
2298            "a truncated metadata payload must not be accepted"
2299        );
2300    }
2301
2302    #[test]
2303    fn retired_tree_pages_remain_pinned_by_a_cursor() {
2304        let directory = tempfile::tempdir().expect("temporary store");
2305        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2306        for id in 1..=100 {
2307            store.insert_auto("rows", row(id, 1)).expect("seed");
2308        }
2309        store.persist().expect("initial checkpoint");
2310        let tree = store
2311            .btree_indices
2312            .read()
2313            .get("rows")
2314            .cloned()
2315            .expect("tree");
2316        let mut cursor = tree.cursor_first().expect("cursor");
2317        drop(tree);
2318        let manager = store.get_collection("rows").expect("rows");
2319        for value in 2..=6 {
2320            manager.update(row(1, value)).expect("change");
2321            store.persist().expect("checkpoint with old reader");
2322        }
2323        let mut count = 0;
2324        while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
2325            let (entity, _) =
2326                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2327                    .expect("decode");
2328            assert_eq!(
2329                entity.data.as_row().expect("row").columns,
2330                [Value::Integer(1)]
2331            );
2332            count += 1;
2333        }
2334        assert_eq!(count, 100);
2335        drop(cursor);
2336        store.persist().expect("reclaim after reader release");
2337        assert!(store.retired_btrees.lock().is_empty());
2338    }
2339
2340    #[test]
2341    fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
2342        let directory = tempfile::tempdir().expect("temporary store");
2343        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2344        store.create_collection("rows").expect("collection");
2345        let manager = store.get_collection("rows").expect("rows");
2346        for id in (1..=1200).rev() {
2347            manager
2348                .insert(UnifiedEntity::table_row(
2349                    EntityId::new(id),
2350                    "rows",
2351                    id,
2352                    vec![Value::text("x".repeat(4096))],
2353                ))
2354                .expect("seed");
2355        }
2356        CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
2357        store.persist().expect("bounded checkpoint");
2358        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
2359        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
2360        assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
2361    }
2362
2363    #[test]
2364    fn unchanged_checkpoint_does_not_serialize_collection_rows() {
2365        let directory = tempfile::tempdir().expect("temporary store");
2366        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2367        store.create_collection("rows").expect("collection");
2368        for id in 1..=40 {
2369            store
2370                .insert(
2371                    "rows",
2372                    UnifiedEntity::table_row(
2373                        EntityId::new(id),
2374                        "rows",
2375                        id,
2376                        vec![Value::Integer(id as i64)],
2377                    ),
2378                )
2379                .expect("seed row");
2380        }
2381        store.persist().expect("initial checkpoint");
2382        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2383        store.persist().expect("unchanged checkpoint");
2384        assert_eq!(
2385            CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
2386            0,
2387            "unchanged collections must not hold the writer fence while serializing every row"
2388        );
2389        drop(store);
2390        let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
2391        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2392        reopened.persist().expect("unchanged recovered generation");
2393        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2394    }
2395
2396    #[test]
2397    fn sparse_wal_replay_does_not_reencode_complete_collections() {
2398        let directory = tempfile::tempdir().expect("temporary store");
2399        let path = directory.path().join("data.rdb");
2400        let store = UnifiedStore::open(&path).expect("paged store");
2401        for (name, first) in [("rows", 1), ("cold", 129)] {
2402            store.create_collection(name).expect("collection");
2403            for id in first..first + 128 {
2404                store
2405                    .insert(
2406                        name,
2407                        UnifiedEntity::table_row(
2408                            EntityId::new(id),
2409                            name,
2410                            id,
2411                            vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2412                        ),
2413                    )
2414                    .expect("seed");
2415            }
2416        }
2417        store.persist().expect("durable baseline");
2418        let action = super::super::commit::StoreWalAction::upsert_entity(
2419            "rows",
2420            &row(1, 42),
2421            None,
2422            STORE_VERSION_V11,
2423        );
2424        // The acknowledged WAL image has not reached its home B-tree yet.
2425        store
2426            .commit
2427            .as_ref()
2428            .unwrap()
2429            .append_actions(&[action])
2430            .unwrap();
2431        drop(store);
2432
2433        REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2434        let recovered = UnifiedStore::open(&path).expect("replay committed change");
2435        assert_eq!(
2436            recovered
2437                .get("rows", EntityId::new(1))
2438                .unwrap()
2439                .data
2440                .as_row()
2441                .unwrap()
2442                .columns,
2443            vec![Value::Integer(42)]
2444        );
2445        for name in ["rows", "cold"] {
2446            assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
2447        }
2448        assert_eq!(
2449            REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
2450            0,
2451            "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
2452        );
2453        for (name, first) in [("rows", 2), ("cold", 129)] {
2454            let end = if name == "rows" { 129 } else { 257 };
2455            for id in first..end {
2456                assert_eq!(
2457                    recovered
2458                        .get(name, EntityId::new(id))
2459                        .unwrap()
2460                        .data
2461                        .as_row()
2462                        .unwrap()
2463                        .columns,
2464                    vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2465                );
2466            }
2467        }
2468
2469        drop(recovered);
2470        let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
2471        assert_eq!(
2472            reopened
2473                .get("rows", EntityId::new(1))
2474                .unwrap()
2475                .data
2476                .as_row()
2477                .unwrap()
2478                .columns,
2479            vec![Value::Integer(42)]
2480        );
2481    }
2482
2483    fn replay_test_payload(bytes: usize) -> String {
2484        let mut state = 0x1234_5678_u32;
2485        (0..bytes)
2486            .map(|_| {
2487                state ^= state << 13;
2488                state ^= state >> 17;
2489                state ^= state << 5;
2490                char::from(b'!' + (state % 90) as u8)
2491            })
2492            .collect()
2493    }
2494
2495    #[test]
2496    fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
2497        let directory = tempfile::tempdir().unwrap();
2498        let path = directory.path().join("data.rdb");
2499        let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
2500        let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2501        for id in 1..=16 {
2502            store.insert_auto("rows", row(id, 1)).unwrap();
2503        }
2504        store.persist().unwrap();
2505        // Larger than the retained-image allowance, so replay must fall back.
2506        let payload = replay_test_payload(64 * 1024);
2507        let changed =
2508            UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
2509        let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2510        store
2511            .commit
2512            .as_ref()
2513            .unwrap()
2514            .append_actions(&[action])
2515            .unwrap();
2516        drop(store);
2517        REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2518        let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
2519        assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
2520        assert_eq!(
2521            recovered
2522                .get("rows", EntityId::new(1))
2523                .unwrap()
2524                .data
2525                .as_row()
2526                .unwrap()
2527                .columns,
2528            changed.data.as_row().unwrap().columns
2529        );
2530        drop(recovered);
2531        let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
2532        assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
2533        assert_eq!(
2534            reopened
2535                .get("rows", EntityId::new(1))
2536                .unwrap()
2537                .data
2538                .as_row()
2539                .unwrap()
2540                .columns,
2541            changed.data.as_row().unwrap().columns
2542        );
2543    }
2544
2545    #[cfg(target_os = "linux")]
2546    #[test]
2547    fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
2548        const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
2549        if let Some(path) = std::env::var_os(CHILD) {
2550            let _store = UnifiedStore::open(path).unwrap();
2551            panic!("replay crash hook was not reached");
2552        }
2553        for overflow in [false, true] {
2554            for (hook, code) in [
2555                ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
2556                ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
2557            ] {
2558                let directory = tempfile::tempdir().unwrap();
2559                let path = directory.path().join("data.rdb");
2560                let store = UnifiedStore::open(&path).unwrap();
2561                for id in 1..=128 {
2562                    store.insert_auto("rows", row(id, 1)).unwrap();
2563                }
2564                store.persist().unwrap();
2565                let changed = if overflow {
2566                    UnifiedEntity::table_row(
2567                        EntityId::new(1),
2568                        "rows",
2569                        1,
2570                        vec![Value::text(replay_test_payload(16 * 1024))],
2571                    )
2572                } else {
2573                    row(1, 42)
2574                };
2575                let action =
2576                    StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2577                store
2578                    .commit
2579                    .as_ref()
2580                    .unwrap()
2581                    .append_actions(&[action])
2582                    .unwrap();
2583                drop(store);
2584                let mut first_size = 0;
2585                for cycle in 0..12 {
2586                    let output = std::process::Command::new(std::env::current_exe().unwrap())
2587                        .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
2588                        .env(CHILD, &path).env(hook, "1").output().unwrap();
2589                    assert_eq!(
2590                        output.status.code(),
2591                        Some(code),
2592                        "overflow={overflow} hook={hook} cycle={cycle}: {}",
2593                        String::from_utf8_lossy(&output.stderr)
2594                    );
2595                    let size = std::fs::metadata(&path).unwrap().len();
2596                    if cycle == 0 {
2597                        first_size = size;
2598                    }
2599                    assert!(
2600                        size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
2601                        "repeated recovery must reuse reservations: {first_size} -> {size}"
2602                    );
2603                }
2604                for _ in 0..2 {
2605                    let recovered = UnifiedStore::open(&path).unwrap();
2606                    assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
2607                    for id in 1..=128 {
2608                        let expected = if id == 1 { changed.clone() } else { row(id, 1) };
2609                        assert_eq!(
2610                            recovered
2611                                .get("rows", EntityId::new(id))
2612                                .unwrap()
2613                                .data
2614                                .as_row()
2615                                .unwrap()
2616                                .columns,
2617                            expected.data.as_row().unwrap().columns
2618                        );
2619                    }
2620                }
2621            }
2622        }
2623    }
2624
2625    #[test]
2626    fn paged_reopen_preserves_stored_sequence_identity() {
2627        let directory = tempfile::tempdir().expect("temporary store");
2628        let path = directory.path().join("data.rdb");
2629        let store = UnifiedStore::open(&path).expect("paged store");
2630        store.create_collection("rows").expect("collection");
2631        for id in [30, 10, 20] {
2632            store.insert("rows", row(id, id as i64)).expect("insert");
2633        }
2634        store.persist().expect("checkpoint");
2635        let sequences = [10, 20, 30].map(|id| {
2636            store
2637                .get("rows", EntityId::new(id))
2638                .expect("row")
2639                .sequence_id
2640        });
2641        drop(store);
2642        let reopened = UnifiedStore::open(&path).expect("reopen");
2643        let recovered = [10, 20, 30].map(|id| {
2644            reopened
2645                .get("rows", EntityId::new(id))
2646                .expect("row")
2647                .sequence_id
2648        });
2649        assert_eq!(
2650            recovered, sequences,
2651            "opening a checkpoint must not rewrite stored row identities"
2652        );
2653    }
2654
2655    #[test]
2656    fn auto_insert_persists_the_assigned_sequence_identity() {
2657        let directory = tempfile::tempdir().expect("temporary store");
2658        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2659        for id in 1u64..=3 {
2660            store
2661                .insert_auto("rows", row(id, id as i64))
2662                .expect("insert");
2663        }
2664        let manager = store.get_collection("rows").expect("rows");
2665        let tree = store
2666            .btree_indices
2667            .read()
2668            .get("rows")
2669            .cloned()
2670            .expect("tree");
2671        for id in 1u64..=3 {
2672            let bytes = tree
2673                .get(&id.to_be_bytes())
2674                .expect("lookup")
2675                .expect("stored row");
2676            let (persisted, _) =
2677                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2678                    .expect("decode");
2679            assert_eq!(
2680                persisted.sequence_id,
2681                manager
2682                    .get(EntityId::new(id))
2683                    .expect("live row")
2684                    .sequence_id
2685            );
2686        }
2687    }
2688
2689    #[test]
2690    fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
2691        let directory = tempfile::tempdir().expect("temporary store");
2692        let path = directory.path().join("data.rdb");
2693        let store = UnifiedStore::open(&path).expect("paged store");
2694        store.insert_auto("rows", row(1, 1)).expect("seed");
2695        store.persist().expect("checkpoint");
2696        store.insert_auto("rows", row(2, 2)).expect("append");
2697        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2698        store.persist().expect("inline append checkpoint");
2699        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2700        store
2701            .get_collection("rows")
2702            .expect("rows")
2703            .update(row(1, 99))
2704            .expect("untracked edit");
2705        store.insert_auto("rows", row(3, 3)).expect("next append");
2706        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2707        store.persist().expect("untracked edit checkpoint");
2708        assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
2709        drop(store);
2710        let reopened = UnifiedStore::open(&path).expect("reopen");
2711        assert_eq!(
2712            reopened
2713                .get("rows", EntityId::new(1))
2714                .expect("row")
2715                .data
2716                .as_row()
2717                .expect("table")
2718                .columns,
2719            vec![Value::Integer(99)]
2720        );
2721    }
2722
2723    #[test]
2724    fn replayed_trees_are_reusable_after_durable_reconciliation() {
2725        let directory = tempfile::tempdir().expect("temporary store");
2726        let path = directory.path().join("data.rdb");
2727        let store = UnifiedStore::open(&path).expect("paged store");
2728        store.insert_auto("rows", row(1, 1)).expect("seed");
2729        store.persist().expect("checkpoint");
2730        store
2731            .insert_auto("rows", row(2, 2))
2732            .expect("WAL-backed append");
2733        drop(store);
2734        let reopened = UnifiedStore::open(&path).expect("replay");
2735        assert!(reopened.get("rows", EntityId::new(2)).is_some());
2736        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2737        reopened.persist().expect("checkpoint reconciled tree");
2738        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2739    }
2740
2741    fn row(id: u64, value: i64) -> UnifiedEntity {
2742        UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
2743    }
2744
2745    #[test]
2746    fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
2747        let directory = tempfile::tempdir().expect("temporary store");
2748        let path = directory.path().join("data.rdb");
2749        let store = UnifiedStore::open(&path).expect("paged store");
2750        store.create_collection("rows").expect("collection");
2751        store.insert("rows", row(1, 1)).expect("seed");
2752        store.persist().expect("initial checkpoint");
2753        let manager = store.get_collection("rows").expect("rows");
2754        manager.update(row(1, 2)).expect("direct update");
2755        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2756        store.persist().expect("updated row checkpoint");
2757        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2758        let mut metadata = Metadata::new();
2759        metadata.set("verified", MetadataValue::Bool(true));
2760        manager
2761            .set_metadata(EntityId::new(1), metadata)
2762            .expect("metadata");
2763        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2764        store.persist().expect("metadata-only checkpoint");
2765        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2766        manager
2767            .bulk_insert(vec![row(2, 2), row(3, 3)])
2768            .expect("bulk insert");
2769        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2770        store.persist().expect("bulk insert checkpoint");
2771        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
2772        manager.delete(EntityId::new(2)).expect("delete");
2773        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2774        store.persist().expect("changed checkpoint");
2775        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
2776        drop(store);
2777        let recovered = UnifiedStore::open(&path).expect("recover changed rows");
2778        let manager = recovered.get_collection("rows").expect("rows");
2779        assert_eq!(manager.count(), 2);
2780        assert_eq!(
2781            manager
2782                .get(EntityId::new(1))
2783                .expect("row")
2784                .data
2785                .as_row()
2786                .expect("table")
2787                .columns,
2788            vec![Value::Integer(2)]
2789        );
2790        assert!(manager.get(EntityId::new(2)).is_none());
2791        assert!(manager
2792            .get_metadata(EntityId::new(1))
2793            .expect("metadata")
2794            .get("verified")
2795            .is_some());
2796    }
2797
2798    #[test]
2799    fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
2800        let directory = tempfile::tempdir().expect("temporary store");
2801        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2802        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2803        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2804        store.create_collection("rows").expect("collection");
2805        for id in 1..=40 {
2806            store.insert("rows", row(id, id as i64)).expect("seed");
2807        }
2808        store.persist().expect("initial checkpoint");
2809        let unrelated = snapshots.begin();
2810        snapshots.rollback(unrelated);
2811        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2812        store.persist().expect("unrelated rollback checkpoint");
2813        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2814
2815        store
2816            .pending_vault_versions
2817            .write()
2818            .insert(EntityId::new(100));
2819        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2820        store.persist().expect("unrelated vault checkpoint");
2821        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2822        store.pending_vault_versions.write().clear();
2823        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2824        store
2825            .persist()
2826            .expect("unrelated vault publication checkpoint");
2827        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2828    }
2829
2830    #[test]
2831    fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
2832        let directory = tempfile::tempdir().expect("temporary store");
2833        let path = directory.path().join("data.rdb");
2834        let store = UnifiedStore::open(&path).expect("paged store");
2835        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2836        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2837        store.create_collection("rows").expect("collection");
2838        let xid = snapshots.begin();
2839        let mut entity = row(1, 1);
2840        entity.xmin = xid;
2841        store.insert("rows", entity).expect("committed row");
2842        snapshots.commit(xid);
2843        store.persist().expect("committed baseline");
2844        snapshots.rollback(xid);
2845        store
2846            .persist()
2847            .expect("untracked outcome requires validation");
2848        drop(store);
2849        let recovered = UnifiedStore::open(&path).expect("recover");
2850        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2851    }
2852
2853    #[test]
2854    fn exhausted_deferred_row_cache_keeps_full_validation() {
2855        let directory = tempfile::tempdir().expect("temporary store");
2856        let path = directory.path().join("data.rdb");
2857        let store = UnifiedStore::open(&path).expect("paged store");
2858        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2859        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2860        store.create_collection("rows").expect("collection");
2861        let xid = snapshots.begin();
2862        for id in 1..=1025 {
2863            let mut entity = row(id, id as i64);
2864            entity.xmin = xid;
2865            store.insert("rows", entity).expect("pending row");
2866        }
2867        store.persist().expect("exclude pending rows");
2868        assert!(store.checkpoint_versions.read()["rows"]
2869            .deferred_rows
2870            .is_none());
2871        snapshots.commit(xid);
2872        store
2873            .persist()
2874            .expect("publish committed rows conservatively");
2875        drop(store);
2876        let recovered = UnifiedStore::open(&path).expect("recover");
2877        assert_eq!(
2878            recovered.get_collection("rows").expect("rows").count(),
2879            1025
2880        );
2881    }
2882
2883    #[test]
2884    fn rollback_invalidates_reuse_without_a_segment_mutation() {
2885        let directory = tempfile::tempdir().expect("temporary store");
2886        let path = directory.path().join("data.rdb");
2887        let store = UnifiedStore::open(&path).expect("paged store");
2888        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2889        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2890        store.create_collection("rows").expect("collection");
2891        let xid = snapshots.begin();
2892        let mut pending = row(1, 1);
2893        pending.xmin = xid;
2894        store.insert("rows", pending).expect("version");
2895        store.persist().expect("before rollback");
2896        snapshots.rollback(xid);
2897        store.persist().expect("after rollback");
2898        drop(store);
2899        let recovered = UnifiedStore::open(&path).expect("recover");
2900        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2901    }
2902
2903    #[test]
2904    fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
2905        let directory = tempfile::tempdir().expect("temporary store");
2906        let path = directory.path().join("data.rdb");
2907        let store = UnifiedStore::open(&path).expect("paged store");
2908        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2909        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2910        store.create_collection("rows").expect("collection");
2911        let xid = snapshots.begin();
2912        let mut deleted = row(1, 1);
2913        deleted.xmax = xid;
2914        store.insert("rows", deleted).expect("version");
2915        store.persist().expect("before rollback");
2916        snapshots.rollback(xid);
2917        store.persist().expect("after rollback");
2918        drop(store);
2919        let recovered = UnifiedStore::open(&path).expect("recover");
2920        let manager = recovered.get_collection("rows").expect("rows");
2921        assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
2922    }
2923
2924    #[test]
2925    fn publishing_a_pending_vault_version_invalidates_reuse() {
2926        let directory = tempfile::tempdir().expect("temporary store");
2927        let path = directory.path().join("data.rdb");
2928        let store = UnifiedStore::open(&path).expect("paged store");
2929        store.create_collection("rows").expect("collection");
2930        store.insert("rows", row(1, 1)).expect("row");
2931        store
2932            .pending_vault_versions
2933            .write()
2934            .insert(EntityId::new(1));
2935        store.persist().expect("exclude pending vault version");
2936        store
2937            .pending_vault_versions
2938            .write()
2939            .remove(&EntityId::new(1));
2940        store.persist().expect("include published vault version");
2941        drop(store);
2942        let recovered = UnifiedStore::open(&path).expect("recover");
2943        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
2944    }
2945}