Skip to main content

reddb_server/storage/unified/store/
impl_pages.rs

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