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 CHECKPOINT_BUFFERED_BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
67}
68
69fn checkpoint_visibility_matches(
70    previous: &CheckpointVersion,
71    current: &CheckpointVersion,
72) -> bool {
73    (previous.visibility_revision == current.visibility_revision
74        || previous.excluded_xids == current.excluded_xids)
75        && previous.pending_vault == current.pending_vault
76}
77
78// ── Pager-meta overflow chain (gh-477) ──────────────────────────────────────
79// When the serialized collection registry + cross-refs exceed a single page,
80// page 1 carries a native metadata-overflow header pointing at an overflow chain of
81// `PageType::Overflow` pages. Single-page metadata keeps the historical
82// bit-identical layout (`METADATA_MAGIC = "RDM2"` written directly at the
83// content offset).
84//
85// Page 1 (overflow form), starting at `HEADER_SIZE`:
86//   [0..4]   native metadata-overflow magic
87//   [4..8]   format_version (u32, mirrors inner payload version for debug)
88//   [8..12]  total_payload_bytes (u32)
89//   [12..16] next_overflow_page_id (u32, > 0)
90//   [16..]   first payload chunk (up to META_V3_FIRST_PAYLOAD_CAP bytes)
91//
92// Overflow continuation page, starting at `HEADER_SIZE`:
93//   [0..4]   next_overflow_page_id (u32, 0 if last)
94//   [4..8]   chunk_bytes (u32)
95//   [8..]    chunk payload (up to META_V3_OVERFLOW_PAYLOAD_CAP bytes)
96const META_PAGE_CONTENT_CAP: usize =
97    crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
98const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
99const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
100const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
101const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
102
103fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
104    let cs = crate::storage::engine::HEADER_SIZE;
105    let page = match pager.read_page(1) {
106        Ok(p) => p,
107        Err(_) => return Ok(Vec::new()),
108    };
109    let bytes = page.as_bytes();
110    if bytes.len() < cs + META_V3_PAGE1_HEADER {
111        return Ok(Vec::new());
112    }
113    let Some(header) =
114        reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
115            PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
116        })?
117    else {
118        return Ok(Vec::new());
119    };
120    let mut next = header.next_overflow_page_id;
121    let mut pages = Vec::new();
122    let mut seen = std::collections::HashSet::new();
123    while next != 0 {
124        if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
125            return Err(PagerError::InvalidDatabase(
126                "cyclic metadata overflow chain".into(),
127            ));
128        }
129        let ov = match pager.read_page(next) {
130            Ok(p) => p,
131            Err(_) => break,
132        };
133        let ob = ov.as_bytes();
134        let nn = match reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..]) {
135            Ok(header) => header.next_overflow_page_id,
136            Err(_) => 0,
137        };
138        pages.push(next);
139        next = nn;
140    }
141    Ok(pages)
142}
143
144fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
145    let retired = existing_overflow_chain(pager)?;
146    let page = build_meta_page1_with_overflow(pager, meta_data)?;
147    // Cache eviction order must not publish a root before its children.
148    pager.sync()?;
149    pager.write_page(1, page)?;
150    pager.sync()?;
151    for id in retired {
152        pager.free_page(id)?;
153    }
154    Ok(())
155}
156
157fn build_meta_page1_with_overflow(
158    pager: &Pager,
159    meta_data: &[u8],
160) -> Result<crate::storage::engine::Page, PagerError> {
161    use crate::storage::engine::{Page, PageType, HEADER_SIZE};
162
163    let mut page1 = Page::new(PageType::Header, 1);
164    let cs = HEADER_SIZE;
165
166    if meta_data.len() <= META_PAGE_CONTENT_CAP {
167        // Single-page: bit-identical to the historical layout.
168        let buf = page1.as_bytes_mut();
169        buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
170        return Ok(page1);
171    }
172
173    // Multi-page overflow form. Split the inner payload into the first chunk
174    // (held on page 1) followed by zero-or-more continuation chunks chained
175    // through `PageType::Overflow` pages.
176    let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
177    let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
178    let mut chunks: Vec<&[u8]> = Vec::new();
179    while !tail.is_empty() {
180        let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
181        chunks.push(&tail[..take]);
182        tail = &tail[take..];
183    }
184
185    let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
186    let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
187    for _ in 0..chunks.len() {
188        let pg = pager.allocate_page(PageType::Overflow)?;
189        overflow_ids.push(pg.page_id());
190        overflow_pages.push(pg);
191    }
192
193    for i in 0..chunks.len() {
194        let next = if i + 1 < chunks.len() {
195            overflow_ids[i + 1]
196        } else {
197            0u32
198        };
199        let len = chunks[i].len() as u32;
200        let buf = overflow_pages[i].as_bytes_mut();
201        reddb_file::encode_native_metadata_overflow_continuation_header(
202            &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
203            reddb_file::NativeMetadataOverflowContinuationHeader {
204                next_overflow_page_id: next,
205                chunk_bytes: len,
206            },
207        )
208        .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
209        buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
210    }
211    for (idx, page) in overflow_pages.into_iter().enumerate() {
212        let id = overflow_ids[idx];
213        pager.write_page(id, page)?;
214    }
215
216    // Mirror the inner format_version for debug-friendly hex dumps.
217    let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
218        .ok()
219        .flatten()
220        .map_or(0, |header| header.format_version);
221
222    let buf = page1.as_bytes_mut();
223    reddb_file::encode_native_metadata_overflow_header(
224        &mut buf[cs..cs + META_V3_PAGE1_HEADER],
225        reddb_file::NativeMetadataOverflowHeader {
226            format_version,
227            total_payload_bytes: meta_data.len() as u32,
228            next_overflow_page_id: overflow_ids[0],
229        },
230    )
231    .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
232    buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
233        .copy_from_slice(first_chunk);
234
235    Ok(page1)
236}
237
238fn storage_integrity(
239    zone: &str,
240    id: impl Into<String>,
241    collection: Option<String>,
242    detail: impl Into<String>,
243) -> StoreError {
244    StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
245        zone, id, collection, detail,
246    ))
247}
248
249fn page_integrity(
250    page_id: u32,
251    collection: Option<String>,
252    detail: impl Into<String>,
253) -> StoreError {
254    storage_integrity("page", page_id.to_string(), collection, detail)
255}
256
257/// Assemble the full metadata payload from the internal manifest zone: page 1
258/// plus its overflow chain when the native overflow wrapper is present. Returns
259/// the bytes that the metadata parser would see starting from the content
260/// offset of page 1. Single-page metadata returns the raw page content
261/// (including trailing zero-pad), so the legacy parser sees the same bytes it
262/// always saw.
263///
264/// The zone is rooted by the superblock and checksummed per ADR 0074 §2: a
265/// failing checksum surfaces the didactic manifest-zone integrity error
266/// directly — there is no sidecar copy to fall back to since ADR 0038 §4
267/// phase 1 retired the shadows.
268fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
269    let cs = crate::storage::engine::HEADER_SIZE;
270    let meta_page = pager.read_manifest_page().map_err(|err| {
271        storage_integrity(
272            "manifest",
273            "manifest",
274            None,
275            format!("internal manifest zone read failed ({err})"),
276        )
277    })?;
278    let bytes = meta_page.as_bytes();
279    if bytes.len() < cs + 4 {
280        return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
281    }
282    let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
283        .map_err(|err| page_integrity(1, None, err.to_string()))?
284    {
285        Some(header) => header,
286        None => {
287            return Ok(Some(bytes[cs..].to_vec()));
288        }
289    };
290    if bytes.len() < cs + META_V3_PAGE1_HEADER {
291        return Err(page_integrity(
292            1,
293            None,
294            "metadata overflow header is truncated",
295        ));
296    }
297    let total = header.total_payload_bytes as usize;
298    let mut next = header.next_overflow_page_id;
299    let mut payload: Vec<u8> = Vec::with_capacity(total);
300    let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
301    payload.extend_from_slice(
302        &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
303    );
304    while next != 0 && payload.len() < total {
305        let ov = pager
306            .read_page(next)
307            .map_err(|err| page_integrity(next, None, err.to_string()))?;
308        let ob = ov.as_bytes();
309        if ob.len() < cs + META_V3_OVERFLOW_HEADER {
310            return Err(page_integrity(
311                next,
312                None,
313                "metadata overflow continuation is truncated",
314            ));
315        }
316        let continuation =
317            reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
318                .map_err(|err| page_integrity(next, None, err.to_string()))?;
319        let nn = continuation.next_overflow_page_id;
320        let len = continuation.chunk_bytes as usize;
321        let remaining = total - payload.len();
322        let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
323        payload.extend_from_slice(
324            &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
325        );
326        next = nn;
327    }
328    Ok(Some(payload))
329}
330
331impl UnifiedStore {
332    /// Compare and encode one borrowed row at a time. Begin a new tree only at
333    /// the first difference, copying its already-verified prefix in bounded
334    /// batches. Unchanged trees require no replacement allocation.
335    fn checkpoint_tree(
336        &self,
337        pager: &Arc<Pager>,
338        manager: &SegmentManager,
339        previous: Option<&Arc<BTree>>,
340        mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
341    ) -> Result<Arc<BTree>, StoreError> {
342        let mut cursor = previous
343            .map(|tree| tree.cursor_first())
344            .transpose()
345            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
346        let mut matched = 0;
347        let mut builder = None;
348        let mut failure = None;
349        manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
350            let Some(value) = encode(entity, metadata) else {
351                return true;
352            };
353            let record = (entity.id.raw().to_be_bytes().to_vec(), value);
354            let result = (|| {
355                if builder.is_none() {
356                    if let Some(cursor) = cursor.as_mut() {
357                        if cursor
358                            .next()
359                            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
360                            .as_ref()
361                            == Some(&record)
362                        {
363                            matched += 1;
364                            return Ok(());
365                        }
366                    }
367                    let mut replacement = CheckpointTreeBuilder::new(pager);
368                    if let Some(previous) = previous {
369                        replacement.copy_prefix(previous, matched)?;
370                    }
371                    builder = Some(replacement);
372                }
373                builder
374                    .as_mut()
375                    .expect("replacement initialized")
376                    .push(record)
377            })();
378            if let Err(error) = result {
379                failure = Some(error);
380                return false;
381            }
382            true
383        });
384        if let Some(error) = failure {
385            return Err(error);
386        }
387        if builder.is_none() {
388            if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
389                if cursor
390                    .next()
391                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
392                    .is_none()
393                {
394                    return Ok(Arc::clone(previous));
395                }
396            }
397            let mut replacement = CheckpointTreeBuilder::new(pager);
398            if let Some(previous) = previous {
399                replacement.copy_prefix(previous, matched)?;
400            }
401            builder = Some(replacement);
402        }
403        let mut builder = builder.expect("replacement or reused tree");
404        builder.flush()?;
405        Ok(builder.tree)
406    }
407
408    /// Publication first, reuse second. The immediately preceding generation
409    /// stays in the durable ledger for one more publication. Cursors and cloned
410    /// tree handles defer retirement further without holding the writer fence.
411    fn publish_reclaimable_trees(
412        &self,
413        pager: &Pager,
414        metadata: &mut Vec<u8>,
415        live: &HashMap<String, Arc<BTree>>,
416    ) -> Result<(), StoreError> {
417        let mut retired = self.retired_btrees.lock();
418        let mut roots = Vec::new();
419        let mut reclaim = Vec::new();
420        let mut remove = std::collections::HashSet::new();
421        for (index, entry) in retired.iter().enumerate() {
422            if live.values().any(|tree| Arc::ptr_eq(tree, &entry.tree)) {
423                remove.insert(index);
424                continue;
425            }
426            if entry.published
427                && Arc::strong_count(&entry.tree) == 1
428                && !entry.tree.has_retirement_readers()
429            {
430                reclaim.extend(
431                    entry
432                        .tree
433                        .retired_pages()
434                        .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
435                );
436                remove.insert(index);
437            } else if entry.tree.root_page_id() != 0 {
438                roots.push(entry.tree.root_page_id());
439            }
440        }
441        roots.sort_unstable();
442        roots.dedup();
443        reddb_file::encode_native_paged_retired_trees(metadata, &roots)
444            .map_err(|e| StoreError::Serialization(e.to_string()))?;
445        let (pending_pages, published_pages) = pager
446            .deferred_reclamation_pages()
447            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
448        let readers = live.values().any(|tree| tree.has_retirement_readers())
449            || retired
450                .iter()
451                .any(|entry| entry.tree.has_retirement_readers());
452        let released_pages = if readers { Vec::new() } else { published_pages };
453        let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
454        let retained_pages: Vec<_> = pending_pages
455            .into_iter()
456            .filter(|id| !released.contains(id))
457            .collect();
458        reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
459            .map_err(|e| StoreError::Serialization(e.to_string()))?;
460        // Do not mutate retirement state or the free list before both the new
461        // children and the manifest that forgets old roots have been synced.
462        publish_paged_metadata(pager, metadata)
463            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
464        let mut index = 0;
465        retired.retain_mut(|entry| {
466            let keep = !remove.contains(&index);
467            entry.published = true;
468            index += 1;
469            keep
470        });
471        if !reclaim.is_empty() {
472            pager
473                .free_retired_pages(&reclaim)
474                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
475            pager
476                .sync()
477                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
478        }
479        pager
480            .complete_deferred_publication(&retained_pages, &released_pages)
481            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
482        if !released_pages.is_empty() {
483            pager
484                .sync()
485                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
486        }
487        Ok(())
488    }
489
490    pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
491        let mut retired = self.retired_btrees.lock();
492        if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
493        {
494            retired.push(RetiredBTree {
495                tree,
496                published: false,
497            });
498        }
499    }
500
501    /// A completed inline insert already made this tree match its manager.
502    /// Advance the reuse proof only for that single, fenced mutation. Any
503    /// untracked edit or segment publication keeps the full comparison path.
504    pub(crate) fn note_paged_insert(
505        &self,
506        collection: &str,
507        manager: &Arc<SegmentManager>,
508        before: &[(u64, u64)],
509        id: EntityId,
510    ) {
511        let after = manager.checkpoint_revisions();
512        if before.len() != after.len() {
513            return;
514        }
515        let mut changed = 0;
516        for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
517            if old_id != new_id {
518                return;
519            }
520            if old_revision != new_revision {
521                if old_revision.checked_add(1) != Some(*new_revision) {
522                    return;
523                }
524                changed += 1;
525            }
526        }
527        if changed != 1 {
528            return;
529        }
530        let mut versions = self.checkpoint_versions.write();
531        let Some(previous) = versions.get_mut(collection) else {
532            return;
533        };
534        if previous.manager.ptr_eq(&Arc::downgrade(manager))
535            && previous.segments == before
536            && previous.format_version == self.format_version()
537            && !previous.pending_vault.contains(&id)
538            && !previous.excluded_xids.contains(&0)
539        {
540            previous.segments = after;
541        }
542    }
543
544    /// Successful physical deletes already changed both the manager and tree.
545    /// Reuse remains valid only when their exact revision delta accounts for
546    /// this batch, with no intervening edits or segment replacement.
547    pub(crate) fn note_paged_delete(
548        &self,
549        collection: &str,
550        manager: &Arc<SegmentManager>,
551        before: &[(u64, u64)],
552        deleted: &[EntityId],
553    ) {
554        let after = manager.checkpoint_revisions();
555        if before.len() != after.len() {
556            return;
557        }
558        let mut changes = 0_u64;
559        for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
560            if old_id != new_id {
561                return;
562            }
563            let Some(delta) = new_revision.checked_sub(*old_revision) else {
564                return;
565            };
566            let Some(total) = changes.checked_add(delta) else {
567                return;
568            };
569            changes = total;
570        }
571        let Ok(expected) = u64::try_from(deleted.len()) else {
572            return;
573        };
574        if changes != expected {
575            return;
576        }
577        let mut versions = self.checkpoint_versions.write();
578        let Some(previous) = versions.get_mut(collection) else {
579            return;
580        };
581        if !previous.manager.ptr_eq(&Arc::downgrade(manager))
582            || previous.format_version != self.format_version()
583            || deleted.iter().any(|id| previous.pending_vault.contains(id))
584            || previous.excluded_xids.contains(&0)
585        {
586            return;
587        }
588        if previous.segments != before {
589            let Some(deferred) = previous.deferred_rows.as_ref() else {
590                return;
591            };
592            let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
593                return;
594            };
595            let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
596            let pending: std::collections::BTreeSet<_> = deferred
597                .iter()
598                .copied()
599                .chain(changed)
600                .filter(|id| !removed.contains(id))
601                .collect();
602            if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
603                return;
604            }
605            // GC already applied its deletes to the tree. Carry earlier dirty
606            // IDs forward instead of spending their journal on these deletes.
607            // Sparse projection still requires exact WAL proof for every
608            // differing row, so an unlogged manager edit cannot be concealed.
609            previous.has_unprojected_rows = !pending.is_empty();
610            previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
611        }
612        previous.segments = after;
613    }
614
615    pub(crate) fn mark_paged_registry_dirty(&self) {
616        self.resident.mark_paged_registry_dirty()
617    }
618
619    /// Get (or lazily create) the per-collection B-tree under a *read*
620    /// lock whenever possible. Returns a cloned `Arc<BTree>` so callers
621    /// can mutate the tree without holding the outer map's RwLock —
622    /// previously every insert serialised on `btree_indices.write()`,
623    /// costing ~60% of the concurrent-insert throughput ceiling.
624    pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
625        let pager = self.pager.as_ref()?;
626        if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
627            return Some(btree);
628        }
629        let mut write = self.btree_indices.write();
630        let btree = write
631            .entry(collection.to_string())
632            .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
633            .clone();
634        Some(btree)
635    }
636
637    pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
638        let Some(pager) = &self.pager else {
639            return Ok(());
640        };
641
642        if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
643            self.flush_paged_registry()?;
644            self.resident
645                .paged_registry_dirty
646                .store(false, Ordering::Release);
647            return Ok(());
648        }
649
650        pager
651            .flush()
652            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
653    }
654
655    pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
656        let _vault_guard = self.vault_publication_lock.lock();
657        let Some(pager) = &self.pager else {
658            return Ok(());
659        };
660
661        match pager.read_page(1) {
662            Ok(_) => {}
663            Err(PagerError::PageNotFound(_)) => {
664                let meta_page = pager
665                    .allocate_page(crate::storage::engine::PageType::Header)
666                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
667                pager
668                    .write_page(meta_page.page_id(), meta_page)
669                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
670            }
671            Err(e) => {
672                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
673            }
674        }
675
676        let format_version = STORE_VERSION_V11;
677        self.set_format_version(format_version);
678
679        let collections = self.resident.collections.read();
680        let btree_indices = self.btree_indices.read();
681        let mut collection_roots = Vec::with_capacity(collections.len());
682        for (name, _) in collections.iter() {
683            let root_page = btree_indices
684                .get(name)
685                .map_or(0, |btree| btree.root_page_id());
686            collection_roots.push((name.clone(), root_page));
687        }
688        drop(btree_indices);
689        drop(collections);
690
691        let mut meta_data = Vec::with_capacity(4096);
692        reddb_file::encode_native_paged_metadata_header(
693            &mut meta_data,
694            reddb_file::NativePagedMetadataHeader {
695                format_version,
696                collection_count: collection_roots.len() as u32,
697            },
698        );
699        for (name, root_page) in &collection_roots {
700            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
701        }
702
703        let cross_refs = self.resident.cross_refs.read();
704        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
705        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
706        for (source_id, refs) in cross_refs.iter() {
707            for (target_id, ref_type, collection) in refs {
708                reddb_file::encode_native_paged_cross_ref(
709                    &mut meta_data,
710                    source_id.raw(),
711                    target_id.raw(),
712                    ref_type.to_byte(),
713                    collection,
714                );
715            }
716        }
717
718        self.publish_reclaimable_trees(pager, &mut meta_data, &self.btree_indices.read())?;
719
720        Ok(())
721    }
722
723    /// Get a reference to the underlying pager (if in paged mode).
724    pub fn pager(&self) -> Option<&Arc<Pager>> {
725        self.pager.as_ref()
726    }
727
728    /// Bytes of page-cache memory currently occupied by resident pages
729    /// (ADR 0073 §2). Slots are fixed size, so occupancy times the page size
730    /// *is* the footprint; a store without a pager occupies none.
731    pub fn page_cache_bytes_in_use(&self) -> u64 {
732        self.pager.as_ref().map_or(0, |pager| {
733            pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
734        })
735    }
736
737    /// Bytes of WAL buffer memory held by this store's group-commit
738    /// coordinator: queued records plus the writer's fixed append buffer.
739    pub fn wal_buffer_bytes_in_use(&self) -> u64 {
740        self.commit
741            .as_ref()
742            .map_or(0, |commit| commit.buffered_bytes())
743    }
744
745    /// Approximate resident bytes across every collection's segment arena
746    /// (ADR 0073 §2). Same number `stats().total_memory_bytes` reports, without
747    /// building the per-collection stats map a sampler would throw away.
748    pub fn segment_memory_bytes(&self) -> u64 {
749        let managers: Vec<Arc<SegmentManager>> = self
750            .resident
751            .collections
752            .read()
753            .values()
754            .map(Arc::clone)
755            .collect();
756
757        managers
758            .iter()
759            .map(|manager| manager.resident_bytes())
760            .fold(0, u64::saturating_add)
761    }
762
763    /// Borrow the immutable store configuration. Runtime hooks (e.g. the
764    /// `auto_index_id` first-insert hook in `MutationEngine`) read knobs
765    /// off this struct without going through the legacy global config tree.
766    pub fn config(&self) -> &UnifiedStoreConfig {
767        &self.config
768    }
769
770    pub fn with_config(config: UnifiedStoreConfig) -> Self {
771        Self {
772            config,
773            resident: reddb_engine::storage::resident_store::ResidentStore::new(),
774            format_version: AtomicU32::new(STORE_VERSION_V11),
775            pager: None,
776            db_path: None,
777            btree_indices: RwLock::new(HashMap::new()),
778            checkpoint_versions: RwLock::new(HashMap::new()),
779            checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
780            retired_btrees: parking_lot::Mutex::new(Vec::new()),
781            detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
782            commit: None,
783            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
784            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
785            snapshot_manager: std::sync::OnceLock::new(),
786            embedded_commit_session: std::sync::OnceLock::new(),
787            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
788            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
789        }
790    }
791
792    /// Open or create a page-based database
793    ///
794    /// This uses the page engine for ACID durability with B-tree indices.
795    /// The database file uses 16 KiB pages with checksums and efficient caching.
796    ///
797    /// # Arguments
798    ///
799    /// * `path` - Path to the database file (e.g., "data.rdb")
800    ///
801    /// # Example
802    ///
803    /// ```rust,ignore
804    /// let store = UnifiedStore::open("security.rdb")?;
805    /// store.create_collection("hosts")?;
806    /// // ... operations ...
807    /// store.persist()?; // Flush to disk
808    /// ```
809    pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
810        Self::open_with_config(path, UnifiedStoreConfig::default())
811    }
812
813    pub fn open_with_config(
814        path: impl AsRef<Path>,
815        config: UnifiedStoreConfig,
816    ) -> Result<Self, StoreError> {
817        let path = path.as_ref();
818        let mut pager_config = PagerConfig::default();
819        // ADR 0073 §2 — the page cache is pre-sized from its budget share.
820        // The pager's own `cache_size` default survives only for direct
821        // library callers who never resolved a budget.
822        if let Some(slots) = config.page_cache_slots {
823            pager_config.cache_size = slots;
824        }
825        // Tunables via env — experimental, used by the benchmark harness
826        // to compare durability profiles head-to-head with Postgres.
827        // REDDB_DOUBLE_WRITE=0 requests skipping the double-write buffer,
828        // which otherwise adds two fsyncs per pager flush (one on DWB, one
829        // on the main file). The pager honors this only when the actual
830        // data file is proven to live on a CoW filesystem with atomic page
831        // writes; otherwise it fails closed and keeps DWB enabled.
832        if matches!(
833            std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
834            Some("0") | Some("false") | Some("off")
835        ) {
836            pager_config.double_write = false;
837        }
838        let pager = Pager::open(path, pager_config)
839            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
840        pager
841            .enable_deferred_reclamation()
842            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
843
844        let wal_path = reddb_file::layout::unified_wal_path(path);
845        // Explicit transactions and WAL-only vault versions need committed
846        // batches in every durability mode, including Strict's inline fsync.
847        let commit = Some(Arc::new(
848            StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
849                .map_err(StoreError::Io)?,
850        ));
851
852        let store = Self {
853            config,
854            resident: reddb_engine::storage::resident_store::ResidentStore::new(),
855            format_version: AtomicU32::new(STORE_VERSION_V11),
856            pager: Some(Arc::new(pager)),
857            db_path: Some(path.to_path_buf()),
858            btree_indices: RwLock::new(HashMap::new()),
859            checkpoint_versions: RwLock::new(HashMap::new()),
860            checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
861            retired_btrees: parking_lot::Mutex::new(Vec::new()),
862            detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
863            commit,
864            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
865            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
866            snapshot_manager: std::sync::OnceLock::new(),
867            embedded_commit_session: std::sync::OnceLock::new(),
868            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
869            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
870        };
871
872        // Load existing data from pages if database exists
873        store.load_from_pages()?;
874        if let Some(commit) = &store.commit {
875            commit.replay_into(&store).map_err(StoreError::Io)?;
876        }
877        store.recover_operational_manifest()?;
878
879        Ok(store)
880    }
881
882    /// Replay changes the authoritative managers first. Build replacement trees
883    /// only for changed collections, leaving every published tree untouched until
884    /// all replacement pages are durable. Retain the WAL for auxiliary recovery.
885    pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
886        let Some(pager) = &self.pager else {
887            return Ok(());
888        };
889        let collections = self.resident.collections.read();
890        let mut indices = self.btree_indices.write();
891        for (name, manager) in collections.iter() {
892            let tree =
893                self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
894                    Some(Self::serialize_entity_record(
895                        entity,
896                        metadata.as_ref(),
897                        self.format_version(),
898                    ))
899                })?;
900            if indices
901                .get(name)
902                .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
903            {
904                continue;
905            }
906            if let Some(previous) = indices.insert(name.clone(), tree) {
907                self.retire_btree(previous);
908            }
909            self.mark_paged_registry_dirty();
910        }
911        drop(indices);
912        drop(collections);
913        self.flush_paged_state()?;
914        // Opening has no concurrent writers. Replay has now durably reconciled
915        // these raw trees; do not immediately rebuild them at the first flush.
916        if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
917            let collections = self.resident.collections.read();
918            let mut versions = self.checkpoint_versions.write();
919            for (name, manager) in collections.iter() {
920                versions.insert(
921                    name.clone(),
922                    CheckpointVersion {
923                        manager: Arc::downgrade(manager),
924                        segments: manager.checkpoint_revisions(),
925                        visibility_revision: 0,
926                        untracked_visibility_revision: 0,
927                        excluded_xids: Arc::default(),
928                        pending_vault: Arc::default(),
929                        format_version: self.format_version(),
930                        deferred_rows: Some(Arc::default()),
931                        has_unprojected_rows: false,
932                    },
933                );
934            }
935        }
936        Ok(())
937    }
938
939    pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
940        let Some(path) = &self.db_path else {
941            return Ok(());
942        };
943        let mut collections = self.list_collections();
944        collections.sort();
945        let pending_drops =
946            crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
947                .recover_or_bootstrap(&collections)
948                .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
949        for name in pending_drops {
950            if self.get_collection(&name).is_some() {
951                self.drop_collection(&name)?;
952            }
953        }
954        Ok(())
955    }
956
957    pub(crate) fn prepare_operational_collection_create(
958        &self,
959        name: &str,
960    ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
961        let Some(path) = &self.db_path else {
962            return Ok(None);
963        };
964        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
965            .prepare_collection_create(name)
966            .map(Some)
967            .map_err(StoreError::Io)
968    }
969
970    pub(crate) fn prepare_operational_collection_drop(
971        &self,
972        name: &str,
973    ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
974        let Some(path) = &self.db_path else {
975            return Ok(None);
976        };
977        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
978            .prepare_collection_drop(name)
979            .map(Some)
980            .map_err(StoreError::Io)
981    }
982
983    /// Load data from page-based storage
984    ///
985    /// Reads the B-tree indices and reconstructs collections from pages.
986    fn load_from_pages(&self) -> Result<(), StoreError> {
987        let pager = match &self.pager {
988            Some(p) => p,
989            None => return Ok(()), // No pager, nothing to load
990        };
991
992        // Get page count
993        let page_count = pager.page_count().map_err(|e| {
994            StoreError::Io(std::io::Error::other(format!(
995                "failed to read page count: {}",
996                e
997            )))
998        })?;
999        if page_count <= 1 {
1000            // Empty database (only header page)
1001            return Ok(());
1002        }
1003
1004        // Read metadata starting from page 1 (collections registry). The
1005        // helper transparently follows the `RDM3` overflow chain when the
1006        // metadata blob spans multiple pages and falls back to the legacy
1007        // `<data>-meta` shadow when page 1 itself is corrupted.
1008        if let Some(content_vec) = read_meta_payload(pager)? {
1009            let content: &[u8] = &content_vec;
1010            if content.len() >= 4 {
1011                let mut pos = 0;
1012                let mut format_version = STORE_VERSION_V1;
1013
1014                let collection_count = if let Some(header) =
1015                    reddb_file::decode_native_paged_metadata_header(content)
1016                        .map_err(|err| StoreError::Serialization(err.to_string()))?
1017                {
1018                    format_version = header.format_version;
1019                    pos += reddb_file::METADATA_HEADER_BYTES;
1020                    header.collection_count as usize
1021                } else {
1022                    let count = u32::from_le_bytes([
1023                        content[pos],
1024                        content[pos + 1],
1025                        content[pos + 2],
1026                        content[pos + 3],
1027                    ]) as usize;
1028                    pos += 4;
1029                    count
1030                };
1031
1032                self.set_format_version(format_version);
1033
1034                if pos > content.len() {
1035                    return Ok(());
1036                }
1037
1038                // Read collection names and their B-tree root pages
1039                for _ in 0..collection_count {
1040                    let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1041                        .map_err(|err| page_integrity(1, None, err.to_string()))?;
1042                    // Root page ID for this collection's B-tree
1043                    let root_page = root.root_page;
1044                    let name = root.collection;
1045
1046                    // Hydrate the collection in memory only. Loading must
1047                    // not emit WAL entries or rewrite the on-disk registry
1048                    // before the existing B-tree roots are attached.
1049                    let _ = self.create_collection_in_memory(&name);
1050
1051                    // Load B-tree with root page if it exists
1052                    if root_page > 0 {
1053                        let btree = BTree::with_root(Arc::clone(pager), root_page);
1054
1055                        // Load all entities from B-tree into the collection
1056                        let mut cursor = btree.cursor_first().map_err(|err| {
1057                            page_integrity(root_page, Some(name.clone()), err.to_string())
1058                        })?;
1059                        let manager = self.get_collection(&name);
1060                        loop {
1061                            let next = cursor.next().map_err(|err| {
1062                                page_integrity(root_page, Some(name.clone()), err.to_string())
1063                            })?;
1064                            let Some((key, value)) = next else {
1065                                break;
1066                            };
1067                            // Deserialize entity from value bytes
1068                            let (entity, metadata) =
1069                                Self::deserialize_entity_record(&value, self.format_version())
1070                                    .map_err(|err| {
1071                                        page_integrity(
1072                                            root_page,
1073                                            Some(name.clone()),
1074                                            err.to_string(),
1075                                        )
1076                                    })?;
1077                            if let Some(m) = &manager {
1078                                let id = entity.id;
1079                                if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1080                                    m.register_row_id(*row_id);
1081                                }
1082                                self.resident.context_index.index_entity(&name, &entity);
1083                                m.restore_snapshot_entity(entity.clone())?;
1084                                if let Some(metadata) = metadata {
1085                                    let _ = m.set_metadata(id, metadata);
1086                                }
1087                                self.register_entity_id(id);
1088                                if self.config.auto_index_refs {
1089                                    self.index_cross_refs(&entity, &name)?;
1090                                }
1091                            }
1092                        }
1093
1094                        // Store the B-tree for future lookups
1095                        self.btree_indices
1096                            .write()
1097                            .insert(name.clone(), Arc::new(btree));
1098                        if format_version == STORE_VERSION_V11 {
1099                            if let Some(manager) = &manager {
1100                                self.checkpoint_versions.write().insert(
1101                                    name,
1102                                    CheckpointVersion {
1103                                        manager: Arc::downgrade(manager),
1104                                        segments: manager.checkpoint_revisions(),
1105                                        visibility_revision: 0,
1106                                        untracked_visibility_revision: 0,
1107                                        excluded_xids: Arc::default(),
1108                                        pending_vault: Arc::default(),
1109                                        format_version,
1110                                        deferred_rows: Some(Arc::default()),
1111                                        has_unprojected_rows: false,
1112                                    },
1113                                );
1114                            }
1115                        }
1116                    }
1117                }
1118
1119                if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1120                    let cross_ref_count = u32::from_le_bytes([
1121                        content[pos],
1122                        content[pos + 1],
1123                        content[pos + 2],
1124                        content[pos + 3],
1125                    ]) as usize;
1126                    pos += 4;
1127
1128                    for _ in 0..cross_ref_count {
1129                        let Ok(cross_ref) =
1130                            reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1131                        else {
1132                            break;
1133                        };
1134                        let source_id = EntityId::new(cross_ref.source_id);
1135                        let target_id = EntityId::new(cross_ref.target_id);
1136                        let ref_type = RefType::from_byte(cross_ref.ref_type);
1137                        let target_collection = cross_ref.target_collection;
1138
1139                        self.resident
1140                            .cross_refs
1141                            .write()
1142                            .entry(source_id)
1143                            .or_default()
1144                            .push((target_id, ref_type, target_collection.clone()));
1145
1146                        if let Some((collection, mut entity)) = self.get_any(source_id) {
1147                            let exists = entity.cross_refs().iter().any(|xref| {
1148                                xref.target == target_id
1149                                    && xref.ref_type == ref_type
1150                                    && xref.target_collection == target_collection
1151                            });
1152                            if !exists {
1153                                entity.cross_refs_mut().push(CrossRef::new(
1154                                    source_id,
1155                                    target_id,
1156                                    target_collection.clone(),
1157                                    ref_type,
1158                                ));
1159                                if let Some(manager) = self.get_collection(&collection) {
1160                                    let _ = manager.update(entity);
1161                                }
1162                            }
1163                        }
1164                    }
1165                }
1166                let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1167                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1168                let live_roots: std::collections::HashSet<_> = self
1169                    .btree_indices
1170                    .read()
1171                    .values()
1172                    .map(|tree| tree.root_page_id())
1173                    .collect();
1174                for root in retired {
1175                    if live_roots.contains(&root)
1176                        || root
1177                            >= pager
1178                                .page_count()
1179                                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1180                    {
1181                        return Err(page_integrity(
1182                            root,
1183                            None,
1184                            "retired root aliases a live or missing page",
1185                        ));
1186                    }
1187                    self.retired_btrees.lock().push(RetiredBTree {
1188                        tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1189                        published: true,
1190                    });
1191                }
1192                let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1193                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1194                if deferred.iter().any(|id| live_roots.contains(id)) {
1195                    return Err(page_integrity(1, None, "deferred page aliases a live root"));
1196                }
1197                pager
1198                    .restore_deferred_reclamation(&deferred)
1199                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1200            }
1201        }
1202
1203        if self.format_version() < STORE_VERSION_V11 {
1204            self.set_format_version(STORE_VERSION_V11);
1205        }
1206
1207        Ok(())
1208    }
1209
1210    /// Deserialize an entity from binary bytes
1211    pub(crate) fn deserialize_entity(
1212        data: &[u8],
1213        format_version: u32,
1214    ) -> Result<UnifiedEntity, StoreError> {
1215        let mut pos = 0;
1216        Self::read_entity_binary(data, &mut pos, format_version)
1217            .map_err(|e| StoreError::Serialization(e.to_string()))
1218    }
1219
1220    /// Serialize an entity to binary bytes
1221    pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1222        // Pre-allocate ~256 bytes to cover the typical 15-column
1223        // typed row without any Vec growth. Bulk insert calls this
1224        // millions of times per bench run; saving 2-3 reallocs per
1225        // entity amortises.
1226        let mut buf = Vec::with_capacity(256);
1227        Self::write_entity_binary(&mut buf, entity, format_version);
1228        buf
1229    }
1230
1231    pub(crate) fn serialize_entity_record(
1232        entity: &UnifiedEntity,
1233        metadata: Option<&Metadata>,
1234        format_version: u32,
1235    ) -> Vec<u8> {
1236        reddb_engine::storage::metadata_codec::serialize_entity_record(
1237            entity,
1238            metadata,
1239            format_version,
1240        )
1241    }
1242
1243    pub(crate) fn deserialize_entity_record(
1244        data: &[u8],
1245        format_version: u32,
1246    ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1247        reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
1248            .map_err(StoreError::Serialization)
1249    }
1250
1251    /// Persist all data to page-based storage
1252    ///
1253    /// Writes all entities to B-tree pages and flushes to disk.
1254    /// This provides ACID durability guarantees.
1255    pub fn persist(&self) -> Result<(), StoreError> {
1256        let _vault_guard = self.vault_publication_lock.lock();
1257        let pending_vault_versions = self.pending_vault_versions.read();
1258        let pager = match &self.pager {
1259            Some(p) => p,
1260            None => {
1261                // No pager attached - use binary file fallback if path available
1262                if let Some(path) = &self.db_path {
1263                    return self
1264                        .save_to_file(path)
1265                        .map_err(|e| StoreError::Serialization(e.to_string()));
1266                }
1267                return Err(StoreError::Io(std::io::Error::other(
1268                    "No pager or path configured for persistence",
1269                )));
1270            }
1271        };
1272
1273        match pager.read_page(1) {
1274            Ok(_) => {}
1275            Err(PagerError::PageNotFound(_)) => {
1276                let meta_page = pager
1277                    .allocate_page(crate::storage::engine::PageType::Header)
1278                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1279                pager
1280                    .write_page(meta_page.page_id(), meta_page)
1281                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1282            }
1283            Err(e) => {
1284                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1285            }
1286        }
1287
1288        if let Some(commit) = &self.commit {
1289            commit.force_sync().map_err(StoreError::Io)?;
1290        }
1291
1292        let collections = self.resident.collections.read();
1293        let mut btree_indices = self.btree_indices.write();
1294
1295        // Collect collection names and their B-tree root pages
1296        let mut collection_roots: Vec<(String, u32)> = Vec::new();
1297
1298        // Build unpublished replacements. Deleting from the current trees here
1299        // would destroy the last checkpoint if a write fails before publication.
1300        let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1301        let previous_versions = self.checkpoint_versions.read();
1302        let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1303        let pending_vault = Arc::new(pending_vault_versions.clone());
1304        let (visibility_revision, excluded, untracked_visibility_revision) =
1305            self.snapshot_manager.get().map_or_else(
1306                || (0, Default::default(), 0),
1307                |manager| manager.checkpoint_projection_state(),
1308            );
1309        let excluded = Arc::new(excluded);
1310        for (name, manager) in collections.iter() {
1311            let mut version = CheckpointVersion {
1312                manager: Arc::downgrade(manager),
1313                segments: manager.checkpoint_revisions(),
1314                visibility_revision,
1315                untracked_visibility_revision,
1316                excluded_xids: Arc::clone(&excluded),
1317                pending_vault: Arc::clone(&pending_vault),
1318                format_version: self.format_version(),
1319                deferred_rows: Some(Arc::default()),
1320                has_unprojected_rows: false,
1321            };
1322            if let (Some(previous), Some(tree)) =
1323                (previous_versions.get(name), btree_indices.get(name))
1324            {
1325                if previous.manager.ptr_eq(&version.manager)
1326                    && previous.segments == version.segments
1327                    && previous.format_version == version.format_version
1328                    && !previous.has_unprojected_rows
1329                    && checkpoint_visibility_matches(previous, &version)
1330                {
1331                    version.deferred_rows = previous.deferred_rows.clone();
1332                    collection_roots.push((name.clone(), tree.root_page_id()));
1333                    rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1334                    checkpoint_versions.insert(name.clone(), version);
1335                    continue;
1336                }
1337            }
1338            if let (Some(previous), Some(tree)) =
1339                (previous_versions.get(name), btree_indices.get(name))
1340            {
1341                if previous.manager.ptr_eq(&version.manager)
1342                    && previous.format_version == version.format_version
1343                {
1344                    if let Some(tree) =
1345                        self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
1346                    {
1347                        collection_roots.push((name.clone(), tree.root_page_id()));
1348                        rebuilt_indices.insert(name.clone(), tree);
1349                        checkpoint_versions.insert(name.clone(), version);
1350                        continue;
1351                    }
1352                }
1353            }
1354            let btree = self.checkpoint_tree(
1355                pager,
1356                manager,
1357                btree_indices.get(name),
1358                |entity, metadata| {
1359                    #[cfg(test)]
1360                    CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1361                    let hidden = pending_vault_versions.contains(&entity.id)
1362                        || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1363                            && excluded.contains(&entity.xmin));
1364                    let normalize_xmax = matches!(entity.data, EntityData::Row(_))
1365                        && excluded.contains(&entity.xmax);
1366                    if hidden || normalize_xmax {
1367                        version.remember_deferred_row(entity.id);
1368                    }
1369                    if hidden {
1370                        return None;
1371                    }
1372                    let mut normalized = std::borrow::Cow::Borrowed(entity);
1373                    if normalize_xmax {
1374                        normalized.to_mut().xmax = 0;
1375                    }
1376                    Some(Self::serialize_entity_record(
1377                        &normalized,
1378                        metadata.as_ref(),
1379                        self.format_version(),
1380                    ))
1381                },
1382            )?;
1383            checkpoint_versions.insert(name.clone(), version);
1384
1385            collection_roots.push((name.clone(), btree.root_page_id()));
1386            rebuilt_indices.insert(name.clone(), btree);
1387        }
1388
1389        // Write collection metadata to page 1
1390        let mut meta_data = Vec::with_capacity(4096);
1391
1392        let format_version = STORE_VERSION_V11;
1393        self.set_format_version(format_version);
1394
1395        reddb_file::encode_native_paged_metadata_header(
1396            &mut meta_data,
1397            reddb_file::NativePagedMetadataHeader {
1398                format_version,
1399                collection_count: collection_roots.len() as u32,
1400            },
1401        );
1402
1403        // Write each collection's name and B-tree root page
1404        for (name, root_page) in &collection_roots {
1405            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1406        }
1407
1408        // Write cross-reference metadata
1409        let cross_refs = self.resident.cross_refs.read();
1410        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1411        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1412        for (source_id, refs) in cross_refs.iter() {
1413            for (target_id, ref_type, collection) in refs {
1414                reddb_file::encode_native_paged_cross_ref(
1415                    &mut meta_data,
1416                    source_id.raw(),
1417                    target_id.raw(),
1418                    ref_type.to_byte(),
1419                    collection,
1420                );
1421            }
1422        }
1423
1424        for (name, previous) in btree_indices.iter() {
1425            if !rebuilt_indices
1426                .get(name)
1427                .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1428            {
1429                self.retire_btree(Arc::clone(previous));
1430            }
1431        }
1432        self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices)?;
1433
1434        // Only the now-durable catalog may replace the live tree handles. The
1435        // previous generation is retained in the manifest retirement ledger.
1436        *btree_indices = rebuilt_indices;
1437        drop(previous_versions);
1438        *self.checkpoint_versions.write() = checkpoint_versions;
1439
1440        if let Some(commit) = &self.commit {
1441            commit.truncate().map_err(StoreError::Io)?;
1442        }
1443        *self.checkpoint_durable_rows.write() = Default::default();
1444
1445        Ok(())
1446    }
1447
1448    /// Check if the store is using page-based persistence
1449    pub fn is_paged(&self) -> bool {
1450        self.pager.is_some()
1451    }
1452
1453    /// Current root page for a collection's primary B-tree, if one has
1454    /// been materialized in this store.
1455    pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1456        self.btree_indices
1457            .read()
1458            .get(collection)
1459            .map(|btree| btree.root_page_id())
1460            .filter(|root| *root != 0)
1461    }
1462
1463    /// Get the database file path (if using paged mode)
1464    pub fn db_path(&self) -> Option<&Path> {
1465        self.db_path.as_deref()
1466    }
1467}
1468
1469#[cfg(test)]
1470mod checkpoint_work_tests {
1471    use super::*;
1472
1473    #[test]
1474    fn retired_tree_pages_remain_pinned_by_a_cursor() {
1475        let directory = tempfile::tempdir().expect("temporary store");
1476        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1477        for id in 1..=100 {
1478            store.insert_auto("rows", row(id, 1)).expect("seed");
1479        }
1480        store.persist().expect("initial checkpoint");
1481        let tree = store
1482            .btree_indices
1483            .read()
1484            .get("rows")
1485            .cloned()
1486            .expect("tree");
1487        let mut cursor = tree.cursor_first().expect("cursor");
1488        drop(tree);
1489        let manager = store.get_collection("rows").expect("rows");
1490        for value in 2..=6 {
1491            manager.update(row(1, value)).expect("change");
1492            store.persist().expect("checkpoint with old reader");
1493        }
1494        let mut count = 0;
1495        while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
1496            let (entity, _) =
1497                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1498                    .expect("decode");
1499            assert_eq!(
1500                entity.data.as_row().expect("row").columns,
1501                [Value::Integer(1)]
1502            );
1503            count += 1;
1504        }
1505        assert_eq!(count, 100);
1506        drop(cursor);
1507        store.persist().expect("reclaim after reader release");
1508        assert!(store.retired_btrees.lock().is_empty());
1509    }
1510
1511    #[test]
1512    fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
1513        let directory = tempfile::tempdir().expect("temporary store");
1514        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1515        store.create_collection("rows").expect("collection");
1516        let manager = store.get_collection("rows").expect("rows");
1517        for id in (1..=1200).rev() {
1518            manager
1519                .insert(UnifiedEntity::table_row(
1520                    EntityId::new(id),
1521                    "rows",
1522                    id,
1523                    vec![Value::text("x".repeat(4096))],
1524                ))
1525                .expect("seed");
1526        }
1527        CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
1528        store.persist().expect("bounded checkpoint");
1529        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
1530        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
1531        assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
1532    }
1533
1534    #[test]
1535    fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1536        let directory = tempfile::tempdir().expect("temporary store");
1537        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1538        store.create_collection("rows").expect("collection");
1539        for id in 1..=40 {
1540            store
1541                .insert(
1542                    "rows",
1543                    UnifiedEntity::table_row(
1544                        EntityId::new(id),
1545                        "rows",
1546                        id,
1547                        vec![Value::Integer(id as i64)],
1548                    ),
1549                )
1550                .expect("seed row");
1551        }
1552        store.persist().expect("initial checkpoint");
1553        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1554        store.persist().expect("unchanged checkpoint");
1555        assert_eq!(
1556            CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1557            0,
1558            "unchanged collections must not hold the writer fence while serializing every row"
1559        );
1560        drop(store);
1561        let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1562        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1563        reopened.persist().expect("unchanged recovered generation");
1564        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1565    }
1566
1567    #[test]
1568    fn paged_reopen_preserves_stored_sequence_identity() {
1569        let directory = tempfile::tempdir().expect("temporary store");
1570        let path = directory.path().join("data.rdb");
1571        let store = UnifiedStore::open(&path).expect("paged store");
1572        store.create_collection("rows").expect("collection");
1573        for id in [30, 10, 20] {
1574            store.insert("rows", row(id, id as i64)).expect("insert");
1575        }
1576        store.persist().expect("checkpoint");
1577        let sequences = [10, 20, 30].map(|id| {
1578            store
1579                .get("rows", EntityId::new(id))
1580                .expect("row")
1581                .sequence_id
1582        });
1583        drop(store);
1584        let reopened = UnifiedStore::open(&path).expect("reopen");
1585        let recovered = [10, 20, 30].map(|id| {
1586            reopened
1587                .get("rows", EntityId::new(id))
1588                .expect("row")
1589                .sequence_id
1590        });
1591        assert_eq!(
1592            recovered, sequences,
1593            "opening a checkpoint must not rewrite stored row identities"
1594        );
1595    }
1596
1597    #[test]
1598    fn auto_insert_persists_the_assigned_sequence_identity() {
1599        let directory = tempfile::tempdir().expect("temporary store");
1600        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1601        for id in 1u64..=3 {
1602            store
1603                .insert_auto("rows", row(id, id as i64))
1604                .expect("insert");
1605        }
1606        let manager = store.get_collection("rows").expect("rows");
1607        let tree = store
1608            .btree_indices
1609            .read()
1610            .get("rows")
1611            .cloned()
1612            .expect("tree");
1613        for id in 1u64..=3 {
1614            let bytes = tree
1615                .get(&id.to_be_bytes())
1616                .expect("lookup")
1617                .expect("stored row");
1618            let (persisted, _) =
1619                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1620                    .expect("decode");
1621            assert_eq!(
1622                persisted.sequence_id,
1623                manager
1624                    .get(EntityId::new(id))
1625                    .expect("live row")
1626                    .sequence_id
1627            );
1628        }
1629    }
1630
1631    #[test]
1632    fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
1633        let directory = tempfile::tempdir().expect("temporary store");
1634        let path = directory.path().join("data.rdb");
1635        let store = UnifiedStore::open(&path).expect("paged store");
1636        store.insert_auto("rows", row(1, 1)).expect("seed");
1637        store.persist().expect("checkpoint");
1638        store.insert_auto("rows", row(2, 2)).expect("append");
1639        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1640        store.persist().expect("inline append checkpoint");
1641        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1642        store
1643            .get_collection("rows")
1644            .expect("rows")
1645            .update(row(1, 99))
1646            .expect("untracked edit");
1647        store.insert_auto("rows", row(3, 3)).expect("next append");
1648        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1649        store.persist().expect("untracked edit checkpoint");
1650        assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
1651        drop(store);
1652        let reopened = UnifiedStore::open(&path).expect("reopen");
1653        assert_eq!(
1654            reopened
1655                .get("rows", EntityId::new(1))
1656                .expect("row")
1657                .data
1658                .as_row()
1659                .expect("table")
1660                .columns,
1661            vec![Value::Integer(99)]
1662        );
1663    }
1664
1665    #[test]
1666    fn replayed_trees_are_reusable_after_durable_reconciliation() {
1667        let directory = tempfile::tempdir().expect("temporary store");
1668        let path = directory.path().join("data.rdb");
1669        let store = UnifiedStore::open(&path).expect("paged store");
1670        store.insert_auto("rows", row(1, 1)).expect("seed");
1671        store.persist().expect("checkpoint");
1672        store
1673            .insert_auto("rows", row(2, 2))
1674            .expect("WAL-backed append");
1675        drop(store);
1676        let reopened = UnifiedStore::open(&path).expect("replay");
1677        assert!(reopened.get("rows", EntityId::new(2)).is_some());
1678        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1679        reopened.persist().expect("checkpoint reconciled tree");
1680        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1681    }
1682
1683    fn row(id: u64, value: i64) -> UnifiedEntity {
1684        UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
1685    }
1686
1687    #[test]
1688    fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
1689        let directory = tempfile::tempdir().expect("temporary store");
1690        let path = directory.path().join("data.rdb");
1691        let store = UnifiedStore::open(&path).expect("paged store");
1692        store.create_collection("rows").expect("collection");
1693        store.insert("rows", row(1, 1)).expect("seed");
1694        store.persist().expect("initial checkpoint");
1695        let manager = store.get_collection("rows").expect("rows");
1696        manager.update(row(1, 2)).expect("direct update");
1697        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1698        store.persist().expect("updated row checkpoint");
1699        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1700        let mut metadata = Metadata::new();
1701        metadata.set("verified", MetadataValue::Bool(true));
1702        manager
1703            .set_metadata(EntityId::new(1), metadata)
1704            .expect("metadata");
1705        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1706        store.persist().expect("metadata-only checkpoint");
1707        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1708        manager
1709            .bulk_insert(vec![row(2, 2), row(3, 3)])
1710            .expect("bulk insert");
1711        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1712        store.persist().expect("bulk insert checkpoint");
1713        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
1714        manager.delete(EntityId::new(2)).expect("delete");
1715        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1716        store.persist().expect("changed checkpoint");
1717        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
1718        drop(store);
1719        let recovered = UnifiedStore::open(&path).expect("recover changed rows");
1720        let manager = recovered.get_collection("rows").expect("rows");
1721        assert_eq!(manager.count(), 2);
1722        assert_eq!(
1723            manager
1724                .get(EntityId::new(1))
1725                .expect("row")
1726                .data
1727                .as_row()
1728                .expect("table")
1729                .columns,
1730            vec![Value::Integer(2)]
1731        );
1732        assert!(manager.get(EntityId::new(2)).is_none());
1733        assert!(manager
1734            .get_metadata(EntityId::new(1))
1735            .expect("metadata")
1736            .get("verified")
1737            .is_some());
1738    }
1739
1740    #[test]
1741    fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
1742        let directory = tempfile::tempdir().expect("temporary store");
1743        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1744        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1745        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1746        store.create_collection("rows").expect("collection");
1747        for id in 1..=40 {
1748            store.insert("rows", row(id, id as i64)).expect("seed");
1749        }
1750        store.persist().expect("initial checkpoint");
1751        let unrelated = snapshots.begin();
1752        snapshots.rollback(unrelated);
1753        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1754        store.persist().expect("unrelated rollback checkpoint");
1755        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1756
1757        store
1758            .pending_vault_versions
1759            .write()
1760            .insert(EntityId::new(100));
1761        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1762        store.persist().expect("unrelated vault checkpoint");
1763        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1764        store.pending_vault_versions.write().clear();
1765        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1766        store
1767            .persist()
1768            .expect("unrelated vault publication checkpoint");
1769        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1770    }
1771
1772    #[test]
1773    fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
1774        let directory = tempfile::tempdir().expect("temporary store");
1775        let path = directory.path().join("data.rdb");
1776        let store = UnifiedStore::open(&path).expect("paged store");
1777        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1778        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1779        store.create_collection("rows").expect("collection");
1780        let xid = snapshots.begin();
1781        let mut entity = row(1, 1);
1782        entity.xmin = xid;
1783        store.insert("rows", entity).expect("committed row");
1784        snapshots.commit(xid);
1785        store.persist().expect("committed baseline");
1786        snapshots.rollback(xid);
1787        store
1788            .persist()
1789            .expect("untracked outcome requires validation");
1790        drop(store);
1791        let recovered = UnifiedStore::open(&path).expect("recover");
1792        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1793    }
1794
1795    #[test]
1796    fn exhausted_deferred_row_cache_keeps_full_validation() {
1797        let directory = tempfile::tempdir().expect("temporary store");
1798        let path = directory.path().join("data.rdb");
1799        let store = UnifiedStore::open(&path).expect("paged store");
1800        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1801        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1802        store.create_collection("rows").expect("collection");
1803        let xid = snapshots.begin();
1804        for id in 1..=1025 {
1805            let mut entity = row(id, id as i64);
1806            entity.xmin = xid;
1807            store.insert("rows", entity).expect("pending row");
1808        }
1809        store.persist().expect("exclude pending rows");
1810        assert!(store.checkpoint_versions.read()["rows"]
1811            .deferred_rows
1812            .is_none());
1813        snapshots.commit(xid);
1814        store
1815            .persist()
1816            .expect("publish committed rows conservatively");
1817        drop(store);
1818        let recovered = UnifiedStore::open(&path).expect("recover");
1819        assert_eq!(
1820            recovered.get_collection("rows").expect("rows").count(),
1821            1025
1822        );
1823    }
1824
1825    #[test]
1826    fn rollback_invalidates_reuse_without_a_segment_mutation() {
1827        let directory = tempfile::tempdir().expect("temporary store");
1828        let path = directory.path().join("data.rdb");
1829        let store = UnifiedStore::open(&path).expect("paged store");
1830        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1831        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1832        store.create_collection("rows").expect("collection");
1833        let xid = snapshots.begin();
1834        let mut pending = row(1, 1);
1835        pending.xmin = xid;
1836        store.insert("rows", pending).expect("version");
1837        store.persist().expect("before rollback");
1838        snapshots.rollback(xid);
1839        store.persist().expect("after rollback");
1840        drop(store);
1841        let recovered = UnifiedStore::open(&path).expect("recover");
1842        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1843    }
1844
1845    #[test]
1846    fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
1847        let directory = tempfile::tempdir().expect("temporary store");
1848        let path = directory.path().join("data.rdb");
1849        let store = UnifiedStore::open(&path).expect("paged store");
1850        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1851        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1852        store.create_collection("rows").expect("collection");
1853        let xid = snapshots.begin();
1854        let mut deleted = row(1, 1);
1855        deleted.xmax = xid;
1856        store.insert("rows", deleted).expect("version");
1857        store.persist().expect("before rollback");
1858        snapshots.rollback(xid);
1859        store.persist().expect("after rollback");
1860        drop(store);
1861        let recovered = UnifiedStore::open(&path).expect("recover");
1862        let manager = recovered.get_collection("rows").expect("rows");
1863        assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
1864    }
1865
1866    #[test]
1867    fn publishing_a_pending_vault_version_invalidates_reuse() {
1868        let directory = tempfile::tempdir().expect("temporary store");
1869        let path = directory.path().join("data.rdb");
1870        let store = UnifiedStore::open(&path).expect("paged store");
1871        store.create_collection("rows").expect("collection");
1872        store.insert("rows", row(1, 1)).expect("row");
1873        store
1874            .pending_vault_versions
1875            .write()
1876            .insert(EntityId::new(1));
1877        store.persist().expect("exclude pending vault version");
1878        store
1879            .pending_vault_versions
1880            .write()
1881            .remove(&EntityId::new(1));
1882        store.persist().expect("include published vault version");
1883        drop(store);
1884        let recovered = UnifiedStore::open(&path).expect("recover");
1885        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
1886    }
1887}