Skip to main content

reddb_server/storage/unified/store/
impl_pages.rs

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