1use super::*;
2use crate::storage::unified::entity_cache::EntityCache;
3use parking_lot::RwLock;
4
5const CHECKPOINT_BATCH_BYTES: usize = 1024 * 1024;
6const CHECKPOINT_BATCH_ROWS: usize = 512;
7
8struct CheckpointTreeBuilder {
11 tree: Arc<BTree>,
12 records: Vec<(Vec<u8>, Vec<u8>)>,
13 bytes: usize,
14}
15
16impl CheckpointTreeBuilder {
17 fn new(pager: &Arc<Pager>) -> Self {
18 Self {
19 tree: Arc::new(BTree::new(Arc::clone(pager))),
20 records: Vec::new(),
21 bytes: 0,
22 }
23 }
24
25 fn push(&mut self, record: (Vec<u8>, Vec<u8>)) -> Result<(), StoreError> {
26 let bytes = record.0.len() + record.1.len();
27 if !self.records.is_empty()
28 && (self.bytes.saturating_add(bytes) > CHECKPOINT_BATCH_BYTES
29 || self.records.len() >= CHECKPOINT_BATCH_ROWS)
30 {
31 self.flush()?;
32 }
33 self.bytes += bytes;
34 self.records.push(record);
35 #[cfg(test)]
36 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(peak.get().max(self.bytes)));
37 Ok(())
38 }
39
40 fn flush(&mut self) -> Result<(), StoreError> {
41 self.tree
42 .bulk_insert_sorted(&self.records)
43 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
44 self.records.clear();
45 self.bytes = 0;
46 Ok(())
47 }
48
49 fn copy_prefix(&mut self, previous: &BTree, count: usize) -> Result<(), StoreError> {
50 let mut cursor = previous
51 .cursor_first()
52 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
53 for _ in 0..count {
54 let record = cursor
55 .next()
56 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
57 .ok_or_else(|| StoreError::Serialization("checkpoint prefix disappeared".into()))?;
58 self.push(record)?;
59 }
60 Ok(())
61 }
62}
63
64#[cfg(test)]
65thread_local! {
66 static CHECKPOINT_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
67 static CHECKPOINT_BUFFERED_BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
68}
69
70fn checkpoint_visibility_matches(
71 previous: &CheckpointVersion,
72 current: &CheckpointVersion,
73 manager: &SegmentManager,
74) -> bool {
75 if (previous.visibility_revision == current.visibility_revision
76 || previous.excluded_xids == current.excluded_xids)
77 && previous.pending_vault == current.pending_vault
78 {
79 return true;
80 }
81 let mut matches = true;
84 manager.for_each_entity(|entity| {
85 let skipped = |version: &CheckpointVersion| {
86 version.pending_vault.contains(&entity.id)
87 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
88 && version.excluded_xids.contains(&entity.xmin))
89 };
90 let previous_skipped = skipped(previous);
91 let current_skipped = skipped(current);
92 matches = previous_skipped == current_skipped
93 && (current_skipped
94 || !matches!(entity.data, EntityData::Row(_))
95 || previous.excluded_xids.contains(&entity.xmax)
96 == current.excluded_xids.contains(&entity.xmax));
97 matches
98 });
99 matches
100}
101
102const META_PAGE_CONTENT_CAP: usize =
121 crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
122const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
123const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
124const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
125const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
126
127fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
128 let cs = crate::storage::engine::HEADER_SIZE;
129 let page = match pager.read_page(1) {
130 Ok(p) => p,
131 Err(_) => return Ok(Vec::new()),
132 };
133 let bytes = page.as_bytes();
134 if bytes.len() < cs + META_V3_PAGE1_HEADER {
135 return Ok(Vec::new());
136 }
137 let Some(header) =
138 reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
139 PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
140 })?
141 else {
142 return Ok(Vec::new());
143 };
144 let mut next = header.next_overflow_page_id;
145 let mut pages = Vec::new();
146 let mut seen = std::collections::HashSet::new();
147 while next != 0 {
148 if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
149 return Err(PagerError::InvalidDatabase(
150 "cyclic metadata overflow chain".into(),
151 ));
152 }
153 let ov = match pager.read_page(next) {
154 Ok(p) => p,
155 Err(_) => break,
156 };
157 let ob = ov.as_bytes();
158 let nn = match reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..]) {
159 Ok(header) => header.next_overflow_page_id,
160 Err(_) => 0,
161 };
162 pages.push(next);
163 next = nn;
164 }
165 Ok(pages)
166}
167
168fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
169 let retired = existing_overflow_chain(pager)?;
170 let page = build_meta_page1_with_overflow(pager, meta_data)?;
171 pager.sync()?;
173 pager.write_page(1, page)?;
174 pager.sync()?;
175 for id in retired {
176 pager.free_page(id)?;
177 }
178 Ok(())
179}
180
181fn build_meta_page1_with_overflow(
182 pager: &Pager,
183 meta_data: &[u8],
184) -> Result<crate::storage::engine::Page, PagerError> {
185 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
186
187 let mut page1 = Page::new(PageType::Header, 1);
188 let cs = HEADER_SIZE;
189
190 if meta_data.len() <= META_PAGE_CONTENT_CAP {
191 let buf = page1.as_bytes_mut();
193 buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
194 return Ok(page1);
195 }
196
197 let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
201 let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
202 let mut chunks: Vec<&[u8]> = Vec::new();
203 while !tail.is_empty() {
204 let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
205 chunks.push(&tail[..take]);
206 tail = &tail[take..];
207 }
208
209 let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
210 let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
211 for _ in 0..chunks.len() {
212 let pg = pager.allocate_page(PageType::Overflow)?;
213 overflow_ids.push(pg.page_id());
214 overflow_pages.push(pg);
215 }
216
217 for i in 0..chunks.len() {
218 let next = if i + 1 < chunks.len() {
219 overflow_ids[i + 1]
220 } else {
221 0u32
222 };
223 let len = chunks[i].len() as u32;
224 let buf = overflow_pages[i].as_bytes_mut();
225 reddb_file::encode_native_metadata_overflow_continuation_header(
226 &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
227 reddb_file::NativeMetadataOverflowContinuationHeader {
228 next_overflow_page_id: next,
229 chunk_bytes: len,
230 },
231 )
232 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
233 buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
234 }
235 for (idx, page) in overflow_pages.into_iter().enumerate() {
236 let id = overflow_ids[idx];
237 pager.write_page(id, page)?;
238 }
239
240 let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
242 .ok()
243 .flatten()
244 .map_or(0, |header| header.format_version);
245
246 let buf = page1.as_bytes_mut();
247 reddb_file::encode_native_metadata_overflow_header(
248 &mut buf[cs..cs + META_V3_PAGE1_HEADER],
249 reddb_file::NativeMetadataOverflowHeader {
250 format_version,
251 total_payload_bytes: meta_data.len() as u32,
252 next_overflow_page_id: overflow_ids[0],
253 },
254 )
255 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
256 buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
257 .copy_from_slice(first_chunk);
258
259 Ok(page1)
260}
261
262fn storage_integrity(
263 zone: &str,
264 id: impl Into<String>,
265 collection: Option<String>,
266 detail: impl Into<String>,
267) -> StoreError {
268 StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
269 zone, id, collection, detail,
270 ))
271}
272
273fn page_integrity(
274 page_id: u32,
275 collection: Option<String>,
276 detail: impl Into<String>,
277) -> StoreError {
278 storage_integrity("page", page_id.to_string(), collection, detail)
279}
280
281fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
293 let cs = crate::storage::engine::HEADER_SIZE;
294 let meta_page = pager.read_manifest_page().map_err(|err| {
295 storage_integrity(
296 "manifest",
297 "manifest",
298 None,
299 format!("internal manifest zone read failed ({err})"),
300 )
301 })?;
302 let bytes = meta_page.as_bytes();
303 if bytes.len() < cs + 4 {
304 return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
305 }
306 let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
307 .map_err(|err| page_integrity(1, None, err.to_string()))?
308 {
309 Some(header) => header,
310 None => {
311 return Ok(Some(bytes[cs..].to_vec()));
312 }
313 };
314 if bytes.len() < cs + META_V3_PAGE1_HEADER {
315 return Err(page_integrity(
316 1,
317 None,
318 "metadata overflow header is truncated",
319 ));
320 }
321 let total = header.total_payload_bytes as usize;
322 let mut next = header.next_overflow_page_id;
323 let mut payload: Vec<u8> = Vec::with_capacity(total);
324 let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
325 payload.extend_from_slice(
326 &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
327 );
328 while next != 0 && payload.len() < total {
329 let ov = pager
330 .read_page(next)
331 .map_err(|err| page_integrity(next, None, err.to_string()))?;
332 let ob = ov.as_bytes();
333 if ob.len() < cs + META_V3_OVERFLOW_HEADER {
334 return Err(page_integrity(
335 next,
336 None,
337 "metadata overflow continuation is truncated",
338 ));
339 }
340 let continuation =
341 reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
342 .map_err(|err| page_integrity(next, None, err.to_string()))?;
343 let nn = continuation.next_overflow_page_id;
344 let len = continuation.chunk_bytes as usize;
345 let remaining = total - payload.len();
346 let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
347 payload.extend_from_slice(
348 &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
349 );
350 next = nn;
351 }
352 Ok(Some(payload))
353}
354
355impl UnifiedStore {
356 fn checkpoint_tree(
360 &self,
361 pager: &Arc<Pager>,
362 manager: &SegmentManager,
363 previous: Option<&Arc<BTree>>,
364 mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
365 ) -> Result<Arc<BTree>, StoreError> {
366 let mut cursor = previous
367 .map(|tree| tree.cursor_first())
368 .transpose()
369 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
370 let mut matched = 0;
371 let mut builder = None;
372 let mut failure = None;
373 manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
374 let Some(value) = encode(entity, metadata) else {
375 return true;
376 };
377 let record = (entity.id.raw().to_be_bytes().to_vec(), value);
378 let result = (|| {
379 if builder.is_none() {
380 if let Some(cursor) = cursor.as_mut() {
381 if cursor
382 .next()
383 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
384 .as_ref()
385 == Some(&record)
386 {
387 matched += 1;
388 return Ok(());
389 }
390 }
391 let mut replacement = CheckpointTreeBuilder::new(pager);
392 if let Some(previous) = previous {
393 replacement.copy_prefix(previous, matched)?;
394 }
395 builder = Some(replacement);
396 }
397 builder
398 .as_mut()
399 .expect("replacement initialized")
400 .push(record)
401 })();
402 if let Err(error) = result {
403 failure = Some(error);
404 return false;
405 }
406 true
407 });
408 if let Some(error) = failure {
409 return Err(error);
410 }
411 if builder.is_none() {
412 if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
413 if cursor
414 .next()
415 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
416 .is_none()
417 {
418 return Ok(Arc::clone(previous));
419 }
420 }
421 let mut replacement = CheckpointTreeBuilder::new(pager);
422 if let Some(previous) = previous {
423 replacement.copy_prefix(previous, matched)?;
424 }
425 builder = Some(replacement);
426 }
427 let mut builder = builder.expect("replacement or reused tree");
428 builder.flush()?;
429 Ok(builder.tree)
430 }
431
432 fn publish_reclaimable_trees(
436 &self,
437 pager: &Pager,
438 metadata: &mut Vec<u8>,
439 live: &HashMap<String, Arc<BTree>>,
440 ) -> Result<(), StoreError> {
441 let mut retired = self.retired_btrees.lock();
442 let mut roots = Vec::new();
443 let mut reclaim = Vec::new();
444 let mut remove = std::collections::HashSet::new();
445 for (index, entry) in retired.iter().enumerate() {
446 if live.values().any(|tree| Arc::ptr_eq(tree, &entry.tree)) {
447 remove.insert(index);
448 continue;
449 }
450 if entry.published
451 && Arc::strong_count(&entry.tree) == 1
452 && !entry.tree.has_retirement_readers()
453 {
454 reclaim.extend(
455 entry
456 .tree
457 .retired_pages()
458 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
459 );
460 remove.insert(index);
461 } else if entry.tree.root_page_id() != 0 {
462 roots.push(entry.tree.root_page_id());
463 }
464 }
465 roots.sort_unstable();
466 roots.dedup();
467 reddb_file::encode_native_paged_retired_trees(metadata, &roots)
468 .map_err(|e| StoreError::Serialization(e.to_string()))?;
469 publish_paged_metadata(pager, metadata)
472 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
473 let mut index = 0;
474 retired.retain_mut(|entry| {
475 let keep = !remove.contains(&index);
476 entry.published = true;
477 index += 1;
478 keep
479 });
480 if !reclaim.is_empty() {
481 pager
482 .free_retired_pages(&reclaim)
483 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
484 pager
485 .sync()
486 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
487 }
488 Ok(())
489 }
490
491 pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
492 let mut retired = self.retired_btrees.lock();
493 if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
494 {
495 retired.push(RetiredBTree {
496 tree,
497 published: false,
498 });
499 }
500 }
501
502 pub(crate) fn note_paged_insert(
506 &self,
507 collection: &str,
508 manager: &Arc<SegmentManager>,
509 before: &[(u64, u64)],
510 id: EntityId,
511 ) {
512 let after = manager.checkpoint_revisions();
513 if before.len() != after.len() {
514 return;
515 }
516 let mut changed = 0;
517 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
518 if old_id != new_id {
519 return;
520 }
521 if old_revision != new_revision {
522 if old_revision.checked_add(1) != Some(*new_revision) {
523 return;
524 }
525 changed += 1;
526 }
527 }
528 if changed != 1 {
529 return;
530 }
531 let mut versions = self.checkpoint_versions.write();
532 let Some(previous) = versions.get_mut(collection) else {
533 return;
534 };
535 if previous.manager.ptr_eq(&Arc::downgrade(manager))
536 && previous.segments == before
537 && previous.format_version == self.format_version()
538 && !previous.pending_vault.contains(&id)
539 && !previous.excluded_xids.contains(&0)
540 {
541 previous.segments = after;
542 }
543 }
544
545 pub(crate) fn note_paged_delete(
549 &self,
550 collection: &str,
551 manager: &Arc<SegmentManager>,
552 before: &[(u64, u64)],
553 deleted: &[EntityId],
554 ) {
555 let after = manager.checkpoint_revisions();
556 if before.len() != after.len() {
557 return;
558 }
559 let mut changes = 0_u64;
560 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
561 if old_id != new_id {
562 return;
563 }
564 let Some(delta) = new_revision.checked_sub(*old_revision) else {
565 return;
566 };
567 let Some(total) = changes.checked_add(delta) else {
568 return;
569 };
570 changes = total;
571 }
572 let Ok(expected) = u64::try_from(deleted.len()) else {
573 return;
574 };
575 if changes != expected {
576 return;
577 }
578 let mut versions = self.checkpoint_versions.write();
579 let Some(previous) = versions.get_mut(collection) else {
580 return;
581 };
582 if previous.manager.ptr_eq(&Arc::downgrade(manager))
583 && previous.segments == before
584 && previous.format_version == self.format_version()
585 && !deleted.iter().any(|id| previous.pending_vault.contains(id))
586 && !previous.excluded_xids.contains(&0)
587 {
588 previous.segments = after;
589 }
590 }
591
592 pub(crate) fn mark_paged_registry_dirty(&self) {
593 self.paged_registry_dirty.store(true, Ordering::Release);
594 }
595
596 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
602 let pager = self.pager.as_ref()?;
603 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
604 return Some(btree);
605 }
606 let mut write = self.btree_indices.write();
607 let btree = write
608 .entry(collection.to_string())
609 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
610 .clone();
611 Some(btree)
612 }
613
614 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
615 let Some(pager) = &self.pager else {
616 return Ok(());
617 };
618
619 if self.paged_registry_dirty.load(Ordering::Acquire) {
620 self.flush_paged_registry()?;
621 self.paged_registry_dirty.store(false, Ordering::Release);
622 return Ok(());
623 }
624
625 pager
626 .flush()
627 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
628 }
629
630 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
631 let _vault_guard = self.vault_publication_lock.lock();
632 let Some(pager) = &self.pager else {
633 return Ok(());
634 };
635
636 match pager.read_page(1) {
637 Ok(_) => {}
638 Err(PagerError::PageNotFound(_)) => {
639 let meta_page = pager
640 .allocate_page(crate::storage::engine::PageType::Header)
641 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
642 pager
643 .write_page(meta_page.page_id(), meta_page)
644 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
645 }
646 Err(e) => {
647 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
648 }
649 }
650
651 let format_version = STORE_VERSION_V11;
652 self.set_format_version(format_version);
653
654 let collections = self.collections.read();
655 let btree_indices = self.btree_indices.read();
656 let mut collection_roots = Vec::with_capacity(collections.len());
657 for (name, _) in collections.iter() {
658 let root_page = btree_indices
659 .get(name)
660 .map_or(0, |btree| btree.root_page_id());
661 collection_roots.push((name.clone(), root_page));
662 }
663 drop(btree_indices);
664 drop(collections);
665
666 let mut meta_data = Vec::with_capacity(4096);
667 reddb_file::encode_native_paged_metadata_header(
668 &mut meta_data,
669 reddb_file::NativePagedMetadataHeader {
670 format_version,
671 collection_count: collection_roots.len() as u32,
672 },
673 );
674 for (name, root_page) in &collection_roots {
675 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
676 }
677
678 let cross_refs = self.cross_refs.read();
679 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
680 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
681 for (source_id, refs) in cross_refs.iter() {
682 for (target_id, ref_type, collection) in refs {
683 reddb_file::encode_native_paged_cross_ref(
684 &mut meta_data,
685 source_id.raw(),
686 target_id.raw(),
687 ref_type.to_byte(),
688 collection,
689 );
690 }
691 }
692
693 self.publish_reclaimable_trees(pager, &mut meta_data, &self.btree_indices.read())?;
694
695 Ok(())
696 }
697
698 pub fn pager(&self) -> Option<&Arc<Pager>> {
700 self.pager.as_ref()
701 }
702
703 pub fn page_cache_bytes_in_use(&self) -> u64 {
707 self.pager.as_ref().map_or(0, |pager| {
708 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
709 })
710 }
711
712 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
715 self.commit
716 .as_ref()
717 .map_or(0, |commit| commit.buffered_bytes())
718 }
719
720 pub fn segment_memory_bytes(&self) -> u64 {
724 let managers: Vec<Arc<SegmentManager>> =
725 self.collections.read().values().map(Arc::clone).collect();
726
727 managers
728 .iter()
729 .map(|manager| manager.resident_bytes())
730 .fold(0, u64::saturating_add)
731 }
732
733 pub fn config(&self) -> &UnifiedStoreConfig {
737 &self.config
738 }
739
740 pub fn with_config(config: UnifiedStoreConfig) -> Self {
741 Self {
742 config,
743 format_version: AtomicU32::new(STORE_VERSION_V11),
744 next_entity_id: AtomicU64::new(1),
745 collections: RwLock::new(HashMap::new()),
746 cross_refs: RwLock::new(HashMap::new()),
747 reverse_refs: RwLock::new(HashMap::new()),
748 pager: None,
749 db_path: None,
750 btree_indices: RwLock::new(HashMap::new()),
751 checkpoint_versions: RwLock::new(HashMap::new()),
752 retired_btrees: parking_lot::Mutex::new(Vec::new()),
753 context_index: ContextIndex::new(),
754 entity_cache: EntityCache::new(),
755 graph_label_index: RwLock::new(HashMap::new()),
756 paged_registry_dirty: AtomicBool::new(false),
757 commit: None,
758 unindex_cross_refs_fast_path: AtomicU64::new(0),
759 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
760 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
761 aux_metadata: RwLock::new(Vec::new()),
762 snapshot_manager: std::sync::OnceLock::new(),
763 embedded_commit_session: std::sync::OnceLock::new(),
764 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
765 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
766 }
767 }
768
769 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
787 Self::open_with_config(path, UnifiedStoreConfig::default())
788 }
789
790 pub fn open_with_config(
791 path: impl AsRef<Path>,
792 config: UnifiedStoreConfig,
793 ) -> Result<Self, StoreError> {
794 let path = path.as_ref();
795 let mut pager_config = PagerConfig::default();
796 if let Some(slots) = config.page_cache_slots {
800 pager_config.cache_size = slots;
801 }
802 if matches!(
810 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
811 Some("0") | Some("false") | Some("off")
812 ) {
813 pager_config.double_write = false;
814 }
815 let pager = Pager::open(path, pager_config)
816 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
817
818 let wal_path = reddb_file::layout::unified_wal_path(path);
819 let commit = Some(Arc::new(
822 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
823 .map_err(StoreError::Io)?,
824 ));
825
826 let store = Self {
827 config,
828 format_version: AtomicU32::new(STORE_VERSION_V11),
829 next_entity_id: AtomicU64::new(1),
830 collections: RwLock::new(HashMap::new()),
831 cross_refs: RwLock::new(HashMap::new()),
832 reverse_refs: RwLock::new(HashMap::new()),
833 pager: Some(Arc::new(pager)),
834 db_path: Some(path.to_path_buf()),
835 btree_indices: RwLock::new(HashMap::new()),
836 checkpoint_versions: RwLock::new(HashMap::new()),
837 retired_btrees: parking_lot::Mutex::new(Vec::new()),
838 context_index: ContextIndex::new(),
839 entity_cache: EntityCache::new(),
840 graph_label_index: RwLock::new(HashMap::new()),
841 paged_registry_dirty: AtomicBool::new(false),
842 commit,
843 unindex_cross_refs_fast_path: AtomicU64::new(0),
844 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
845 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
846 aux_metadata: RwLock::new(Vec::new()),
847 snapshot_manager: std::sync::OnceLock::new(),
848 embedded_commit_session: std::sync::OnceLock::new(),
849 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
850 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
851 };
852
853 store.load_from_pages()?;
855 if let Some(commit) = &store.commit {
856 commit.replay_into(&store).map_err(StoreError::Io)?;
857 }
858 store.recover_operational_manifest()?;
859
860 Ok(store)
861 }
862
863 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
867 let Some(pager) = &self.pager else {
868 return Ok(());
869 };
870 let collections = self.collections.read();
871 let mut indices = self.btree_indices.write();
872 for (name, manager) in collections.iter() {
873 let tree =
874 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
875 Some(Self::serialize_entity_record(
876 entity,
877 metadata.as_ref(),
878 self.format_version(),
879 ))
880 })?;
881 if indices
882 .get(name)
883 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
884 {
885 continue;
886 }
887 if let Some(previous) = indices.insert(name.clone(), tree) {
888 self.retire_btree(previous);
889 }
890 self.mark_paged_registry_dirty();
891 }
892 drop(indices);
893 drop(collections);
894 self.flush_paged_state()?;
895 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
898 let collections = self.collections.read();
899 let mut versions = self.checkpoint_versions.write();
900 for (name, manager) in collections.iter() {
901 versions.insert(
902 name.clone(),
903 CheckpointVersion {
904 manager: Arc::downgrade(manager),
905 segments: manager.checkpoint_revisions(),
906 visibility_revision: 0,
907 excluded_xids: Arc::default(),
908 pending_vault: Arc::default(),
909 format_version: self.format_version(),
910 },
911 );
912 }
913 }
914 Ok(())
915 }
916
917 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
918 let Some(path) = &self.db_path else {
919 return Ok(());
920 };
921 let mut collections = self.list_collections();
922 collections.sort();
923 let pending_drops =
924 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
925 .recover_or_bootstrap(&collections)
926 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
927 for name in pending_drops {
928 if self.get_collection(&name).is_some() {
929 self.drop_collection(&name)?;
930 }
931 }
932 Ok(())
933 }
934
935 pub(crate) fn prepare_operational_collection_create(
936 &self,
937 name: &str,
938 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
939 let Some(path) = &self.db_path else {
940 return Ok(None);
941 };
942 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
943 .prepare_collection_create(name)
944 .map(Some)
945 .map_err(StoreError::Io)
946 }
947
948 pub(crate) fn prepare_operational_collection_drop(
949 &self,
950 name: &str,
951 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
952 let Some(path) = &self.db_path else {
953 return Ok(None);
954 };
955 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
956 .prepare_collection_drop(name)
957 .map(Some)
958 .map_err(StoreError::Io)
959 }
960
961 fn load_from_pages(&self) -> Result<(), StoreError> {
965 let pager = match &self.pager {
966 Some(p) => p,
967 None => return Ok(()), };
969
970 let page_count = pager.page_count().map_err(|e| {
972 StoreError::Io(std::io::Error::other(format!(
973 "failed to read page count: {}",
974 e
975 )))
976 })?;
977 if page_count <= 1 {
978 return Ok(());
980 }
981
982 if let Some(content_vec) = read_meta_payload(pager)? {
987 let content: &[u8] = &content_vec;
988 if content.len() >= 4 {
989 let mut pos = 0;
990 let mut format_version = STORE_VERSION_V1;
991
992 let collection_count = if let Some(header) =
993 reddb_file::decode_native_paged_metadata_header(content)
994 .map_err(|err| StoreError::Serialization(err.to_string()))?
995 {
996 format_version = header.format_version;
997 pos += reddb_file::METADATA_HEADER_BYTES;
998 header.collection_count as usize
999 } else {
1000 let count = u32::from_le_bytes([
1001 content[pos],
1002 content[pos + 1],
1003 content[pos + 2],
1004 content[pos + 3],
1005 ]) as usize;
1006 pos += 4;
1007 count
1008 };
1009
1010 self.set_format_version(format_version);
1011
1012 if pos > content.len() {
1013 return Ok(());
1014 }
1015
1016 for _ in 0..collection_count {
1018 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1019 .map_err(|err| page_integrity(1, None, err.to_string()))?;
1020 let root_page = root.root_page;
1022 let name = root.collection;
1023
1024 let _ = self.create_collection_in_memory(&name);
1028
1029 if root_page > 0 {
1031 let btree = BTree::with_root(Arc::clone(pager), root_page);
1032
1033 let mut cursor = btree.cursor_first().map_err(|err| {
1035 page_integrity(root_page, Some(name.clone()), err.to_string())
1036 })?;
1037 let manager = self.get_collection(&name);
1038 loop {
1039 let next = cursor.next().map_err(|err| {
1040 page_integrity(root_page, Some(name.clone()), err.to_string())
1041 })?;
1042 let Some((key, value)) = next else {
1043 break;
1044 };
1045 let (entity, metadata) =
1047 Self::deserialize_entity_record(&value, self.format_version())
1048 .map_err(|err| {
1049 page_integrity(
1050 root_page,
1051 Some(name.clone()),
1052 err.to_string(),
1053 )
1054 })?;
1055 if let Some(m) = &manager {
1056 let id = entity.id;
1057 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1058 m.register_row_id(*row_id);
1059 }
1060 self.context_index.index_entity(&name, &entity);
1061 m.restore_snapshot_entity(entity.clone())?;
1062 if let Some(metadata) = metadata {
1063 let _ = m.set_metadata(id, metadata);
1064 }
1065 self.register_entity_id(id);
1066 if self.config.auto_index_refs {
1067 self.index_cross_refs(&entity, &name)?;
1068 }
1069 }
1070 }
1071
1072 self.btree_indices
1074 .write()
1075 .insert(name.clone(), Arc::new(btree));
1076 if format_version == STORE_VERSION_V11 {
1077 if let Some(manager) = &manager {
1078 self.checkpoint_versions.write().insert(
1079 name,
1080 CheckpointVersion {
1081 manager: Arc::downgrade(manager),
1082 segments: manager.checkpoint_revisions(),
1083 visibility_revision: 0,
1084 excluded_xids: Arc::default(),
1085 pending_vault: Arc::default(),
1086 format_version,
1087 },
1088 );
1089 }
1090 }
1091 }
1092 }
1093
1094 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1095 let cross_ref_count = u32::from_le_bytes([
1096 content[pos],
1097 content[pos + 1],
1098 content[pos + 2],
1099 content[pos + 3],
1100 ]) as usize;
1101 pos += 4;
1102
1103 for _ in 0..cross_ref_count {
1104 let Ok(cross_ref) =
1105 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1106 else {
1107 break;
1108 };
1109 let source_id = EntityId::new(cross_ref.source_id);
1110 let target_id = EntityId::new(cross_ref.target_id);
1111 let ref_type = RefType::from_byte(cross_ref.ref_type);
1112 let target_collection = cross_ref.target_collection;
1113
1114 self.cross_refs.write().entry(source_id).or_default().push((
1115 target_id,
1116 ref_type,
1117 target_collection.clone(),
1118 ));
1119
1120 if let Some((collection, mut entity)) = self.get_any(source_id) {
1121 let exists = entity.cross_refs().iter().any(|xref| {
1122 xref.target == target_id
1123 && xref.ref_type == ref_type
1124 && xref.target_collection == target_collection
1125 });
1126 if !exists {
1127 entity.cross_refs_mut().push(CrossRef::new(
1128 source_id,
1129 target_id,
1130 target_collection.clone(),
1131 ref_type,
1132 ));
1133 if let Some(manager) = self.get_collection(&collection) {
1134 let _ = manager.update(entity);
1135 }
1136 }
1137 }
1138 }
1139 }
1140 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1141 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1142 let live_roots: std::collections::HashSet<_> = self
1143 .btree_indices
1144 .read()
1145 .values()
1146 .map(|tree| tree.root_page_id())
1147 .collect();
1148 for root in retired {
1149 if live_roots.contains(&root)
1150 || root
1151 >= pager
1152 .page_count()
1153 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1154 {
1155 return Err(page_integrity(
1156 root,
1157 None,
1158 "retired root aliases a live or missing page",
1159 ));
1160 }
1161 self.retired_btrees.lock().push(RetiredBTree {
1162 tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1163 published: true,
1164 });
1165 }
1166 }
1167 }
1168
1169 if self.format_version() < STORE_VERSION_V11 {
1170 self.set_format_version(STORE_VERSION_V11);
1171 }
1172
1173 Ok(())
1174 }
1175
1176 pub(crate) fn deserialize_entity(
1178 data: &[u8],
1179 format_version: u32,
1180 ) -> Result<UnifiedEntity, StoreError> {
1181 let mut pos = 0;
1182 Self::read_entity_binary(data, &mut pos, format_version)
1183 .map_err(|e| StoreError::Serialization(e.to_string()))
1184 }
1185
1186 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1188 let mut buf = Vec::with_capacity(256);
1193 Self::write_entity_binary(&mut buf, entity, format_version);
1194 buf
1195 }
1196
1197 pub(crate) fn serialize_entity_record(
1198 entity: &UnifiedEntity,
1199 metadata: Option<&Metadata>,
1200 format_version: u32,
1201 ) -> Vec<u8> {
1202 let entity_bytes = Self::serialize_entity(entity, format_version);
1203 let has_meta = matches!(metadata, Some(m) if !m.fields.is_empty());
1209 if has_meta {
1210 let metadata_bytes = serialize_metadata(metadata);
1211 reddb_file::encode_native_entity_record_frame(&entity_bytes, Some(&metadata_bytes))
1212 } else {
1213 reddb_file::encode_native_entity_record_frame(&entity_bytes, None)
1214 }
1215 }
1216
1217 pub(crate) fn deserialize_entity_record(
1218 data: &[u8],
1219 format_version: u32,
1220 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1221 let Some(frame) = reddb_file::decode_native_entity_record_frame(data)
1222 .map_err(|err| StoreError::Serialization(err.to_string()))?
1223 else {
1224 return Self::deserialize_entity(data, format_version).map(|entity| (entity, None));
1225 };
1226
1227 let entity = Self::deserialize_entity(frame.entity, format_version)?;
1228 let metadata = if frame.metadata.is_empty() {
1229 None
1230 } else {
1231 let metadata = deserialize_metadata(frame.metadata)?;
1232 if metadata.is_empty() {
1233 None
1234 } else {
1235 Some(metadata)
1236 }
1237 };
1238
1239 Ok((entity, metadata))
1240 }
1241
1242 pub fn persist(&self) -> Result<(), StoreError> {
1247 let _vault_guard = self.vault_publication_lock.lock();
1248 let pending_vault_versions = self.pending_vault_versions.read();
1249 let pager = match &self.pager {
1250 Some(p) => p,
1251 None => {
1252 if let Some(path) = &self.db_path {
1254 return self
1255 .save_to_file(path)
1256 .map_err(|e| StoreError::Serialization(e.to_string()));
1257 }
1258 return Err(StoreError::Io(std::io::Error::other(
1259 "No pager or path configured for persistence",
1260 )));
1261 }
1262 };
1263
1264 match pager.read_page(1) {
1265 Ok(_) => {}
1266 Err(PagerError::PageNotFound(_)) => {
1267 let meta_page = pager
1268 .allocate_page(crate::storage::engine::PageType::Header)
1269 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1270 pager
1271 .write_page(meta_page.page_id(), meta_page)
1272 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1273 }
1274 Err(e) => {
1275 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1276 }
1277 }
1278
1279 if let Some(commit) = &self.commit {
1280 commit.force_sync().map_err(StoreError::Io)?;
1281 }
1282
1283 let collections = self.collections.read();
1284 let mut btree_indices = self.btree_indices.write();
1285
1286 let mut collection_roots: Vec<(String, u32)> = Vec::new();
1288
1289 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1292 let previous_versions = self.checkpoint_versions.read();
1293 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1294 let pending_vault = Arc::new(pending_vault_versions.clone());
1295 let (visibility_revision, excluded) = self.snapshot_manager.get().map_or_else(
1296 || (0, Default::default()),
1297 |manager| manager.checkpoint_excluded_state(),
1298 );
1299 let excluded = Arc::new(excluded);
1300 for (name, manager) in collections.iter() {
1301 let version = CheckpointVersion {
1302 manager: Arc::downgrade(manager),
1303 segments: manager.checkpoint_revisions(),
1304 visibility_revision,
1305 excluded_xids: Arc::clone(&excluded),
1306 pending_vault: Arc::clone(&pending_vault),
1307 format_version: self.format_version(),
1308 };
1309 if let (Some(previous), Some(tree)) =
1310 (previous_versions.get(name), btree_indices.get(name))
1311 {
1312 if previous.manager.ptr_eq(&version.manager)
1313 && previous.segments == version.segments
1314 && previous.format_version == version.format_version
1315 && checkpoint_visibility_matches(previous, &version, manager)
1316 {
1317 collection_roots.push((name.clone(), tree.root_page_id()));
1318 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1319 checkpoint_versions.insert(name.clone(), version);
1320 continue;
1321 }
1322 }
1323 checkpoint_versions.insert(name.clone(), version);
1324 let btree = self.checkpoint_tree(
1325 pager,
1326 manager,
1327 btree_indices.get(name),
1328 |entity, metadata| {
1329 #[cfg(test)]
1330 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1331 if pending_vault_versions.contains(&entity.id)
1332 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1333 && excluded.contains(&entity.xmin))
1334 {
1335 return None;
1336 }
1337 let mut normalized = std::borrow::Cow::Borrowed(entity);
1338 if matches!(entity.data, EntityData::Row(_)) && excluded.contains(&entity.xmax)
1339 {
1340 normalized.to_mut().xmax = 0;
1341 }
1342 Some(Self::serialize_entity_record(
1343 &normalized,
1344 metadata.as_ref(),
1345 self.format_version(),
1346 ))
1347 },
1348 )?;
1349
1350 collection_roots.push((name.clone(), btree.root_page_id()));
1351 rebuilt_indices.insert(name.clone(), btree);
1352 }
1353
1354 let mut meta_data = Vec::with_capacity(4096);
1356
1357 let format_version = STORE_VERSION_V11;
1358 self.set_format_version(format_version);
1359
1360 reddb_file::encode_native_paged_metadata_header(
1361 &mut meta_data,
1362 reddb_file::NativePagedMetadataHeader {
1363 format_version,
1364 collection_count: collection_roots.len() as u32,
1365 },
1366 );
1367
1368 for (name, root_page) in &collection_roots {
1370 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1371 }
1372
1373 let cross_refs = self.cross_refs.read();
1375 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1376 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1377 for (source_id, refs) in cross_refs.iter() {
1378 for (target_id, ref_type, collection) in refs {
1379 reddb_file::encode_native_paged_cross_ref(
1380 &mut meta_data,
1381 source_id.raw(),
1382 target_id.raw(),
1383 ref_type.to_byte(),
1384 collection,
1385 );
1386 }
1387 }
1388
1389 for (name, previous) in btree_indices.iter() {
1390 if !rebuilt_indices
1391 .get(name)
1392 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1393 {
1394 self.retire_btree(Arc::clone(previous));
1395 }
1396 }
1397 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices)?;
1398
1399 *btree_indices = rebuilt_indices;
1402 drop(previous_versions);
1403 *self.checkpoint_versions.write() = checkpoint_versions;
1404
1405 if let Some(commit) = &self.commit {
1406 commit.truncate().map_err(StoreError::Io)?;
1407 }
1408
1409 Ok(())
1410 }
1411
1412 pub fn is_paged(&self) -> bool {
1414 self.pager.is_some()
1415 }
1416
1417 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1420 self.btree_indices
1421 .read()
1422 .get(collection)
1423 .map(|btree| btree.root_page_id())
1424 .filter(|root| *root != 0)
1425 }
1426
1427 pub fn db_path(&self) -> Option<&Path> {
1429 self.db_path.as_deref()
1430 }
1431}
1432
1433fn serialize_metadata(metadata: Option<&Metadata>) -> Vec<u8> {
1434 let Some(metadata) = metadata else {
1435 return Vec::new();
1436 };
1437 if metadata.is_empty() {
1438 return Vec::new();
1439 }
1440
1441 let mut entries: Vec<_> = metadata.iter().collect();
1442 entries.sort_by_key(|(a, _)| *a);
1443
1444 let mut buf = Vec::new();
1445 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1446 for (key, value) in entries {
1447 write_string(&mut buf, key);
1448 write_metadata_value(&mut buf, value);
1449 }
1450 buf
1451}
1452
1453fn deserialize_metadata(data: &[u8]) -> Result<Metadata, StoreError> {
1454 let mut pos = 0usize;
1455 let count = read_u32(data, &mut pos)? as usize;
1456 let mut metadata = Metadata::new();
1457 for _ in 0..count {
1458 let key = read_string(data, &mut pos)?;
1459 let value = read_metadata_value(data, &mut pos)?;
1460 metadata.set(key, value);
1461 }
1462 Ok(metadata)
1463}
1464
1465fn write_string(buf: &mut Vec<u8>, value: &str) {
1466 reddb_file::encode_native_len_prefixed_str(buf, value);
1467}
1468
1469fn write_bytes(buf: &mut Vec<u8>, value: &[u8]) {
1470 reddb_file::encode_native_len_prefixed_bytes(buf, value);
1471}
1472
1473fn write_ref_target(buf: &mut Vec<u8>, target: &crate::storage::unified::RefTarget) {
1474 use crate::storage::unified::RefTarget;
1475
1476 match target {
1477 RefTarget::TableRow { table, row_id } => {
1478 buf.push(0);
1479 write_string(buf, table);
1480 buf.extend_from_slice(&row_id.to_le_bytes());
1481 }
1482 RefTarget::Node {
1483 collection,
1484 node_id,
1485 } => {
1486 buf.push(1);
1487 write_string(buf, collection);
1488 buf.extend_from_slice(&node_id.raw().to_le_bytes());
1489 }
1490 RefTarget::Edge {
1491 collection,
1492 edge_id,
1493 } => {
1494 buf.push(2);
1495 write_string(buf, collection);
1496 buf.extend_from_slice(&edge_id.raw().to_le_bytes());
1497 }
1498 RefTarget::Vector {
1499 collection,
1500 vector_id,
1501 } => {
1502 buf.push(3);
1503 write_string(buf, collection);
1504 buf.extend_from_slice(&vector_id.raw().to_le_bytes());
1505 }
1506 RefTarget::Entity {
1507 collection,
1508 entity_id,
1509 } => {
1510 buf.push(4);
1511 write_string(buf, collection);
1512 buf.extend_from_slice(&entity_id.raw().to_le_bytes());
1513 }
1514 }
1515}
1516
1517fn write_metadata_value(buf: &mut Vec<u8>, value: &MetadataValue) {
1518 match value {
1519 MetadataValue::Null => buf.push(0),
1520 MetadataValue::Bool(v) => {
1521 buf.push(1);
1522 buf.push(u8::from(*v));
1523 }
1524 MetadataValue::Int(v) => {
1525 buf.push(2);
1526 buf.extend_from_slice(&v.to_le_bytes());
1527 }
1528 MetadataValue::Float(v) => {
1529 buf.push(3);
1530 buf.extend_from_slice(&v.to_le_bytes());
1531 }
1532 MetadataValue::String(v) => {
1533 buf.push(4);
1534 write_string(buf, v);
1535 }
1536 MetadataValue::Bytes(v) => {
1537 buf.push(5);
1538 write_bytes(buf, v);
1539 }
1540 MetadataValue::Array(values) => {
1541 buf.push(6);
1542 buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
1543 for value in values {
1544 write_metadata_value(buf, value);
1545 }
1546 }
1547 MetadataValue::Object(values) => {
1548 buf.push(7);
1549 let mut entries: Vec<_> = values.iter().collect();
1550 entries.sort_by_key(|(a, _)| *a);
1551 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1552 for (key, value) in entries {
1553 write_string(buf, key);
1554 write_metadata_value(buf, value);
1555 }
1556 }
1557 MetadataValue::Timestamp(v) => {
1558 buf.push(8);
1559 buf.extend_from_slice(&v.to_le_bytes());
1560 }
1561 MetadataValue::Geo { lat, lon } => {
1562 buf.push(9);
1563 buf.extend_from_slice(&lat.to_le_bytes());
1564 buf.extend_from_slice(&lon.to_le_bytes());
1565 }
1566 MetadataValue::Reference(target) => {
1567 buf.push(10);
1568 write_ref_target(buf, target);
1569 }
1570 MetadataValue::References(targets) => {
1571 buf.push(11);
1572 buf.extend_from_slice(&(targets.len() as u32).to_le_bytes());
1573 for target in targets {
1574 write_ref_target(buf, target);
1575 }
1576 }
1577 }
1578}
1579
1580fn read_exact_slice<'a>(
1581 data: &'a [u8],
1582 pos: &mut usize,
1583 len: usize,
1584) -> Result<&'a [u8], StoreError> {
1585 if *pos + len > data.len() {
1586 return Err(StoreError::Serialization(
1587 "truncated metadata payload".to_string(),
1588 ));
1589 }
1590 let slice = &data[*pos..*pos + len];
1591 *pos += len;
1592 Ok(slice)
1593}
1594
1595fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, StoreError> {
1596 let bytes = read_exact_slice(data, pos, 4)?;
1597 let mut raw = [0u8; 4];
1598 raw.copy_from_slice(bytes);
1599 Ok(u32::from_le_bytes(raw))
1600}
1601
1602fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, StoreError> {
1603 let bytes = read_exact_slice(data, pos, 8)?;
1604 let mut raw = [0u8; 8];
1605 raw.copy_from_slice(bytes);
1606 Ok(u64::from_le_bytes(raw))
1607}
1608
1609fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, StoreError> {
1610 let bytes = read_exact_slice(data, pos, 8)?;
1611 let mut raw = [0u8; 8];
1612 raw.copy_from_slice(bytes);
1613 Ok(i64::from_le_bytes(raw))
1614}
1615
1616fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, StoreError> {
1617 let bytes = read_exact_slice(data, pos, 8)?;
1618 let mut raw = [0u8; 8];
1619 raw.copy_from_slice(bytes);
1620 Ok(f64::from_le_bytes(raw))
1621}
1622
1623fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, StoreError> {
1624 let bytes = read_exact_slice(data, pos, 1)?;
1625 Ok(bytes[0])
1626}
1627
1628fn read_string(data: &[u8], pos: &mut usize) -> Result<String, StoreError> {
1629 reddb_file::decode_native_len_prefixed_string(data, pos)
1630 .map_err(|err| StoreError::Serialization(err.to_string()))
1631}
1632
1633fn read_bytes(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, StoreError> {
1634 reddb_file::decode_native_len_prefixed_bytes(data, pos)
1635 .map(|bytes| bytes.to_vec())
1636 .map_err(|err| StoreError::Serialization(err.to_string()))
1637}
1638
1639fn read_ref_target(
1640 data: &[u8],
1641 pos: &mut usize,
1642) -> Result<crate::storage::unified::RefTarget, StoreError> {
1643 use crate::storage::unified::RefTarget;
1644
1645 match read_u8(data, pos)? {
1646 0 => Ok(RefTarget::TableRow {
1647 table: read_string(data, pos)?,
1648 row_id: read_u64(data, pos)?,
1649 }),
1650 1 => Ok(RefTarget::Node {
1651 collection: read_string(data, pos)?,
1652 node_id: EntityId::new(read_u64(data, pos)?),
1653 }),
1654 2 => Ok(RefTarget::Edge {
1655 collection: read_string(data, pos)?,
1656 edge_id: EntityId::new(read_u64(data, pos)?),
1657 }),
1658 3 => Ok(RefTarget::Vector {
1659 collection: read_string(data, pos)?,
1660 vector_id: EntityId::new(read_u64(data, pos)?),
1661 }),
1662 4 => Ok(RefTarget::Entity {
1663 collection: read_string(data, pos)?,
1664 entity_id: EntityId::new(read_u64(data, pos)?),
1665 }),
1666 tag => Err(StoreError::Serialization(format!(
1667 "unknown metadata ref target tag {tag}"
1668 ))),
1669 }
1670}
1671
1672fn read_metadata_value(data: &[u8], pos: &mut usize) -> Result<MetadataValue, StoreError> {
1673 match read_u8(data, pos)? {
1674 0 => Ok(MetadataValue::Null),
1675 1 => Ok(MetadataValue::Bool(read_u8(data, pos)? != 0)),
1676 2 => Ok(MetadataValue::Int(read_i64(data, pos)?)),
1677 3 => Ok(MetadataValue::Float(read_f64(data, pos)?)),
1678 4 => Ok(MetadataValue::String(read_string(data, pos)?)),
1679 5 => Ok(MetadataValue::Bytes(read_bytes(data, pos)?)),
1680 6 => {
1681 let count = read_u32(data, pos)? as usize;
1682 let mut values = Vec::with_capacity(count);
1683 for _ in 0..count {
1684 values.push(read_metadata_value(data, pos)?);
1685 }
1686 Ok(MetadataValue::Array(values))
1687 }
1688 7 => {
1689 let count = read_u32(data, pos)? as usize;
1690 let mut values = std::collections::HashMap::with_capacity(count);
1691 for _ in 0..count {
1692 let key = read_string(data, pos)?;
1693 let value = read_metadata_value(data, pos)?;
1694 values.insert(key, value);
1695 }
1696 Ok(MetadataValue::Object(values))
1697 }
1698 8 => Ok(MetadataValue::Timestamp(read_u64(data, pos)?)),
1699 9 => Ok(MetadataValue::Geo {
1700 lat: read_f64(data, pos)?,
1701 lon: read_f64(data, pos)?,
1702 }),
1703 10 => Ok(MetadataValue::Reference(read_ref_target(data, pos)?)),
1704 11 => {
1705 let count = read_u32(data, pos)? as usize;
1706 let mut targets = Vec::with_capacity(count);
1707 for _ in 0..count {
1708 targets.push(read_ref_target(data, pos)?);
1709 }
1710 Ok(MetadataValue::References(targets))
1711 }
1712 tag => Err(StoreError::Serialization(format!(
1713 "unknown metadata value tag {tag}"
1714 ))),
1715 }
1716}
1717
1718#[cfg(test)]
1719mod checkpoint_work_tests {
1720 use super::*;
1721
1722 #[test]
1723 fn retired_tree_pages_remain_pinned_by_a_cursor() {
1724 let directory = tempfile::tempdir().expect("temporary store");
1725 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1726 for id in 1..=100 {
1727 store.insert_auto("rows", row(id, 1)).expect("seed");
1728 }
1729 store.persist().expect("initial checkpoint");
1730 let tree = store
1731 .btree_indices
1732 .read()
1733 .get("rows")
1734 .cloned()
1735 .expect("tree");
1736 let mut cursor = tree.cursor_first().expect("cursor");
1737 drop(tree);
1738 let manager = store.get_collection("rows").expect("rows");
1739 for value in 2..=6 {
1740 manager.update(row(1, value)).expect("change");
1741 store.persist().expect("checkpoint with old reader");
1742 }
1743 let mut count = 0;
1744 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
1745 let (entity, _) =
1746 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1747 .expect("decode");
1748 assert_eq!(
1749 entity.data.as_row().expect("row").columns,
1750 [Value::Integer(1)]
1751 );
1752 count += 1;
1753 }
1754 assert_eq!(count, 100);
1755 drop(cursor);
1756 store.persist().expect("reclaim after reader release");
1757 assert!(store.retired_btrees.lock().is_empty());
1758 }
1759
1760 #[test]
1761 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
1762 let directory = tempfile::tempdir().expect("temporary store");
1763 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1764 store.create_collection("rows").expect("collection");
1765 let manager = store.get_collection("rows").expect("rows");
1766 for id in (1..=1200).rev() {
1767 manager
1768 .insert(UnifiedEntity::table_row(
1769 EntityId::new(id),
1770 "rows",
1771 id,
1772 vec![Value::text("x".repeat(4096))],
1773 ))
1774 .expect("seed");
1775 }
1776 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
1777 store.persist().expect("bounded checkpoint");
1778 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
1779 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
1780 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
1781 }
1782
1783 #[test]
1784 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1785 let directory = tempfile::tempdir().expect("temporary store");
1786 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1787 store.create_collection("rows").expect("collection");
1788 for id in 1..=40 {
1789 store
1790 .insert(
1791 "rows",
1792 UnifiedEntity::table_row(
1793 EntityId::new(id),
1794 "rows",
1795 id,
1796 vec![Value::Integer(id as i64)],
1797 ),
1798 )
1799 .expect("seed row");
1800 }
1801 store.persist().expect("initial checkpoint");
1802 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1803 store.persist().expect("unchanged checkpoint");
1804 assert_eq!(
1805 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1806 0,
1807 "unchanged collections must not hold the writer fence while serializing every row"
1808 );
1809 drop(store);
1810 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1811 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1812 reopened.persist().expect("unchanged recovered generation");
1813 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1814 }
1815
1816 #[test]
1817 fn paged_reopen_preserves_stored_sequence_identity() {
1818 let directory = tempfile::tempdir().expect("temporary store");
1819 let path = directory.path().join("data.rdb");
1820 let store = UnifiedStore::open(&path).expect("paged store");
1821 store.create_collection("rows").expect("collection");
1822 for id in [30, 10, 20] {
1823 store.insert("rows", row(id, id as i64)).expect("insert");
1824 }
1825 store.persist().expect("checkpoint");
1826 let sequences = [10, 20, 30].map(|id| {
1827 store
1828 .get("rows", EntityId::new(id))
1829 .expect("row")
1830 .sequence_id
1831 });
1832 drop(store);
1833 let reopened = UnifiedStore::open(&path).expect("reopen");
1834 let recovered = [10, 20, 30].map(|id| {
1835 reopened
1836 .get("rows", EntityId::new(id))
1837 .expect("row")
1838 .sequence_id
1839 });
1840 assert_eq!(
1841 recovered, sequences,
1842 "opening a checkpoint must not rewrite stored row identities"
1843 );
1844 }
1845
1846 #[test]
1847 fn auto_insert_persists_the_assigned_sequence_identity() {
1848 let directory = tempfile::tempdir().expect("temporary store");
1849 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1850 for id in 1u64..=3 {
1851 store
1852 .insert_auto("rows", row(id, id as i64))
1853 .expect("insert");
1854 }
1855 let manager = store.get_collection("rows").expect("rows");
1856 let tree = store
1857 .btree_indices
1858 .read()
1859 .get("rows")
1860 .cloned()
1861 .expect("tree");
1862 for id in 1u64..=3 {
1863 let bytes = tree
1864 .get(&id.to_be_bytes())
1865 .expect("lookup")
1866 .expect("stored row");
1867 let (persisted, _) =
1868 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1869 .expect("decode");
1870 assert_eq!(
1871 persisted.sequence_id,
1872 manager
1873 .get(EntityId::new(id))
1874 .expect("live row")
1875 .sequence_id
1876 );
1877 }
1878 }
1879
1880 #[test]
1881 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
1882 let directory = tempfile::tempdir().expect("temporary store");
1883 let path = directory.path().join("data.rdb");
1884 let store = UnifiedStore::open(&path).expect("paged store");
1885 store.insert_auto("rows", row(1, 1)).expect("seed");
1886 store.persist().expect("checkpoint");
1887 store.insert_auto("rows", row(2, 2)).expect("append");
1888 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1889 store.persist().expect("inline append checkpoint");
1890 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1891 store
1892 .get_collection("rows")
1893 .expect("rows")
1894 .update(row(1, 99))
1895 .expect("untracked edit");
1896 store.insert_auto("rows", row(3, 3)).expect("next append");
1897 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1898 store.persist().expect("untracked edit checkpoint");
1899 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
1900 drop(store);
1901 let reopened = UnifiedStore::open(&path).expect("reopen");
1902 assert_eq!(
1903 reopened
1904 .get("rows", EntityId::new(1))
1905 .expect("row")
1906 .data
1907 .as_row()
1908 .expect("table")
1909 .columns,
1910 vec![Value::Integer(99)]
1911 );
1912 }
1913
1914 #[test]
1915 fn replayed_trees_are_reusable_after_durable_reconciliation() {
1916 let directory = tempfile::tempdir().expect("temporary store");
1917 let path = directory.path().join("data.rdb");
1918 let store = UnifiedStore::open(&path).expect("paged store");
1919 store.insert_auto("rows", row(1, 1)).expect("seed");
1920 store.persist().expect("checkpoint");
1921 store
1922 .insert_auto("rows", row(2, 2))
1923 .expect("WAL-backed append");
1924 drop(store);
1925 let reopened = UnifiedStore::open(&path).expect("replay");
1926 assert!(reopened.get("rows", EntityId::new(2)).is_some());
1927 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1928 reopened.persist().expect("checkpoint reconciled tree");
1929 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1930 }
1931
1932 fn row(id: u64, value: i64) -> UnifiedEntity {
1933 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
1934 }
1935
1936 #[test]
1937 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
1938 let directory = tempfile::tempdir().expect("temporary store");
1939 let path = directory.path().join("data.rdb");
1940 let store = UnifiedStore::open(&path).expect("paged store");
1941 store.create_collection("rows").expect("collection");
1942 store.insert("rows", row(1, 1)).expect("seed");
1943 store.persist().expect("initial checkpoint");
1944 let manager = store.get_collection("rows").expect("rows");
1945 manager.update(row(1, 2)).expect("direct update");
1946 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1947 store.persist().expect("updated row checkpoint");
1948 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1949 let mut metadata = Metadata::new();
1950 metadata.set("verified", MetadataValue::Bool(true));
1951 manager
1952 .set_metadata(EntityId::new(1), metadata)
1953 .expect("metadata");
1954 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1955 store.persist().expect("metadata-only checkpoint");
1956 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1957 manager
1958 .bulk_insert(vec![row(2, 2), row(3, 3)])
1959 .expect("bulk insert");
1960 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1961 store.persist().expect("bulk insert checkpoint");
1962 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
1963 manager.delete(EntityId::new(2)).expect("delete");
1964 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1965 store.persist().expect("changed checkpoint");
1966 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
1967 drop(store);
1968 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
1969 let manager = recovered.get_collection("rows").expect("rows");
1970 assert_eq!(manager.count(), 2);
1971 assert_eq!(
1972 manager
1973 .get(EntityId::new(1))
1974 .expect("row")
1975 .data
1976 .as_row()
1977 .expect("table")
1978 .columns,
1979 vec![Value::Integer(2)]
1980 );
1981 assert!(manager.get(EntityId::new(2)).is_none());
1982 assert!(manager
1983 .get_metadata(EntityId::new(1))
1984 .expect("metadata")
1985 .get("verified")
1986 .is_some());
1987 }
1988
1989 #[test]
1990 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
1991 let directory = tempfile::tempdir().expect("temporary store");
1992 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1993 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1994 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1995 store.create_collection("rows").expect("collection");
1996 for id in 1..=40 {
1997 store.insert("rows", row(id, id as i64)).expect("seed");
1998 }
1999 store.persist().expect("initial checkpoint");
2000 let unrelated = snapshots.begin();
2001 snapshots.rollback(unrelated);
2002 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2003 store.persist().expect("unrelated rollback checkpoint");
2004 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2005
2006 store
2007 .pending_vault_versions
2008 .write()
2009 .insert(EntityId::new(100));
2010 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2011 store.persist().expect("unrelated vault checkpoint");
2012 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2013 store.pending_vault_versions.write().clear();
2014 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2015 store
2016 .persist()
2017 .expect("unrelated vault publication checkpoint");
2018 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2019 }
2020
2021 #[test]
2022 fn rollback_invalidates_reuse_without_a_segment_mutation() {
2023 let directory = tempfile::tempdir().expect("temporary store");
2024 let path = directory.path().join("data.rdb");
2025 let store = UnifiedStore::open(&path).expect("paged store");
2026 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2027 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2028 store.create_collection("rows").expect("collection");
2029 let xid = snapshots.begin();
2030 let mut pending = row(1, 1);
2031 pending.xmin = xid;
2032 store.insert("rows", pending).expect("version");
2033 store.persist().expect("before rollback");
2034 snapshots.rollback(xid);
2035 store.persist().expect("after rollback");
2036 drop(store);
2037 let recovered = UnifiedStore::open(&path).expect("recover");
2038 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2039 }
2040
2041 #[test]
2042 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
2043 let directory = tempfile::tempdir().expect("temporary store");
2044 let path = directory.path().join("data.rdb");
2045 let store = UnifiedStore::open(&path).expect("paged store");
2046 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2047 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2048 store.create_collection("rows").expect("collection");
2049 let xid = snapshots.begin();
2050 let mut deleted = row(1, 1);
2051 deleted.xmax = xid;
2052 store.insert("rows", deleted).expect("version");
2053 store.persist().expect("before rollback");
2054 snapshots.rollback(xid);
2055 store.persist().expect("after rollback");
2056 drop(store);
2057 let recovered = UnifiedStore::open(&path).expect("recover");
2058 let manager = recovered.get_collection("rows").expect("rows");
2059 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
2060 }
2061
2062 #[test]
2063 fn publishing_a_pending_vault_version_invalidates_reuse() {
2064 let directory = tempfile::tempdir().expect("temporary store");
2065 let path = directory.path().join("data.rdb");
2066 let store = UnifiedStore::open(&path).expect("paged store");
2067 store.create_collection("rows").expect("collection");
2068 store.insert("rows", row(1, 1)).expect("row");
2069 store
2070 .pending_vault_versions
2071 .write()
2072 .insert(EntityId::new(1));
2073 store.persist().expect("exclude pending vault version");
2074 store
2075 .pending_vault_versions
2076 .write()
2077 .remove(&EntityId::new(1));
2078 store.persist().expect("include published vault version");
2079 drop(store);
2080 let recovered = UnifiedStore::open(&path).expect("recover");
2081 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
2082 }
2083}