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#[cfg(test)]
6thread_local! {
7    static CHECKPOINT_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
8}
9
10fn checkpoint_visibility_matches(
11    previous: &CheckpointVersion,
12    current: &CheckpointVersion,
13    manager: &SegmentManager,
14) -> bool {
15    if previous.aborted_revision == current.aborted_revision
16        && previous.pending_vault == current.pending_vault
17    {
18        return true;
19    }
20    // A rollback or vault publication in another collection must not force
21    // payload serialization here. Only the persisted visibility matters.
22    let mut matches = true;
23    manager.for_each_entity(|entity| {
24        let skipped = |version: &CheckpointVersion| {
25            version.pending_vault.contains(&entity.id)
26                || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
27                    && version.aborted.contains(&entity.xmin))
28        };
29        let previous_skipped = skipped(previous);
30        let current_skipped = skipped(current);
31        matches = previous_skipped == current_skipped
32            && (current_skipped
33                || !matches!(entity.data, EntityData::Row(_))
34                || previous.aborted.contains(&entity.xmax)
35                    == current.aborted.contains(&entity.xmax));
36        matches
37    });
38    matches
39}
40
41// ── Pager-meta overflow chain (gh-477) ──────────────────────────────────────
42// When the serialized collection registry + cross-refs exceed a single page,
43// page 1 carries a native metadata-overflow header pointing at an overflow chain of
44// `PageType::Overflow` pages. Single-page metadata keeps the historical
45// bit-identical layout (`METADATA_MAGIC = "RDM2"` written directly at the
46// content offset).
47//
48// Page 1 (overflow form), starting at `HEADER_SIZE`:
49//   [0..4]   native metadata-overflow magic
50//   [4..8]   format_version (u32, mirrors inner payload version for debug)
51//   [8..12]  total_payload_bytes (u32)
52//   [12..16] next_overflow_page_id (u32, > 0)
53//   [16..]   first payload chunk (up to META_V3_FIRST_PAYLOAD_CAP bytes)
54//
55// Overflow continuation page, starting at `HEADER_SIZE`:
56//   [0..4]   next_overflow_page_id (u32, 0 if last)
57//   [4..8]   chunk_bytes (u32)
58//   [8..]    chunk payload (up to META_V3_OVERFLOW_PAYLOAD_CAP bytes)
59const META_PAGE_CONTENT_CAP: usize =
60    crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
61const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
62const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
63const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
64const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
65
66fn tree_matches_records(tree: &BTree, records: &[(Vec<u8>, Vec<u8>)]) -> Result<bool, StoreError> {
67    let mut cursor = tree
68        .cursor_first()
69        .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
70    for record in records {
71        if cursor
72            .next()
73            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
74            .as_ref()
75            != Some(record)
76        {
77            return Ok(false);
78        }
79    }
80    Ok(cursor
81        .next()
82        .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
83        .is_none())
84}
85
86fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
87    let cs = crate::storage::engine::HEADER_SIZE;
88    let page = match pager.read_page(1) {
89        Ok(p) => p,
90        Err(_) => return Ok(Vec::new()),
91    };
92    let bytes = page.as_bytes();
93    if bytes.len() < cs + META_V3_PAGE1_HEADER {
94        return Ok(Vec::new());
95    }
96    let Some(header) =
97        reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
98            PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
99        })?
100    else {
101        return Ok(Vec::new());
102    };
103    let mut next = header.next_overflow_page_id;
104    let mut pages = Vec::new();
105    let mut seen = std::collections::HashSet::new();
106    while next != 0 {
107        if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
108            return Err(PagerError::InvalidDatabase(
109                "cyclic metadata overflow chain".into(),
110            ));
111        }
112        let ov = match pager.read_page(next) {
113            Ok(p) => p,
114            Err(_) => break,
115        };
116        let ob = ov.as_bytes();
117        let nn = match reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..]) {
118            Ok(header) => header.next_overflow_page_id,
119            Err(_) => 0,
120        };
121        pages.push(next);
122        next = nn;
123    }
124    Ok(pages)
125}
126
127fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
128    let retired = existing_overflow_chain(pager)?;
129    let page = build_meta_page1_with_overflow(pager, meta_data)?;
130    // Cache eviction order must not publish a root before its children.
131    pager.sync()?;
132    pager.write_page(1, page)?;
133    pager.sync()?;
134    for id in retired {
135        pager.free_page(id)?;
136    }
137    Ok(())
138}
139
140fn build_meta_page1_with_overflow(
141    pager: &Pager,
142    meta_data: &[u8],
143) -> Result<crate::storage::engine::Page, PagerError> {
144    use crate::storage::engine::{Page, PageType, HEADER_SIZE};
145
146    let mut page1 = Page::new(PageType::Header, 1);
147    let cs = HEADER_SIZE;
148
149    if meta_data.len() <= META_PAGE_CONTENT_CAP {
150        // Single-page: bit-identical to the historical layout.
151        let buf = page1.as_bytes_mut();
152        buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
153        return Ok(page1);
154    }
155
156    // Multi-page overflow form. Split the inner payload into the first chunk
157    // (held on page 1) followed by zero-or-more continuation chunks chained
158    // through `PageType::Overflow` pages.
159    let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
160    let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
161    let mut chunks: Vec<&[u8]> = Vec::new();
162    while !tail.is_empty() {
163        let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
164        chunks.push(&tail[..take]);
165        tail = &tail[take..];
166    }
167
168    let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
169    let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
170    for _ in 0..chunks.len() {
171        let pg = pager.allocate_page(PageType::Overflow)?;
172        overflow_ids.push(pg.page_id());
173        overflow_pages.push(pg);
174    }
175
176    for i in 0..chunks.len() {
177        let next = if i + 1 < chunks.len() {
178            overflow_ids[i + 1]
179        } else {
180            0u32
181        };
182        let len = chunks[i].len() as u32;
183        let buf = overflow_pages[i].as_bytes_mut();
184        reddb_file::encode_native_metadata_overflow_continuation_header(
185            &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
186            reddb_file::NativeMetadataOverflowContinuationHeader {
187                next_overflow_page_id: next,
188                chunk_bytes: len,
189            },
190        )
191        .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
192        buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
193    }
194    for (idx, page) in overflow_pages.into_iter().enumerate() {
195        let id = overflow_ids[idx];
196        pager.write_page(id, page)?;
197    }
198
199    // Mirror the inner format_version for debug-friendly hex dumps.
200    let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
201        .ok()
202        .flatten()
203        .map_or(0, |header| header.format_version);
204
205    let buf = page1.as_bytes_mut();
206    reddb_file::encode_native_metadata_overflow_header(
207        &mut buf[cs..cs + META_V3_PAGE1_HEADER],
208        reddb_file::NativeMetadataOverflowHeader {
209            format_version,
210            total_payload_bytes: meta_data.len() as u32,
211            next_overflow_page_id: overflow_ids[0],
212        },
213    )
214    .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
215    buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
216        .copy_from_slice(first_chunk);
217
218    Ok(page1)
219}
220
221fn storage_integrity(
222    zone: &str,
223    id: impl Into<String>,
224    collection: Option<String>,
225    detail: impl Into<String>,
226) -> StoreError {
227    StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
228        zone, id, collection, detail,
229    ))
230}
231
232fn page_integrity(
233    page_id: u32,
234    collection: Option<String>,
235    detail: impl Into<String>,
236) -> StoreError {
237    storage_integrity("page", page_id.to_string(), collection, detail)
238}
239
240/// Assemble the full metadata payload from the internal manifest zone: page 1
241/// plus its overflow chain when the native overflow wrapper is present. Returns
242/// the bytes that the metadata parser would see starting from the content
243/// offset of page 1. Single-page metadata returns the raw page content
244/// (including trailing zero-pad), so the legacy parser sees the same bytes it
245/// always saw.
246///
247/// The zone is rooted by the superblock and checksummed per ADR 0074 §2: a
248/// failing checksum surfaces the didactic manifest-zone integrity error
249/// directly — there is no sidecar copy to fall back to since ADR 0038 §4
250/// phase 1 retired the shadows.
251fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
252    let cs = crate::storage::engine::HEADER_SIZE;
253    let meta_page = pager.read_manifest_page().map_err(|err| {
254        storage_integrity(
255            "manifest",
256            "manifest",
257            None,
258            format!("internal manifest zone read failed ({err})"),
259        )
260    })?;
261    let bytes = meta_page.as_bytes();
262    if bytes.len() < cs + 4 {
263        return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
264    }
265    let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
266        .map_err(|err| page_integrity(1, None, err.to_string()))?
267    {
268        Some(header) => header,
269        None => {
270            return Ok(Some(bytes[cs..].to_vec()));
271        }
272    };
273    if bytes.len() < cs + META_V3_PAGE1_HEADER {
274        return Err(page_integrity(
275            1,
276            None,
277            "metadata overflow header is truncated",
278        ));
279    }
280    let total = header.total_payload_bytes as usize;
281    let mut next = header.next_overflow_page_id;
282    let mut payload: Vec<u8> = Vec::with_capacity(total);
283    let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
284    payload.extend_from_slice(
285        &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
286    );
287    while next != 0 && payload.len() < total {
288        let ov = pager
289            .read_page(next)
290            .map_err(|err| page_integrity(next, None, err.to_string()))?;
291        let ob = ov.as_bytes();
292        if ob.len() < cs + META_V3_OVERFLOW_HEADER {
293            return Err(page_integrity(
294                next,
295                None,
296                "metadata overflow continuation is truncated",
297            ));
298        }
299        let continuation =
300            reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
301                .map_err(|err| page_integrity(next, None, err.to_string()))?;
302        let nn = continuation.next_overflow_page_id;
303        let len = continuation.chunk_bytes as usize;
304        let remaining = total - payload.len();
305        let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
306        payload.extend_from_slice(
307            &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
308        );
309        next = nn;
310    }
311    Ok(Some(payload))
312}
313
314impl UnifiedStore {
315    /// A completed inline insert already made this tree match its manager.
316    /// Advance the reuse proof only for that single, fenced mutation. Any
317    /// untracked edit or segment publication keeps the full comparison path.
318    pub(crate) fn note_paged_insert(
319        &self,
320        collection: &str,
321        manager: &Arc<SegmentManager>,
322        before: &[(u64, u64)],
323        id: EntityId,
324    ) {
325        let after = manager.checkpoint_revisions();
326        if before.len() != after.len() {
327            return;
328        }
329        let mut changed = 0;
330        for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
331            if old_id != new_id {
332                return;
333            }
334            if old_revision != new_revision {
335                if old_revision.checked_add(1) != Some(*new_revision) {
336                    return;
337                }
338                changed += 1;
339            }
340        }
341        if changed != 1 {
342            return;
343        }
344        let mut versions = self.checkpoint_versions.write();
345        let Some(previous) = versions.get_mut(collection) else {
346            return;
347        };
348        if previous.manager.ptr_eq(&Arc::downgrade(manager))
349            && previous.segments == before
350            && previous.format_version == self.format_version()
351            && !previous.pending_vault.contains(&id)
352            && !previous.aborted.contains(&0)
353        {
354            previous.segments = after;
355        }
356    }
357
358    pub(crate) fn mark_paged_registry_dirty(&self) {
359        self.paged_registry_dirty.store(true, Ordering::Release);
360    }
361
362    /// Get (or lazily create) the per-collection B-tree under a *read*
363    /// lock whenever possible. Returns a cloned `Arc<BTree>` so callers
364    /// can mutate the tree without holding the outer map's RwLock —
365    /// previously every insert serialised on `btree_indices.write()`,
366    /// costing ~60% of the concurrent-insert throughput ceiling.
367    pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
368        let pager = self.pager.as_ref()?;
369        if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
370            return Some(btree);
371        }
372        let mut write = self.btree_indices.write();
373        let btree = write
374            .entry(collection.to_string())
375            .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
376            .clone();
377        Some(btree)
378    }
379
380    pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
381        let Some(pager) = &self.pager else {
382            return Ok(());
383        };
384
385        if self.paged_registry_dirty.load(Ordering::Acquire) {
386            self.flush_paged_registry()?;
387            self.paged_registry_dirty.store(false, Ordering::Release);
388            return Ok(());
389        }
390
391        pager
392            .flush()
393            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
394    }
395
396    pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
397        let Some(pager) = &self.pager else {
398            return Ok(());
399        };
400
401        match pager.read_page(1) {
402            Ok(_) => {}
403            Err(PagerError::PageNotFound(_)) => {
404                let meta_page = pager
405                    .allocate_page(crate::storage::engine::PageType::Header)
406                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
407                pager
408                    .write_page(meta_page.page_id(), meta_page)
409                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
410            }
411            Err(e) => {
412                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
413            }
414        }
415
416        let format_version = STORE_VERSION_V11;
417        self.set_format_version(format_version);
418
419        let collections = self.collections.read();
420        let btree_indices = self.btree_indices.read();
421        let mut collection_roots = Vec::with_capacity(collections.len());
422        for (name, _) in collections.iter() {
423            let root_page = btree_indices
424                .get(name)
425                .map_or(0, |btree| btree.root_page_id());
426            collection_roots.push((name.clone(), root_page));
427        }
428        drop(btree_indices);
429        drop(collections);
430
431        let mut meta_data = Vec::with_capacity(4096);
432        reddb_file::encode_native_paged_metadata_header(
433            &mut meta_data,
434            reddb_file::NativePagedMetadataHeader {
435                format_version,
436                collection_count: collection_roots.len() as u32,
437            },
438        );
439        for (name, root_page) in &collection_roots {
440            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
441        }
442
443        let cross_refs = self.cross_refs.read();
444        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
445        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
446        for (source_id, refs) in cross_refs.iter() {
447            for (target_id, ref_type, collection) in refs {
448                reddb_file::encode_native_paged_cross_ref(
449                    &mut meta_data,
450                    source_id.raw(),
451                    target_id.raw(),
452                    ref_type.to_byte(),
453                    collection,
454                );
455            }
456        }
457
458        publish_paged_metadata(pager, &meta_data)
459            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
460
461        Ok(())
462    }
463
464    /// Get a reference to the underlying pager (if in paged mode).
465    pub fn pager(&self) -> Option<&Arc<Pager>> {
466        self.pager.as_ref()
467    }
468
469    /// Bytes of page-cache memory currently occupied by resident pages
470    /// (ADR 0073 §2). Slots are fixed size, so occupancy times the page size
471    /// *is* the footprint; a store without a pager occupies none.
472    pub fn page_cache_bytes_in_use(&self) -> u64 {
473        self.pager.as_ref().map_or(0, |pager| {
474            pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
475        })
476    }
477
478    /// Bytes of WAL buffer memory held by this store's group-commit
479    /// coordinator: queued records plus the writer's fixed append buffer.
480    pub fn wal_buffer_bytes_in_use(&self) -> u64 {
481        self.commit
482            .as_ref()
483            .map_or(0, |commit| commit.buffered_bytes())
484    }
485
486    /// Approximate resident bytes across every collection's segment arena
487    /// (ADR 0073 §2). Same number `stats().total_memory_bytes` reports, without
488    /// building the per-collection stats map a sampler would throw away.
489    pub fn segment_memory_bytes(&self) -> u64 {
490        let managers: Vec<Arc<SegmentManager>> =
491            self.collections.read().values().map(Arc::clone).collect();
492
493        managers
494            .iter()
495            .map(|manager| manager.resident_bytes())
496            .fold(0, u64::saturating_add)
497    }
498
499    /// Borrow the immutable store configuration. Runtime hooks (e.g. the
500    /// `auto_index_id` first-insert hook in `MutationEngine`) read knobs
501    /// off this struct without going through the legacy global config tree.
502    pub fn config(&self) -> &UnifiedStoreConfig {
503        &self.config
504    }
505
506    pub fn with_config(config: UnifiedStoreConfig) -> Self {
507        Self {
508            config,
509            format_version: AtomicU32::new(STORE_VERSION_V11),
510            next_entity_id: AtomicU64::new(1),
511            collections: RwLock::new(HashMap::new()),
512            cross_refs: RwLock::new(HashMap::new()),
513            reverse_refs: RwLock::new(HashMap::new()),
514            pager: None,
515            db_path: None,
516            btree_indices: RwLock::new(HashMap::new()),
517            checkpoint_versions: RwLock::new(HashMap::new()),
518            context_index: ContextIndex::new(),
519            entity_cache: EntityCache::new(),
520            graph_label_index: RwLock::new(HashMap::new()),
521            paged_registry_dirty: AtomicBool::new(false),
522            commit: None,
523            unindex_cross_refs_fast_path: AtomicU64::new(0),
524            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
525            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
526            aux_metadata: RwLock::new(Vec::new()),
527            snapshot_manager: std::sync::OnceLock::new(),
528            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
529            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
530        }
531    }
532
533    /// Open or create a page-based database
534    ///
535    /// This uses the page engine for ACID durability with B-tree indices.
536    /// The database file uses 16 KiB pages with checksums and efficient caching.
537    ///
538    /// # Arguments
539    ///
540    /// * `path` - Path to the database file (e.g., "data.rdb")
541    ///
542    /// # Example
543    ///
544    /// ```rust,ignore
545    /// let store = UnifiedStore::open("security.rdb")?;
546    /// store.create_collection("hosts")?;
547    /// // ... operations ...
548    /// store.persist()?; // Flush to disk
549    /// ```
550    pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
551        Self::open_with_config(path, UnifiedStoreConfig::default())
552    }
553
554    pub fn open_with_config(
555        path: impl AsRef<Path>,
556        config: UnifiedStoreConfig,
557    ) -> Result<Self, StoreError> {
558        let path = path.as_ref();
559        let mut pager_config = PagerConfig::default();
560        // ADR 0073 §2 — the page cache is pre-sized from its budget share.
561        // The pager's own `cache_size` default survives only for direct
562        // library callers who never resolved a budget.
563        if let Some(slots) = config.page_cache_slots {
564            pager_config.cache_size = slots;
565        }
566        // Tunables via env — experimental, used by the benchmark harness
567        // to compare durability profiles head-to-head with Postgres.
568        // REDDB_DOUBLE_WRITE=0 requests skipping the double-write buffer,
569        // which otherwise adds two fsyncs per pager flush (one on DWB, one
570        // on the main file). The pager honors this only when the actual
571        // data file is proven to live on a CoW filesystem with atomic page
572        // writes; otherwise it fails closed and keeps DWB enabled.
573        if matches!(
574            std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
575            Some("0") | Some("false") | Some("off")
576        ) {
577            pager_config.double_write = false;
578        }
579        let pager = Pager::open(path, pager_config)
580            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
581
582        let wal_path = reddb_file::layout::unified_wal_path(path);
583        // Explicit transactions and WAL-only vault versions need committed
584        // batches in every durability mode, including Strict's inline fsync.
585        let commit = Some(Arc::new(
586            StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
587                .map_err(StoreError::Io)?,
588        ));
589
590        let store = Self {
591            config,
592            format_version: AtomicU32::new(STORE_VERSION_V11),
593            next_entity_id: AtomicU64::new(1),
594            collections: RwLock::new(HashMap::new()),
595            cross_refs: RwLock::new(HashMap::new()),
596            reverse_refs: RwLock::new(HashMap::new()),
597            pager: Some(Arc::new(pager)),
598            db_path: Some(path.to_path_buf()),
599            btree_indices: RwLock::new(HashMap::new()),
600            checkpoint_versions: RwLock::new(HashMap::new()),
601            context_index: ContextIndex::new(),
602            entity_cache: EntityCache::new(),
603            graph_label_index: RwLock::new(HashMap::new()),
604            paged_registry_dirty: AtomicBool::new(false),
605            commit,
606            unindex_cross_refs_fast_path: AtomicU64::new(0),
607            replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
608            replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
609            aux_metadata: RwLock::new(Vec::new()),
610            snapshot_manager: std::sync::OnceLock::new(),
611            vault_publication_lock: parking_lot::ReentrantMutex::new(()),
612            pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
613        };
614
615        // Load existing data from pages if database exists
616        store.load_from_pages()?;
617        if let Some(commit) = &store.commit {
618            commit.replay_into(&store).map_err(StoreError::Io)?;
619        }
620        store.recover_operational_manifest()?;
621
622        Ok(store)
623    }
624
625    /// Replay changes the authoritative managers first. Build replacement trees
626    /// only for changed collections, leaving every published tree untouched until
627    /// all replacement pages are durable. Retain the WAL for auxiliary recovery.
628    pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
629        let Some(pager) = &self.pager else {
630            return Ok(());
631        };
632        let collections = self.collections.read();
633        let mut indices = self.btree_indices.write();
634        for (name, manager) in collections.iter() {
635            let mut records: Vec<_> = manager
636                .query_all(|_| true)
637                .into_iter()
638                .map(|entity| {
639                    let metadata = manager.get_metadata(entity.id);
640                    (
641                        entity.id.raw().to_be_bytes().to_vec(),
642                        Self::serialize_entity_record(
643                            &entity,
644                            metadata.as_ref(),
645                            self.format_version(),
646                        ),
647                    )
648                })
649                .collect();
650            records.sort_by(|left, right| left.0.cmp(&right.0));
651            let matches = if let Some(tree) = indices.get(name) {
652                tree_matches_records(tree, &records)?
653            } else {
654                records.is_empty()
655            };
656            if matches {
657                continue;
658            }
659            let tree = Arc::new(BTree::new(Arc::clone(pager)));
660            tree.bulk_insert_sorted(&records).map_err(|e| {
661                StoreError::Io(std::io::Error::other(format!(
662                    "WAL tree rebuild for '{name}': {e}"
663                )))
664            })?;
665            indices.insert(name.clone(), tree);
666            self.mark_paged_registry_dirty();
667        }
668        drop(indices);
669        drop(collections);
670        self.flush_paged_state()?;
671        // Opening has no concurrent writers. Replay has now durably reconciled
672        // these raw trees; do not immediately rebuild them at the first flush.
673        if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
674            let collections = self.collections.read();
675            let mut versions = self.checkpoint_versions.write();
676            for (name, manager) in collections.iter() {
677                versions.insert(
678                    name.clone(),
679                    CheckpointVersion {
680                        manager: Arc::downgrade(manager),
681                        segments: manager.checkpoint_revisions(),
682                        aborted_revision: 0,
683                        aborted: Arc::default(),
684                        pending_vault: Arc::default(),
685                        format_version: self.format_version(),
686                    },
687                );
688            }
689        }
690        Ok(())
691    }
692
693    pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
694        let Some(path) = &self.db_path else {
695            return Ok(());
696        };
697        let mut collections = self.list_collections();
698        collections.sort();
699        let pending_drops =
700            crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
701                .recover_or_bootstrap(&collections)
702                .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
703        for name in pending_drops {
704            if self.get_collection(&name).is_some() {
705                self.drop_collection(&name)?;
706            }
707        }
708        Ok(())
709    }
710
711    pub(crate) fn prepare_operational_collection_create(
712        &self,
713        name: &str,
714    ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
715        let Some(path) = &self.db_path else {
716            return Ok(None);
717        };
718        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
719            .prepare_collection_create(name)
720            .map(Some)
721            .map_err(StoreError::Io)
722    }
723
724    pub(crate) fn prepare_operational_collection_drop(
725        &self,
726        name: &str,
727    ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
728        let Some(path) = &self.db_path else {
729            return Ok(None);
730        };
731        crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
732            .prepare_collection_drop(name)
733            .map(Some)
734            .map_err(StoreError::Io)
735    }
736
737    /// Load data from page-based storage
738    ///
739    /// Reads the B-tree indices and reconstructs collections from pages.
740    fn load_from_pages(&self) -> Result<(), StoreError> {
741        let pager = match &self.pager {
742            Some(p) => p,
743            None => return Ok(()), // No pager, nothing to load
744        };
745
746        // Get page count
747        let page_count = pager.page_count().map_err(|e| {
748            StoreError::Io(std::io::Error::other(format!(
749                "failed to read page count: {}",
750                e
751            )))
752        })?;
753        if page_count <= 1 {
754            // Empty database (only header page)
755            return Ok(());
756        }
757
758        // Read metadata starting from page 1 (collections registry). The
759        // helper transparently follows the `RDM3` overflow chain when the
760        // metadata blob spans multiple pages and falls back to the legacy
761        // `<data>-meta` shadow when page 1 itself is corrupted.
762        if let Some(content_vec) = read_meta_payload(pager)? {
763            let content: &[u8] = &content_vec;
764            if content.len() >= 4 {
765                let mut pos = 0;
766                let mut format_version = STORE_VERSION_V1;
767
768                let collection_count = if let Some(header) =
769                    reddb_file::decode_native_paged_metadata_header(content)
770                        .map_err(|err| StoreError::Serialization(err.to_string()))?
771                {
772                    format_version = header.format_version;
773                    pos += reddb_file::METADATA_HEADER_BYTES;
774                    header.collection_count as usize
775                } else {
776                    let count = u32::from_le_bytes([
777                        content[pos],
778                        content[pos + 1],
779                        content[pos + 2],
780                        content[pos + 3],
781                    ]) as usize;
782                    pos += 4;
783                    count
784                };
785
786                self.set_format_version(format_version);
787
788                if pos > content.len() {
789                    return Ok(());
790                }
791
792                // Read collection names and their B-tree root pages
793                for _ in 0..collection_count {
794                    if let Ok(root) =
795                        reddb_file::decode_native_paged_collection_root(content, &mut pos)
796                    {
797                        // Root page ID for this collection's B-tree
798                        let root_page = root.root_page;
799                        let name = root.collection;
800
801                        // Hydrate the collection in memory only. Loading must
802                        // not emit WAL entries or rewrite the on-disk registry
803                        // before the existing B-tree roots are attached.
804                        let _ = self.create_collection_in_memory(&name);
805
806                        // Load B-tree with root page if it exists
807                        if root_page > 0 {
808                            let btree = BTree::with_root(Arc::clone(pager), root_page);
809
810                            // Load all entities from B-tree into the collection
811                            let mut cursor = btree.cursor_first().map_err(|err| {
812                                page_integrity(root_page, Some(name.clone()), err.to_string())
813                            })?;
814                            let manager = self.get_collection(&name);
815                            loop {
816                                let next = cursor.next().map_err(|err| {
817                                    page_integrity(root_page, Some(name.clone()), err.to_string())
818                                })?;
819                                let Some((key, value)) = next else {
820                                    break;
821                                };
822                                // Deserialize entity from value bytes
823                                if let Ok((entity, metadata)) =
824                                    Self::deserialize_entity_record(&value, self.format_version())
825                                {
826                                    if let Some(m) = &manager {
827                                        let id = entity.id;
828                                        if let EntityKind::TableRow { row_id, .. } = &entity.kind {
829                                            m.register_row_id(*row_id);
830                                        }
831                                        self.context_index.index_entity(&name, &entity);
832                                        m.restore_snapshot_entity(entity.clone())?;
833                                        if let Some(metadata) = metadata {
834                                            let _ = m.set_metadata(id, metadata);
835                                        }
836                                        self.register_entity_id(id);
837                                        if self.config.auto_index_refs {
838                                            self.index_cross_refs(&entity, &name)?;
839                                        }
840                                    }
841                                }
842                            }
843
844                            // Store the B-tree for future lookups
845                            self.btree_indices
846                                .write()
847                                .insert(name.clone(), Arc::new(btree));
848                            if format_version == STORE_VERSION_V11 {
849                                if let Some(manager) = &manager {
850                                    self.checkpoint_versions.write().insert(
851                                        name,
852                                        CheckpointVersion {
853                                            manager: Arc::downgrade(manager),
854                                            segments: manager.checkpoint_revisions(),
855                                            aborted_revision: 0,
856                                            aborted: Arc::default(),
857                                            pending_vault: Arc::default(),
858                                            format_version,
859                                        },
860                                    );
861                                }
862                            }
863                        }
864                    } else {
865                        break;
866                    }
867                }
868
869                if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
870                    let cross_ref_count = u32::from_le_bytes([
871                        content[pos],
872                        content[pos + 1],
873                        content[pos + 2],
874                        content[pos + 3],
875                    ]) as usize;
876                    pos += 4;
877
878                    for _ in 0..cross_ref_count {
879                        let Ok(cross_ref) =
880                            reddb_file::decode_native_paged_cross_ref(content, &mut pos)
881                        else {
882                            break;
883                        };
884                        let source_id = EntityId::new(cross_ref.source_id);
885                        let target_id = EntityId::new(cross_ref.target_id);
886                        let ref_type = RefType::from_byte(cross_ref.ref_type);
887                        let target_collection = cross_ref.target_collection;
888
889                        self.cross_refs.write().entry(source_id).or_default().push((
890                            target_id,
891                            ref_type,
892                            target_collection.clone(),
893                        ));
894
895                        if let Some((collection, mut entity)) = self.get_any(source_id) {
896                            let exists = entity.cross_refs().iter().any(|xref| {
897                                xref.target == target_id
898                                    && xref.ref_type == ref_type
899                                    && xref.target_collection == target_collection
900                            });
901                            if !exists {
902                                entity.cross_refs_mut().push(CrossRef::new(
903                                    source_id,
904                                    target_id,
905                                    target_collection.clone(),
906                                    ref_type,
907                                ));
908                                if let Some(manager) = self.get_collection(&collection) {
909                                    let _ = manager.update(entity);
910                                }
911                            }
912                        }
913                    }
914                }
915            }
916        }
917
918        if self.format_version() < STORE_VERSION_V11 {
919            self.set_format_version(STORE_VERSION_V11);
920        }
921
922        Ok(())
923    }
924
925    /// Deserialize an entity from binary bytes
926    pub(crate) fn deserialize_entity(
927        data: &[u8],
928        format_version: u32,
929    ) -> Result<UnifiedEntity, StoreError> {
930        let mut pos = 0;
931        Self::read_entity_binary(data, &mut pos, format_version)
932            .map_err(|e| StoreError::Serialization(e.to_string()))
933    }
934
935    /// Serialize an entity to binary bytes
936    pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
937        // Pre-allocate ~256 bytes to cover the typical 15-column
938        // typed row without any Vec growth. Bulk insert calls this
939        // millions of times per bench run; saving 2-3 reallocs per
940        // entity amortises.
941        let mut buf = Vec::with_capacity(256);
942        Self::write_entity_binary(&mut buf, entity, format_version);
943        buf
944    }
945
946    pub(crate) fn serialize_entity_record(
947        entity: &UnifiedEntity,
948        metadata: Option<&Metadata>,
949        format_version: u32,
950    ) -> Vec<u8> {
951        let entity_bytes = Self::serialize_entity(entity, format_version);
952        // Skip the intermediate metadata Vec when there's no metadata
953        // (common OLTP bulk-insert case): write a zero-length prefix
954        // directly into the record buffer. Only fall back to the old
955        // serialize_metadata() allocation when the caller actually
956        // has fields to persist.
957        let has_meta = matches!(metadata, Some(m) if !m.fields.is_empty());
958        if has_meta {
959            let metadata_bytes = serialize_metadata(metadata);
960            reddb_file::encode_native_entity_record_frame(&entity_bytes, Some(&metadata_bytes))
961        } else {
962            reddb_file::encode_native_entity_record_frame(&entity_bytes, None)
963        }
964    }
965
966    pub(crate) fn deserialize_entity_record(
967        data: &[u8],
968        format_version: u32,
969    ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
970        let Some(frame) = reddb_file::decode_native_entity_record_frame(data)
971            .map_err(|err| StoreError::Serialization(err.to_string()))?
972        else {
973            return Self::deserialize_entity(data, format_version).map(|entity| (entity, None));
974        };
975
976        let entity = Self::deserialize_entity(frame.entity, format_version)?;
977        let metadata = if frame.metadata.is_empty() {
978            None
979        } else {
980            let metadata = deserialize_metadata(frame.metadata)?;
981            if metadata.is_empty() {
982                None
983            } else {
984                Some(metadata)
985            }
986        };
987
988        Ok((entity, metadata))
989    }
990
991    /// Persist all data to page-based storage
992    ///
993    /// Writes all entities to B-tree pages and flushes to disk.
994    /// This provides ACID durability guarantees.
995    pub fn persist(&self) -> Result<(), StoreError> {
996        let _vault_guard = self.vault_publication_lock.lock();
997        let pending_vault_versions = self.pending_vault_versions.read();
998        let pager = match &self.pager {
999            Some(p) => p,
1000            None => {
1001                // No pager attached - use binary file fallback if path available
1002                if let Some(path) = &self.db_path {
1003                    return self
1004                        .save_to_file(path)
1005                        .map_err(|e| StoreError::Serialization(e.to_string()));
1006                }
1007                return Err(StoreError::Io(std::io::Error::other(
1008                    "No pager or path configured for persistence",
1009                )));
1010            }
1011        };
1012
1013        match pager.read_page(1) {
1014            Ok(_) => {}
1015            Err(PagerError::PageNotFound(_)) => {
1016                let meta_page = pager
1017                    .allocate_page(crate::storage::engine::PageType::Header)
1018                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1019                pager
1020                    .write_page(meta_page.page_id(), meta_page)
1021                    .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1022            }
1023            Err(e) => {
1024                return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1025            }
1026        }
1027
1028        if let Some(commit) = &self.commit {
1029            commit.force_sync().map_err(StoreError::Io)?;
1030        }
1031
1032        let collections = self.collections.read();
1033        let mut btree_indices = self.btree_indices.write();
1034
1035        // Collect collection names and their B-tree root pages
1036        let mut collection_roots: Vec<(String, u32)> = Vec::new();
1037
1038        // Build unpublished replacements. Deleting from the current trees here
1039        // would destroy the last checkpoint if a write fails before publication.
1040        let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1041        let previous_versions = self.checkpoint_versions.read();
1042        let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1043        let pending_vault = Arc::new(pending_vault_versions.clone());
1044        let (aborted_revision, aborted) = self.snapshot_manager.get().map_or_else(
1045            || (0, Default::default()),
1046            |manager| manager.checkpoint_aborted_state(),
1047        );
1048        let aborted = Arc::new(aborted);
1049        for (name, manager) in collections.iter() {
1050            let version = CheckpointVersion {
1051                manager: Arc::downgrade(manager),
1052                segments: manager.checkpoint_revisions(),
1053                aborted_revision,
1054                aborted: Arc::clone(&aborted),
1055                pending_vault: Arc::clone(&pending_vault),
1056                format_version: self.format_version(),
1057            };
1058            if let (Some(previous), Some(tree)) =
1059                (previous_versions.get(name), btree_indices.get(name))
1060            {
1061                if previous.manager.ptr_eq(&version.manager)
1062                    && previous.segments == version.segments
1063                    && previous.format_version == version.format_version
1064                    && checkpoint_visibility_matches(previous, &version, manager)
1065                {
1066                    collection_roots.push((name.clone(), tree.root_page_id()));
1067                    rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1068                    checkpoint_versions.insert(name.clone(), version);
1069                    continue;
1070                }
1071            }
1072            checkpoint_versions.insert(name.clone(), version);
1073            let mut records = Vec::new();
1074            manager.for_each_entity_with_metadata(|entity, metadata| {
1075                #[cfg(test)]
1076                CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1077                if pending_vault_versions.contains(&entity.id)
1078                    || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1079                        && aborted.contains(&entity.xmin))
1080                {
1081                    return true;
1082                }
1083                // Borrow ordinary rows instead of materializing a second full
1084                // collection of cloned entities alongside the encoded records.
1085                let mut normalized = std::borrow::Cow::Borrowed(entity);
1086                if matches!(entity.data, EntityData::Row(_)) && aborted.contains(&entity.xmax) {
1087                    normalized.to_mut().xmax = 0;
1088                }
1089                records.push((
1090                    entity.id.raw().to_be_bytes().to_vec(),
1091                    Self::serialize_entity_record(
1092                        &normalized,
1093                        metadata.as_ref(),
1094                        self.format_version(),
1095                    ),
1096                ));
1097                true
1098            });
1099            records.sort_by(|left, right| left.0.cmp(&right.0));
1100
1101            if let Some(tree) = btree_indices.get(name) {
1102                if tree_matches_records(tree, &records)? {
1103                    collection_roots.push((name.clone(), tree.root_page_id()));
1104                    rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1105                    continue;
1106                }
1107            }
1108
1109            let btree = Arc::new(BTree::new(Arc::clone(pager)));
1110
1111            // Slice G (#704): no per-row skip. Oversized values are
1112            // spilled through the slice-E write ladder inside
1113            // `bulk_insert_sorted` (inline → compressed inline →
1114            // overflow chain). The only rejection is the hard
1115            // `MAX_VALUE_SIZE` (256 MiB) ceiling, which surfaces as
1116            // `ValueTooLarge` from the bulk path after the rest of
1117            // the batch has landed.
1118            if !records.is_empty() {
1119                btree.bulk_insert_sorted(&records).map_err(|e| {
1120                    StoreError::Io(std::io::Error::other(format!(
1121                        "B-tree bulk rebuild error for '{name}': {e}"
1122                    )))
1123                })?;
1124            }
1125
1126            collection_roots.push((name.clone(), btree.root_page_id()));
1127            rebuilt_indices.insert(name.clone(), btree);
1128        }
1129
1130        // Write collection metadata to page 1
1131        let mut meta_data = Vec::with_capacity(4096);
1132
1133        let format_version = STORE_VERSION_V11;
1134        self.set_format_version(format_version);
1135
1136        reddb_file::encode_native_paged_metadata_header(
1137            &mut meta_data,
1138            reddb_file::NativePagedMetadataHeader {
1139                format_version,
1140                collection_count: collection_roots.len() as u32,
1141            },
1142        );
1143
1144        // Write each collection's name and B-tree root page
1145        for (name, root_page) in &collection_roots {
1146            reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1147        }
1148
1149        // Write cross-reference metadata
1150        let cross_refs = self.cross_refs.read();
1151        let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1152        meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1153        for (source_id, refs) in cross_refs.iter() {
1154            for (target_id, ref_type, collection) in refs {
1155                reddb_file::encode_native_paged_cross_ref(
1156                    &mut meta_data,
1157                    source_id.raw(),
1158                    target_id.raw(),
1159                    ref_type.to_byte(),
1160                    collection,
1161                );
1162            }
1163        }
1164
1165        publish_paged_metadata(pager, &meta_data)
1166            .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1167
1168        // Only the now-durable catalog may replace the live tree handles. Keep
1169        // previous tree pages retained for recovery; do not recycle them early.
1170        *btree_indices = rebuilt_indices;
1171        drop(previous_versions);
1172        *self.checkpoint_versions.write() = checkpoint_versions;
1173
1174        if let Some(commit) = &self.commit {
1175            commit.truncate().map_err(StoreError::Io)?;
1176        }
1177
1178        Ok(())
1179    }
1180
1181    /// Check if the store is using page-based persistence
1182    pub fn is_paged(&self) -> bool {
1183        self.pager.is_some()
1184    }
1185
1186    /// Current root page for a collection's primary B-tree, if one has
1187    /// been materialized in this store.
1188    pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1189        self.btree_indices
1190            .read()
1191            .get(collection)
1192            .map(|btree| btree.root_page_id())
1193            .filter(|root| *root != 0)
1194    }
1195
1196    /// Get the database file path (if using paged mode)
1197    pub fn db_path(&self) -> Option<&Path> {
1198        self.db_path.as_deref()
1199    }
1200}
1201
1202fn serialize_metadata(metadata: Option<&Metadata>) -> Vec<u8> {
1203    let Some(metadata) = metadata else {
1204        return Vec::new();
1205    };
1206    if metadata.is_empty() {
1207        return Vec::new();
1208    }
1209
1210    let mut entries: Vec<_> = metadata.iter().collect();
1211    entries.sort_by_key(|(a, _)| *a);
1212
1213    let mut buf = Vec::new();
1214    buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1215    for (key, value) in entries {
1216        write_string(&mut buf, key);
1217        write_metadata_value(&mut buf, value);
1218    }
1219    buf
1220}
1221
1222fn deserialize_metadata(data: &[u8]) -> Result<Metadata, StoreError> {
1223    let mut pos = 0usize;
1224    let count = read_u32(data, &mut pos)? as usize;
1225    let mut metadata = Metadata::new();
1226    for _ in 0..count {
1227        let key = read_string(data, &mut pos)?;
1228        let value = read_metadata_value(data, &mut pos)?;
1229        metadata.set(key, value);
1230    }
1231    Ok(metadata)
1232}
1233
1234fn write_string(buf: &mut Vec<u8>, value: &str) {
1235    reddb_file::encode_native_len_prefixed_str(buf, value);
1236}
1237
1238fn write_bytes(buf: &mut Vec<u8>, value: &[u8]) {
1239    reddb_file::encode_native_len_prefixed_bytes(buf, value);
1240}
1241
1242fn write_ref_target(buf: &mut Vec<u8>, target: &crate::storage::unified::RefTarget) {
1243    use crate::storage::unified::RefTarget;
1244
1245    match target {
1246        RefTarget::TableRow { table, row_id } => {
1247            buf.push(0);
1248            write_string(buf, table);
1249            buf.extend_from_slice(&row_id.to_le_bytes());
1250        }
1251        RefTarget::Node {
1252            collection,
1253            node_id,
1254        } => {
1255            buf.push(1);
1256            write_string(buf, collection);
1257            buf.extend_from_slice(&node_id.raw().to_le_bytes());
1258        }
1259        RefTarget::Edge {
1260            collection,
1261            edge_id,
1262        } => {
1263            buf.push(2);
1264            write_string(buf, collection);
1265            buf.extend_from_slice(&edge_id.raw().to_le_bytes());
1266        }
1267        RefTarget::Vector {
1268            collection,
1269            vector_id,
1270        } => {
1271            buf.push(3);
1272            write_string(buf, collection);
1273            buf.extend_from_slice(&vector_id.raw().to_le_bytes());
1274        }
1275        RefTarget::Entity {
1276            collection,
1277            entity_id,
1278        } => {
1279            buf.push(4);
1280            write_string(buf, collection);
1281            buf.extend_from_slice(&entity_id.raw().to_le_bytes());
1282        }
1283    }
1284}
1285
1286fn write_metadata_value(buf: &mut Vec<u8>, value: &MetadataValue) {
1287    match value {
1288        MetadataValue::Null => buf.push(0),
1289        MetadataValue::Bool(v) => {
1290            buf.push(1);
1291            buf.push(u8::from(*v));
1292        }
1293        MetadataValue::Int(v) => {
1294            buf.push(2);
1295            buf.extend_from_slice(&v.to_le_bytes());
1296        }
1297        MetadataValue::Float(v) => {
1298            buf.push(3);
1299            buf.extend_from_slice(&v.to_le_bytes());
1300        }
1301        MetadataValue::String(v) => {
1302            buf.push(4);
1303            write_string(buf, v);
1304        }
1305        MetadataValue::Bytes(v) => {
1306            buf.push(5);
1307            write_bytes(buf, v);
1308        }
1309        MetadataValue::Array(values) => {
1310            buf.push(6);
1311            buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
1312            for value in values {
1313                write_metadata_value(buf, value);
1314            }
1315        }
1316        MetadataValue::Object(values) => {
1317            buf.push(7);
1318            let mut entries: Vec<_> = values.iter().collect();
1319            entries.sort_by_key(|(a, _)| *a);
1320            buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1321            for (key, value) in entries {
1322                write_string(buf, key);
1323                write_metadata_value(buf, value);
1324            }
1325        }
1326        MetadataValue::Timestamp(v) => {
1327            buf.push(8);
1328            buf.extend_from_slice(&v.to_le_bytes());
1329        }
1330        MetadataValue::Geo { lat, lon } => {
1331            buf.push(9);
1332            buf.extend_from_slice(&lat.to_le_bytes());
1333            buf.extend_from_slice(&lon.to_le_bytes());
1334        }
1335        MetadataValue::Reference(target) => {
1336            buf.push(10);
1337            write_ref_target(buf, target);
1338        }
1339        MetadataValue::References(targets) => {
1340            buf.push(11);
1341            buf.extend_from_slice(&(targets.len() as u32).to_le_bytes());
1342            for target in targets {
1343                write_ref_target(buf, target);
1344            }
1345        }
1346    }
1347}
1348
1349fn read_exact_slice<'a>(
1350    data: &'a [u8],
1351    pos: &mut usize,
1352    len: usize,
1353) -> Result<&'a [u8], StoreError> {
1354    if *pos + len > data.len() {
1355        return Err(StoreError::Serialization(
1356            "truncated metadata payload".to_string(),
1357        ));
1358    }
1359    let slice = &data[*pos..*pos + len];
1360    *pos += len;
1361    Ok(slice)
1362}
1363
1364fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, StoreError> {
1365    let bytes = read_exact_slice(data, pos, 4)?;
1366    let mut raw = [0u8; 4];
1367    raw.copy_from_slice(bytes);
1368    Ok(u32::from_le_bytes(raw))
1369}
1370
1371fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, StoreError> {
1372    let bytes = read_exact_slice(data, pos, 8)?;
1373    let mut raw = [0u8; 8];
1374    raw.copy_from_slice(bytes);
1375    Ok(u64::from_le_bytes(raw))
1376}
1377
1378fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, StoreError> {
1379    let bytes = read_exact_slice(data, pos, 8)?;
1380    let mut raw = [0u8; 8];
1381    raw.copy_from_slice(bytes);
1382    Ok(i64::from_le_bytes(raw))
1383}
1384
1385fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, StoreError> {
1386    let bytes = read_exact_slice(data, pos, 8)?;
1387    let mut raw = [0u8; 8];
1388    raw.copy_from_slice(bytes);
1389    Ok(f64::from_le_bytes(raw))
1390}
1391
1392fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, StoreError> {
1393    let bytes = read_exact_slice(data, pos, 1)?;
1394    Ok(bytes[0])
1395}
1396
1397fn read_string(data: &[u8], pos: &mut usize) -> Result<String, StoreError> {
1398    reddb_file::decode_native_len_prefixed_string(data, pos)
1399        .map_err(|err| StoreError::Serialization(err.to_string()))
1400}
1401
1402fn read_bytes(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, StoreError> {
1403    reddb_file::decode_native_len_prefixed_bytes(data, pos)
1404        .map(|bytes| bytes.to_vec())
1405        .map_err(|err| StoreError::Serialization(err.to_string()))
1406}
1407
1408fn read_ref_target(
1409    data: &[u8],
1410    pos: &mut usize,
1411) -> Result<crate::storage::unified::RefTarget, StoreError> {
1412    use crate::storage::unified::RefTarget;
1413
1414    match read_u8(data, pos)? {
1415        0 => Ok(RefTarget::TableRow {
1416            table: read_string(data, pos)?,
1417            row_id: read_u64(data, pos)?,
1418        }),
1419        1 => Ok(RefTarget::Node {
1420            collection: read_string(data, pos)?,
1421            node_id: EntityId::new(read_u64(data, pos)?),
1422        }),
1423        2 => Ok(RefTarget::Edge {
1424            collection: read_string(data, pos)?,
1425            edge_id: EntityId::new(read_u64(data, pos)?),
1426        }),
1427        3 => Ok(RefTarget::Vector {
1428            collection: read_string(data, pos)?,
1429            vector_id: EntityId::new(read_u64(data, pos)?),
1430        }),
1431        4 => Ok(RefTarget::Entity {
1432            collection: read_string(data, pos)?,
1433            entity_id: EntityId::new(read_u64(data, pos)?),
1434        }),
1435        tag => Err(StoreError::Serialization(format!(
1436            "unknown metadata ref target tag {tag}"
1437        ))),
1438    }
1439}
1440
1441fn read_metadata_value(data: &[u8], pos: &mut usize) -> Result<MetadataValue, StoreError> {
1442    match read_u8(data, pos)? {
1443        0 => Ok(MetadataValue::Null),
1444        1 => Ok(MetadataValue::Bool(read_u8(data, pos)? != 0)),
1445        2 => Ok(MetadataValue::Int(read_i64(data, pos)?)),
1446        3 => Ok(MetadataValue::Float(read_f64(data, pos)?)),
1447        4 => Ok(MetadataValue::String(read_string(data, pos)?)),
1448        5 => Ok(MetadataValue::Bytes(read_bytes(data, pos)?)),
1449        6 => {
1450            let count = read_u32(data, pos)? as usize;
1451            let mut values = Vec::with_capacity(count);
1452            for _ in 0..count {
1453                values.push(read_metadata_value(data, pos)?);
1454            }
1455            Ok(MetadataValue::Array(values))
1456        }
1457        7 => {
1458            let count = read_u32(data, pos)? as usize;
1459            let mut values = std::collections::HashMap::with_capacity(count);
1460            for _ in 0..count {
1461                let key = read_string(data, pos)?;
1462                let value = read_metadata_value(data, pos)?;
1463                values.insert(key, value);
1464            }
1465            Ok(MetadataValue::Object(values))
1466        }
1467        8 => Ok(MetadataValue::Timestamp(read_u64(data, pos)?)),
1468        9 => Ok(MetadataValue::Geo {
1469            lat: read_f64(data, pos)?,
1470            lon: read_f64(data, pos)?,
1471        }),
1472        10 => Ok(MetadataValue::Reference(read_ref_target(data, pos)?)),
1473        11 => {
1474            let count = read_u32(data, pos)? as usize;
1475            let mut targets = Vec::with_capacity(count);
1476            for _ in 0..count {
1477                targets.push(read_ref_target(data, pos)?);
1478            }
1479            Ok(MetadataValue::References(targets))
1480        }
1481        tag => Err(StoreError::Serialization(format!(
1482            "unknown metadata value tag {tag}"
1483        ))),
1484    }
1485}
1486
1487#[cfg(test)]
1488mod checkpoint_work_tests {
1489    use super::*;
1490
1491    #[test]
1492    fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1493        let directory = tempfile::tempdir().expect("temporary store");
1494        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1495        store.create_collection("rows").expect("collection");
1496        for id in 1..=40 {
1497            store
1498                .insert(
1499                    "rows",
1500                    UnifiedEntity::table_row(
1501                        EntityId::new(id),
1502                        "rows",
1503                        id,
1504                        vec![Value::Integer(id as i64)],
1505                    ),
1506                )
1507                .expect("seed row");
1508        }
1509        store.persist().expect("initial checkpoint");
1510        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1511        store.persist().expect("unchanged checkpoint");
1512        assert_eq!(
1513            CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1514            0,
1515            "unchanged collections must not hold the writer fence while serializing every row"
1516        );
1517        drop(store);
1518        let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1519        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1520        reopened.persist().expect("unchanged recovered generation");
1521        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1522    }
1523
1524    #[test]
1525    fn paged_reopen_preserves_stored_sequence_identity() {
1526        let directory = tempfile::tempdir().expect("temporary store");
1527        let path = directory.path().join("data.rdb");
1528        let store = UnifiedStore::open(&path).expect("paged store");
1529        store.create_collection("rows").expect("collection");
1530        for id in [30, 10, 20] {
1531            store.insert("rows", row(id, id as i64)).expect("insert");
1532        }
1533        store.persist().expect("checkpoint");
1534        let sequences = [10, 20, 30].map(|id| {
1535            store
1536                .get("rows", EntityId::new(id))
1537                .expect("row")
1538                .sequence_id
1539        });
1540        drop(store);
1541        let reopened = UnifiedStore::open(&path).expect("reopen");
1542        let recovered = [10, 20, 30].map(|id| {
1543            reopened
1544                .get("rows", EntityId::new(id))
1545                .expect("row")
1546                .sequence_id
1547        });
1548        assert_eq!(
1549            recovered, sequences,
1550            "opening a checkpoint must not rewrite stored row identities"
1551        );
1552    }
1553
1554    #[test]
1555    fn auto_insert_persists_the_assigned_sequence_identity() {
1556        let directory = tempfile::tempdir().expect("temporary store");
1557        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1558        for id in 1u64..=3 {
1559            store
1560                .insert_auto("rows", row(id, id as i64))
1561                .expect("insert");
1562        }
1563        let manager = store.get_collection("rows").expect("rows");
1564        let tree = store
1565            .btree_indices
1566            .read()
1567            .get("rows")
1568            .cloned()
1569            .expect("tree");
1570        for id in 1u64..=3 {
1571            let bytes = tree
1572                .get(&id.to_be_bytes())
1573                .expect("lookup")
1574                .expect("stored row");
1575            let (persisted, _) =
1576                UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1577                    .expect("decode");
1578            assert_eq!(
1579                persisted.sequence_id,
1580                manager
1581                    .get(EntityId::new(id))
1582                    .expect("live row")
1583                    .sequence_id
1584            );
1585        }
1586    }
1587
1588    #[test]
1589    fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
1590        let directory = tempfile::tempdir().expect("temporary store");
1591        let path = directory.path().join("data.rdb");
1592        let store = UnifiedStore::open(&path).expect("paged store");
1593        store.insert_auto("rows", row(1, 1)).expect("seed");
1594        store.persist().expect("checkpoint");
1595        store.insert_auto("rows", row(2, 2)).expect("append");
1596        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1597        store.persist().expect("inline append checkpoint");
1598        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1599        store
1600            .get_collection("rows")
1601            .expect("rows")
1602            .update(row(1, 99))
1603            .expect("untracked edit");
1604        store.insert_auto("rows", row(3, 3)).expect("next append");
1605        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1606        store.persist().expect("untracked edit checkpoint");
1607        assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
1608        drop(store);
1609        let reopened = UnifiedStore::open(&path).expect("reopen");
1610        assert_eq!(
1611            reopened
1612                .get("rows", EntityId::new(1))
1613                .expect("row")
1614                .data
1615                .as_row()
1616                .expect("table")
1617                .columns,
1618            vec![Value::Integer(99)]
1619        );
1620    }
1621
1622    #[test]
1623    fn replayed_trees_are_reusable_after_durable_reconciliation() {
1624        let directory = tempfile::tempdir().expect("temporary store");
1625        let path = directory.path().join("data.rdb");
1626        let store = UnifiedStore::open(&path).expect("paged store");
1627        store.insert_auto("rows", row(1, 1)).expect("seed");
1628        store.persist().expect("checkpoint");
1629        store
1630            .insert_auto("rows", row(2, 2))
1631            .expect("WAL-backed append");
1632        drop(store);
1633        let reopened = UnifiedStore::open(&path).expect("replay");
1634        assert!(reopened.get("rows", EntityId::new(2)).is_some());
1635        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1636        reopened.persist().expect("checkpoint reconciled tree");
1637        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1638    }
1639
1640    fn row(id: u64, value: i64) -> UnifiedEntity {
1641        UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
1642    }
1643
1644    #[test]
1645    fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
1646        let directory = tempfile::tempdir().expect("temporary store");
1647        let path = directory.path().join("data.rdb");
1648        let store = UnifiedStore::open(&path).expect("paged store");
1649        store.create_collection("rows").expect("collection");
1650        store.insert("rows", row(1, 1)).expect("seed");
1651        store.persist().expect("initial checkpoint");
1652        let manager = store.get_collection("rows").expect("rows");
1653        manager.update(row(1, 2)).expect("direct update");
1654        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1655        store.persist().expect("updated row checkpoint");
1656        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1657        let mut metadata = Metadata::new();
1658        metadata.set("verified", MetadataValue::Bool(true));
1659        manager
1660            .set_metadata(EntityId::new(1), metadata)
1661            .expect("metadata");
1662        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1663        store.persist().expect("metadata-only checkpoint");
1664        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1665        manager
1666            .bulk_insert(vec![row(2, 2), row(3, 3)])
1667            .expect("bulk insert");
1668        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1669        store.persist().expect("bulk insert checkpoint");
1670        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
1671        manager.delete(EntityId::new(2)).expect("delete");
1672        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1673        store.persist().expect("changed checkpoint");
1674        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
1675        drop(store);
1676        let recovered = UnifiedStore::open(&path).expect("recover changed rows");
1677        let manager = recovered.get_collection("rows").expect("rows");
1678        assert_eq!(manager.count(), 2);
1679        assert_eq!(
1680            manager
1681                .get(EntityId::new(1))
1682                .expect("row")
1683                .data
1684                .as_row()
1685                .expect("table")
1686                .columns,
1687            vec![Value::Integer(2)]
1688        );
1689        assert!(manager.get(EntityId::new(2)).is_none());
1690        assert!(manager
1691            .get_metadata(EntityId::new(1))
1692            .expect("metadata")
1693            .get("verified")
1694            .is_some());
1695    }
1696
1697    #[test]
1698    fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
1699        let directory = tempfile::tempdir().expect("temporary store");
1700        let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1701        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1702        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1703        store.create_collection("rows").expect("collection");
1704        for id in 1..=40 {
1705            store.insert("rows", row(id, id as i64)).expect("seed");
1706        }
1707        store.persist().expect("initial checkpoint");
1708        let unrelated = snapshots.begin();
1709        snapshots.rollback(unrelated);
1710        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1711        store.persist().expect("unrelated rollback checkpoint");
1712        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1713
1714        store
1715            .pending_vault_versions
1716            .write()
1717            .insert(EntityId::new(100));
1718        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1719        store.persist().expect("unrelated vault checkpoint");
1720        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1721        store.pending_vault_versions.write().clear();
1722        CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1723        store
1724            .persist()
1725            .expect("unrelated vault publication checkpoint");
1726        assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1727    }
1728
1729    #[test]
1730    fn rollback_invalidates_reuse_without_a_segment_mutation() {
1731        let directory = tempfile::tempdir().expect("temporary store");
1732        let path = directory.path().join("data.rdb");
1733        let store = UnifiedStore::open(&path).expect("paged store");
1734        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1735        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1736        store.create_collection("rows").expect("collection");
1737        let xid = snapshots.begin();
1738        let mut pending = row(1, 1);
1739        pending.xmin = xid;
1740        store.insert("rows", pending).expect("version");
1741        store.persist().expect("before rollback");
1742        snapshots.rollback(xid);
1743        store.persist().expect("after rollback");
1744        drop(store);
1745        let recovered = UnifiedStore::open(&path).expect("recover");
1746        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1747    }
1748
1749    #[test]
1750    fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
1751        let directory = tempfile::tempdir().expect("temporary store");
1752        let path = directory.path().join("data.rdb");
1753        let store = UnifiedStore::open(&path).expect("paged store");
1754        let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1755        assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1756        store.create_collection("rows").expect("collection");
1757        let xid = snapshots.begin();
1758        let mut deleted = row(1, 1);
1759        deleted.xmax = xid;
1760        store.insert("rows", deleted).expect("version");
1761        store.persist().expect("before rollback");
1762        snapshots.rollback(xid);
1763        store.persist().expect("after rollback");
1764        drop(store);
1765        let recovered = UnifiedStore::open(&path).expect("recover");
1766        let manager = recovered.get_collection("rows").expect("rows");
1767        assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
1768    }
1769
1770    #[test]
1771    fn publishing_a_pending_vault_version_invalidates_reuse() {
1772        let directory = tempfile::tempdir().expect("temporary store");
1773        let path = directory.path().join("data.rdb");
1774        let store = UnifiedStore::open(&path).expect("paged store");
1775        store.create_collection("rows").expect("collection");
1776        store.insert("rows", row(1, 1)).expect("row");
1777        store
1778            .pending_vault_versions
1779            .write()
1780            .insert(EntityId::new(1));
1781        store.persist().expect("exclude pending vault version");
1782        store
1783            .pending_vault_versions
1784            .write()
1785            .remove(&EntityId::new(1));
1786        store.persist().expect("include published vault version");
1787        drop(store);
1788        let recovered = UnifiedStore::open(&path).expect("recover");
1789        assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
1790    }
1791}