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