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