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