Skip to main content

reddb_server/storage/unified/store/
impl_pages.rs

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