Skip to main content

reddb_server/storage/unified/store/
impl_pages.rs

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