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