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        let mut cursor = previous
373            .map(|tree| tree.cursor_first())
374            .transpose()
375            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
376        let mut matched = 0;
377        let mut builder = None;
378        let mut failure = None;
379        manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
380            let Some(value) = encode(entity, metadata) else {
381                return true;
382            };
383            let record = (entity.id.raw().to_be_bytes().to_vec(), value);
384            let result = (|| {
385                if builder.is_none() {
386                    if let Some(cursor) = cursor.as_mut() {
387                        if cursor
388                            .next()
389                            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
390                            .as_ref()
391                            == Some(&record)
392                        {
393                            matched += 1;
394                            return Ok(());
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                builder
404                    .as_mut()
405                    .expect("replacement initialized")
406                    .push(record)
407            })();
408            if let Err(error) = result {
409                failure = Some(error);
410                return false;
411            }
412            true
413        });
414        if let Some(error) = failure {
415            return Err(error);
416        }
417        if builder.is_none() {
418            if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
419                if cursor
420                    .next()
421                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
422                    .is_none()
423                {
424                    return Ok(Arc::clone(previous));
425                }
426            }
427            let mut replacement = CheckpointTreeBuilder::new(pager);
428            if let Some(previous) = previous {
429                replacement.copy_prefix(previous, matched)?;
430            }
431            builder = Some(replacement);
432        }
433        let mut builder = builder.expect("replacement or reused tree");
434        builder.flush()?;
435        Ok(builder.tree)
436    }
437
438    /// Publication first, reuse second. The immediately preceding generation
439    /// stays in the durable ledger for one more publication. Cursors and cloned
440    /// tree handles defer retirement further without holding the writer fence.
441    fn publish_reclaimable_trees(
442        &self,
443        pager: &Pager,
444        metadata: &mut Vec<u8>,
445        live: &HashMap<String, Arc<BTree>>,
446    ) -> Result<(), StoreError> {
447        let mut retired = self.retired_btrees.lock();
448        let mut roots = Vec::new();
449        let mut reclaim = Vec::new();
450        let mut remove = std::collections::HashSet::new();
451        for (index, entry) in retired.iter().enumerate() {
452            if live.values().any(|tree| Arc::ptr_eq(tree, &entry.tree)) {
453                remove.insert(index);
454                continue;
455            }
456            if entry.published
457                && Arc::strong_count(&entry.tree) == 1
458                && !entry.tree.has_retirement_readers()
459            {
460                // Deliberately unsafe experimental control: keep the old
461                // root in the catalog while releasing its pages. Never enable
462                // this outside the isolated recovery test.
463                #[cfg(test)]
464                if std::env::var_os("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT").is_some() {
465                    roots.push(entry.tree.root_page_id());
466                }
467                reclaim.extend(
468                    entry
469                        .tree
470                        .retired_pages()
471                        .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
472                );
473                remove.insert(index);
474            } else if entry.tree.root_page_id() != 0 {
475                roots.push(entry.tree.root_page_id());
476            }
477        }
478        roots.sort_unstable();
479        roots.dedup();
480        reddb_file::encode_native_paged_retired_trees(metadata, &roots)
481            .map_err(|e| StoreError::Serialization(e.to_string()))?;
482        let (pending_pages, published_pages) = pager
483            .deferred_reclamation_pages()
484            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
485        let readers = live.values().any(|tree| tree.has_retirement_readers())
486            || retired
487                .iter()
488                .any(|entry| entry.tree.has_retirement_readers());
489        let released_pages = if readers { Vec::new() } else { published_pages };
490        let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
491        let retained_pages: Vec<_> = pending_pages
492            .into_iter()
493            .filter(|id| !released.contains(id))
494            .collect();
495        reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
496            .map_err(|e| StoreError::Serialization(e.to_string()))?;
497        let active_epoch = self.ownership_active_epoch.load(Ordering::Acquire);
498        let catalog_epoch = if active_epoch == 0 {
499            self.ownership_catalog_epoch.load(Ordering::Acquire)
500        } else {
501            active_epoch
502        };
503        if catalog_epoch != 0 {
504            reddb_file::native_store::encode_native_paged_ownership_epoch(metadata, catalog_epoch)
505                .map_err(|e| StoreError::Serialization(e.to_string()))?;
506        }
507        // Record the exact pages before the catalog can forget their owners.
508        // Recovery resolves this intent against the durable catalog epoch.
509        let mut retirement_intent = reclaim.clone();
510        retirement_intent.extend_from_slice(&released_pages);
511        if !retirement_intent.is_empty() {
512            pager
513                .record_retirement_intent(&retirement_intent)
514                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
515        }
516        // Do not mutate retirement state or the free list before both the new
517        // children and the manifest that forgets old roots have been synced.
518        publish_paged_metadata(pager, metadata)
519            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
520        self.ownership_catalog_epoch
521            .store(catalog_epoch, Ordering::Release);
522        #[cfg(test)]
523        if !reclaim.is_empty()
524            && std::env::var_os("REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE").is_some()
525        {
526            eprintln!(
527                "experimental exit before retired free pages={}",
528                reclaim.len()
529            );
530            std::process::exit(184);
531        }
532        let mut index = 0;
533        retired.retain_mut(|entry| {
534            let keep = !remove.contains(&index);
535            entry.published = true;
536            index += 1;
537            keep
538        });
539        if !reclaim.is_empty() {
540            pager
541                .free_retired_pages(&reclaim)
542                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
543            #[cfg(test)]
544            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE").is_some() {
545                eprintln!(
546                    "experimental exit after retired enqueue pages={}",
547                    reclaim.len()
548                );
549                std::process::exit(186);
550            }
551            pager
552                .sync()
553                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
554            #[cfg(test)]
555            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE").is_some() {
556                eprintln!(
557                    "experimental exit after retired free pages={}",
558                    reclaim.len()
559                );
560                std::process::exit(185);
561            }
562        }
563        pager
564            .complete_deferred_publication(&retained_pages, &released_pages)
565            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
566        if !released_pages.is_empty() {
567            pager
568                .sync()
569                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
570        }
571        Ok(())
572    }
573
574    pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
575        let mut retired = self.retired_btrees.lock();
576        if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
577        {
578            retired.push(RetiredBTree {
579                tree,
580                published: false,
581            });
582        }
583    }
584
585    /// A completed inline insert already made this tree match its manager.
586    /// Advance the reuse proof only for that single, fenced mutation. Any
587    /// untracked edit or segment publication keeps the full comparison path.
588    pub(crate) fn note_paged_insert(
589        &self,
590        collection: &str,
591        manager: &Arc<SegmentManager>,
592        before: &[(u64, u64)],
593        id: EntityId,
594    ) {
595        let after = manager.checkpoint_revisions();
596        if before.len() != after.len() {
597            return;
598        }
599        let mut changed = 0;
600        for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
601            if old_id != new_id {
602                return;
603            }
604            if old_revision != new_revision {
605                if old_revision.checked_add(1) != Some(*new_revision) {
606                    return;
607                }
608                changed += 1;
609            }
610        }
611        if changed != 1 {
612            return;
613        }
614        let mut versions = self.checkpoint_versions.write();
615        let Some(previous) = versions.get_mut(collection) else {
616            return;
617        };
618        if previous.manager.ptr_eq(&Arc::downgrade(manager))
619            && previous.segments == before
620            && previous.format_version == self.format_version()
621            && !previous.pending_vault.contains(&id)
622            && !previous.excluded_xids.contains(&0)
623        {
624            previous.segments = after;
625        }
626    }
627
628    /// Successful physical deletes already changed both the manager and tree.
629    /// Reuse remains valid only when their exact revision delta accounts for
630    /// this batch, with no intervening edits or segment replacement.
631    pub(crate) fn note_paged_delete(
632        &self,
633        collection: &str,
634        manager: &Arc<SegmentManager>,
635        before: &[(u64, u64)],
636        deleted: &[EntityId],
637    ) {
638        let after = manager.checkpoint_revisions();
639        if before.len() != after.len() {
640            return;
641        }
642        let mut changes = 0_u64;
643        for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
644            if old_id != new_id {
645                return;
646            }
647            let Some(delta) = new_revision.checked_sub(*old_revision) else {
648                return;
649            };
650            let Some(total) = changes.checked_add(delta) else {
651                return;
652            };
653            changes = total;
654        }
655        let Ok(expected) = u64::try_from(deleted.len()) else {
656            return;
657        };
658        if changes != expected {
659            return;
660        }
661        let mut versions = self.checkpoint_versions.write();
662        let Some(previous) = versions.get_mut(collection) else {
663            return;
664        };
665        if !previous.manager.ptr_eq(&Arc::downgrade(manager))
666            || previous.format_version != self.format_version()
667            || deleted.iter().any(|id| previous.pending_vault.contains(id))
668            || previous.excluded_xids.contains(&0)
669        {
670            return;
671        }
672        if previous.segments != before {
673            let Some(deferred) = previous.deferred_rows.as_ref() else {
674                return;
675            };
676            let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
677                return;
678            };
679            let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
680            let pending: std::collections::BTreeSet<_> = deferred
681                .iter()
682                .copied()
683                .chain(changed)
684                .filter(|id| !removed.contains(id))
685                .collect();
686            if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
687                return;
688            }
689            // GC already applied its deletes to the tree. Carry earlier dirty
690            // IDs forward instead of spending their journal on these deletes.
691            // Sparse projection still requires exact WAL proof for every
692            // differing row, so an unlogged manager edit cannot be concealed.
693            previous.has_unprojected_rows = !pending.is_empty();
694            previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
695        }
696        previous.segments = after;
697    }
698
699    pub(crate) fn mark_paged_registry_dirty(&self) {
700        self.resident.mark_paged_registry_dirty()
701    }
702
703    /// Get (or lazily create) the per-collection B-tree under a *read*
704    /// lock whenever possible. Returns a cloned `Arc<BTree>` so callers
705    /// can mutate the tree without holding the outer map's RwLock —
706    /// previously every insert serialised on `btree_indices.write()`,
707    /// costing ~60% of the concurrent-insert throughput ceiling.
708    pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
709        let pager = self.pager.as_ref()?;
710        if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
711            return Some(btree);
712        }
713        let mut write = self.btree_indices.write();
714        let btree = write
715            .entry(collection.to_string())
716            .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
717            .clone();
718        Some(btree)
719    }
720
721    pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
722        let Some(pager) = &self.pager else {
723            return Ok(());
724        };
725
726        if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
727            self.flush_paged_registry()?;
728            self.resident
729                .paged_registry_dirty
730                .store(false, Ordering::Release);
731            return Ok(());
732        }
733
734        pager
735            .flush()
736            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
737    }
738
739    pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
740        let _vault_guard = self.vault_publication_lock.lock();
741        let Some(pager) = &self.pager else {
742            return Ok(());
743        };
744
745        match pager.read_page(1) {
746            Ok(_) => {}
747            Err(PagerError::PageNotFound(_)) => {
748                let meta_page = pager
749                    .allocate_page(crate::storage::engine::PageType::Header)
750                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
751                pager
752                    .write_page(meta_page.page_id(), meta_page)
753                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
754            }
755            Err(e) => {
756                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
757            }
758        }
759
760        let format_version = STORE_VERSION_V11;
761        self.set_format_version(format_version);
762
763        let collections = self.resident.collections.read();
764        let btree_indices = self.btree_indices.read();
765        let mut collection_roots = Vec::with_capacity(collections.len());
766        for (name, _) in collections.iter() {
767            let root_page = btree_indices
768                .get(name)
769                .map_or(0, |btree| btree.root_page_id());
770            collection_roots.push((name.clone(), root_page));
771        }
772        drop(btree_indices);
773        drop(collections);
774
775        let mut meta_data = Vec::with_capacity(4096);
776        reddb_file::encode_native_paged_metadata_header(
777            &mut meta_data,
778            reddb_file::NativePagedMetadataHeader {
779                format_version,
780                collection_count: collection_roots.len() as u32,
781            },
782        );
783        for (name, root_page) in &collection_roots {
784            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
785        }
786
787        let cross_refs = self.resident.cross_refs.read();
788        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
789        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
790        for (source_id, refs) in cross_refs.iter() {
791            for (target_id, ref_type, collection) in refs {
792                reddb_file::encode_native_paged_cross_ref(
793                    &mut meta_data,
794                    source_id.raw(),
795                    target_id.raw(),
796                    ref_type.to_byte(),
797                    collection,
798                );
799            }
800        }
801
802        self.publish_reclaimable_trees(pager, &mut meta_data, &self.btree_indices.read())?;
803
804        Ok(())
805    }
806
807    /// Get a reference to the underlying pager (if in paged mode).
808    pub fn pager(&self) -> Option<&Arc<Pager>> {
809        self.pager.as_ref()
810    }
811
812    /// Bytes of page-cache memory currently occupied by resident pages
813    /// (ADR 0073 §2). Slots are fixed size, so occupancy times the page size
814    /// *is* the footprint; a store without a pager occupies none.
815    pub fn page_cache_bytes_in_use(&self) -> u64 {
816        self.pager.as_ref().map_or(0, |pager| {
817            pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
818        })
819    }
820
821    /// Bytes of WAL buffer memory held by this store's group-commit
822    /// coordinator: queued records plus the writer's fixed append buffer.
823    pub fn wal_buffer_bytes_in_use(&self) -> u64 {
824        self.commit
825            .as_ref()
826            .map_or(0, |commit| commit.buffered_bytes())
827    }
828
829    /// Approximate resident bytes across every collection's segment arena
830    /// (ADR 0073 §2). Same number `stats().total_memory_bytes` reports, without
831    /// building the per-collection stats map a sampler would throw away.
832    pub fn segment_memory_bytes(&self) -> u64 {
833        let managers: Vec<Arc<SegmentManager>> = self
834            .resident
835            .collections
836            .read()
837            .values()
838            .map(Arc::clone)
839            .collect();
840
841        managers
842            .iter()
843            .map(|manager| manager.resident_bytes())
844            .fold(0, u64::saturating_add)
845    }
846
847    /// Borrow the immutable store configuration. Runtime hooks (e.g. the
848    /// `auto_index_id` first-insert hook in `MutationEngine`) read knobs
849    /// off this struct without going through the legacy global config tree.
850    pub fn config(&self) -> &UnifiedStoreConfig {
851        &self.config
852    }
853
854    pub fn with_config(config: UnifiedStoreConfig) -> Self {
855        Self {
856            config,
857            resident: reddb_engine::storage::resident_store::ResidentStore::new(),
858            format_version: AtomicU32::new(STORE_VERSION_V11),
859            pager: None,
860            db_path: None,
861            btree_indices: RwLock::new(HashMap::new()),
862            checkpoint_versions: RwLock::new(HashMap::new()),
863            checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
864            retired_btrees: parking_lot::Mutex::new(Vec::new()),
865            ownership_catalog_epoch: AtomicU64::new(0),
866            ownership_active_epoch: AtomicU64::new(0),
867            detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
868            commit: None,
869            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
870            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
871            snapshot_manager: std::sync::OnceLock::new(),
872            embedded_commit_session: std::sync::OnceLock::new(),
873            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
874            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
875        }
876    }
877
878    /// Open or create a page-based database
879    ///
880    /// This uses the page engine for ACID durability with B-tree indices.
881    /// The database file uses 16 KiB pages with checksums and efficient caching.
882    ///
883    /// # Arguments
884    ///
885    /// * `path` - Path to the database file (e.g., "data.rdb")
886    ///
887    /// # Example
888    ///
889    /// ```rust,ignore
890    /// let store = UnifiedStore::open("security.rdb")?;
891    /// store.create_collection("hosts")?;
892    /// // ... operations ...
893    /// store.persist()?; // Flush to disk
894    /// ```
895    pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
896        Self::open_with_config(path, UnifiedStoreConfig::default())
897    }
898
899    pub fn open_with_config(
900        path: impl AsRef<Path>,
901        config: UnifiedStoreConfig,
902    ) -> Result<Self, StoreError> {
903        let path = path.as_ref();
904        let mut pager_config = PagerConfig::default();
905        // ADR 0073 §2 — the page cache is pre-sized from its budget share.
906        // The pager's own `cache_size` default survives only for direct
907        // library callers who never resolved a budget.
908        if let Some(slots) = config.page_cache_slots {
909            pager_config.cache_size = slots;
910        }
911        // Tunables via env — experimental, used by the benchmark harness
912        // to compare durability profiles head-to-head with Postgres.
913        // REDDB_DOUBLE_WRITE=0 requests skipping the double-write buffer,
914        // which otherwise adds two fsyncs per pager flush (one on DWB, one
915        // on the main file). The pager honors this only when the actual
916        // data file is proven to live on a CoW filesystem with atomic page
917        // writes; otherwise it fails closed and keeps DWB enabled.
918        if matches!(
919            std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
920            Some("0") | Some("false") | Some("off")
921        ) {
922            pager_config.double_write = false;
923        }
924        let pager = Pager::open(path, pager_config)
925            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
926        pager
927            .enable_deferred_reclamation()
928            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
929
930        let wal_path = reddb_file::layout::unified_wal_path(path);
931        // Explicit transactions and WAL-only vault versions need committed
932        // batches in every durability mode, including Strict's inline fsync.
933        let commit = Some(Arc::new(
934            StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
935                .map_err(StoreError::Io)?,
936        ));
937
938        let store = Self {
939            config,
940            resident: reddb_engine::storage::resident_store::ResidentStore::new(),
941            format_version: AtomicU32::new(STORE_VERSION_V11),
942            pager: Some(Arc::new(pager)),
943            db_path: Some(path.to_path_buf()),
944            btree_indices: RwLock::new(HashMap::new()),
945            checkpoint_versions: RwLock::new(HashMap::new()),
946            checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
947            retired_btrees: parking_lot::Mutex::new(Vec::new()),
948            ownership_catalog_epoch: AtomicU64::new(0),
949            ownership_active_epoch: AtomicU64::new(0),
950            detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
951            commit,
952            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
953            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
954            snapshot_manager: std::sync::OnceLock::new(),
955            embedded_commit_session: std::sync::OnceLock::new(),
956            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
957            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
958        };
959
960        // An interrupted structural edit may have reached home pages without
961        // publishing its catalog. Restore those pages before following roots.
962        if let Some(commit) = &store.commit {
963            commit
964                .restore_checkpoint_page_images(&store)
965                .map_err(StoreError::Io)?;
966        }
967        // Load existing data from pages if database exists
968        store.load_from_pages()?;
969        if let Some(commit) = &store.commit {
970            commit.replay_into(&store).map_err(StoreError::Io)?;
971        }
972        store.recover_operational_manifest()?;
973
974        Ok(store)
975    }
976
977    /// Replay changes the authoritative managers first. Build replacement trees
978    /// only for changed collections, leaving every published tree untouched until
979    /// all replacement pages are durable. Retain the WAL for auxiliary recovery.
980    pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
981        let Some(pager) = &self.pager else {
982            return Ok(());
983        };
984        let collections = self.resident.collections.read();
985        let mut indices = self.btree_indices.write();
986        let previous_versions = self.checkpoint_versions.read();
987        let raw_replay =
988            self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
989        for (name, manager) in collections.iter() {
990            let mut reusable = None;
991            if raw_replay {
992                if let (Some(previous), Some(tree)) =
993                    (previous_versions.get(name), indices.get(name))
994                {
995                    let mut current = CheckpointVersion {
996                        manager: Arc::downgrade(manager),
997                        segments: manager.checkpoint_revisions(),
998                        visibility_revision: 0,
999                        untracked_visibility_revision: 0,
1000                        excluded_xids: Arc::default(),
1001                        pending_vault: Arc::default(),
1002                        format_version: self.format_version(),
1003                        deferred_rows: Some(Arc::default()),
1004                        has_unprojected_rows: false,
1005                    };
1006                    if previous.manager.ptr_eq(&current.manager)
1007                        && previous.format_version == current.format_version
1008                    {
1009                        if previous.segments == current.segments
1010                            && !previous.has_unprojected_rows
1011                            && checkpoint_visibility_matches(previous, &current)
1012                        {
1013                            reusable = Some(Arc::clone(tree));
1014                        } else {
1015                            // The replay ownership epoch protects in-place
1016                            // changes with the same undo/publication protocol
1017                            // used by normal sparse checkpoints. Missing WAL
1018                            // proof, old formats and exhausted journals retain
1019                            // the full comparison/rebuild path below.
1020                            reusable = self.sparse_checkpoint_tree(
1021                                name,
1022                                manager,
1023                                tree,
1024                                previous,
1025                                &mut current,
1026                            )?;
1027                        }
1028                    }
1029                }
1030            }
1031            let tree = match reusable {
1032                Some(tree) => tree,
1033                None => {
1034                    self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1035                        #[cfg(test)]
1036                        REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1037                        Some(Self::serialize_entity_record(
1038                            entity,
1039                            metadata.as_ref(),
1040                            self.format_version(),
1041                        ))
1042                    })?
1043                }
1044            };
1045            if indices
1046                .get(name)
1047                .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1048            {
1049                continue;
1050            }
1051            if let Some(previous) = indices.insert(name.clone(), tree) {
1052                self.retire_btree(previous);
1053            }
1054            self.mark_paged_registry_dirty();
1055            #[cfg(test)]
1056            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1057                if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1058                    pager.sync().map_err(|error| {
1059                        StoreError::Io(std::io::Error::other(error.to_string()))
1060                    })?;
1061                }
1062                eprintln!("experimental replay rebuild exit before manifest");
1063                std::process::exit(182);
1064            }
1065        }
1066        drop(previous_versions);
1067        drop(indices);
1068        drop(collections);
1069        self.flush_paged_state()?;
1070        #[cfg(test)]
1071        if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1072            eprintln!("experimental replay exit after manifest");
1073            std::process::exit(182);
1074        }
1075        // Opening has no concurrent writers. Replay has now durably reconciled
1076        // these raw trees; do not immediately rebuild them at the first flush.
1077        if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1078            let collections = self.resident.collections.read();
1079            let mut versions = self.checkpoint_versions.write();
1080            for (name, manager) in collections.iter() {
1081                versions.insert(
1082                    name.clone(),
1083                    CheckpointVersion {
1084                        manager: Arc::downgrade(manager),
1085                        segments: manager.checkpoint_revisions(),
1086                        visibility_revision: 0,
1087                        untracked_visibility_revision: 0,
1088                        excluded_xids: Arc::default(),
1089                        pending_vault: Arc::default(),
1090                        format_version: self.format_version(),
1091                        deferred_rows: Some(Arc::default()),
1092                        has_unprojected_rows: false,
1093                    },
1094                );
1095            }
1096        }
1097        Ok(())
1098    }
1099
1100    pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
1101        let Some(path) = &self.db_path else {
1102            return Ok(());
1103        };
1104        let mut collections = self.list_collections();
1105        collections.sort();
1106        let pending_drops =
1107            crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1108                .recover_or_bootstrap(&collections)
1109                .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1110        for name in pending_drops {
1111            if self.get_collection(&name).is_some() {
1112                self.drop_collection(&name)?;
1113            }
1114        }
1115        Ok(())
1116    }
1117
1118    pub(crate) fn prepare_operational_collection_create(
1119        &self,
1120        name: &str,
1121    ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
1122        let Some(path) = &self.db_path else {
1123            return Ok(None);
1124        };
1125        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1126            .prepare_collection_create(name)
1127            .map(Some)
1128            .map_err(StoreError::Io)
1129    }
1130
1131    pub(crate) fn prepare_operational_collection_drop(
1132        &self,
1133        name: &str,
1134    ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
1135        let Some(path) = &self.db_path else {
1136            return Ok(None);
1137        };
1138        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1139            .prepare_collection_drop(name)
1140            .map(Some)
1141            .map_err(StoreError::Io)
1142    }
1143
1144    /// Read the commit boundary before following potentially interrupted trees.
1145    pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1146        let Some(pager) = &self.pager else {
1147            return Ok(0);
1148        };
1149        let Some(content) = read_meta_payload(pager)? else {
1150            return Ok(0);
1151        };
1152        reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1153            .map(|epoch| epoch.unwrap_or(0))
1154            .map_err(|error| StoreError::Serialization(error.to_string()))
1155    }
1156
1157    /// Read the B-tree indices and reconstruct collections from pages.
1158    fn load_from_pages(&self) -> Result<(), StoreError> {
1159        let pager = match &self.pager {
1160            Some(p) => p,
1161            None => return Ok(()), // No pager, nothing to load
1162        };
1163
1164        // Get page count
1165        let page_count = pager.page_count().map_err(|e| {
1166            StoreError::Io(std::io::Error::other(format!(
1167                "failed to read page count: {}",
1168                e
1169            )))
1170        })?;
1171        if page_count <= 1 {
1172            // Empty database (only header page)
1173            return Ok(());
1174        }
1175
1176        // Read metadata starting from page 1 (collections registry). The
1177        // helper transparently follows the `RDM3` overflow chain when the
1178        // metadata blob spans multiple pages and falls back to the legacy
1179        // `<data>-meta` shadow when page 1 itself is corrupted.
1180        if let Some(content_vec) = read_meta_payload(pager)? {
1181            let content: &[u8] = &content_vec;
1182            if content.len() >= 4 {
1183                let mut pos = 0;
1184                let mut format_version = STORE_VERSION_V1;
1185
1186                let collection_count = if let Some(header) =
1187                    reddb_file::decode_native_paged_metadata_header(content)
1188                        .map_err(|err| StoreError::Serialization(err.to_string()))?
1189                {
1190                    format_version = header.format_version;
1191                    pos += reddb_file::METADATA_HEADER_BYTES;
1192                    header.collection_count as usize
1193                } else {
1194                    let count = u32::from_le_bytes([
1195                        content[pos],
1196                        content[pos + 1],
1197                        content[pos + 2],
1198                        content[pos + 3],
1199                    ]) as usize;
1200                    pos += 4;
1201                    count
1202                };
1203
1204                self.set_format_version(format_version);
1205
1206                if pos > content.len() {
1207                    return Ok(());
1208                }
1209
1210                // Read collection names and their B-tree root pages
1211                for _ in 0..collection_count {
1212                    let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1213                        .map_err(|err| page_integrity(1, None, err.to_string()))?;
1214                    // Root page ID for this collection's B-tree
1215                    let root_page = root.root_page;
1216                    let name = root.collection;
1217
1218                    // Hydrate the collection in memory only. Loading must
1219                    // not emit WAL entries or rewrite the on-disk registry
1220                    // before the existing B-tree roots are attached.
1221                    let _ = self.create_collection_in_memory(&name);
1222
1223                    // Load B-tree with root page if it exists
1224                    if root_page > 0 {
1225                        let btree = BTree::with_root(Arc::clone(pager), root_page);
1226
1227                        // Load all entities from B-tree into the collection
1228                        let mut cursor = btree.cursor_first().map_err(|err| {
1229                            page_integrity(root_page, Some(name.clone()), err.to_string())
1230                        })?;
1231                        let manager = self.get_collection(&name);
1232                        loop {
1233                            let next = cursor.next().map_err(|err| {
1234                                page_integrity(root_page, Some(name.clone()), err.to_string())
1235                            })?;
1236                            let Some((key, value)) = next else {
1237                                break;
1238                            };
1239                            // Deserialize entity from value bytes
1240                            let (entity, metadata) =
1241                                Self::deserialize_entity_record(&value, self.format_version())
1242                                    .map_err(|err| {
1243                                        page_integrity(
1244                                            root_page,
1245                                            Some(name.clone()),
1246                                            err.to_string(),
1247                                        )
1248                                    })?;
1249                            if let Some(m) = &manager {
1250                                let id = entity.id;
1251                                if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1252                                    m.register_row_id(*row_id);
1253                                }
1254                                self.resident.context_index.index_entity(&name, &entity);
1255                                m.restore_snapshot_entity(entity.clone())?;
1256                                if let Some(metadata) = metadata {
1257                                    let _ = m.set_metadata(id, metadata);
1258                                }
1259                                self.register_entity_id(id);
1260                                if self.config.auto_index_refs {
1261                                    self.index_cross_refs(&entity, &name)?;
1262                                }
1263                            }
1264                        }
1265
1266                        // Store the B-tree for future lookups
1267                        self.btree_indices
1268                            .write()
1269                            .insert(name.clone(), Arc::new(btree));
1270                        if format_version == STORE_VERSION_V11 {
1271                            if let Some(manager) = &manager {
1272                                self.checkpoint_versions.write().insert(
1273                                    name,
1274                                    CheckpointVersion {
1275                                        manager: Arc::downgrade(manager),
1276                                        segments: manager.checkpoint_revisions(),
1277                                        visibility_revision: 0,
1278                                        untracked_visibility_revision: 0,
1279                                        excluded_xids: Arc::default(),
1280                                        pending_vault: Arc::default(),
1281                                        format_version,
1282                                        deferred_rows: Some(Arc::default()),
1283                                        has_unprojected_rows: false,
1284                                    },
1285                                );
1286                            }
1287                        }
1288                    }
1289                }
1290
1291                if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1292                    let cross_ref_count = u32::from_le_bytes([
1293                        content[pos],
1294                        content[pos + 1],
1295                        content[pos + 2],
1296                        content[pos + 3],
1297                    ]) as usize;
1298                    pos += 4;
1299
1300                    for _ in 0..cross_ref_count {
1301                        let Ok(cross_ref) =
1302                            reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1303                        else {
1304                            break;
1305                        };
1306                        let source_id = EntityId::new(cross_ref.source_id);
1307                        let target_id = EntityId::new(cross_ref.target_id);
1308                        let ref_type = RefType::from_byte(cross_ref.ref_type);
1309                        let target_collection = cross_ref.target_collection;
1310
1311                        self.resident
1312                            .cross_refs
1313                            .write()
1314                            .entry(source_id)
1315                            .or_default()
1316                            .push((target_id, ref_type, target_collection.clone()));
1317
1318                        if let Some((collection, mut entity)) = self.get_any(source_id) {
1319                            let exists = entity.cross_refs().iter().any(|xref| {
1320                                xref.target == target_id
1321                                    && xref.ref_type == ref_type
1322                                    && xref.target_collection == target_collection
1323                            });
1324                            if !exists {
1325                                entity.cross_refs_mut().push(CrossRef::new(
1326                                    source_id,
1327                                    target_id,
1328                                    target_collection.clone(),
1329                                    ref_type,
1330                                ));
1331                                if let Some(manager) = self.get_collection(&collection) {
1332                                    let _ = manager.update(entity);
1333                                }
1334                            }
1335                        }
1336                    }
1337                }
1338                let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1339                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1340                let live_roots: std::collections::HashSet<_> = self
1341                    .btree_indices
1342                    .read()
1343                    .values()
1344                    .map(|tree| tree.root_page_id())
1345                    .collect();
1346                for root in retired {
1347                    if live_roots.contains(&root)
1348                        || root
1349                            >= pager
1350                                .page_count()
1351                                .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1352                    {
1353                        return Err(page_integrity(
1354                            root,
1355                            None,
1356                            "retired root aliases a live or missing page",
1357                        ));
1358                    }
1359                    self.retired_btrees.lock().push(RetiredBTree {
1360                        tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1361                        published: true,
1362                    });
1363                }
1364                let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1365                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1366                if deferred.iter().any(|id| live_roots.contains(id)) {
1367                    return Err(page_integrity(1, None, "deferred page aliases a live root"));
1368                }
1369                pager
1370                    .restore_deferred_reclamation(&deferred)
1371                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1372                let ownership_epoch =
1373                    reddb_file::native_store::decode_native_paged_ownership_epoch(
1374                        content, &mut pos,
1375                    )
1376                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
1377                self.ownership_catalog_epoch
1378                    .store(ownership_epoch.unwrap_or(0), Ordering::Release);
1379            }
1380        }
1381
1382        if self.format_version() < STORE_VERSION_V11 {
1383            self.set_format_version(STORE_VERSION_V11);
1384        }
1385
1386        Ok(())
1387    }
1388
1389    /// Deserialize an entity from binary bytes
1390    pub(crate) fn deserialize_entity(
1391        data: &[u8],
1392        format_version: u32,
1393    ) -> Result<UnifiedEntity, StoreError> {
1394        let mut pos = 0;
1395        Self::read_entity_binary(data, &mut pos, format_version)
1396            .map_err(|e| StoreError::Serialization(e.to_string()))
1397    }
1398
1399    /// Serialize an entity to binary bytes
1400    pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1401        // Pre-allocate ~256 bytes to cover the typical 15-column
1402        // typed row without any Vec growth. Bulk insert calls this
1403        // millions of times per bench run; saving 2-3 reallocs per
1404        // entity amortises.
1405        let mut buf = Vec::with_capacity(256);
1406        Self::write_entity_binary(&mut buf, entity, format_version);
1407        buf
1408    }
1409
1410    pub(crate) fn serialize_entity_record(
1411        entity: &UnifiedEntity,
1412        metadata: Option<&Metadata>,
1413        format_version: u32,
1414    ) -> Vec<u8> {
1415        reddb_engine::storage::metadata_codec::serialize_entity_record(
1416            entity,
1417            metadata,
1418            format_version,
1419        )
1420    }
1421
1422    pub(crate) fn deserialize_entity_record(
1423        data: &[u8],
1424        format_version: u32,
1425    ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1426        reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
1427            .map_err(StoreError::Serialization)
1428    }
1429
1430    /// Persist all data to page-based storage
1431    ///
1432    /// Writes all entities to B-tree pages and flushes to disk.
1433    /// This provides ACID durability guarantees.
1434    pub fn persist(&self) -> Result<(), StoreError> {
1435        let _vault_guard = self.vault_publication_lock.lock();
1436        let pending_vault_versions = self.pending_vault_versions.read();
1437        let pager = match &self.pager {
1438            Some(p) => p,
1439            None => {
1440                // No pager attached - use binary file fallback if path available
1441                if let Some(path) = &self.db_path {
1442                    return self
1443                        .save_to_file(path)
1444                        .map_err(|e| StoreError::Serialization(e.to_string()));
1445                }
1446                return Err(StoreError::Io(std::io::Error::other(
1447                    "No pager or path configured for persistence",
1448                )));
1449            }
1450        };
1451        // Prevent unrelated allocations or frees from being attributed to
1452        // this checkpoint's ownership epoch.
1453        let _ownership_guard = pager.lock_ownership_mutations();
1454
1455        match pager.read_page(1) {
1456            Ok(_) => {}
1457            Err(PagerError::PageNotFound(_)) => {
1458                let meta_page = pager
1459                    .allocate_page(crate::storage::engine::PageType::Header)
1460                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1461                pager
1462                    .write_page(meta_page.page_id(), meta_page)
1463                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1464            }
1465            Err(e) => {
1466                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1467            }
1468        }
1469
1470        if let Some(commit) = &self.commit {
1471            commit.force_sync().map_err(StoreError::Io)?;
1472        }
1473
1474        struct InterruptedOwnership<'a> {
1475            pager: &'a Pager,
1476            active: bool,
1477        }
1478        impl Drop for InterruptedOwnership<'_> {
1479            fn drop(&mut self) {
1480                if self.active {
1481                    self.pager.fence_interrupted_ownership();
1482                }
1483            }
1484        }
1485        let mut ownership = InterruptedOwnership {
1486            pager,
1487            active: self.commit.is_some(),
1488        };
1489        let epoch = if let Some(commit) = &self.commit {
1490            let epoch = commit
1491                .next_page_ownership_epoch(self)
1492                .map_err(StoreError::Io)?;
1493            commit
1494                .begin_page_ownership_epoch(self, epoch)
1495                .map_err(StoreError::Io)?;
1496            Some(epoch)
1497        } else {
1498            None
1499        };
1500
1501        let collections = self.resident.collections.read();
1502        let mut btree_indices = self.btree_indices.write();
1503
1504        // Collect collection names and their B-tree root pages
1505        let mut collection_roots: Vec<(String, u32)> = Vec::new();
1506
1507        // Build unpublished replacements. Deleting from the current trees here
1508        // would destroy the last checkpoint if a write fails before publication.
1509        let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1510        let previous_versions = self.checkpoint_versions.read();
1511        let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1512        let pending_vault = Arc::new(pending_vault_versions.clone());
1513        let (visibility_revision, excluded, untracked_visibility_revision) =
1514            self.snapshot_manager.get().map_or_else(
1515                || (0, Default::default(), 0),
1516                |manager| manager.checkpoint_projection_state(),
1517            );
1518        let excluded = Arc::new(excluded);
1519        for (name, manager) in collections.iter() {
1520            let mut version = CheckpointVersion {
1521                manager: Arc::downgrade(manager),
1522                segments: manager.checkpoint_revisions(),
1523                visibility_revision,
1524                untracked_visibility_revision,
1525                excluded_xids: Arc::clone(&excluded),
1526                pending_vault: Arc::clone(&pending_vault),
1527                format_version: self.format_version(),
1528                deferred_rows: Some(Arc::default()),
1529                has_unprojected_rows: false,
1530            };
1531            if let (Some(previous), Some(tree)) =
1532                (previous_versions.get(name), btree_indices.get(name))
1533            {
1534                if previous.manager.ptr_eq(&version.manager)
1535                    && previous.segments == version.segments
1536                    && previous.format_version == version.format_version
1537                    && !previous.has_unprojected_rows
1538                    && checkpoint_visibility_matches(previous, &version)
1539                {
1540                    version.deferred_rows = previous.deferred_rows.clone();
1541                    collection_roots.push((name.clone(), tree.root_page_id()));
1542                    rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1543                    checkpoint_versions.insert(name.clone(), version);
1544                    continue;
1545                }
1546            }
1547            if let (Some(previous), Some(tree)) =
1548                (previous_versions.get(name), btree_indices.get(name))
1549            {
1550                if previous.manager.ptr_eq(&version.manager)
1551                    && previous.format_version == version.format_version
1552                {
1553                    if let Some(tree) =
1554                        self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
1555                    {
1556                        collection_roots.push((name.clone(), tree.root_page_id()));
1557                        rebuilt_indices.insert(name.clone(), tree);
1558                        checkpoint_versions.insert(name.clone(), version);
1559                        continue;
1560                    }
1561                }
1562            }
1563            let btree = self.checkpoint_tree(
1564                pager,
1565                manager,
1566                btree_indices.get(name),
1567                |entity, metadata| {
1568                    #[cfg(test)]
1569                    CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1570                    let hidden = pending_vault_versions.contains(&entity.id)
1571                        || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1572                            && excluded.contains(&entity.xmin));
1573                    let normalize_xmax = matches!(entity.data, EntityData::Row(_))
1574                        && excluded.contains(&entity.xmax);
1575                    if hidden || normalize_xmax {
1576                        version.remember_deferred_row(entity.id);
1577                    }
1578                    if hidden {
1579                        return None;
1580                    }
1581                    let mut normalized = std::borrow::Cow::Borrowed(entity);
1582                    if normalize_xmax {
1583                        normalized.to_mut().xmax = 0;
1584                    }
1585                    Some(Self::serialize_entity_record(
1586                        &normalized,
1587                        metadata.as_ref(),
1588                        self.format_version(),
1589                    ))
1590                },
1591            )?;
1592            checkpoint_versions.insert(name.clone(), version);
1593
1594            collection_roots.push((name.clone(), btree.root_page_id()));
1595            rebuilt_indices.insert(name.clone(), btree);
1596        }
1597
1598        // Write collection metadata to page 1
1599        let mut meta_data = Vec::with_capacity(4096);
1600
1601        let format_version = STORE_VERSION_V11;
1602        self.set_format_version(format_version);
1603
1604        reddb_file::encode_native_paged_metadata_header(
1605            &mut meta_data,
1606            reddb_file::NativePagedMetadataHeader {
1607                format_version,
1608                collection_count: collection_roots.len() as u32,
1609            },
1610        );
1611
1612        // Write each collection's name and B-tree root page
1613        for (name, root_page) in &collection_roots {
1614            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1615        }
1616
1617        // Write cross-reference metadata
1618        let cross_refs = self.resident.cross_refs.read();
1619        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1620        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1621        for (source_id, refs) in cross_refs.iter() {
1622            for (target_id, ref_type, collection) in refs {
1623                reddb_file::encode_native_paged_cross_ref(
1624                    &mut meta_data,
1625                    source_id.raw(),
1626                    target_id.raw(),
1627                    ref_type.to_byte(),
1628                    collection,
1629                );
1630            }
1631        }
1632
1633        for (name, previous) in btree_indices.iter() {
1634            if !rebuilt_indices
1635                .get(name)
1636                .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1637            {
1638                self.retire_btree(Arc::clone(previous));
1639            }
1640        }
1641        #[cfg(test)]
1642        if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
1643            pager
1644                .sync()
1645                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1646            eprintln!("experimental checkpoint exit before manifest");
1647            std::process::exit(183);
1648        }
1649        self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices)?;
1650
1651        #[cfg(test)]
1652        if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
1653            std::process::exit(195);
1654        }
1655
1656        // Only the now-durable catalog may replace the live tree handles. The
1657        // previous generation is retained in the manifest retirement ledger.
1658        *btree_indices = rebuilt_indices;
1659        drop(previous_versions);
1660        *self.checkpoint_versions.write() = checkpoint_versions;
1661
1662        if let Some(commit) = &self.commit {
1663            commit
1664                .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
1665                .map_err(StoreError::Io)?;
1666            ownership.active = false;
1667            commit.truncate().map_err(StoreError::Io)?;
1668        }
1669        *self.checkpoint_durable_rows.write() = Default::default();
1670
1671        Ok(())
1672    }
1673
1674    /// Check if the store is using page-based persistence
1675    pub fn is_paged(&self) -> bool {
1676        self.pager.is_some()
1677    }
1678
1679    /// Current root page for a collection's primary B-tree, if one has
1680    /// been materialized in this store.
1681    pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1682        self.btree_indices
1683            .read()
1684            .get(collection)
1685            .map(|btree| btree.root_page_id())
1686            .filter(|root| *root != 0)
1687    }
1688
1689    /// Get the database file path (if using paged mode)
1690    pub fn db_path(&self) -> Option<&Path> {
1691        self.db_path.as_deref()
1692    }
1693}
1694
1695#[cfg(test)]
1696mod checkpoint_work_tests {
1697    use super::*;
1698
1699    #[cfg(target_os = "linux")]
1700    #[test]
1701    fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
1702        const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
1703        if let Some(path) = std::env::var_os(CHILD) {
1704            let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
1705            let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
1706            let store = UnifiedStore::open(path).expect("open store");
1707            let initial_count = if multi { 600 } else { 1 };
1708            for id in 1..=initial_count {
1709                store.insert_auto("rows", row(id, 1)).expect("seed");
1710            }
1711            store.persist().expect("baseline");
1712            let manager = store.get_collection("rows").expect("collection");
1713            let ids: Vec<u64> = if multi {
1714                (1..=initial_count).collect()
1715            } else {
1716                vec![if insert { 2 } else { 1 }]
1717            };
1718            let mut actions = Vec::new();
1719            for id in ids {
1720                if insert {
1721                    manager.insert(row(id, 9)).expect("insert version");
1722                } else {
1723                    manager.update(row(id, 9)).expect("update version");
1724                }
1725                let entity = manager.get(EntityId::new(id)).expect("changed entity");
1726                actions.push(StoreWalAction::upsert_entity(
1727                    "rows",
1728                    &entity,
1729                    None,
1730                    store.format_version(),
1731                ));
1732            }
1733            let references = store
1734                .commit
1735                .as_ref()
1736                .expect("coordinator")
1737                .append_checkpoint_actions(&actions)
1738                .expect("durable WAL");
1739            store.remember_checkpoint_rows(actions, references);
1740            store.persist().expect("exit after sparse write");
1741            panic!("inline sparse crash hook was not reached");
1742        }
1743        for (insert, multi) in [(false, false), (true, false), (false, true)] {
1744            let directory = tempfile::tempdir().expect("temporary store");
1745            let path = directory.path().join("data.rdb");
1746            let mut command =
1747                std::process::Command::new(std::env::current_exe().expect("test executable"));
1748            command
1749                .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
1750                .env(CHILD, &path)
1751                .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
1752            if insert {
1753                command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
1754            }
1755            if multi {
1756                command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
1757            }
1758            let output = command.output().expect("crash child");
1759            assert_eq!(
1760                output.status.code(),
1761                Some(190),
1762                "insert={insert} multi={multi}: {}",
1763                String::from_utf8_lossy(&output.stderr)
1764            );
1765            let recovered = UnifiedStore::open(&path).expect("recover");
1766            let ids: Vec<u64> = if multi {
1767                (1..=600).collect()
1768            } else {
1769                vec![if insert { 2 } else { 1 }]
1770            };
1771            for id in ids {
1772                assert_eq!(
1773                    recovered
1774                        .get("rows", EntityId::new(id))
1775                        .expect("recovered row")
1776                        .data
1777                        .as_row()
1778                        .expect("row")
1779                        .columns,
1780                    vec![Value::Integer(9)]
1781                );
1782            }
1783            assert_eq!(
1784                recovered.get_collection("rows").expect("rows").count(),
1785                if multi {
1786                    600
1787                } else if insert {
1788                    2
1789                } else {
1790                    1
1791                }
1792            );
1793        }
1794    }
1795
1796    #[test]
1797    fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
1798        let directory = tempfile::tempdir().expect("temporary store");
1799        let path = directory.path().join("data.rdb");
1800        {
1801            let store = UnifiedStore::open(&path).expect("open store");
1802            store.create_collection("rows").expect("create collection");
1803            store.persist().expect("publish ownership epoch");
1804            assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
1805            store.persist().expect("publish next ownership epoch");
1806        }
1807        let reopened = UnifiedStore::open(&path).expect("reopen store");
1808        assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
1809    }
1810
1811    #[test]
1812    fn metadata_overflow_with_missing_continuation_fails_closed() {
1813        use crate::storage::engine::{Page, PageType, HEADER_SIZE};
1814
1815        let directory = tempfile::tempdir().expect("temporary store");
1816        let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
1817        let pager = store.pager.as_ref().unwrap();
1818        let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
1819        let valid_manifest = build_meta_page1_with_overflow(pager, &valid_payload).unwrap();
1820        pager.write_page(1, valid_manifest).unwrap();
1821        assert!(!existing_overflow_chain(pager).unwrap().is_empty());
1822        assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
1823
1824        let missing = pager.page_count().unwrap() + 100;
1825        let mut manifest = Page::new(PageType::Header, 1);
1826        reddb_file::encode_native_metadata_overflow_header(
1827            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1828            reddb_file::NativeMetadataOverflowHeader {
1829                format_version: STORE_VERSION_V11,
1830                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
1831                next_overflow_page_id: missing,
1832            },
1833        )
1834        .unwrap();
1835        pager.write_page(1, manifest).unwrap();
1836
1837        assert!(
1838            existing_overflow_chain(pager).is_err(),
1839            "a missing continuation must not be treated as an empty chain"
1840        );
1841
1842        let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
1843        let mut manifest = Page::new(PageType::Header, 1);
1844        reddb_file::encode_native_metadata_overflow_header(
1845            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1846            reddb_file::NativeMetadataOverflowHeader {
1847                format_version: STORE_VERSION_V11,
1848                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
1849                next_overflow_page_id: wrong_type.page_id(),
1850            },
1851        )
1852        .unwrap();
1853        pager.write_page(1, manifest).unwrap();
1854        assert!(
1855            existing_overflow_chain(pager).is_err(),
1856            "a live page must not be mistaken for metadata overflow"
1857        );
1858        assert!(
1859            read_meta_payload(pager).is_err(),
1860            "a live page must not be read as metadata overflow"
1861        );
1862
1863        let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
1864        let cycle_id = cycle.page_id();
1865        reddb_file::encode_native_metadata_overflow_continuation_header(
1866            &mut cycle.as_bytes_mut()[HEADER_SIZE..],
1867            reddb_file::NativeMetadataOverflowContinuationHeader {
1868                next_overflow_page_id: cycle_id,
1869                chunk_bytes: 1,
1870            },
1871        )
1872        .unwrap();
1873        pager.write_page(cycle_id, cycle).unwrap();
1874        let mut manifest = Page::new(PageType::Header, 1);
1875        reddb_file::encode_native_metadata_overflow_header(
1876            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1877            reddb_file::NativeMetadataOverflowHeader {
1878                format_version: STORE_VERSION_V11,
1879                total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
1880                next_overflow_page_id: cycle_id,
1881            },
1882        )
1883        .unwrap();
1884        pager.write_page(1, manifest).unwrap();
1885        assert!(
1886            read_meta_payload(pager).is_err(),
1887            "cyclic overflow must not be accepted"
1888        );
1889
1890        let mut manifest = Page::new(PageType::Header, 1);
1891        reddb_file::encode_native_metadata_overflow_header(
1892            &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1893            reddb_file::NativeMetadataOverflowHeader {
1894                format_version: STORE_VERSION_V11,
1895                total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
1896                next_overflow_page_id: 0,
1897            },
1898        )
1899        .unwrap();
1900        pager.write_page(1, manifest).unwrap();
1901        assert!(
1902            read_meta_payload(pager).is_err(),
1903            "a truncated metadata payload must not be accepted"
1904        );
1905    }
1906
1907    #[test]
1908    fn retired_tree_pages_remain_pinned_by_a_cursor() {
1909        let directory = tempfile::tempdir().expect("temporary store");
1910        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1911        for id in 1..=100 {
1912            store.insert_auto("rows", row(id, 1)).expect("seed");
1913        }
1914        store.persist().expect("initial checkpoint");
1915        let tree = store
1916            .btree_indices
1917            .read()
1918            .get("rows")
1919            .cloned()
1920            .expect("tree");
1921        let mut cursor = tree.cursor_first().expect("cursor");
1922        drop(tree);
1923        let manager = store.get_collection("rows").expect("rows");
1924        for value in 2..=6 {
1925            manager.update(row(1, value)).expect("change");
1926            store.persist().expect("checkpoint with old reader");
1927        }
1928        let mut count = 0;
1929        while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
1930            let (entity, _) =
1931                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1932                    .expect("decode");
1933            assert_eq!(
1934                entity.data.as_row().expect("row").columns,
1935                [Value::Integer(1)]
1936            );
1937            count += 1;
1938        }
1939        assert_eq!(count, 100);
1940        drop(cursor);
1941        store.persist().expect("reclaim after reader release");
1942        assert!(store.retired_btrees.lock().is_empty());
1943    }
1944
1945    #[test]
1946    fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
1947        let directory = tempfile::tempdir().expect("temporary store");
1948        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1949        store.create_collection("rows").expect("collection");
1950        let manager = store.get_collection("rows").expect("rows");
1951        for id in (1..=1200).rev() {
1952            manager
1953                .insert(UnifiedEntity::table_row(
1954                    EntityId::new(id),
1955                    "rows",
1956                    id,
1957                    vec![Value::text("x".repeat(4096))],
1958                ))
1959                .expect("seed");
1960        }
1961        CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
1962        store.persist().expect("bounded checkpoint");
1963        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
1964        assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
1965        assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
1966    }
1967
1968    #[test]
1969    fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1970        let directory = tempfile::tempdir().expect("temporary store");
1971        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1972        store.create_collection("rows").expect("collection");
1973        for id in 1..=40 {
1974            store
1975                .insert(
1976                    "rows",
1977                    UnifiedEntity::table_row(
1978                        EntityId::new(id),
1979                        "rows",
1980                        id,
1981                        vec![Value::Integer(id as i64)],
1982                    ),
1983                )
1984                .expect("seed row");
1985        }
1986        store.persist().expect("initial checkpoint");
1987        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1988        store.persist().expect("unchanged checkpoint");
1989        assert_eq!(
1990            CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1991            0,
1992            "unchanged collections must not hold the writer fence while serializing every row"
1993        );
1994        drop(store);
1995        let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1996        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1997        reopened.persist().expect("unchanged recovered generation");
1998        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1999    }
2000
2001    #[test]
2002    fn sparse_wal_replay_does_not_reencode_complete_collections() {
2003        let directory = tempfile::tempdir().expect("temporary store");
2004        let path = directory.path().join("data.rdb");
2005        let store = UnifiedStore::open(&path).expect("paged store");
2006        for (name, first) in [("rows", 1), ("cold", 129)] {
2007            store.create_collection(name).expect("collection");
2008            for id in first..first + 128 {
2009                store
2010                    .insert(
2011                        name,
2012                        UnifiedEntity::table_row(
2013                            EntityId::new(id),
2014                            name,
2015                            id,
2016                            vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2017                        ),
2018                    )
2019                    .expect("seed");
2020            }
2021        }
2022        store.persist().expect("durable baseline");
2023        let action = super::super::commit::StoreWalAction::upsert_entity(
2024            "rows",
2025            &row(1, 42),
2026            None,
2027            STORE_VERSION_V11,
2028        );
2029        // The acknowledged WAL image has not reached its home B-tree yet.
2030        store
2031            .commit
2032            .as_ref()
2033            .unwrap()
2034            .append_actions(&[action])
2035            .unwrap();
2036        drop(store);
2037
2038        REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2039        let recovered = UnifiedStore::open(&path).expect("replay committed change");
2040        assert_eq!(
2041            recovered
2042                .get("rows", EntityId::new(1))
2043                .unwrap()
2044                .data
2045                .as_row()
2046                .unwrap()
2047                .columns,
2048            vec![Value::Integer(42)]
2049        );
2050        for name in ["rows", "cold"] {
2051            assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
2052        }
2053        assert_eq!(
2054            REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
2055            0,
2056            "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
2057        );
2058        for (name, first) in [("rows", 2), ("cold", 129)] {
2059            let end = if name == "rows" { 129 } else { 257 };
2060            for id in first..end {
2061                assert_eq!(
2062                    recovered
2063                        .get(name, EntityId::new(id))
2064                        .unwrap()
2065                        .data
2066                        .as_row()
2067                        .unwrap()
2068                        .columns,
2069                    vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2070                );
2071            }
2072        }
2073
2074        drop(recovered);
2075        let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
2076        assert_eq!(
2077            reopened
2078                .get("rows", EntityId::new(1))
2079                .unwrap()
2080                .data
2081                .as_row()
2082                .unwrap()
2083                .columns,
2084            vec![Value::Integer(42)]
2085        );
2086    }
2087
2088    fn replay_test_payload(bytes: usize) -> String {
2089        let mut state = 0x1234_5678_u32;
2090        (0..bytes)
2091            .map(|_| {
2092                state ^= state << 13;
2093                state ^= state >> 17;
2094                state ^= state << 5;
2095                char::from(b'!' + (state % 90) as u8)
2096            })
2097            .collect()
2098    }
2099
2100    #[test]
2101    fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
2102        let directory = tempfile::tempdir().unwrap();
2103        let path = directory.path().join("data.rdb");
2104        let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
2105        let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2106        for id in 1..=16 {
2107            store.insert_auto("rows", row(id, 1)).unwrap();
2108        }
2109        store.persist().unwrap();
2110        // Larger than the retained-image allowance, so replay must fall back.
2111        let payload = replay_test_payload(64 * 1024);
2112        let changed =
2113            UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
2114        let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2115        store
2116            .commit
2117            .as_ref()
2118            .unwrap()
2119            .append_actions(&[action])
2120            .unwrap();
2121        drop(store);
2122        REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2123        let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
2124        assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
2125        assert_eq!(
2126            recovered
2127                .get("rows", EntityId::new(1))
2128                .unwrap()
2129                .data
2130                .as_row()
2131                .unwrap()
2132                .columns,
2133            changed.data.as_row().unwrap().columns
2134        );
2135        drop(recovered);
2136        let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
2137        assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
2138        assert_eq!(
2139            reopened
2140                .get("rows", EntityId::new(1))
2141                .unwrap()
2142                .data
2143                .as_row()
2144                .unwrap()
2145                .columns,
2146            changed.data.as_row().unwrap().columns
2147        );
2148    }
2149
2150    #[cfg(target_os = "linux")]
2151    #[test]
2152    fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
2153        const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
2154        if let Some(path) = std::env::var_os(CHILD) {
2155            let _store = UnifiedStore::open(path).unwrap();
2156            panic!("replay crash hook was not reached");
2157        }
2158        for overflow in [false, true] {
2159            for (hook, code) in [
2160                ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
2161                ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
2162            ] {
2163                let directory = tempfile::tempdir().unwrap();
2164                let path = directory.path().join("data.rdb");
2165                let store = UnifiedStore::open(&path).unwrap();
2166                for id in 1..=128 {
2167                    store.insert_auto("rows", row(id, 1)).unwrap();
2168                }
2169                store.persist().unwrap();
2170                let changed = if overflow {
2171                    UnifiedEntity::table_row(
2172                        EntityId::new(1),
2173                        "rows",
2174                        1,
2175                        vec![Value::text(replay_test_payload(16 * 1024))],
2176                    )
2177                } else {
2178                    row(1, 42)
2179                };
2180                let action =
2181                    StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2182                store
2183                    .commit
2184                    .as_ref()
2185                    .unwrap()
2186                    .append_actions(&[action])
2187                    .unwrap();
2188                drop(store);
2189                let mut first_size = 0;
2190                for cycle in 0..12 {
2191                    let output = std::process::Command::new(std::env::current_exe().unwrap())
2192                        .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
2193                        .env(CHILD, &path).env(hook, "1").output().unwrap();
2194                    assert_eq!(
2195                        output.status.code(),
2196                        Some(code),
2197                        "overflow={overflow} hook={hook} cycle={cycle}: {}",
2198                        String::from_utf8_lossy(&output.stderr)
2199                    );
2200                    let size = std::fs::metadata(&path).unwrap().len();
2201                    if cycle == 0 {
2202                        first_size = size;
2203                    }
2204                    assert!(
2205                        size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
2206                        "repeated recovery must reuse reservations: {first_size} -> {size}"
2207                    );
2208                }
2209                for _ in 0..2 {
2210                    let recovered = UnifiedStore::open(&path).unwrap();
2211                    assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
2212                    for id in 1..=128 {
2213                        let expected = if id == 1 { changed.clone() } else { row(id, 1) };
2214                        assert_eq!(
2215                            recovered
2216                                .get("rows", EntityId::new(id))
2217                                .unwrap()
2218                                .data
2219                                .as_row()
2220                                .unwrap()
2221                                .columns,
2222                            expected.data.as_row().unwrap().columns
2223                        );
2224                    }
2225                }
2226            }
2227        }
2228    }
2229
2230    #[test]
2231    fn paged_reopen_preserves_stored_sequence_identity() {
2232        let directory = tempfile::tempdir().expect("temporary store");
2233        let path = directory.path().join("data.rdb");
2234        let store = UnifiedStore::open(&path).expect("paged store");
2235        store.create_collection("rows").expect("collection");
2236        for id in [30, 10, 20] {
2237            store.insert("rows", row(id, id as i64)).expect("insert");
2238        }
2239        store.persist().expect("checkpoint");
2240        let sequences = [10, 20, 30].map(|id| {
2241            store
2242                .get("rows", EntityId::new(id))
2243                .expect("row")
2244                .sequence_id
2245        });
2246        drop(store);
2247        let reopened = UnifiedStore::open(&path).expect("reopen");
2248        let recovered = [10, 20, 30].map(|id| {
2249            reopened
2250                .get("rows", EntityId::new(id))
2251                .expect("row")
2252                .sequence_id
2253        });
2254        assert_eq!(
2255            recovered, sequences,
2256            "opening a checkpoint must not rewrite stored row identities"
2257        );
2258    }
2259
2260    #[test]
2261    fn auto_insert_persists_the_assigned_sequence_identity() {
2262        let directory = tempfile::tempdir().expect("temporary store");
2263        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2264        for id in 1u64..=3 {
2265            store
2266                .insert_auto("rows", row(id, id as i64))
2267                .expect("insert");
2268        }
2269        let manager = store.get_collection("rows").expect("rows");
2270        let tree = store
2271            .btree_indices
2272            .read()
2273            .get("rows")
2274            .cloned()
2275            .expect("tree");
2276        for id in 1u64..=3 {
2277            let bytes = tree
2278                .get(&id.to_be_bytes())
2279                .expect("lookup")
2280                .expect("stored row");
2281            let (persisted, _) =
2282                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2283                    .expect("decode");
2284            assert_eq!(
2285                persisted.sequence_id,
2286                manager
2287                    .get(EntityId::new(id))
2288                    .expect("live row")
2289                    .sequence_id
2290            );
2291        }
2292    }
2293
2294    #[test]
2295    fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
2296        let directory = tempfile::tempdir().expect("temporary store");
2297        let path = directory.path().join("data.rdb");
2298        let store = UnifiedStore::open(&path).expect("paged store");
2299        store.insert_auto("rows", row(1, 1)).expect("seed");
2300        store.persist().expect("checkpoint");
2301        store.insert_auto("rows", row(2, 2)).expect("append");
2302        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2303        store.persist().expect("inline append checkpoint");
2304        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2305        store
2306            .get_collection("rows")
2307            .expect("rows")
2308            .update(row(1, 99))
2309            .expect("untracked edit");
2310        store.insert_auto("rows", row(3, 3)).expect("next append");
2311        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2312        store.persist().expect("untracked edit checkpoint");
2313        assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
2314        drop(store);
2315        let reopened = UnifiedStore::open(&path).expect("reopen");
2316        assert_eq!(
2317            reopened
2318                .get("rows", EntityId::new(1))
2319                .expect("row")
2320                .data
2321                .as_row()
2322                .expect("table")
2323                .columns,
2324            vec![Value::Integer(99)]
2325        );
2326    }
2327
2328    #[test]
2329    fn replayed_trees_are_reusable_after_durable_reconciliation() {
2330        let directory = tempfile::tempdir().expect("temporary store");
2331        let path = directory.path().join("data.rdb");
2332        let store = UnifiedStore::open(&path).expect("paged store");
2333        store.insert_auto("rows", row(1, 1)).expect("seed");
2334        store.persist().expect("checkpoint");
2335        store
2336            .insert_auto("rows", row(2, 2))
2337            .expect("WAL-backed append");
2338        drop(store);
2339        let reopened = UnifiedStore::open(&path).expect("replay");
2340        assert!(reopened.get("rows", EntityId::new(2)).is_some());
2341        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2342        reopened.persist().expect("checkpoint reconciled tree");
2343        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2344    }
2345
2346    fn row(id: u64, value: i64) -> UnifiedEntity {
2347        UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
2348    }
2349
2350    #[test]
2351    fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
2352        let directory = tempfile::tempdir().expect("temporary store");
2353        let path = directory.path().join("data.rdb");
2354        let store = UnifiedStore::open(&path).expect("paged store");
2355        store.create_collection("rows").expect("collection");
2356        store.insert("rows", row(1, 1)).expect("seed");
2357        store.persist().expect("initial checkpoint");
2358        let manager = store.get_collection("rows").expect("rows");
2359        manager.update(row(1, 2)).expect("direct update");
2360        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2361        store.persist().expect("updated row checkpoint");
2362        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2363        let mut metadata = Metadata::new();
2364        metadata.set("verified", MetadataValue::Bool(true));
2365        manager
2366            .set_metadata(EntityId::new(1), metadata)
2367            .expect("metadata");
2368        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2369        store.persist().expect("metadata-only checkpoint");
2370        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2371        manager
2372            .bulk_insert(vec![row(2, 2), row(3, 3)])
2373            .expect("bulk insert");
2374        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2375        store.persist().expect("bulk insert checkpoint");
2376        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
2377        manager.delete(EntityId::new(2)).expect("delete");
2378        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2379        store.persist().expect("changed checkpoint");
2380        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
2381        drop(store);
2382        let recovered = UnifiedStore::open(&path).expect("recover changed rows");
2383        let manager = recovered.get_collection("rows").expect("rows");
2384        assert_eq!(manager.count(), 2);
2385        assert_eq!(
2386            manager
2387                .get(EntityId::new(1))
2388                .expect("row")
2389                .data
2390                .as_row()
2391                .expect("table")
2392                .columns,
2393            vec![Value::Integer(2)]
2394        );
2395        assert!(manager.get(EntityId::new(2)).is_none());
2396        assert!(manager
2397            .get_metadata(EntityId::new(1))
2398            .expect("metadata")
2399            .get("verified")
2400            .is_some());
2401    }
2402
2403    #[test]
2404    fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
2405        let directory = tempfile::tempdir().expect("temporary store");
2406        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2407        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2408        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2409        store.create_collection("rows").expect("collection");
2410        for id in 1..=40 {
2411            store.insert("rows", row(id, id as i64)).expect("seed");
2412        }
2413        store.persist().expect("initial checkpoint");
2414        let unrelated = snapshots.begin();
2415        snapshots.rollback(unrelated);
2416        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2417        store.persist().expect("unrelated rollback checkpoint");
2418        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2419
2420        store
2421            .pending_vault_versions
2422            .write()
2423            .insert(EntityId::new(100));
2424        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2425        store.persist().expect("unrelated vault checkpoint");
2426        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2427        store.pending_vault_versions.write().clear();
2428        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2429        store
2430            .persist()
2431            .expect("unrelated vault publication checkpoint");
2432        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2433    }
2434
2435    #[test]
2436    fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
2437        let directory = tempfile::tempdir().expect("temporary store");
2438        let path = directory.path().join("data.rdb");
2439        let store = UnifiedStore::open(&path).expect("paged store");
2440        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2441        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2442        store.create_collection("rows").expect("collection");
2443        let xid = snapshots.begin();
2444        let mut entity = row(1, 1);
2445        entity.xmin = xid;
2446        store.insert("rows", entity).expect("committed row");
2447        snapshots.commit(xid);
2448        store.persist().expect("committed baseline");
2449        snapshots.rollback(xid);
2450        store
2451            .persist()
2452            .expect("untracked outcome requires validation");
2453        drop(store);
2454        let recovered = UnifiedStore::open(&path).expect("recover");
2455        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2456    }
2457
2458    #[test]
2459    fn exhausted_deferred_row_cache_keeps_full_validation() {
2460        let directory = tempfile::tempdir().expect("temporary store");
2461        let path = directory.path().join("data.rdb");
2462        let store = UnifiedStore::open(&path).expect("paged store");
2463        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2464        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2465        store.create_collection("rows").expect("collection");
2466        let xid = snapshots.begin();
2467        for id in 1..=1025 {
2468            let mut entity = row(id, id as i64);
2469            entity.xmin = xid;
2470            store.insert("rows", entity).expect("pending row");
2471        }
2472        store.persist().expect("exclude pending rows");
2473        assert!(store.checkpoint_versions.read()["rows"]
2474            .deferred_rows
2475            .is_none());
2476        snapshots.commit(xid);
2477        store
2478            .persist()
2479            .expect("publish committed rows conservatively");
2480        drop(store);
2481        let recovered = UnifiedStore::open(&path).expect("recover");
2482        assert_eq!(
2483            recovered.get_collection("rows").expect("rows").count(),
2484            1025
2485        );
2486    }
2487
2488    #[test]
2489    fn rollback_invalidates_reuse_without_a_segment_mutation() {
2490        let directory = tempfile::tempdir().expect("temporary store");
2491        let path = directory.path().join("data.rdb");
2492        let store = UnifiedStore::open(&path).expect("paged store");
2493        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2494        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2495        store.create_collection("rows").expect("collection");
2496        let xid = snapshots.begin();
2497        let mut pending = row(1, 1);
2498        pending.xmin = xid;
2499        store.insert("rows", pending).expect("version");
2500        store.persist().expect("before rollback");
2501        snapshots.rollback(xid);
2502        store.persist().expect("after rollback");
2503        drop(store);
2504        let recovered = UnifiedStore::open(&path).expect("recover");
2505        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2506    }
2507
2508    #[test]
2509    fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
2510        let directory = tempfile::tempdir().expect("temporary store");
2511        let path = directory.path().join("data.rdb");
2512        let store = UnifiedStore::open(&path).expect("paged store");
2513        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2514        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2515        store.create_collection("rows").expect("collection");
2516        let xid = snapshots.begin();
2517        let mut deleted = row(1, 1);
2518        deleted.xmax = xid;
2519        store.insert("rows", deleted).expect("version");
2520        store.persist().expect("before rollback");
2521        snapshots.rollback(xid);
2522        store.persist().expect("after rollback");
2523        drop(store);
2524        let recovered = UnifiedStore::open(&path).expect("recover");
2525        let manager = recovered.get_collection("rows").expect("rows");
2526        assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
2527    }
2528
2529    #[test]
2530    fn publishing_a_pending_vault_version_invalidates_reuse() {
2531        let directory = tempfile::tempdir().expect("temporary store");
2532        let path = directory.path().join("data.rdb");
2533        let store = UnifiedStore::open(&path).expect("paged store");
2534        store.create_collection("rows").expect("collection");
2535        store.insert("rows", row(1, 1)).expect("row");
2536        store
2537            .pending_vault_versions
2538            .write()
2539            .insert(EntityId::new(1));
2540        store.persist().expect("exclude pending vault version");
2541        store
2542            .pending_vault_versions
2543            .write()
2544            .remove(&EntityId::new(1));
2545        store.persist().expect("include published vault version");
2546        drop(store);
2547        let recovered = UnifiedStore::open(&path).expect("recover");
2548        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
2549    }
2550}