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.resident_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            snapshot_manager: std::sync::OnceLock::new(),
416        }
417    }
418
419    /// Open or create a page-based database
420    ///
421    /// This uses the page engine for ACID durability with B-tree indices.
422    /// The database file uses 16 KiB pages with checksums and efficient caching.
423    ///
424    /// # Arguments
425    ///
426    /// * `path` - Path to the database file (e.g., "data.rdb")
427    ///
428    /// # Example
429    ///
430    /// ```rust,ignore
431    /// let store = UnifiedStore::open("security.rdb")?;
432    /// store.create_collection("hosts")?;
433    /// // ... operations ...
434    /// store.persist()?; // Flush to disk
435    /// ```
436    pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
437        Self::open_with_config(path, UnifiedStoreConfig::default())
438    }
439
440    pub fn open_with_config(
441        path: impl AsRef<Path>,
442        config: UnifiedStoreConfig,
443    ) -> Result<Self, StoreError> {
444        let path = path.as_ref();
445        let mut pager_config = PagerConfig::default();
446        // ADR 0073 §2 — the page cache is pre-sized from its budget share.
447        // The pager's own `cache_size` default survives only for direct
448        // library callers who never resolved a budget.
449        if let Some(slots) = config.page_cache_slots {
450            pager_config.cache_size = slots;
451        }
452        // Tunables via env — experimental, used by the benchmark harness
453        // to compare durability profiles head-to-head with Postgres.
454        // REDDB_DOUBLE_WRITE=0 requests skipping the double-write buffer,
455        // which otherwise adds two fsyncs per pager flush (one on DWB, one
456        // on the main file). The pager honors this only when the actual
457        // data file is proven to live on a CoW filesystem with atomic page
458        // writes; otherwise it fails closed and keeps DWB enabled.
459        if matches!(
460            std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
461            Some("0") | Some("false") | Some("off")
462        ) {
463            pager_config.double_write = false;
464        }
465        let pager = Pager::open(path, pager_config)
466            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
467
468        let wal_path = reddb_file::layout::unified_wal_path(path);
469        let commit = if StoreCommitCoordinator::should_open(&wal_path, config.durability_mode) {
470            Some(Arc::new(
471                StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
472                    .map_err(StoreError::Io)?,
473            ))
474        } else {
475            None
476        };
477
478        let store = Self {
479            config,
480            format_version: AtomicU32::new(STORE_VERSION_V11),
481            next_entity_id: AtomicU64::new(1),
482            collections: RwLock::new(HashMap::new()),
483            cross_refs: RwLock::new(HashMap::new()),
484            reverse_refs: RwLock::new(HashMap::new()),
485            pager: Some(Arc::new(pager)),
486            db_path: Some(path.to_path_buf()),
487            btree_indices: RwLock::new(HashMap::new()),
488            context_index: ContextIndex::new(),
489            entity_cache: EntityCache::new(),
490            graph_label_index: RwLock::new(HashMap::new()),
491            paged_registry_dirty: AtomicBool::new(false),
492            commit,
493            unindex_cross_refs_fast_path: AtomicU64::new(0),
494            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
495            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
496            aux_metadata: RwLock::new(Vec::new()),
497            snapshot_manager: std::sync::OnceLock::new(),
498        };
499
500        // Load existing data from pages if database exists
501        store.load_from_pages()?;
502        if let Some(commit) = &store.commit {
503            commit.replay_into(&store).map_err(StoreError::Io)?;
504        }
505        store.recover_operational_manifest()?;
506
507        Ok(store)
508    }
509
510    pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
511        let Some(path) = &self.db_path else {
512            return Ok(());
513        };
514        let mut collections = self.list_collections();
515        collections.sort();
516        let pending_drops =
517            crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
518                .recover_or_bootstrap(&collections)
519                .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
520        for name in pending_drops {
521            if self.get_collection(&name).is_some() {
522                self.drop_collection(&name)?;
523            }
524        }
525        Ok(())
526    }
527
528    pub(crate) fn prepare_operational_collection_create(
529        &self,
530        name: &str,
531    ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
532        let Some(path) = &self.db_path else {
533            return Ok(None);
534        };
535        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
536            .prepare_collection_create(name)
537            .map(Some)
538            .map_err(StoreError::Io)
539    }
540
541    pub(crate) fn prepare_operational_collection_drop(
542        &self,
543        name: &str,
544    ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
545        let Some(path) = &self.db_path else {
546            return Ok(None);
547        };
548        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
549            .prepare_collection_drop(name)
550            .map(Some)
551            .map_err(StoreError::Io)
552    }
553
554    /// Load data from page-based storage
555    ///
556    /// Reads the B-tree indices and reconstructs collections from pages.
557    fn load_from_pages(&self) -> Result<(), StoreError> {
558        let pager = match &self.pager {
559            Some(p) => p,
560            None => return Ok(()), // No pager, nothing to load
561        };
562
563        // Get page count
564        let page_count = pager.page_count().map_err(|e| {
565            StoreError::Io(std::io::Error::other(format!(
566                "failed to read page count: {}",
567                e
568            )))
569        })?;
570        if page_count <= 1 {
571            // Empty database (only header page)
572            return Ok(());
573        }
574
575        // Read metadata starting from page 1 (collections registry). The
576        // helper transparently follows the `RDM3` overflow chain when the
577        // metadata blob spans multiple pages and falls back to the legacy
578        // `<data>-meta` shadow when page 1 itself is corrupted.
579        if let Some(content_vec) = read_meta_payload(pager)? {
580            let content: &[u8] = &content_vec;
581            if content.len() >= 4 {
582                let mut pos = 0;
583                let mut format_version = STORE_VERSION_V1;
584
585                let collection_count = if let Some(header) =
586                    reddb_file::decode_native_paged_metadata_header(content)
587                        .map_err(|err| StoreError::Serialization(err.to_string()))?
588                {
589                    format_version = header.format_version;
590                    pos += reddb_file::METADATA_HEADER_BYTES;
591                    header.collection_count as usize
592                } else {
593                    let count = u32::from_le_bytes([
594                        content[pos],
595                        content[pos + 1],
596                        content[pos + 2],
597                        content[pos + 3],
598                    ]) as usize;
599                    pos += 4;
600                    count
601                };
602
603                self.set_format_version(format_version);
604
605                if pos > content.len() {
606                    return Ok(());
607                }
608
609                // Read collection names and their B-tree root pages
610                for _ in 0..collection_count {
611                    if let Ok(root) =
612                        reddb_file::decode_native_paged_collection_root(content, &mut pos)
613                    {
614                        // Root page ID for this collection's B-tree
615                        let root_page = root.root_page;
616                        let name = root.collection;
617
618                        // Hydrate the collection in memory only. Loading must
619                        // not emit WAL entries or rewrite the on-disk registry
620                        // before the existing B-tree roots are attached.
621                        let _ = self.create_collection_in_memory(&name);
622
623                        // Load B-tree with root page if it exists
624                        if root_page > 0 {
625                            let btree = BTree::with_root(Arc::clone(pager), root_page);
626
627                            // Load all entities from B-tree into the collection
628                            let mut cursor = btree.cursor_first().map_err(|err| {
629                                page_integrity(root_page, Some(name.clone()), err.to_string())
630                            })?;
631                            let manager = self.get_collection(&name);
632                            loop {
633                                let next = cursor.next().map_err(|err| {
634                                    page_integrity(root_page, Some(name.clone()), err.to_string())
635                                })?;
636                                let Some((key, value)) = next else {
637                                    break;
638                                };
639                                // Deserialize entity from value bytes
640                                if let Ok((entity, metadata)) =
641                                    Self::deserialize_entity_record(&value, self.format_version())
642                                {
643                                    if let Some(m) = &manager {
644                                        let id = entity.id;
645                                        if let EntityKind::TableRow { row_id, .. } = &entity.kind {
646                                            m.register_row_id(*row_id);
647                                        }
648                                        self.context_index.index_entity(&name, &entity);
649                                        let _ = m.insert(entity.clone());
650                                        if let Some(metadata) = metadata {
651                                            let _ = m.set_metadata(id, metadata);
652                                        }
653                                        self.register_entity_id(id);
654                                        if self.config.auto_index_refs {
655                                            self.index_cross_refs(&entity, &name)?;
656                                        }
657                                    }
658                                }
659                            }
660
661                            // Store the B-tree for future lookups
662                            self.btree_indices.write().insert(name, Arc::new(btree));
663                        }
664                    } else {
665                        break;
666                    }
667                }
668
669                if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
670                    let cross_ref_count = u32::from_le_bytes([
671                        content[pos],
672                        content[pos + 1],
673                        content[pos + 2],
674                        content[pos + 3],
675                    ]) as usize;
676                    pos += 4;
677
678                    for _ in 0..cross_ref_count {
679                        let Ok(cross_ref) =
680                            reddb_file::decode_native_paged_cross_ref(content, &mut pos)
681                        else {
682                            break;
683                        };
684                        let source_id = EntityId::new(cross_ref.source_id);
685                        let target_id = EntityId::new(cross_ref.target_id);
686                        let ref_type = RefType::from_byte(cross_ref.ref_type);
687                        let target_collection = cross_ref.target_collection;
688
689                        self.cross_refs.write().entry(source_id).or_default().push((
690                            target_id,
691                            ref_type,
692                            target_collection.clone(),
693                        ));
694
695                        if let Some((collection, mut entity)) = self.get_any(source_id) {
696                            let exists = entity.cross_refs().iter().any(|xref| {
697                                xref.target == target_id
698                                    && xref.ref_type == ref_type
699                                    && xref.target_collection == target_collection
700                            });
701                            if !exists {
702                                entity.cross_refs_mut().push(CrossRef::new(
703                                    source_id,
704                                    target_id,
705                                    target_collection.clone(),
706                                    ref_type,
707                                ));
708                                if let Some(manager) = self.get_collection(&collection) {
709                                    let _ = manager.update(entity);
710                                }
711                            }
712                        }
713                    }
714                }
715            }
716        }
717
718        if self.format_version() < STORE_VERSION_V11 {
719            self.set_format_version(STORE_VERSION_V11);
720        }
721
722        Ok(())
723    }
724
725    /// Deserialize an entity from binary bytes
726    pub(crate) fn deserialize_entity(
727        data: &[u8],
728        format_version: u32,
729    ) -> Result<UnifiedEntity, StoreError> {
730        let mut pos = 0;
731        Self::read_entity_binary(data, &mut pos, format_version)
732            .map_err(|e| StoreError::Serialization(e.to_string()))
733    }
734
735    /// Serialize an entity to binary bytes
736    pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
737        // Pre-allocate ~256 bytes to cover the typical 15-column
738        // typed row without any Vec growth. Bulk insert calls this
739        // millions of times per bench run; saving 2-3 reallocs per
740        // entity amortises.
741        let mut buf = Vec::with_capacity(256);
742        Self::write_entity_binary(&mut buf, entity, format_version);
743        buf
744    }
745
746    pub(crate) fn serialize_entity_record(
747        entity: &UnifiedEntity,
748        metadata: Option<&Metadata>,
749        format_version: u32,
750    ) -> Vec<u8> {
751        let entity_bytes = Self::serialize_entity(entity, format_version);
752        // Skip the intermediate metadata Vec when there's no metadata
753        // (common OLTP bulk-insert case): write a zero-length prefix
754        // directly into the record buffer. Only fall back to the old
755        // serialize_metadata() allocation when the caller actually
756        // has fields to persist.
757        let has_meta = matches!(metadata, Some(m) if !m.fields.is_empty());
758        if has_meta {
759            let metadata_bytes = serialize_metadata(metadata);
760            reddb_file::encode_native_entity_record_frame(&entity_bytes, Some(&metadata_bytes))
761        } else {
762            reddb_file::encode_native_entity_record_frame(&entity_bytes, None)
763        }
764    }
765
766    pub(crate) fn deserialize_entity_record(
767        data: &[u8],
768        format_version: u32,
769    ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
770        let Some(frame) = reddb_file::decode_native_entity_record_frame(data)
771            .map_err(|err| StoreError::Serialization(err.to_string()))?
772        else {
773            return Self::deserialize_entity(data, format_version).map(|entity| (entity, None));
774        };
775
776        let entity = Self::deserialize_entity(frame.entity, format_version)?;
777        let metadata = if frame.metadata.is_empty() {
778            None
779        } else {
780            let metadata = deserialize_metadata(frame.metadata)?;
781            if metadata.is_empty() {
782                None
783            } else {
784                Some(metadata)
785            }
786        };
787
788        Ok((entity, metadata))
789    }
790
791    /// Persist all data to page-based storage
792    ///
793    /// Writes all entities to B-tree pages and flushes to disk.
794    /// This provides ACID durability guarantees.
795    pub fn persist(&self) -> Result<(), StoreError> {
796        let pager = match &self.pager {
797            Some(p) => p,
798            None => {
799                // No pager attached - use binary file fallback if path available
800                if let Some(path) = &self.db_path {
801                    return self
802                        .save_to_file(path)
803                        .map_err(|e| StoreError::Serialization(e.to_string()));
804                }
805                return Err(StoreError::Io(std::io::Error::other(
806                    "No pager or path configured for persistence",
807                )));
808            }
809        };
810
811        match pager.read_page(1) {
812            Ok(_) => {}
813            Err(PagerError::PageNotFound(_)) => {
814                let meta_page = pager
815                    .allocate_page(crate::storage::engine::PageType::Header)
816                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
817                pager
818                    .write_page(meta_page.page_id(), meta_page)
819                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
820            }
821            Err(e) => {
822                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
823            }
824        }
825
826        if let Some(commit) = &self.commit {
827            commit.force_sync().map_err(StoreError::Io)?;
828        }
829
830        let collections = self.collections.read();
831        let mut btree_indices = self.btree_indices.write();
832
833        // Collect collection names and their B-tree root pages
834        let mut collection_roots: Vec<(String, u32)> = Vec::new();
835
836        // For each collection, rebuild the B-tree from the live manager state.
837        // A checkpoint must preserve deletes too, not just upsert the current rows.
838        for (name, manager) in collections.iter() {
839            let btree = btree_indices
840                .entry(name.clone())
841                .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
842
843            let mut existing_keys = Vec::new();
844            if !btree.is_empty() {
845                let mut cursor = btree.cursor_first().map_err(|e| {
846                    StoreError::Io(std::io::Error::other(format!(
847                        "B-tree cursor error while rebuilding '{name}': {e}"
848                    )))
849                })?;
850                while let Some((key, _)) = cursor.next().map_err(|e| {
851                    StoreError::Io(std::io::Error::other(format!(
852                        "B-tree scan error while rebuilding '{name}': {e}"
853                    )))
854                })? {
855                    existing_keys.push(key);
856                }
857            }
858
859            for key in existing_keys {
860                btree.delete(&key).map_err(|e| {
861                    StoreError::Io(std::io::Error::other(format!(
862                        "B-tree delete error while rebuilding '{name}': {e}"
863                    )))
864                })?;
865            }
866
867            let mut records: Vec<(Vec<u8>, Vec<u8>)> = manager
868                .query_all(|_| true)
869                .into_iter()
870                .map(|entity| {
871                    let metadata = manager.get_metadata(entity.id);
872                    (
873                        entity.id.raw().to_be_bytes().to_vec(),
874                        Self::serialize_entity_record(
875                            &entity,
876                            metadata.as_ref(),
877                            self.format_version(),
878                        ),
879                    )
880                })
881                .collect();
882            records.sort_by(|left, right| left.0.cmp(&right.0));
883
884            // Slice G (#704): no per-row skip. Oversized values are
885            // spilled through the slice-E write ladder inside
886            // `bulk_insert_sorted` (inline → compressed inline →
887            // overflow chain). The only rejection is the hard
888            // `MAX_VALUE_SIZE` (256 MiB) ceiling, which surfaces as
889            // `ValueTooLarge` from the bulk path after the rest of
890            // the batch has landed.
891            if !records.is_empty() {
892                btree.bulk_insert_sorted(&records).map_err(|e| {
893                    StoreError::Io(std::io::Error::other(format!(
894                        "B-tree bulk rebuild error for '{name}': {e}"
895                    )))
896                })?;
897            }
898
899            collection_roots.push((name.clone(), btree.root_page_id()));
900        }
901
902        // Write collection metadata to page 1
903        let mut meta_data = Vec::with_capacity(4096);
904
905        let format_version = STORE_VERSION_V11;
906        self.set_format_version(format_version);
907
908        reddb_file::encode_native_paged_metadata_header(
909            &mut meta_data,
910            reddb_file::NativePagedMetadataHeader {
911                format_version,
912                collection_count: collection_roots.len() as u32,
913            },
914        );
915
916        // Write each collection's name and B-tree root page
917        for (name, root_page) in &collection_roots {
918            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
919        }
920
921        // Write cross-reference metadata
922        let cross_refs = self.cross_refs.read();
923        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
924        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
925        for (source_id, refs) in cross_refs.iter() {
926            for (target_id, ref_type, collection) in refs {
927                reddb_file::encode_native_paged_cross_ref(
928                    &mut meta_data,
929                    source_id.raw(),
930                    target_id.raw(),
931                    ref_type.to_byte(),
932                    collection,
933                );
934            }
935        }
936
937        // Build page 1 (+ overflow chain when needed) for the metadata blob.
938        let meta_page = build_meta_page1_with_overflow(pager, &meta_data)
939            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
940
941        // Write the manifest zone (page 1 + overflow chain). It is rooted by
942        // the superblock and carries its own page checksum; there is no
943        // sidecar copy to keep in step.
944        pager
945            .write_page(1, meta_page)
946            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
947
948        // Flush and fsync all pages to disk
949        pager
950            .sync()
951            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
952
953        if let Some(commit) = &self.commit {
954            commit.truncate().map_err(StoreError::Io)?;
955        }
956
957        Ok(())
958    }
959
960    /// Check if the store is using page-based persistence
961    pub fn is_paged(&self) -> bool {
962        self.pager.is_some()
963    }
964
965    /// Current root page for a collection's primary B-tree, if one has
966    /// been materialized in this store.
967    pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
968        self.btree_indices
969            .read()
970            .get(collection)
971            .map(|btree| btree.root_page_id())
972            .filter(|root| *root != 0)
973    }
974
975    /// Get the database file path (if using paged mode)
976    pub fn db_path(&self) -> Option<&Path> {
977        self.db_path.as_deref()
978    }
979}
980
981fn serialize_metadata(metadata: Option<&Metadata>) -> Vec<u8> {
982    let Some(metadata) = metadata else {
983        return Vec::new();
984    };
985    if metadata.is_empty() {
986        return Vec::new();
987    }
988
989    let mut entries: Vec<_> = metadata.iter().collect();
990    entries.sort_by_key(|(a, _)| *a);
991
992    let mut buf = Vec::new();
993    buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
994    for (key, value) in entries {
995        write_string(&mut buf, key);
996        write_metadata_value(&mut buf, value);
997    }
998    buf
999}
1000
1001fn deserialize_metadata(data: &[u8]) -> Result<Metadata, StoreError> {
1002    let mut pos = 0usize;
1003    let count = read_u32(data, &mut pos)? as usize;
1004    let mut metadata = Metadata::new();
1005    for _ in 0..count {
1006        let key = read_string(data, &mut pos)?;
1007        let value = read_metadata_value(data, &mut pos)?;
1008        metadata.set(key, value);
1009    }
1010    Ok(metadata)
1011}
1012
1013fn write_string(buf: &mut Vec<u8>, value: &str) {
1014    reddb_file::encode_native_len_prefixed_str(buf, value);
1015}
1016
1017fn write_bytes(buf: &mut Vec<u8>, value: &[u8]) {
1018    reddb_file::encode_native_len_prefixed_bytes(buf, value);
1019}
1020
1021fn write_ref_target(buf: &mut Vec<u8>, target: &crate::storage::unified::RefTarget) {
1022    use crate::storage::unified::RefTarget;
1023
1024    match target {
1025        RefTarget::TableRow { table, row_id } => {
1026            buf.push(0);
1027            write_string(buf, table);
1028            buf.extend_from_slice(&row_id.to_le_bytes());
1029        }
1030        RefTarget::Node {
1031            collection,
1032            node_id,
1033        } => {
1034            buf.push(1);
1035            write_string(buf, collection);
1036            buf.extend_from_slice(&node_id.raw().to_le_bytes());
1037        }
1038        RefTarget::Edge {
1039            collection,
1040            edge_id,
1041        } => {
1042            buf.push(2);
1043            write_string(buf, collection);
1044            buf.extend_from_slice(&edge_id.raw().to_le_bytes());
1045        }
1046        RefTarget::Vector {
1047            collection,
1048            vector_id,
1049        } => {
1050            buf.push(3);
1051            write_string(buf, collection);
1052            buf.extend_from_slice(&vector_id.raw().to_le_bytes());
1053        }
1054        RefTarget::Entity {
1055            collection,
1056            entity_id,
1057        } => {
1058            buf.push(4);
1059            write_string(buf, collection);
1060            buf.extend_from_slice(&entity_id.raw().to_le_bytes());
1061        }
1062    }
1063}
1064
1065fn write_metadata_value(buf: &mut Vec<u8>, value: &MetadataValue) {
1066    match value {
1067        MetadataValue::Null => buf.push(0),
1068        MetadataValue::Bool(v) => {
1069            buf.push(1);
1070            buf.push(u8::from(*v));
1071        }
1072        MetadataValue::Int(v) => {
1073            buf.push(2);
1074            buf.extend_from_slice(&v.to_le_bytes());
1075        }
1076        MetadataValue::Float(v) => {
1077            buf.push(3);
1078            buf.extend_from_slice(&v.to_le_bytes());
1079        }
1080        MetadataValue::String(v) => {
1081            buf.push(4);
1082            write_string(buf, v);
1083        }
1084        MetadataValue::Bytes(v) => {
1085            buf.push(5);
1086            write_bytes(buf, v);
1087        }
1088        MetadataValue::Array(values) => {
1089            buf.push(6);
1090            buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
1091            for value in values {
1092                write_metadata_value(buf, value);
1093            }
1094        }
1095        MetadataValue::Object(values) => {
1096            buf.push(7);
1097            let mut entries: Vec<_> = values.iter().collect();
1098            entries.sort_by_key(|(a, _)| *a);
1099            buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1100            for (key, value) in entries {
1101                write_string(buf, key);
1102                write_metadata_value(buf, value);
1103            }
1104        }
1105        MetadataValue::Timestamp(v) => {
1106            buf.push(8);
1107            buf.extend_from_slice(&v.to_le_bytes());
1108        }
1109        MetadataValue::Geo { lat, lon } => {
1110            buf.push(9);
1111            buf.extend_from_slice(&lat.to_le_bytes());
1112            buf.extend_from_slice(&lon.to_le_bytes());
1113        }
1114        MetadataValue::Reference(target) => {
1115            buf.push(10);
1116            write_ref_target(buf, target);
1117        }
1118        MetadataValue::References(targets) => {
1119            buf.push(11);
1120            buf.extend_from_slice(&(targets.len() as u32).to_le_bytes());
1121            for target in targets {
1122                write_ref_target(buf, target);
1123            }
1124        }
1125    }
1126}
1127
1128fn read_exact_slice<'a>(
1129    data: &'a [u8],
1130    pos: &mut usize,
1131    len: usize,
1132) -> Result<&'a [u8], StoreError> {
1133    if *pos + len > data.len() {
1134        return Err(StoreError::Serialization(
1135            "truncated metadata payload".to_string(),
1136        ));
1137    }
1138    let slice = &data[*pos..*pos + len];
1139    *pos += len;
1140    Ok(slice)
1141}
1142
1143fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, StoreError> {
1144    let bytes = read_exact_slice(data, pos, 4)?;
1145    let mut raw = [0u8; 4];
1146    raw.copy_from_slice(bytes);
1147    Ok(u32::from_le_bytes(raw))
1148}
1149
1150fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, StoreError> {
1151    let bytes = read_exact_slice(data, pos, 8)?;
1152    let mut raw = [0u8; 8];
1153    raw.copy_from_slice(bytes);
1154    Ok(u64::from_le_bytes(raw))
1155}
1156
1157fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, StoreError> {
1158    let bytes = read_exact_slice(data, pos, 8)?;
1159    let mut raw = [0u8; 8];
1160    raw.copy_from_slice(bytes);
1161    Ok(i64::from_le_bytes(raw))
1162}
1163
1164fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, StoreError> {
1165    let bytes = read_exact_slice(data, pos, 8)?;
1166    let mut raw = [0u8; 8];
1167    raw.copy_from_slice(bytes);
1168    Ok(f64::from_le_bytes(raw))
1169}
1170
1171fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, StoreError> {
1172    let bytes = read_exact_slice(data, pos, 1)?;
1173    Ok(bytes[0])
1174}
1175
1176fn read_string(data: &[u8], pos: &mut usize) -> Result<String, StoreError> {
1177    reddb_file::decode_native_len_prefixed_string(data, pos)
1178        .map_err(|err| StoreError::Serialization(err.to_string()))
1179}
1180
1181fn read_bytes(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, StoreError> {
1182    reddb_file::decode_native_len_prefixed_bytes(data, pos)
1183        .map(|bytes| bytes.to_vec())
1184        .map_err(|err| StoreError::Serialization(err.to_string()))
1185}
1186
1187fn read_ref_target(
1188    data: &[u8],
1189    pos: &mut usize,
1190) -> Result<crate::storage::unified::RefTarget, StoreError> {
1191    use crate::storage::unified::RefTarget;
1192
1193    match read_u8(data, pos)? {
1194        0 => Ok(RefTarget::TableRow {
1195            table: read_string(data, pos)?,
1196            row_id: read_u64(data, pos)?,
1197        }),
1198        1 => Ok(RefTarget::Node {
1199            collection: read_string(data, pos)?,
1200            node_id: EntityId::new(read_u64(data, pos)?),
1201        }),
1202        2 => Ok(RefTarget::Edge {
1203            collection: read_string(data, pos)?,
1204            edge_id: EntityId::new(read_u64(data, pos)?),
1205        }),
1206        3 => Ok(RefTarget::Vector {
1207            collection: read_string(data, pos)?,
1208            vector_id: EntityId::new(read_u64(data, pos)?),
1209        }),
1210        4 => Ok(RefTarget::Entity {
1211            collection: read_string(data, pos)?,
1212            entity_id: EntityId::new(read_u64(data, pos)?),
1213        }),
1214        tag => Err(StoreError::Serialization(format!(
1215            "unknown metadata ref target tag {tag}"
1216        ))),
1217    }
1218}
1219
1220fn read_metadata_value(data: &[u8], pos: &mut usize) -> Result<MetadataValue, StoreError> {
1221    match read_u8(data, pos)? {
1222        0 => Ok(MetadataValue::Null),
1223        1 => Ok(MetadataValue::Bool(read_u8(data, pos)? != 0)),
1224        2 => Ok(MetadataValue::Int(read_i64(data, pos)?)),
1225        3 => Ok(MetadataValue::Float(read_f64(data, pos)?)),
1226        4 => Ok(MetadataValue::String(read_string(data, pos)?)),
1227        5 => Ok(MetadataValue::Bytes(read_bytes(data, pos)?)),
1228        6 => {
1229            let count = read_u32(data, pos)? as usize;
1230            let mut values = Vec::with_capacity(count);
1231            for _ in 0..count {
1232                values.push(read_metadata_value(data, pos)?);
1233            }
1234            Ok(MetadataValue::Array(values))
1235        }
1236        7 => {
1237            let count = read_u32(data, pos)? as usize;
1238            let mut values = std::collections::HashMap::with_capacity(count);
1239            for _ in 0..count {
1240                let key = read_string(data, pos)?;
1241                let value = read_metadata_value(data, pos)?;
1242                values.insert(key, value);
1243            }
1244            Ok(MetadataValue::Object(values))
1245        }
1246        8 => Ok(MetadataValue::Timestamp(read_u64(data, pos)?)),
1247        9 => Ok(MetadataValue::Geo {
1248            lat: read_f64(data, pos)?,
1249            lon: read_f64(data, pos)?,
1250        }),
1251        10 => Ok(MetadataValue::Reference(read_ref_target(data, pos)?)),
1252        11 => {
1253            let count = read_u32(data, pos)? as usize;
1254            let mut targets = Vec::with_capacity(count);
1255            for _ in 0..count {
1256                targets.push(read_ref_target(data, pos)?);
1257            }
1258            Ok(MetadataValue::References(targets))
1259        }
1260        tag => Err(StoreError::Serialization(format!(
1261            "unknown metadata value tag {tag}"
1262        ))),
1263    }
1264}