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