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
5#[cfg(test)]
6thread_local! {
7    static CHECKPOINT_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
8}
9
10// ── Pager-meta overflow chain (gh-477) ──────────────────────────────────────
11// When the serialized collection registry + cross-refs exceed a single page,
12// page 1 carries a native metadata-overflow header pointing at an overflow chain of
13// `PageType::Overflow` pages. Single-page metadata keeps the historical
14// bit-identical layout (`METADATA_MAGIC = "RDM2"` written directly at the
15// content offset).
16//
17// Page 1 (overflow form), starting at `HEADER_SIZE`:
18//   [0..4]   native metadata-overflow magic
19//   [4..8]   format_version (u32, mirrors inner payload version for debug)
20//   [8..12]  total_payload_bytes (u32)
21//   [12..16] next_overflow_page_id (u32, > 0)
22//   [16..]   first payload chunk (up to META_V3_FIRST_PAYLOAD_CAP bytes)
23//
24// Overflow continuation page, starting at `HEADER_SIZE`:
25//   [0..4]   next_overflow_page_id (u32, 0 if last)
26//   [4..8]   chunk_bytes (u32)
27//   [8..]    chunk payload (up to META_V3_OVERFLOW_PAYLOAD_CAP bytes)
28const META_PAGE_CONTENT_CAP: usize =
29    crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
30const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
31const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
32const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
33const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
34
35fn tree_matches_records(tree: &BTree, records: &[(Vec<u8>, Vec<u8>)]) -> Result<bool, StoreError> {
36    let mut cursor = tree
37        .cursor_first()
38        .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
39    for record in records {
40        if cursor
41            .next()
42            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
43            .as_ref()
44            != Some(record)
45        {
46            return Ok(false);
47        }
48    }
49    Ok(cursor
50        .next()
51        .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
52        .is_none())
53}
54
55fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
56    let cs = crate::storage::engine::HEADER_SIZE;
57    let page = match pager.read_page(1) {
58        Ok(p) => p,
59        Err(_) => return Ok(Vec::new()),
60    };
61    let bytes = page.as_bytes();
62    if bytes.len() < cs + META_V3_PAGE1_HEADER {
63        return Ok(Vec::new());
64    }
65    let Some(header) =
66        reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
67            PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
68        })?
69    else {
70        return Ok(Vec::new());
71    };
72    let mut next = header.next_overflow_page_id;
73    let mut pages = Vec::new();
74    let mut seen = std::collections::HashSet::new();
75    while next != 0 {
76        if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
77            return Err(PagerError::InvalidDatabase(
78                "cyclic metadata overflow chain".into(),
79            ));
80        }
81        let ov = match pager.read_page(next) {
82            Ok(p) => p,
83            Err(_) => break,
84        };
85        let ob = ov.as_bytes();
86        let nn = match reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..]) {
87            Ok(header) => header.next_overflow_page_id,
88            Err(_) => 0,
89        };
90        pages.push(next);
91        next = nn;
92    }
93    Ok(pages)
94}
95
96fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
97    let retired = existing_overflow_chain(pager)?;
98    let page = build_meta_page1_with_overflow(pager, meta_data)?;
99    // Cache eviction order must not publish a root before its children.
100    pager.sync()?;
101    pager.write_page(1, page)?;
102    pager.sync()?;
103    for id in retired {
104        pager.free_page(id)?;
105    }
106    Ok(())
107}
108
109fn build_meta_page1_with_overflow(
110    pager: &Pager,
111    meta_data: &[u8],
112) -> Result<crate::storage::engine::Page, PagerError> {
113    use crate::storage::engine::{Page, PageType, HEADER_SIZE};
114
115    let mut page1 = Page::new(PageType::Header, 1);
116    let cs = HEADER_SIZE;
117
118    if meta_data.len() <= META_PAGE_CONTENT_CAP {
119        // Single-page: bit-identical to the historical layout.
120        let buf = page1.as_bytes_mut();
121        buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
122        return Ok(page1);
123    }
124
125    // Multi-page overflow form. Split the inner payload into the first chunk
126    // (held on page 1) followed by zero-or-more continuation chunks chained
127    // through `PageType::Overflow` pages.
128    let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
129    let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
130    let mut chunks: Vec<&[u8]> = Vec::new();
131    while !tail.is_empty() {
132        let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
133        chunks.push(&tail[..take]);
134        tail = &tail[take..];
135    }
136
137    let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
138    let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
139    for _ in 0..chunks.len() {
140        let pg = pager.allocate_page(PageType::Overflow)?;
141        overflow_ids.push(pg.page_id());
142        overflow_pages.push(pg);
143    }
144
145    for i in 0..chunks.len() {
146        let next = if i + 1 < chunks.len() {
147            overflow_ids[i + 1]
148        } else {
149            0u32
150        };
151        let len = chunks[i].len() as u32;
152        let buf = overflow_pages[i].as_bytes_mut();
153        reddb_file::encode_native_metadata_overflow_continuation_header(
154            &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
155            reddb_file::NativeMetadataOverflowContinuationHeader {
156                next_overflow_page_id: next,
157                chunk_bytes: len,
158            },
159        )
160        .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
161        buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
162    }
163    for (idx, page) in overflow_pages.into_iter().enumerate() {
164        let id = overflow_ids[idx];
165        pager.write_page(id, page)?;
166    }
167
168    // Mirror the inner format_version for debug-friendly hex dumps.
169    let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
170        .ok()
171        .flatten()
172        .map_or(0, |header| header.format_version);
173
174    let buf = page1.as_bytes_mut();
175    reddb_file::encode_native_metadata_overflow_header(
176        &mut buf[cs..cs + META_V3_PAGE1_HEADER],
177        reddb_file::NativeMetadataOverflowHeader {
178            format_version,
179            total_payload_bytes: meta_data.len() as u32,
180            next_overflow_page_id: overflow_ids[0],
181        },
182    )
183    .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
184    buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
185        .copy_from_slice(first_chunk);
186
187    Ok(page1)
188}
189
190fn storage_integrity(
191    zone: &str,
192    id: impl Into<String>,
193    collection: Option<String>,
194    detail: impl Into<String>,
195) -> StoreError {
196    StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
197        zone, id, collection, detail,
198    ))
199}
200
201fn page_integrity(
202    page_id: u32,
203    collection: Option<String>,
204    detail: impl Into<String>,
205) -> StoreError {
206    storage_integrity("page", page_id.to_string(), collection, detail)
207}
208
209/// Assemble the full metadata payload from the internal manifest zone: page 1
210/// plus its overflow chain when the native overflow wrapper is present. Returns
211/// the bytes that the metadata parser would see starting from the content
212/// offset of page 1. Single-page metadata returns the raw page content
213/// (including trailing zero-pad), so the legacy parser sees the same bytes it
214/// always saw.
215///
216/// The zone is rooted by the superblock and checksummed per ADR 0074 §2: a
217/// failing checksum surfaces the didactic manifest-zone integrity error
218/// directly — there is no sidecar copy to fall back to since ADR 0038 §4
219/// phase 1 retired the shadows.
220fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
221    let cs = crate::storage::engine::HEADER_SIZE;
222    let meta_page = pager.read_manifest_page().map_err(|err| {
223        storage_integrity(
224            "manifest",
225            "manifest",
226            None,
227            format!("internal manifest zone read failed ({err})"),
228        )
229    })?;
230    let bytes = meta_page.as_bytes();
231    if bytes.len() < cs + 4 {
232        return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
233    }
234    let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
235        .map_err(|err| page_integrity(1, None, err.to_string()))?
236    {
237        Some(header) => header,
238        None => {
239            return Ok(Some(bytes[cs..].to_vec()));
240        }
241    };
242    if bytes.len() < cs + META_V3_PAGE1_HEADER {
243        return Err(page_integrity(
244            1,
245            None,
246            "metadata overflow header is truncated",
247        ));
248    }
249    let total = header.total_payload_bytes as usize;
250    let mut next = header.next_overflow_page_id;
251    let mut payload: Vec<u8> = Vec::with_capacity(total);
252    let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
253    payload.extend_from_slice(
254        &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
255    );
256    while next != 0 && payload.len() < total {
257        let ov = pager
258            .read_page(next)
259            .map_err(|err| page_integrity(next, None, err.to_string()))?;
260        let ob = ov.as_bytes();
261        if ob.len() < cs + META_V3_OVERFLOW_HEADER {
262            return Err(page_integrity(
263                next,
264                None,
265                "metadata overflow continuation is truncated",
266            ));
267        }
268        let continuation =
269            reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
270                .map_err(|err| page_integrity(next, None, err.to_string()))?;
271        let nn = continuation.next_overflow_page_id;
272        let len = continuation.chunk_bytes as usize;
273        let remaining = total - payload.len();
274        let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
275        payload.extend_from_slice(
276            &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
277        );
278        next = nn;
279    }
280    Ok(Some(payload))
281}
282
283impl UnifiedStore {
284    pub(crate) fn mark_paged_registry_dirty(&self) {
285        self.paged_registry_dirty.store(true, Ordering::Release);
286    }
287
288    /// Get (or lazily create) the per-collection B-tree under a *read*
289    /// lock whenever possible. Returns a cloned `Arc<BTree>` so callers
290    /// can mutate the tree without holding the outer map's RwLock —
291    /// previously every insert serialised on `btree_indices.write()`,
292    /// costing ~60% of the concurrent-insert throughput ceiling.
293    pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
294        let pager = self.pager.as_ref()?;
295        if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
296            return Some(btree);
297        }
298        let mut write = self.btree_indices.write();
299        let btree = write
300            .entry(collection.to_string())
301            .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
302            .clone();
303        Some(btree)
304    }
305
306    pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
307        let Some(pager) = &self.pager else {
308            return Ok(());
309        };
310
311        if self.paged_registry_dirty.load(Ordering::Acquire) {
312            self.flush_paged_registry()?;
313            self.paged_registry_dirty.store(false, Ordering::Release);
314            return Ok(());
315        }
316
317        pager
318            .flush()
319            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
320    }
321
322    pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
323        let Some(pager) = &self.pager else {
324            return Ok(());
325        };
326
327        match pager.read_page(1) {
328            Ok(_) => {}
329            Err(PagerError::PageNotFound(_)) => {
330                let meta_page = pager
331                    .allocate_page(crate::storage::engine::PageType::Header)
332                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
333                pager
334                    .write_page(meta_page.page_id(), meta_page)
335                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
336            }
337            Err(e) => {
338                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
339            }
340        }
341
342        let format_version = STORE_VERSION_V11;
343        self.set_format_version(format_version);
344
345        let collections = self.collections.read();
346        let btree_indices = self.btree_indices.read();
347        let mut collection_roots = Vec::with_capacity(collections.len());
348        for (name, _) in collections.iter() {
349            let root_page = btree_indices
350                .get(name)
351                .map_or(0, |btree| btree.root_page_id());
352            collection_roots.push((name.clone(), root_page));
353        }
354        drop(btree_indices);
355        drop(collections);
356
357        let mut meta_data = Vec::with_capacity(4096);
358        reddb_file::encode_native_paged_metadata_header(
359            &mut meta_data,
360            reddb_file::NativePagedMetadataHeader {
361                format_version,
362                collection_count: collection_roots.len() as u32,
363            },
364        );
365        for (name, root_page) in &collection_roots {
366            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
367        }
368
369        let cross_refs = self.cross_refs.read();
370        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
371        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
372        for (source_id, refs) in cross_refs.iter() {
373            for (target_id, ref_type, collection) in refs {
374                reddb_file::encode_native_paged_cross_ref(
375                    &mut meta_data,
376                    source_id.raw(),
377                    target_id.raw(),
378                    ref_type.to_byte(),
379                    collection,
380                );
381            }
382        }
383
384        publish_paged_metadata(pager, &meta_data)
385            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
386
387        Ok(())
388    }
389
390    /// Get a reference to the underlying pager (if in paged mode).
391    pub fn pager(&self) -> Option<&Arc<Pager>> {
392        self.pager.as_ref()
393    }
394
395    /// Bytes of page-cache memory currently occupied by resident pages
396    /// (ADR 0073 §2). Slots are fixed size, so occupancy times the page size
397    /// *is* the footprint; a store without a pager occupies none.
398    pub fn page_cache_bytes_in_use(&self) -> u64 {
399        self.pager.as_ref().map_or(0, |pager| {
400            pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
401        })
402    }
403
404    /// Bytes of WAL buffer memory held by this store's group-commit
405    /// coordinator: queued records plus the writer's fixed append buffer.
406    pub fn wal_buffer_bytes_in_use(&self) -> u64 {
407        self.commit
408            .as_ref()
409            .map_or(0, |commit| commit.buffered_bytes())
410    }
411
412    /// Approximate resident bytes across every collection's segment arena
413    /// (ADR 0073 §2). Same number `stats().total_memory_bytes` reports, without
414    /// building the per-collection stats map a sampler would throw away.
415    pub fn segment_memory_bytes(&self) -> u64 {
416        let managers: Vec<Arc<SegmentManager>> =
417            self.collections.read().values().map(Arc::clone).collect();
418
419        managers
420            .iter()
421            .map(|manager| manager.resident_bytes())
422            .fold(0, u64::saturating_add)
423    }
424
425    /// Borrow the immutable store configuration. Runtime hooks (e.g. the
426    /// `auto_index_id` first-insert hook in `MutationEngine`) read knobs
427    /// off this struct without going through the legacy global config tree.
428    pub fn config(&self) -> &UnifiedStoreConfig {
429        &self.config
430    }
431
432    pub fn with_config(config: UnifiedStoreConfig) -> Self {
433        Self {
434            config,
435            format_version: AtomicU32::new(STORE_VERSION_V11),
436            next_entity_id: AtomicU64::new(1),
437            collections: RwLock::new(HashMap::new()),
438            cross_refs: RwLock::new(HashMap::new()),
439            reverse_refs: RwLock::new(HashMap::new()),
440            pager: None,
441            db_path: None,
442            btree_indices: RwLock::new(HashMap::new()),
443            checkpoint_versions: RwLock::new(HashMap::new()),
444            context_index: ContextIndex::new(),
445            entity_cache: EntityCache::new(),
446            graph_label_index: RwLock::new(HashMap::new()),
447            paged_registry_dirty: AtomicBool::new(false),
448            commit: None,
449            unindex_cross_refs_fast_path: AtomicU64::new(0),
450            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
451            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
452            aux_metadata: RwLock::new(Vec::new()),
453            snapshot_manager: std::sync::OnceLock::new(),
454            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
455            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
456        }
457    }
458
459    /// Open or create a page-based database
460    ///
461    /// This uses the page engine for ACID durability with B-tree indices.
462    /// The database file uses 16 KiB pages with checksums and efficient caching.
463    ///
464    /// # Arguments
465    ///
466    /// * `path` - Path to the database file (e.g., "data.rdb")
467    ///
468    /// # Example
469    ///
470    /// ```rust,ignore
471    /// let store = UnifiedStore::open("security.rdb")?;
472    /// store.create_collection("hosts")?;
473    /// // ... operations ...
474    /// store.persist()?; // Flush to disk
475    /// ```
476    pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
477        Self::open_with_config(path, UnifiedStoreConfig::default())
478    }
479
480    pub fn open_with_config(
481        path: impl AsRef<Path>,
482        config: UnifiedStoreConfig,
483    ) -> Result<Self, StoreError> {
484        let path = path.as_ref();
485        let mut pager_config = PagerConfig::default();
486        // ADR 0073 §2 — the page cache is pre-sized from its budget share.
487        // The pager's own `cache_size` default survives only for direct
488        // library callers who never resolved a budget.
489        if let Some(slots) = config.page_cache_slots {
490            pager_config.cache_size = slots;
491        }
492        // Tunables via env — experimental, used by the benchmark harness
493        // to compare durability profiles head-to-head with Postgres.
494        // REDDB_DOUBLE_WRITE=0 requests skipping the double-write buffer,
495        // which otherwise adds two fsyncs per pager flush (one on DWB, one
496        // on the main file). The pager honors this only when the actual
497        // data file is proven to live on a CoW filesystem with atomic page
498        // writes; otherwise it fails closed and keeps DWB enabled.
499        if matches!(
500            std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
501            Some("0") | Some("false") | Some("off")
502        ) {
503            pager_config.double_write = false;
504        }
505        let pager = Pager::open(path, pager_config)
506            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
507
508        let wal_path = reddb_file::layout::unified_wal_path(path);
509        // Explicit transactions and WAL-only vault versions need committed
510        // batches in every durability mode, including Strict's inline fsync.
511        let commit = Some(Arc::new(
512            StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
513                .map_err(StoreError::Io)?,
514        ));
515
516        let store = Self {
517            config,
518            format_version: AtomicU32::new(STORE_VERSION_V11),
519            next_entity_id: AtomicU64::new(1),
520            collections: RwLock::new(HashMap::new()),
521            cross_refs: RwLock::new(HashMap::new()),
522            reverse_refs: RwLock::new(HashMap::new()),
523            pager: Some(Arc::new(pager)),
524            db_path: Some(path.to_path_buf()),
525            btree_indices: RwLock::new(HashMap::new()),
526            checkpoint_versions: RwLock::new(HashMap::new()),
527            context_index: ContextIndex::new(),
528            entity_cache: EntityCache::new(),
529            graph_label_index: RwLock::new(HashMap::new()),
530            paged_registry_dirty: AtomicBool::new(false),
531            commit,
532            unindex_cross_refs_fast_path: AtomicU64::new(0),
533            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
534            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
535            aux_metadata: RwLock::new(Vec::new()),
536            snapshot_manager: std::sync::OnceLock::new(),
537            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
538            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
539        };
540
541        // Load existing data from pages if database exists
542        store.load_from_pages()?;
543        if let Some(commit) = &store.commit {
544            commit.replay_into(&store).map_err(StoreError::Io)?;
545        }
546        store.recover_operational_manifest()?;
547
548        Ok(store)
549    }
550
551    /// Replay changes the authoritative managers first. Build replacement trees
552    /// only for changed collections, leaving every published tree untouched until
553    /// all replacement pages are durable. Retain the WAL for auxiliary recovery.
554    pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
555        let Some(pager) = &self.pager else {
556            return Ok(());
557        };
558        let collections = self.collections.read();
559        let mut indices = self.btree_indices.write();
560        for (name, manager) in collections.iter() {
561            let mut records: Vec<_> = manager
562                .query_all(|_| true)
563                .into_iter()
564                .map(|entity| {
565                    let metadata = manager.get_metadata(entity.id);
566                    (
567                        entity.id.raw().to_be_bytes().to_vec(),
568                        Self::serialize_entity_record(
569                            &entity,
570                            metadata.as_ref(),
571                            self.format_version(),
572                        ),
573                    )
574                })
575                .collect();
576            records.sort_by(|left, right| left.0.cmp(&right.0));
577            let matches = if let Some(tree) = indices.get(name) {
578                tree_matches_records(tree, &records)?
579            } else {
580                records.is_empty()
581            };
582            if matches {
583                continue;
584            }
585            let tree = Arc::new(BTree::new(Arc::clone(pager)));
586            tree.bulk_insert_sorted(&records).map_err(|e| {
587                StoreError::Io(std::io::Error::other(format!(
588                    "WAL tree rebuild for '{name}': {e}"
589                )))
590            })?;
591            indices.insert(name.clone(), tree);
592            self.mark_paged_registry_dirty();
593        }
594        drop(indices);
595        drop(collections);
596        self.flush_paged_state()
597    }
598
599    pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
600        let Some(path) = &self.db_path else {
601            return Ok(());
602        };
603        let mut collections = self.list_collections();
604        collections.sort();
605        let pending_drops =
606            crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
607                .recover_or_bootstrap(&collections)
608                .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
609        for name in pending_drops {
610            if self.get_collection(&name).is_some() {
611                self.drop_collection(&name)?;
612            }
613        }
614        Ok(())
615    }
616
617    pub(crate) fn prepare_operational_collection_create(
618        &self,
619        name: &str,
620    ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
621        let Some(path) = &self.db_path else {
622            return Ok(None);
623        };
624        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
625            .prepare_collection_create(name)
626            .map(Some)
627            .map_err(StoreError::Io)
628    }
629
630    pub(crate) fn prepare_operational_collection_drop(
631        &self,
632        name: &str,
633    ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
634        let Some(path) = &self.db_path else {
635            return Ok(None);
636        };
637        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
638            .prepare_collection_drop(name)
639            .map(Some)
640            .map_err(StoreError::Io)
641    }
642
643    /// Load data from page-based storage
644    ///
645    /// Reads the B-tree indices and reconstructs collections from pages.
646    fn load_from_pages(&self) -> Result<(), StoreError> {
647        let pager = match &self.pager {
648            Some(p) => p,
649            None => return Ok(()), // No pager, nothing to load
650        };
651
652        // Get page count
653        let page_count = pager.page_count().map_err(|e| {
654            StoreError::Io(std::io::Error::other(format!(
655                "failed to read page count: {}",
656                e
657            )))
658        })?;
659        if page_count <= 1 {
660            // Empty database (only header page)
661            return Ok(());
662        }
663
664        // Read metadata starting from page 1 (collections registry). The
665        // helper transparently follows the `RDM3` overflow chain when the
666        // metadata blob spans multiple pages and falls back to the legacy
667        // `<data>-meta` shadow when page 1 itself is corrupted.
668        if let Some(content_vec) = read_meta_payload(pager)? {
669            let content: &[u8] = &content_vec;
670            if content.len() >= 4 {
671                let mut pos = 0;
672                let mut format_version = STORE_VERSION_V1;
673
674                let collection_count = if let Some(header) =
675                    reddb_file::decode_native_paged_metadata_header(content)
676                        .map_err(|err| StoreError::Serialization(err.to_string()))?
677                {
678                    format_version = header.format_version;
679                    pos += reddb_file::METADATA_HEADER_BYTES;
680                    header.collection_count as usize
681                } else {
682                    let count = u32::from_le_bytes([
683                        content[pos],
684                        content[pos + 1],
685                        content[pos + 2],
686                        content[pos + 3],
687                    ]) as usize;
688                    pos += 4;
689                    count
690                };
691
692                self.set_format_version(format_version);
693
694                if pos > content.len() {
695                    return Ok(());
696                }
697
698                // Read collection names and their B-tree root pages
699                for _ in 0..collection_count {
700                    if let Ok(root) =
701                        reddb_file::decode_native_paged_collection_root(content, &mut pos)
702                    {
703                        // Root page ID for this collection's B-tree
704                        let root_page = root.root_page;
705                        let name = root.collection;
706
707                        // Hydrate the collection in memory only. Loading must
708                        // not emit WAL entries or rewrite the on-disk registry
709                        // before the existing B-tree roots are attached.
710                        let _ = self.create_collection_in_memory(&name);
711
712                        // Load B-tree with root page if it exists
713                        if root_page > 0 {
714                            let btree = BTree::with_root(Arc::clone(pager), root_page);
715
716                            // Load all entities from B-tree into the collection
717                            let mut cursor = btree.cursor_first().map_err(|err| {
718                                page_integrity(root_page, Some(name.clone()), err.to_string())
719                            })?;
720                            let manager = self.get_collection(&name);
721                            loop {
722                                let next = cursor.next().map_err(|err| {
723                                    page_integrity(root_page, Some(name.clone()), err.to_string())
724                                })?;
725                                let Some((key, value)) = next else {
726                                    break;
727                                };
728                                // Deserialize entity from value bytes
729                                if let Ok((entity, metadata)) =
730                                    Self::deserialize_entity_record(&value, self.format_version())
731                                {
732                                    if let Some(m) = &manager {
733                                        let id = entity.id;
734                                        if let EntityKind::TableRow { row_id, .. } = &entity.kind {
735                                            m.register_row_id(*row_id);
736                                        }
737                                        self.context_index.index_entity(&name, &entity);
738                                        let _ = m.insert(entity.clone());
739                                        if let Some(metadata) = metadata {
740                                            let _ = m.set_metadata(id, metadata);
741                                        }
742                                        self.register_entity_id(id);
743                                        if self.config.auto_index_refs {
744                                            self.index_cross_refs(&entity, &name)?;
745                                        }
746                                    }
747                                }
748                            }
749
750                            // Store the B-tree for future lookups
751                            self.btree_indices
752                                .write()
753                                .insert(name.clone(), Arc::new(btree));
754                            if format_version == STORE_VERSION_V11 {
755                                if let Some(manager) = &manager {
756                                    self.checkpoint_versions.write().insert(
757                                        name,
758                                        CheckpointVersion {
759                                            manager: Arc::downgrade(manager),
760                                            segments: manager.checkpoint_revisions(),
761                                            aborted_revision: 0,
762                                            pending_vault: Arc::default(),
763                                            format_version,
764                                        },
765                                    );
766                                }
767                            }
768                        }
769                    } else {
770                        break;
771                    }
772                }
773
774                if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
775                    let cross_ref_count = u32::from_le_bytes([
776                        content[pos],
777                        content[pos + 1],
778                        content[pos + 2],
779                        content[pos + 3],
780                    ]) as usize;
781                    pos += 4;
782
783                    for _ in 0..cross_ref_count {
784                        let Ok(cross_ref) =
785                            reddb_file::decode_native_paged_cross_ref(content, &mut pos)
786                        else {
787                            break;
788                        };
789                        let source_id = EntityId::new(cross_ref.source_id);
790                        let target_id = EntityId::new(cross_ref.target_id);
791                        let ref_type = RefType::from_byte(cross_ref.ref_type);
792                        let target_collection = cross_ref.target_collection;
793
794                        self.cross_refs.write().entry(source_id).or_default().push((
795                            target_id,
796                            ref_type,
797                            target_collection.clone(),
798                        ));
799
800                        if let Some((collection, mut entity)) = self.get_any(source_id) {
801                            let exists = entity.cross_refs().iter().any(|xref| {
802                                xref.target == target_id
803                                    && xref.ref_type == ref_type
804                                    && xref.target_collection == target_collection
805                            });
806                            if !exists {
807                                entity.cross_refs_mut().push(CrossRef::new(
808                                    source_id,
809                                    target_id,
810                                    target_collection.clone(),
811                                    ref_type,
812                                ));
813                                if let Some(manager) = self.get_collection(&collection) {
814                                    let _ = manager.update(entity);
815                                }
816                            }
817                        }
818                    }
819                }
820            }
821        }
822
823        if self.format_version() < STORE_VERSION_V11 {
824            self.set_format_version(STORE_VERSION_V11);
825        }
826
827        Ok(())
828    }
829
830    /// Deserialize an entity from binary bytes
831    pub(crate) fn deserialize_entity(
832        data: &[u8],
833        format_version: u32,
834    ) -> Result<UnifiedEntity, StoreError> {
835        let mut pos = 0;
836        Self::read_entity_binary(data, &mut pos, format_version)
837            .map_err(|e| StoreError::Serialization(e.to_string()))
838    }
839
840    /// Serialize an entity to binary bytes
841    pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
842        // Pre-allocate ~256 bytes to cover the typical 15-column
843        // typed row without any Vec growth. Bulk insert calls this
844        // millions of times per bench run; saving 2-3 reallocs per
845        // entity amortises.
846        let mut buf = Vec::with_capacity(256);
847        Self::write_entity_binary(&mut buf, entity, format_version);
848        buf
849    }
850
851    pub(crate) fn serialize_entity_record(
852        entity: &UnifiedEntity,
853        metadata: Option<&Metadata>,
854        format_version: u32,
855    ) -> Vec<u8> {
856        let entity_bytes = Self::serialize_entity(entity, format_version);
857        // Skip the intermediate metadata Vec when there's no metadata
858        // (common OLTP bulk-insert case): write a zero-length prefix
859        // directly into the record buffer. Only fall back to the old
860        // serialize_metadata() allocation when the caller actually
861        // has fields to persist.
862        let has_meta = matches!(metadata, Some(m) if !m.fields.is_empty());
863        if has_meta {
864            let metadata_bytes = serialize_metadata(metadata);
865            reddb_file::encode_native_entity_record_frame(&entity_bytes, Some(&metadata_bytes))
866        } else {
867            reddb_file::encode_native_entity_record_frame(&entity_bytes, None)
868        }
869    }
870
871    pub(crate) fn deserialize_entity_record(
872        data: &[u8],
873        format_version: u32,
874    ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
875        let Some(frame) = reddb_file::decode_native_entity_record_frame(data)
876            .map_err(|err| StoreError::Serialization(err.to_string()))?
877        else {
878            return Self::deserialize_entity(data, format_version).map(|entity| (entity, None));
879        };
880
881        let entity = Self::deserialize_entity(frame.entity, format_version)?;
882        let metadata = if frame.metadata.is_empty() {
883            None
884        } else {
885            let metadata = deserialize_metadata(frame.metadata)?;
886            if metadata.is_empty() {
887                None
888            } else {
889                Some(metadata)
890            }
891        };
892
893        Ok((entity, metadata))
894    }
895
896    /// Persist all data to page-based storage
897    ///
898    /// Writes all entities to B-tree pages and flushes to disk.
899    /// This provides ACID durability guarantees.
900    pub fn persist(&self) -> Result<(), StoreError> {
901        let _vault_guard = self.vault_publication_lock.lock();
902        let pending_vault_versions = self.pending_vault_versions.read();
903        let pager = match &self.pager {
904            Some(p) => p,
905            None => {
906                // No pager attached - use binary file fallback if path available
907                if let Some(path) = &self.db_path {
908                    return self
909                        .save_to_file(path)
910                        .map_err(|e| StoreError::Serialization(e.to_string()));
911                }
912                return Err(StoreError::Io(std::io::Error::other(
913                    "No pager or path configured for persistence",
914                )));
915            }
916        };
917
918        match pager.read_page(1) {
919            Ok(_) => {}
920            Err(PagerError::PageNotFound(_)) => {
921                let meta_page = pager
922                    .allocate_page(crate::storage::engine::PageType::Header)
923                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
924                pager
925                    .write_page(meta_page.page_id(), meta_page)
926                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
927            }
928            Err(e) => {
929                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
930            }
931        }
932
933        if let Some(commit) = &self.commit {
934            commit.force_sync().map_err(StoreError::Io)?;
935        }
936
937        let collections = self.collections.read();
938        let mut btree_indices = self.btree_indices.write();
939
940        // Collect collection names and their B-tree root pages
941        let mut collection_roots: Vec<(String, u32)> = Vec::new();
942
943        // Build unpublished replacements. Deleting from the current trees here
944        // would destroy the last checkpoint if a write fails before publication.
945        let mut rebuilt_indices = HashMap::with_capacity(collections.len());
946        let previous_versions = self.checkpoint_versions.read();
947        let mut checkpoint_versions = HashMap::with_capacity(collections.len());
948        let pending_vault = Arc::new(pending_vault_versions.clone());
949        let aborted_revision = self
950            .snapshot_manager
951            .get()
952            .map_or(0, |manager| manager.checkpoint_aborted_revision());
953        for (name, manager) in collections.iter() {
954            let version = CheckpointVersion {
955                manager: Arc::downgrade(manager),
956                segments: manager.checkpoint_revisions(),
957                aborted_revision,
958                pending_vault: Arc::clone(&pending_vault),
959                format_version: self.format_version(),
960            };
961            if let (Some(previous), Some(tree)) =
962                (previous_versions.get(name), btree_indices.get(name))
963            {
964                if previous.manager.ptr_eq(&version.manager)
965                    && previous.segments == version.segments
966                    && previous.aborted_revision == version.aborted_revision
967                    && previous.pending_vault == version.pending_vault
968                    && previous.format_version == version.format_version
969                {
970                    collection_roots.push((name.clone(), tree.root_page_id()));
971                    rebuilt_indices.insert(name.clone(), Arc::clone(tree));
972                    checkpoint_versions.insert(name.clone(), version);
973                    continue;
974                }
975            }
976            checkpoint_versions.insert(name.clone(), version);
977            let mut records = Vec::new();
978            manager.for_each_entity_with_metadata(|entity, metadata| {
979                #[cfg(test)]
980                CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
981                if pending_vault_versions.contains(&entity.id)
982                    || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
983                        && self
984                            .snapshot_manager
985                            .get()
986                            .is_some_and(|m| m.is_aborted(entity.xmin)))
987                {
988                    return true;
989                }
990                // Borrow ordinary rows instead of materializing a second full
991                // collection of cloned entities alongside the encoded records.
992                let mut normalized = std::borrow::Cow::Borrowed(entity);
993                if matches!(entity.data, EntityData::Row(_))
994                    && self
995                        .snapshot_manager
996                        .get()
997                        .is_some_and(|m| m.is_aborted(entity.xmax))
998                {
999                    normalized.to_mut().xmax = 0;
1000                }
1001                records.push((
1002                    entity.id.raw().to_be_bytes().to_vec(),
1003                    Self::serialize_entity_record(
1004                        &normalized,
1005                        metadata.as_ref(),
1006                        self.format_version(),
1007                    ),
1008                ));
1009                true
1010            });
1011            records.sort_by(|left, right| left.0.cmp(&right.0));
1012
1013            if let Some(tree) = btree_indices.get(name) {
1014                if tree_matches_records(tree, &records)? {
1015                    collection_roots.push((name.clone(), tree.root_page_id()));
1016                    rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1017                    continue;
1018                }
1019            }
1020
1021            let btree = Arc::new(BTree::new(Arc::clone(pager)));
1022
1023            // Slice G (#704): no per-row skip. Oversized values are
1024            // spilled through the slice-E write ladder inside
1025            // `bulk_insert_sorted` (inline → compressed inline →
1026            // overflow chain). The only rejection is the hard
1027            // `MAX_VALUE_SIZE` (256 MiB) ceiling, which surfaces as
1028            // `ValueTooLarge` from the bulk path after the rest of
1029            // the batch has landed.
1030            if !records.is_empty() {
1031                btree.bulk_insert_sorted(&records).map_err(|e| {
1032                    StoreError::Io(std::io::Error::other(format!(
1033                        "B-tree bulk rebuild error for '{name}': {e}"
1034                    )))
1035                })?;
1036            }
1037
1038            collection_roots.push((name.clone(), btree.root_page_id()));
1039            rebuilt_indices.insert(name.clone(), btree);
1040        }
1041
1042        // Write collection metadata to page 1
1043        let mut meta_data = Vec::with_capacity(4096);
1044
1045        let format_version = STORE_VERSION_V11;
1046        self.set_format_version(format_version);
1047
1048        reddb_file::encode_native_paged_metadata_header(
1049            &mut meta_data,
1050            reddb_file::NativePagedMetadataHeader {
1051                format_version,
1052                collection_count: collection_roots.len() as u32,
1053            },
1054        );
1055
1056        // Write each collection's name and B-tree root page
1057        for (name, root_page) in &collection_roots {
1058            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1059        }
1060
1061        // Write cross-reference metadata
1062        let cross_refs = self.cross_refs.read();
1063        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1064        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1065        for (source_id, refs) in cross_refs.iter() {
1066            for (target_id, ref_type, collection) in refs {
1067                reddb_file::encode_native_paged_cross_ref(
1068                    &mut meta_data,
1069                    source_id.raw(),
1070                    target_id.raw(),
1071                    ref_type.to_byte(),
1072                    collection,
1073                );
1074            }
1075        }
1076
1077        publish_paged_metadata(pager, &meta_data)
1078            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1079
1080        // Only the now-durable catalog may replace the live tree handles. Keep
1081        // previous tree pages retained for recovery; do not recycle them early.
1082        *btree_indices = rebuilt_indices;
1083        drop(previous_versions);
1084        *self.checkpoint_versions.write() = checkpoint_versions;
1085
1086        if let Some(commit) = &self.commit {
1087            commit.truncate().map_err(StoreError::Io)?;
1088        }
1089
1090        Ok(())
1091    }
1092
1093    /// Check if the store is using page-based persistence
1094    pub fn is_paged(&self) -> bool {
1095        self.pager.is_some()
1096    }
1097
1098    /// Current root page for a collection's primary B-tree, if one has
1099    /// been materialized in this store.
1100    pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1101        self.btree_indices
1102            .read()
1103            .get(collection)
1104            .map(|btree| btree.root_page_id())
1105            .filter(|root| *root != 0)
1106    }
1107
1108    /// Get the database file path (if using paged mode)
1109    pub fn db_path(&self) -> Option<&Path> {
1110        self.db_path.as_deref()
1111    }
1112}
1113
1114fn serialize_metadata(metadata: Option<&Metadata>) -> Vec<u8> {
1115    let Some(metadata) = metadata else {
1116        return Vec::new();
1117    };
1118    if metadata.is_empty() {
1119        return Vec::new();
1120    }
1121
1122    let mut entries: Vec<_> = metadata.iter().collect();
1123    entries.sort_by_key(|(a, _)| *a);
1124
1125    let mut buf = Vec::new();
1126    buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1127    for (key, value) in entries {
1128        write_string(&mut buf, key);
1129        write_metadata_value(&mut buf, value);
1130    }
1131    buf
1132}
1133
1134fn deserialize_metadata(data: &[u8]) -> Result<Metadata, StoreError> {
1135    let mut pos = 0usize;
1136    let count = read_u32(data, &mut pos)? as usize;
1137    let mut metadata = Metadata::new();
1138    for _ in 0..count {
1139        let key = read_string(data, &mut pos)?;
1140        let value = read_metadata_value(data, &mut pos)?;
1141        metadata.set(key, value);
1142    }
1143    Ok(metadata)
1144}
1145
1146fn write_string(buf: &mut Vec<u8>, value: &str) {
1147    reddb_file::encode_native_len_prefixed_str(buf, value);
1148}
1149
1150fn write_bytes(buf: &mut Vec<u8>, value: &[u8]) {
1151    reddb_file::encode_native_len_prefixed_bytes(buf, value);
1152}
1153
1154fn write_ref_target(buf: &mut Vec<u8>, target: &crate::storage::unified::RefTarget) {
1155    use crate::storage::unified::RefTarget;
1156
1157    match target {
1158        RefTarget::TableRow { table, row_id } => {
1159            buf.push(0);
1160            write_string(buf, table);
1161            buf.extend_from_slice(&row_id.to_le_bytes());
1162        }
1163        RefTarget::Node {
1164            collection,
1165            node_id,
1166        } => {
1167            buf.push(1);
1168            write_string(buf, collection);
1169            buf.extend_from_slice(&node_id.raw().to_le_bytes());
1170        }
1171        RefTarget::Edge {
1172            collection,
1173            edge_id,
1174        } => {
1175            buf.push(2);
1176            write_string(buf, collection);
1177            buf.extend_from_slice(&edge_id.raw().to_le_bytes());
1178        }
1179        RefTarget::Vector {
1180            collection,
1181            vector_id,
1182        } => {
1183            buf.push(3);
1184            write_string(buf, collection);
1185            buf.extend_from_slice(&vector_id.raw().to_le_bytes());
1186        }
1187        RefTarget::Entity {
1188            collection,
1189            entity_id,
1190        } => {
1191            buf.push(4);
1192            write_string(buf, collection);
1193            buf.extend_from_slice(&entity_id.raw().to_le_bytes());
1194        }
1195    }
1196}
1197
1198fn write_metadata_value(buf: &mut Vec<u8>, value: &MetadataValue) {
1199    match value {
1200        MetadataValue::Null => buf.push(0),
1201        MetadataValue::Bool(v) => {
1202            buf.push(1);
1203            buf.push(u8::from(*v));
1204        }
1205        MetadataValue::Int(v) => {
1206            buf.push(2);
1207            buf.extend_from_slice(&v.to_le_bytes());
1208        }
1209        MetadataValue::Float(v) => {
1210            buf.push(3);
1211            buf.extend_from_slice(&v.to_le_bytes());
1212        }
1213        MetadataValue::String(v) => {
1214            buf.push(4);
1215            write_string(buf, v);
1216        }
1217        MetadataValue::Bytes(v) => {
1218            buf.push(5);
1219            write_bytes(buf, v);
1220        }
1221        MetadataValue::Array(values) => {
1222            buf.push(6);
1223            buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
1224            for value in values {
1225                write_metadata_value(buf, value);
1226            }
1227        }
1228        MetadataValue::Object(values) => {
1229            buf.push(7);
1230            let mut entries: Vec<_> = values.iter().collect();
1231            entries.sort_by_key(|(a, _)| *a);
1232            buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1233            for (key, value) in entries {
1234                write_string(buf, key);
1235                write_metadata_value(buf, value);
1236            }
1237        }
1238        MetadataValue::Timestamp(v) => {
1239            buf.push(8);
1240            buf.extend_from_slice(&v.to_le_bytes());
1241        }
1242        MetadataValue::Geo { lat, lon } => {
1243            buf.push(9);
1244            buf.extend_from_slice(&lat.to_le_bytes());
1245            buf.extend_from_slice(&lon.to_le_bytes());
1246        }
1247        MetadataValue::Reference(target) => {
1248            buf.push(10);
1249            write_ref_target(buf, target);
1250        }
1251        MetadataValue::References(targets) => {
1252            buf.push(11);
1253            buf.extend_from_slice(&(targets.len() as u32).to_le_bytes());
1254            for target in targets {
1255                write_ref_target(buf, target);
1256            }
1257        }
1258    }
1259}
1260
1261fn read_exact_slice<'a>(
1262    data: &'a [u8],
1263    pos: &mut usize,
1264    len: usize,
1265) -> Result<&'a [u8], StoreError> {
1266    if *pos + len > data.len() {
1267        return Err(StoreError::Serialization(
1268            "truncated metadata payload".to_string(),
1269        ));
1270    }
1271    let slice = &data[*pos..*pos + len];
1272    *pos += len;
1273    Ok(slice)
1274}
1275
1276fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, StoreError> {
1277    let bytes = read_exact_slice(data, pos, 4)?;
1278    let mut raw = [0u8; 4];
1279    raw.copy_from_slice(bytes);
1280    Ok(u32::from_le_bytes(raw))
1281}
1282
1283fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, StoreError> {
1284    let bytes = read_exact_slice(data, pos, 8)?;
1285    let mut raw = [0u8; 8];
1286    raw.copy_from_slice(bytes);
1287    Ok(u64::from_le_bytes(raw))
1288}
1289
1290fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, StoreError> {
1291    let bytes = read_exact_slice(data, pos, 8)?;
1292    let mut raw = [0u8; 8];
1293    raw.copy_from_slice(bytes);
1294    Ok(i64::from_le_bytes(raw))
1295}
1296
1297fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, StoreError> {
1298    let bytes = read_exact_slice(data, pos, 8)?;
1299    let mut raw = [0u8; 8];
1300    raw.copy_from_slice(bytes);
1301    Ok(f64::from_le_bytes(raw))
1302}
1303
1304fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, StoreError> {
1305    let bytes = read_exact_slice(data, pos, 1)?;
1306    Ok(bytes[0])
1307}
1308
1309fn read_string(data: &[u8], pos: &mut usize) -> Result<String, StoreError> {
1310    reddb_file::decode_native_len_prefixed_string(data, pos)
1311        .map_err(|err| StoreError::Serialization(err.to_string()))
1312}
1313
1314fn read_bytes(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, StoreError> {
1315    reddb_file::decode_native_len_prefixed_bytes(data, pos)
1316        .map(|bytes| bytes.to_vec())
1317        .map_err(|err| StoreError::Serialization(err.to_string()))
1318}
1319
1320fn read_ref_target(
1321    data: &[u8],
1322    pos: &mut usize,
1323) -> Result<crate::storage::unified::RefTarget, StoreError> {
1324    use crate::storage::unified::RefTarget;
1325
1326    match read_u8(data, pos)? {
1327        0 => Ok(RefTarget::TableRow {
1328            table: read_string(data, pos)?,
1329            row_id: read_u64(data, pos)?,
1330        }),
1331        1 => Ok(RefTarget::Node {
1332            collection: read_string(data, pos)?,
1333            node_id: EntityId::new(read_u64(data, pos)?),
1334        }),
1335        2 => Ok(RefTarget::Edge {
1336            collection: read_string(data, pos)?,
1337            edge_id: EntityId::new(read_u64(data, pos)?),
1338        }),
1339        3 => Ok(RefTarget::Vector {
1340            collection: read_string(data, pos)?,
1341            vector_id: EntityId::new(read_u64(data, pos)?),
1342        }),
1343        4 => Ok(RefTarget::Entity {
1344            collection: read_string(data, pos)?,
1345            entity_id: EntityId::new(read_u64(data, pos)?),
1346        }),
1347        tag => Err(StoreError::Serialization(format!(
1348            "unknown metadata ref target tag {tag}"
1349        ))),
1350    }
1351}
1352
1353fn read_metadata_value(data: &[u8], pos: &mut usize) -> Result<MetadataValue, StoreError> {
1354    match read_u8(data, pos)? {
1355        0 => Ok(MetadataValue::Null),
1356        1 => Ok(MetadataValue::Bool(read_u8(data, pos)? != 0)),
1357        2 => Ok(MetadataValue::Int(read_i64(data, pos)?)),
1358        3 => Ok(MetadataValue::Float(read_f64(data, pos)?)),
1359        4 => Ok(MetadataValue::String(read_string(data, pos)?)),
1360        5 => Ok(MetadataValue::Bytes(read_bytes(data, pos)?)),
1361        6 => {
1362            let count = read_u32(data, pos)? as usize;
1363            let mut values = Vec::with_capacity(count);
1364            for _ in 0..count {
1365                values.push(read_metadata_value(data, pos)?);
1366            }
1367            Ok(MetadataValue::Array(values))
1368        }
1369        7 => {
1370            let count = read_u32(data, pos)? as usize;
1371            let mut values = std::collections::HashMap::with_capacity(count);
1372            for _ in 0..count {
1373                let key = read_string(data, pos)?;
1374                let value = read_metadata_value(data, pos)?;
1375                values.insert(key, value);
1376            }
1377            Ok(MetadataValue::Object(values))
1378        }
1379        8 => Ok(MetadataValue::Timestamp(read_u64(data, pos)?)),
1380        9 => Ok(MetadataValue::Geo {
1381            lat: read_f64(data, pos)?,
1382            lon: read_f64(data, pos)?,
1383        }),
1384        10 => Ok(MetadataValue::Reference(read_ref_target(data, pos)?)),
1385        11 => {
1386            let count = read_u32(data, pos)? as usize;
1387            let mut targets = Vec::with_capacity(count);
1388            for _ in 0..count {
1389                targets.push(read_ref_target(data, pos)?);
1390            }
1391            Ok(MetadataValue::References(targets))
1392        }
1393        tag => Err(StoreError::Serialization(format!(
1394            "unknown metadata value tag {tag}"
1395        ))),
1396    }
1397}
1398
1399#[cfg(test)]
1400mod checkpoint_work_tests {
1401    use super::*;
1402
1403    #[test]
1404    fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1405        let directory = tempfile::tempdir().expect("temporary store");
1406        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1407        store.create_collection("rows").expect("collection");
1408        for id in 1..=40 {
1409            store
1410                .insert(
1411                    "rows",
1412                    UnifiedEntity::table_row(
1413                        EntityId::new(id),
1414                        "rows",
1415                        id,
1416                        vec![Value::Integer(id as i64)],
1417                    ),
1418                )
1419                .expect("seed row");
1420        }
1421        store.persist().expect("initial checkpoint");
1422        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1423        store.persist().expect("unchanged checkpoint");
1424        assert_eq!(
1425            CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1426            0,
1427            "unchanged collections must not hold the writer fence while serializing every row"
1428        );
1429        drop(store);
1430        let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1431        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1432        reopened.persist().expect("unchanged recovered generation");
1433        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1434    }
1435
1436    fn row(id: u64, value: i64) -> UnifiedEntity {
1437        UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
1438    }
1439
1440    #[test]
1441    fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
1442        let directory = tempfile::tempdir().expect("temporary store");
1443        let path = directory.path().join("data.rdb");
1444        let store = UnifiedStore::open(&path).expect("paged store");
1445        store.create_collection("rows").expect("collection");
1446        store.insert("rows", row(1, 1)).expect("seed");
1447        store.persist().expect("initial checkpoint");
1448        let manager = store.get_collection("rows").expect("rows");
1449        manager.update(row(1, 2)).expect("direct update");
1450        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1451        store.persist().expect("updated row checkpoint");
1452        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1453        let mut metadata = Metadata::new();
1454        metadata.set("verified", MetadataValue::Bool(true));
1455        manager
1456            .set_metadata(EntityId::new(1), metadata)
1457            .expect("metadata");
1458        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1459        store.persist().expect("metadata-only checkpoint");
1460        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1461        manager
1462            .bulk_insert(vec![row(2, 2), row(3, 3)])
1463            .expect("bulk insert");
1464        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1465        store.persist().expect("bulk insert checkpoint");
1466        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
1467        manager.delete(EntityId::new(2)).expect("delete");
1468        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1469        store.persist().expect("changed checkpoint");
1470        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
1471        drop(store);
1472        let recovered = UnifiedStore::open(&path).expect("recover changed rows");
1473        let manager = recovered.get_collection("rows").expect("rows");
1474        assert_eq!(manager.count(), 2);
1475        assert_eq!(
1476            manager
1477                .get(EntityId::new(1))
1478                .expect("row")
1479                .data
1480                .as_row()
1481                .expect("table")
1482                .columns,
1483            vec![Value::Integer(2)]
1484        );
1485        assert!(manager.get(EntityId::new(2)).is_none());
1486        assert!(manager
1487            .get_metadata(EntityId::new(1))
1488            .expect("metadata")
1489            .get("verified")
1490            .is_some());
1491    }
1492
1493    #[test]
1494    fn rollback_invalidates_reuse_without_a_segment_mutation() {
1495        let directory = tempfile::tempdir().expect("temporary store");
1496        let path = directory.path().join("data.rdb");
1497        let store = UnifiedStore::open(&path).expect("paged store");
1498        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1499        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1500        store.create_collection("rows").expect("collection");
1501        let xid = snapshots.begin();
1502        let mut pending = row(1, 1);
1503        pending.xmin = xid;
1504        store.insert("rows", pending).expect("version");
1505        store.persist().expect("before rollback");
1506        snapshots.rollback(xid);
1507        store.persist().expect("after rollback");
1508        drop(store);
1509        let recovered = UnifiedStore::open(&path).expect("recover");
1510        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1511    }
1512
1513    #[test]
1514    fn publishing_a_pending_vault_version_invalidates_reuse() {
1515        let directory = tempfile::tempdir().expect("temporary store");
1516        let path = directory.path().join("data.rdb");
1517        let store = UnifiedStore::open(&path).expect("paged store");
1518        store.create_collection("rows").expect("collection");
1519        store.insert("rows", row(1, 1)).expect("row");
1520        store
1521            .pending_vault_versions
1522            .write()
1523            .insert(EntityId::new(1));
1524        store.persist().expect("exclude pending vault version");
1525        store
1526            .pending_vault_versions
1527            .write()
1528            .remove(&EntityId::new(1));
1529        store.persist().expect("include published vault version");
1530        drop(store);
1531        let recovered = UnifiedStore::open(&path).expect("recover");
1532        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
1533    }
1534}