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