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 let (pending_pages, published_pages) = pager
470 .deferred_reclamation_pages()
471 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
472 let readers = live.values().any(|tree| tree.has_retirement_readers())
473 || retired
474 .iter()
475 .any(|entry| entry.tree.has_retirement_readers());
476 let released_pages = if readers { Vec::new() } else { published_pages };
477 let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
478 let retained_pages: Vec<_> = pending_pages
479 .into_iter()
480 .filter(|id| !released.contains(id))
481 .collect();
482 reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
483 .map_err(|e| StoreError::Serialization(e.to_string()))?;
484 publish_paged_metadata(pager, metadata)
487 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
488 let mut index = 0;
489 retired.retain_mut(|entry| {
490 let keep = !remove.contains(&index);
491 entry.published = true;
492 index += 1;
493 keep
494 });
495 if !reclaim.is_empty() {
496 pager
497 .free_retired_pages(&reclaim)
498 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
499 pager
500 .sync()
501 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
502 }
503 pager
504 .complete_deferred_publication(&retained_pages, &released_pages)
505 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
506 if !released_pages.is_empty() {
507 pager
508 .sync()
509 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
510 }
511 Ok(())
512 }
513
514 pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
515 let mut retired = self.retired_btrees.lock();
516 if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
517 {
518 retired.push(RetiredBTree {
519 tree,
520 published: false,
521 });
522 }
523 }
524
525 pub(crate) fn note_paged_insert(
529 &self,
530 collection: &str,
531 manager: &Arc<SegmentManager>,
532 before: &[(u64, u64)],
533 id: EntityId,
534 ) {
535 let after = manager.checkpoint_revisions();
536 if before.len() != after.len() {
537 return;
538 }
539 let mut changed = 0;
540 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
541 if old_id != new_id {
542 return;
543 }
544 if old_revision != new_revision {
545 if old_revision.checked_add(1) != Some(*new_revision) {
546 return;
547 }
548 changed += 1;
549 }
550 }
551 if changed != 1 {
552 return;
553 }
554 let mut versions = self.checkpoint_versions.write();
555 let Some(previous) = versions.get_mut(collection) else {
556 return;
557 };
558 if previous.manager.ptr_eq(&Arc::downgrade(manager))
559 && previous.segments == before
560 && previous.format_version == self.format_version()
561 && !previous.pending_vault.contains(&id)
562 && !previous.excluded_xids.contains(&0)
563 {
564 previous.segments = after;
565 }
566 }
567
568 pub(crate) fn note_paged_delete(
572 &self,
573 collection: &str,
574 manager: &Arc<SegmentManager>,
575 before: &[(u64, u64)],
576 deleted: &[EntityId],
577 ) {
578 let after = manager.checkpoint_revisions();
579 if before.len() != after.len() {
580 return;
581 }
582 let mut changes = 0_u64;
583 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
584 if old_id != new_id {
585 return;
586 }
587 let Some(delta) = new_revision.checked_sub(*old_revision) else {
588 return;
589 };
590 let Some(total) = changes.checked_add(delta) else {
591 return;
592 };
593 changes = total;
594 }
595 let Ok(expected) = u64::try_from(deleted.len()) else {
596 return;
597 };
598 if changes != expected {
599 return;
600 }
601 let mut versions = self.checkpoint_versions.write();
602 let Some(previous) = versions.get_mut(collection) else {
603 return;
604 };
605 if previous.manager.ptr_eq(&Arc::downgrade(manager))
606 && previous.segments == before
607 && previous.format_version == self.format_version()
608 && !deleted.iter().any(|id| previous.pending_vault.contains(id))
609 && !previous.excluded_xids.contains(&0)
610 {
611 previous.segments = after;
612 }
613 }
614
615 pub(crate) fn mark_paged_registry_dirty(&self) {
616 self.paged_registry_dirty.store(true, Ordering::Release);
617 }
618
619 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
625 let pager = self.pager.as_ref()?;
626 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
627 return Some(btree);
628 }
629 let mut write = self.btree_indices.write();
630 let btree = write
631 .entry(collection.to_string())
632 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
633 .clone();
634 Some(btree)
635 }
636
637 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
638 let Some(pager) = &self.pager else {
639 return Ok(());
640 };
641
642 if self.paged_registry_dirty.load(Ordering::Acquire) {
643 self.flush_paged_registry()?;
644 self.paged_registry_dirty.store(false, Ordering::Release);
645 return Ok(());
646 }
647
648 pager
649 .flush()
650 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
651 }
652
653 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
654 let _vault_guard = self.vault_publication_lock.lock();
655 let Some(pager) = &self.pager else {
656 return Ok(());
657 };
658
659 match pager.read_page(1) {
660 Ok(_) => {}
661 Err(PagerError::PageNotFound(_)) => {
662 let meta_page = pager
663 .allocate_page(crate::storage::engine::PageType::Header)
664 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
665 pager
666 .write_page(meta_page.page_id(), meta_page)
667 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
668 }
669 Err(e) => {
670 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
671 }
672 }
673
674 let format_version = STORE_VERSION_V11;
675 self.set_format_version(format_version);
676
677 let collections = self.collections.read();
678 let btree_indices = self.btree_indices.read();
679 let mut collection_roots = Vec::with_capacity(collections.len());
680 for (name, _) in collections.iter() {
681 let root_page = btree_indices
682 .get(name)
683 .map_or(0, |btree| btree.root_page_id());
684 collection_roots.push((name.clone(), root_page));
685 }
686 drop(btree_indices);
687 drop(collections);
688
689 let mut meta_data = Vec::with_capacity(4096);
690 reddb_file::encode_native_paged_metadata_header(
691 &mut meta_data,
692 reddb_file::NativePagedMetadataHeader {
693 format_version,
694 collection_count: collection_roots.len() as u32,
695 },
696 );
697 for (name, root_page) in &collection_roots {
698 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
699 }
700
701 let cross_refs = self.cross_refs.read();
702 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
703 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
704 for (source_id, refs) in cross_refs.iter() {
705 for (target_id, ref_type, collection) in refs {
706 reddb_file::encode_native_paged_cross_ref(
707 &mut meta_data,
708 source_id.raw(),
709 target_id.raw(),
710 ref_type.to_byte(),
711 collection,
712 );
713 }
714 }
715
716 self.publish_reclaimable_trees(pager, &mut meta_data, &self.btree_indices.read())?;
717
718 Ok(())
719 }
720
721 pub fn pager(&self) -> Option<&Arc<Pager>> {
723 self.pager.as_ref()
724 }
725
726 pub fn page_cache_bytes_in_use(&self) -> u64 {
730 self.pager.as_ref().map_or(0, |pager| {
731 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
732 })
733 }
734
735 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
738 self.commit
739 .as_ref()
740 .map_or(0, |commit| commit.buffered_bytes())
741 }
742
743 pub fn segment_memory_bytes(&self) -> u64 {
747 let managers: Vec<Arc<SegmentManager>> =
748 self.collections.read().values().map(Arc::clone).collect();
749
750 managers
751 .iter()
752 .map(|manager| manager.resident_bytes())
753 .fold(0, u64::saturating_add)
754 }
755
756 pub fn config(&self) -> &UnifiedStoreConfig {
760 &self.config
761 }
762
763 pub fn with_config(config: UnifiedStoreConfig) -> Self {
764 Self {
765 config,
766 format_version: AtomicU32::new(STORE_VERSION_V11),
767 next_entity_id: AtomicU64::new(1),
768 collections: RwLock::new(HashMap::new()),
769 cross_refs: RwLock::new(HashMap::new()),
770 reverse_refs: RwLock::new(HashMap::new()),
771 pager: None,
772 db_path: None,
773 btree_indices: RwLock::new(HashMap::new()),
774 checkpoint_versions: RwLock::new(HashMap::new()),
775 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
776 retired_btrees: parking_lot::Mutex::new(Vec::new()),
777 context_index: ContextIndex::new(),
778 entity_cache: EntityCache::new(),
779 graph_label_index: RwLock::new(HashMap::new()),
780 paged_registry_dirty: AtomicBool::new(false),
781 commit: None,
782 unindex_cross_refs_fast_path: AtomicU64::new(0),
783 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
784 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
785 aux_metadata: RwLock::new(Vec::new()),
786 snapshot_manager: std::sync::OnceLock::new(),
787 embedded_commit_session: std::sync::OnceLock::new(),
788 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
789 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
790 }
791 }
792
793 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
811 Self::open_with_config(path, UnifiedStoreConfig::default())
812 }
813
814 pub fn open_with_config(
815 path: impl AsRef<Path>,
816 config: UnifiedStoreConfig,
817 ) -> Result<Self, StoreError> {
818 let path = path.as_ref();
819 let mut pager_config = PagerConfig::default();
820 if let Some(slots) = config.page_cache_slots {
824 pager_config.cache_size = slots;
825 }
826 if matches!(
834 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
835 Some("0") | Some("false") | Some("off")
836 ) {
837 pager_config.double_write = false;
838 }
839 let pager = Pager::open(path, pager_config)
840 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
841 pager
842 .enable_deferred_reclamation()
843 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
844
845 let wal_path = reddb_file::layout::unified_wal_path(path);
846 let commit = Some(Arc::new(
849 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
850 .map_err(StoreError::Io)?,
851 ));
852
853 let store = Self {
854 config,
855 format_version: AtomicU32::new(STORE_VERSION_V11),
856 next_entity_id: AtomicU64::new(1),
857 collections: RwLock::new(HashMap::new()),
858 cross_refs: RwLock::new(HashMap::new()),
859 reverse_refs: RwLock::new(HashMap::new()),
860 pager: Some(Arc::new(pager)),
861 db_path: Some(path.to_path_buf()),
862 btree_indices: RwLock::new(HashMap::new()),
863 checkpoint_versions: RwLock::new(HashMap::new()),
864 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
865 retired_btrees: parking_lot::Mutex::new(Vec::new()),
866 context_index: ContextIndex::new(),
867 entity_cache: EntityCache::new(),
868 graph_label_index: RwLock::new(HashMap::new()),
869 paged_registry_dirty: AtomicBool::new(false),
870 commit,
871 unindex_cross_refs_fast_path: AtomicU64::new(0),
872 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
873 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
874 aux_metadata: RwLock::new(Vec::new()),
875 snapshot_manager: std::sync::OnceLock::new(),
876 embedded_commit_session: std::sync::OnceLock::new(),
877 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
878 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
879 };
880
881 store.load_from_pages()?;
883 if let Some(commit) = &store.commit {
884 commit.replay_into(&store).map_err(StoreError::Io)?;
885 }
886 store.recover_operational_manifest()?;
887
888 Ok(store)
889 }
890
891 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
895 let Some(pager) = &self.pager else {
896 return Ok(());
897 };
898 let collections = self.collections.read();
899 let mut indices = self.btree_indices.write();
900 for (name, manager) in collections.iter() {
901 let tree =
902 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
903 Some(Self::serialize_entity_record(
904 entity,
905 metadata.as_ref(),
906 self.format_version(),
907 ))
908 })?;
909 if indices
910 .get(name)
911 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
912 {
913 continue;
914 }
915 if let Some(previous) = indices.insert(name.clone(), tree) {
916 self.retire_btree(previous);
917 }
918 self.mark_paged_registry_dirty();
919 }
920 drop(indices);
921 drop(collections);
922 self.flush_paged_state()?;
923 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
926 let collections = self.collections.read();
927 let mut versions = self.checkpoint_versions.write();
928 for (name, manager) in collections.iter() {
929 versions.insert(
930 name.clone(),
931 CheckpointVersion {
932 manager: Arc::downgrade(manager),
933 segments: manager.checkpoint_revisions(),
934 visibility_revision: 0,
935 excluded_xids: Arc::default(),
936 pending_vault: Arc::default(),
937 format_version: self.format_version(),
938 },
939 );
940 }
941 }
942 Ok(())
943 }
944
945 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
946 let Some(path) = &self.db_path else {
947 return Ok(());
948 };
949 let mut collections = self.list_collections();
950 collections.sort();
951 let pending_drops =
952 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
953 .recover_or_bootstrap(&collections)
954 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
955 for name in pending_drops {
956 if self.get_collection(&name).is_some() {
957 self.drop_collection(&name)?;
958 }
959 }
960 Ok(())
961 }
962
963 pub(crate) fn prepare_operational_collection_create(
964 &self,
965 name: &str,
966 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
967 let Some(path) = &self.db_path else {
968 return Ok(None);
969 };
970 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
971 .prepare_collection_create(name)
972 .map(Some)
973 .map_err(StoreError::Io)
974 }
975
976 pub(crate) fn prepare_operational_collection_drop(
977 &self,
978 name: &str,
979 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
980 let Some(path) = &self.db_path else {
981 return Ok(None);
982 };
983 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
984 .prepare_collection_drop(name)
985 .map(Some)
986 .map_err(StoreError::Io)
987 }
988
989 fn load_from_pages(&self) -> Result<(), StoreError> {
993 let pager = match &self.pager {
994 Some(p) => p,
995 None => return Ok(()), };
997
998 let page_count = pager.page_count().map_err(|e| {
1000 StoreError::Io(std::io::Error::other(format!(
1001 "failed to read page count: {}",
1002 e
1003 )))
1004 })?;
1005 if page_count <= 1 {
1006 return Ok(());
1008 }
1009
1010 if let Some(content_vec) = read_meta_payload(pager)? {
1015 let content: &[u8] = &content_vec;
1016 if content.len() >= 4 {
1017 let mut pos = 0;
1018 let mut format_version = STORE_VERSION_V1;
1019
1020 let collection_count = if let Some(header) =
1021 reddb_file::decode_native_paged_metadata_header(content)
1022 .map_err(|err| StoreError::Serialization(err.to_string()))?
1023 {
1024 format_version = header.format_version;
1025 pos += reddb_file::METADATA_HEADER_BYTES;
1026 header.collection_count as usize
1027 } else {
1028 let count = u32::from_le_bytes([
1029 content[pos],
1030 content[pos + 1],
1031 content[pos + 2],
1032 content[pos + 3],
1033 ]) as usize;
1034 pos += 4;
1035 count
1036 };
1037
1038 self.set_format_version(format_version);
1039
1040 if pos > content.len() {
1041 return Ok(());
1042 }
1043
1044 for _ in 0..collection_count {
1046 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1047 .map_err(|err| page_integrity(1, None, err.to_string()))?;
1048 let root_page = root.root_page;
1050 let name = root.collection;
1051
1052 let _ = self.create_collection_in_memory(&name);
1056
1057 if root_page > 0 {
1059 let btree = BTree::with_root(Arc::clone(pager), root_page);
1060
1061 let mut cursor = btree.cursor_first().map_err(|err| {
1063 page_integrity(root_page, Some(name.clone()), err.to_string())
1064 })?;
1065 let manager = self.get_collection(&name);
1066 loop {
1067 let next = cursor.next().map_err(|err| {
1068 page_integrity(root_page, Some(name.clone()), err.to_string())
1069 })?;
1070 let Some((key, value)) = next else {
1071 break;
1072 };
1073 let (entity, metadata) =
1075 Self::deserialize_entity_record(&value, self.format_version())
1076 .map_err(|err| {
1077 page_integrity(
1078 root_page,
1079 Some(name.clone()),
1080 err.to_string(),
1081 )
1082 })?;
1083 if let Some(m) = &manager {
1084 let id = entity.id;
1085 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1086 m.register_row_id(*row_id);
1087 }
1088 self.context_index.index_entity(&name, &entity);
1089 m.restore_snapshot_entity(entity.clone())?;
1090 if let Some(metadata) = metadata {
1091 let _ = m.set_metadata(id, metadata);
1092 }
1093 self.register_entity_id(id);
1094 if self.config.auto_index_refs {
1095 self.index_cross_refs(&entity, &name)?;
1096 }
1097 }
1098 }
1099
1100 self.btree_indices
1102 .write()
1103 .insert(name.clone(), Arc::new(btree));
1104 if format_version == STORE_VERSION_V11 {
1105 if let Some(manager) = &manager {
1106 self.checkpoint_versions.write().insert(
1107 name,
1108 CheckpointVersion {
1109 manager: Arc::downgrade(manager),
1110 segments: manager.checkpoint_revisions(),
1111 visibility_revision: 0,
1112 excluded_xids: Arc::default(),
1113 pending_vault: Arc::default(),
1114 format_version,
1115 },
1116 );
1117 }
1118 }
1119 }
1120 }
1121
1122 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1123 let cross_ref_count = u32::from_le_bytes([
1124 content[pos],
1125 content[pos + 1],
1126 content[pos + 2],
1127 content[pos + 3],
1128 ]) as usize;
1129 pos += 4;
1130
1131 for _ in 0..cross_ref_count {
1132 let Ok(cross_ref) =
1133 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1134 else {
1135 break;
1136 };
1137 let source_id = EntityId::new(cross_ref.source_id);
1138 let target_id = EntityId::new(cross_ref.target_id);
1139 let ref_type = RefType::from_byte(cross_ref.ref_type);
1140 let target_collection = cross_ref.target_collection;
1141
1142 self.cross_refs.write().entry(source_id).or_default().push((
1143 target_id,
1144 ref_type,
1145 target_collection.clone(),
1146 ));
1147
1148 if let Some((collection, mut entity)) = self.get_any(source_id) {
1149 let exists = entity.cross_refs().iter().any(|xref| {
1150 xref.target == target_id
1151 && xref.ref_type == ref_type
1152 && xref.target_collection == target_collection
1153 });
1154 if !exists {
1155 entity.cross_refs_mut().push(CrossRef::new(
1156 source_id,
1157 target_id,
1158 target_collection.clone(),
1159 ref_type,
1160 ));
1161 if let Some(manager) = self.get_collection(&collection) {
1162 let _ = manager.update(entity);
1163 }
1164 }
1165 }
1166 }
1167 }
1168 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1169 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1170 let live_roots: std::collections::HashSet<_> = self
1171 .btree_indices
1172 .read()
1173 .values()
1174 .map(|tree| tree.root_page_id())
1175 .collect();
1176 for root in retired {
1177 if live_roots.contains(&root)
1178 || root
1179 >= pager
1180 .page_count()
1181 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1182 {
1183 return Err(page_integrity(
1184 root,
1185 None,
1186 "retired root aliases a live or missing page",
1187 ));
1188 }
1189 self.retired_btrees.lock().push(RetiredBTree {
1190 tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1191 published: true,
1192 });
1193 }
1194 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1195 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1196 if deferred.iter().any(|id| live_roots.contains(id)) {
1197 return Err(page_integrity(1, None, "deferred page aliases a live root"));
1198 }
1199 pager
1200 .restore_deferred_reclamation(&deferred)
1201 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1202 }
1203 }
1204
1205 if self.format_version() < STORE_VERSION_V11 {
1206 self.set_format_version(STORE_VERSION_V11);
1207 }
1208
1209 Ok(())
1210 }
1211
1212 pub(crate) fn deserialize_entity(
1214 data: &[u8],
1215 format_version: u32,
1216 ) -> Result<UnifiedEntity, StoreError> {
1217 let mut pos = 0;
1218 Self::read_entity_binary(data, &mut pos, format_version)
1219 .map_err(|e| StoreError::Serialization(e.to_string()))
1220 }
1221
1222 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1224 let mut buf = Vec::with_capacity(256);
1229 Self::write_entity_binary(&mut buf, entity, format_version);
1230 buf
1231 }
1232
1233 pub(crate) fn serialize_entity_record(
1234 entity: &UnifiedEntity,
1235 metadata: Option<&Metadata>,
1236 format_version: u32,
1237 ) -> Vec<u8> {
1238 let entity_bytes = Self::serialize_entity(entity, format_version);
1239 let has_meta = matches!(metadata, Some(m) if !m.fields.is_empty());
1245 if has_meta {
1246 let metadata_bytes = serialize_metadata(metadata);
1247 reddb_file::encode_native_entity_record_frame(&entity_bytes, Some(&metadata_bytes))
1248 } else {
1249 reddb_file::encode_native_entity_record_frame(&entity_bytes, None)
1250 }
1251 }
1252
1253 pub(crate) fn deserialize_entity_record(
1254 data: &[u8],
1255 format_version: u32,
1256 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1257 let Some(frame) = reddb_file::decode_native_entity_record_frame(data)
1258 .map_err(|err| StoreError::Serialization(err.to_string()))?
1259 else {
1260 return Self::deserialize_entity(data, format_version).map(|entity| (entity, None));
1261 };
1262
1263 let entity = Self::deserialize_entity(frame.entity, format_version)?;
1264 let metadata = if frame.metadata.is_empty() {
1265 None
1266 } else {
1267 let metadata = deserialize_metadata(frame.metadata)?;
1268 if metadata.is_empty() {
1269 None
1270 } else {
1271 Some(metadata)
1272 }
1273 };
1274
1275 Ok((entity, metadata))
1276 }
1277
1278 pub fn persist(&self) -> Result<(), StoreError> {
1283 let _vault_guard = self.vault_publication_lock.lock();
1284 let pending_vault_versions = self.pending_vault_versions.read();
1285 let pager = match &self.pager {
1286 Some(p) => p,
1287 None => {
1288 if let Some(path) = &self.db_path {
1290 return self
1291 .save_to_file(path)
1292 .map_err(|e| StoreError::Serialization(e.to_string()));
1293 }
1294 return Err(StoreError::Io(std::io::Error::other(
1295 "No pager or path configured for persistence",
1296 )));
1297 }
1298 };
1299
1300 match pager.read_page(1) {
1301 Ok(_) => {}
1302 Err(PagerError::PageNotFound(_)) => {
1303 let meta_page = pager
1304 .allocate_page(crate::storage::engine::PageType::Header)
1305 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1306 pager
1307 .write_page(meta_page.page_id(), meta_page)
1308 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1309 }
1310 Err(e) => {
1311 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1312 }
1313 }
1314
1315 if let Some(commit) = &self.commit {
1316 commit.force_sync().map_err(StoreError::Io)?;
1317 }
1318
1319 let collections = self.collections.read();
1320 let mut btree_indices = self.btree_indices.write();
1321
1322 let mut collection_roots: Vec<(String, u32)> = Vec::new();
1324
1325 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1328 let previous_versions = self.checkpoint_versions.read();
1329 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1330 let pending_vault = Arc::new(pending_vault_versions.clone());
1331 let (visibility_revision, excluded) = self.snapshot_manager.get().map_or_else(
1332 || (0, Default::default()),
1333 |manager| manager.checkpoint_excluded_state(),
1334 );
1335 let excluded = Arc::new(excluded);
1336 for (name, manager) in collections.iter() {
1337 let version = CheckpointVersion {
1338 manager: Arc::downgrade(manager),
1339 segments: manager.checkpoint_revisions(),
1340 visibility_revision,
1341 excluded_xids: Arc::clone(&excluded),
1342 pending_vault: Arc::clone(&pending_vault),
1343 format_version: self.format_version(),
1344 };
1345 if let (Some(previous), Some(tree)) =
1346 (previous_versions.get(name), btree_indices.get(name))
1347 {
1348 if previous.manager.ptr_eq(&version.manager)
1349 && previous.segments == version.segments
1350 && previous.format_version == version.format_version
1351 && checkpoint_visibility_matches(previous, &version, manager)
1352 {
1353 collection_roots.push((name.clone(), tree.root_page_id()));
1354 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1355 checkpoint_versions.insert(name.clone(), version);
1356 continue;
1357 }
1358 }
1359 if let (Some(previous), Some(tree)) =
1360 (previous_versions.get(name), btree_indices.get(name))
1361 {
1362 if previous.manager.ptr_eq(&version.manager)
1363 && previous.format_version == version.format_version
1364 && checkpoint_visibility_matches(previous, &version, manager)
1365 {
1366 if let Some(tree) = self.sparse_checkpoint_tree(
1367 name,
1368 manager,
1369 tree,
1370 &previous.segments,
1371 &version,
1372 )? {
1373 collection_roots.push((name.clone(), tree.root_page_id()));
1374 rebuilt_indices.insert(name.clone(), tree);
1375 checkpoint_versions.insert(name.clone(), version);
1376 continue;
1377 }
1378 }
1379 }
1380 checkpoint_versions.insert(name.clone(), version);
1381 let btree = self.checkpoint_tree(
1382 pager,
1383 manager,
1384 btree_indices.get(name),
1385 |entity, metadata| {
1386 #[cfg(test)]
1387 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1388 if pending_vault_versions.contains(&entity.id)
1389 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1390 && excluded.contains(&entity.xmin))
1391 {
1392 return None;
1393 }
1394 let mut normalized = std::borrow::Cow::Borrowed(entity);
1395 if matches!(entity.data, EntityData::Row(_)) && excluded.contains(&entity.xmax)
1396 {
1397 normalized.to_mut().xmax = 0;
1398 }
1399 Some(Self::serialize_entity_record(
1400 &normalized,
1401 metadata.as_ref(),
1402 self.format_version(),
1403 ))
1404 },
1405 )?;
1406
1407 collection_roots.push((name.clone(), btree.root_page_id()));
1408 rebuilt_indices.insert(name.clone(), btree);
1409 }
1410
1411 let mut meta_data = Vec::with_capacity(4096);
1413
1414 let format_version = STORE_VERSION_V11;
1415 self.set_format_version(format_version);
1416
1417 reddb_file::encode_native_paged_metadata_header(
1418 &mut meta_data,
1419 reddb_file::NativePagedMetadataHeader {
1420 format_version,
1421 collection_count: collection_roots.len() as u32,
1422 },
1423 );
1424
1425 for (name, root_page) in &collection_roots {
1427 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1428 }
1429
1430 let cross_refs = self.cross_refs.read();
1432 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1433 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1434 for (source_id, refs) in cross_refs.iter() {
1435 for (target_id, ref_type, collection) in refs {
1436 reddb_file::encode_native_paged_cross_ref(
1437 &mut meta_data,
1438 source_id.raw(),
1439 target_id.raw(),
1440 ref_type.to_byte(),
1441 collection,
1442 );
1443 }
1444 }
1445
1446 for (name, previous) in btree_indices.iter() {
1447 if !rebuilt_indices
1448 .get(name)
1449 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1450 {
1451 self.retire_btree(Arc::clone(previous));
1452 }
1453 }
1454 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices)?;
1455
1456 *btree_indices = rebuilt_indices;
1459 drop(previous_versions);
1460 *self.checkpoint_versions.write() = checkpoint_versions;
1461
1462 if let Some(commit) = &self.commit {
1463 commit.truncate().map_err(StoreError::Io)?;
1464 }
1465 *self.checkpoint_durable_rows.write() = Default::default();
1466
1467 Ok(())
1468 }
1469
1470 pub fn is_paged(&self) -> bool {
1472 self.pager.is_some()
1473 }
1474
1475 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1478 self.btree_indices
1479 .read()
1480 .get(collection)
1481 .map(|btree| btree.root_page_id())
1482 .filter(|root| *root != 0)
1483 }
1484
1485 pub fn db_path(&self) -> Option<&Path> {
1487 self.db_path.as_deref()
1488 }
1489}
1490
1491fn serialize_metadata(metadata: Option<&Metadata>) -> Vec<u8> {
1492 let Some(metadata) = metadata else {
1493 return Vec::new();
1494 };
1495 if metadata.is_empty() {
1496 return Vec::new();
1497 }
1498
1499 let mut entries: Vec<_> = metadata.iter().collect();
1500 entries.sort_by_key(|(a, _)| *a);
1501
1502 let mut buf = Vec::new();
1503 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1504 for (key, value) in entries {
1505 write_string(&mut buf, key);
1506 write_metadata_value(&mut buf, value);
1507 }
1508 buf
1509}
1510
1511fn deserialize_metadata(data: &[u8]) -> Result<Metadata, StoreError> {
1512 let mut pos = 0usize;
1513 let count = read_u32(data, &mut pos)? as usize;
1514 let mut metadata = Metadata::new();
1515 for _ in 0..count {
1516 let key = read_string(data, &mut pos)?;
1517 let value = read_metadata_value(data, &mut pos)?;
1518 metadata.set(key, value);
1519 }
1520 Ok(metadata)
1521}
1522
1523fn write_string(buf: &mut Vec<u8>, value: &str) {
1524 reddb_file::encode_native_len_prefixed_str(buf, value);
1525}
1526
1527fn write_bytes(buf: &mut Vec<u8>, value: &[u8]) {
1528 reddb_file::encode_native_len_prefixed_bytes(buf, value);
1529}
1530
1531fn write_ref_target(buf: &mut Vec<u8>, target: &crate::storage::unified::RefTarget) {
1532 use crate::storage::unified::RefTarget;
1533
1534 match target {
1535 RefTarget::TableRow { table, row_id } => {
1536 buf.push(0);
1537 write_string(buf, table);
1538 buf.extend_from_slice(&row_id.to_le_bytes());
1539 }
1540 RefTarget::Node {
1541 collection,
1542 node_id,
1543 } => {
1544 buf.push(1);
1545 write_string(buf, collection);
1546 buf.extend_from_slice(&node_id.raw().to_le_bytes());
1547 }
1548 RefTarget::Edge {
1549 collection,
1550 edge_id,
1551 } => {
1552 buf.push(2);
1553 write_string(buf, collection);
1554 buf.extend_from_slice(&edge_id.raw().to_le_bytes());
1555 }
1556 RefTarget::Vector {
1557 collection,
1558 vector_id,
1559 } => {
1560 buf.push(3);
1561 write_string(buf, collection);
1562 buf.extend_from_slice(&vector_id.raw().to_le_bytes());
1563 }
1564 RefTarget::Entity {
1565 collection,
1566 entity_id,
1567 } => {
1568 buf.push(4);
1569 write_string(buf, collection);
1570 buf.extend_from_slice(&entity_id.raw().to_le_bytes());
1571 }
1572 }
1573}
1574
1575fn write_metadata_value(buf: &mut Vec<u8>, value: &MetadataValue) {
1576 match value {
1577 MetadataValue::Null => buf.push(0),
1578 MetadataValue::Bool(v) => {
1579 buf.push(1);
1580 buf.push(u8::from(*v));
1581 }
1582 MetadataValue::Int(v) => {
1583 buf.push(2);
1584 buf.extend_from_slice(&v.to_le_bytes());
1585 }
1586 MetadataValue::Float(v) => {
1587 buf.push(3);
1588 buf.extend_from_slice(&v.to_le_bytes());
1589 }
1590 MetadataValue::String(v) => {
1591 buf.push(4);
1592 write_string(buf, v);
1593 }
1594 MetadataValue::Bytes(v) => {
1595 buf.push(5);
1596 write_bytes(buf, v);
1597 }
1598 MetadataValue::Array(values) => {
1599 buf.push(6);
1600 buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
1601 for value in values {
1602 write_metadata_value(buf, value);
1603 }
1604 }
1605 MetadataValue::Object(values) => {
1606 buf.push(7);
1607 let mut entries: Vec<_> = values.iter().collect();
1608 entries.sort_by_key(|(a, _)| *a);
1609 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1610 for (key, value) in entries {
1611 write_string(buf, key);
1612 write_metadata_value(buf, value);
1613 }
1614 }
1615 MetadataValue::Timestamp(v) => {
1616 buf.push(8);
1617 buf.extend_from_slice(&v.to_le_bytes());
1618 }
1619 MetadataValue::Geo { lat, lon } => {
1620 buf.push(9);
1621 buf.extend_from_slice(&lat.to_le_bytes());
1622 buf.extend_from_slice(&lon.to_le_bytes());
1623 }
1624 MetadataValue::Reference(target) => {
1625 buf.push(10);
1626 write_ref_target(buf, target);
1627 }
1628 MetadataValue::References(targets) => {
1629 buf.push(11);
1630 buf.extend_from_slice(&(targets.len() as u32).to_le_bytes());
1631 for target in targets {
1632 write_ref_target(buf, target);
1633 }
1634 }
1635 }
1636}
1637
1638fn read_exact_slice<'a>(
1639 data: &'a [u8],
1640 pos: &mut usize,
1641 len: usize,
1642) -> Result<&'a [u8], StoreError> {
1643 if *pos + len > data.len() {
1644 return Err(StoreError::Serialization(
1645 "truncated metadata payload".to_string(),
1646 ));
1647 }
1648 let slice = &data[*pos..*pos + len];
1649 *pos += len;
1650 Ok(slice)
1651}
1652
1653fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, StoreError> {
1654 let bytes = read_exact_slice(data, pos, 4)?;
1655 let mut raw = [0u8; 4];
1656 raw.copy_from_slice(bytes);
1657 Ok(u32::from_le_bytes(raw))
1658}
1659
1660fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, StoreError> {
1661 let bytes = read_exact_slice(data, pos, 8)?;
1662 let mut raw = [0u8; 8];
1663 raw.copy_from_slice(bytes);
1664 Ok(u64::from_le_bytes(raw))
1665}
1666
1667fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, StoreError> {
1668 let bytes = read_exact_slice(data, pos, 8)?;
1669 let mut raw = [0u8; 8];
1670 raw.copy_from_slice(bytes);
1671 Ok(i64::from_le_bytes(raw))
1672}
1673
1674fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, StoreError> {
1675 let bytes = read_exact_slice(data, pos, 8)?;
1676 let mut raw = [0u8; 8];
1677 raw.copy_from_slice(bytes);
1678 Ok(f64::from_le_bytes(raw))
1679}
1680
1681fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, StoreError> {
1682 let bytes = read_exact_slice(data, pos, 1)?;
1683 Ok(bytes[0])
1684}
1685
1686fn read_string(data: &[u8], pos: &mut usize) -> Result<String, StoreError> {
1687 reddb_file::decode_native_len_prefixed_string(data, pos)
1688 .map_err(|err| StoreError::Serialization(err.to_string()))
1689}
1690
1691fn read_bytes(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, StoreError> {
1692 reddb_file::decode_native_len_prefixed_bytes(data, pos)
1693 .map(|bytes| bytes.to_vec())
1694 .map_err(|err| StoreError::Serialization(err.to_string()))
1695}
1696
1697fn read_ref_target(
1698 data: &[u8],
1699 pos: &mut usize,
1700) -> Result<crate::storage::unified::RefTarget, StoreError> {
1701 use crate::storage::unified::RefTarget;
1702
1703 match read_u8(data, pos)? {
1704 0 => Ok(RefTarget::TableRow {
1705 table: read_string(data, pos)?,
1706 row_id: read_u64(data, pos)?,
1707 }),
1708 1 => Ok(RefTarget::Node {
1709 collection: read_string(data, pos)?,
1710 node_id: EntityId::new(read_u64(data, pos)?),
1711 }),
1712 2 => Ok(RefTarget::Edge {
1713 collection: read_string(data, pos)?,
1714 edge_id: EntityId::new(read_u64(data, pos)?),
1715 }),
1716 3 => Ok(RefTarget::Vector {
1717 collection: read_string(data, pos)?,
1718 vector_id: EntityId::new(read_u64(data, pos)?),
1719 }),
1720 4 => Ok(RefTarget::Entity {
1721 collection: read_string(data, pos)?,
1722 entity_id: EntityId::new(read_u64(data, pos)?),
1723 }),
1724 tag => Err(StoreError::Serialization(format!(
1725 "unknown metadata ref target tag {tag}"
1726 ))),
1727 }
1728}
1729
1730fn read_metadata_value(data: &[u8], pos: &mut usize) -> Result<MetadataValue, StoreError> {
1731 match read_u8(data, pos)? {
1732 0 => Ok(MetadataValue::Null),
1733 1 => Ok(MetadataValue::Bool(read_u8(data, pos)? != 0)),
1734 2 => Ok(MetadataValue::Int(read_i64(data, pos)?)),
1735 3 => Ok(MetadataValue::Float(read_f64(data, pos)?)),
1736 4 => Ok(MetadataValue::String(read_string(data, pos)?)),
1737 5 => Ok(MetadataValue::Bytes(read_bytes(data, pos)?)),
1738 6 => {
1739 let count = read_u32(data, pos)? as usize;
1740 let mut values = Vec::with_capacity(count);
1741 for _ in 0..count {
1742 values.push(read_metadata_value(data, pos)?);
1743 }
1744 Ok(MetadataValue::Array(values))
1745 }
1746 7 => {
1747 let count = read_u32(data, pos)? as usize;
1748 let mut values = std::collections::HashMap::with_capacity(count);
1749 for _ in 0..count {
1750 let key = read_string(data, pos)?;
1751 let value = read_metadata_value(data, pos)?;
1752 values.insert(key, value);
1753 }
1754 Ok(MetadataValue::Object(values))
1755 }
1756 8 => Ok(MetadataValue::Timestamp(read_u64(data, pos)?)),
1757 9 => Ok(MetadataValue::Geo {
1758 lat: read_f64(data, pos)?,
1759 lon: read_f64(data, pos)?,
1760 }),
1761 10 => Ok(MetadataValue::Reference(read_ref_target(data, pos)?)),
1762 11 => {
1763 let count = read_u32(data, pos)? as usize;
1764 let mut targets = Vec::with_capacity(count);
1765 for _ in 0..count {
1766 targets.push(read_ref_target(data, pos)?);
1767 }
1768 Ok(MetadataValue::References(targets))
1769 }
1770 tag => Err(StoreError::Serialization(format!(
1771 "unknown metadata value tag {tag}"
1772 ))),
1773 }
1774}
1775
1776#[cfg(test)]
1777mod checkpoint_work_tests {
1778 use super::*;
1779
1780 #[test]
1781 fn retired_tree_pages_remain_pinned_by_a_cursor() {
1782 let directory = tempfile::tempdir().expect("temporary store");
1783 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1784 for id in 1..=100 {
1785 store.insert_auto("rows", row(id, 1)).expect("seed");
1786 }
1787 store.persist().expect("initial checkpoint");
1788 let tree = store
1789 .btree_indices
1790 .read()
1791 .get("rows")
1792 .cloned()
1793 .expect("tree");
1794 let mut cursor = tree.cursor_first().expect("cursor");
1795 drop(tree);
1796 let manager = store.get_collection("rows").expect("rows");
1797 for value in 2..=6 {
1798 manager.update(row(1, value)).expect("change");
1799 store.persist().expect("checkpoint with old reader");
1800 }
1801 let mut count = 0;
1802 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
1803 let (entity, _) =
1804 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1805 .expect("decode");
1806 assert_eq!(
1807 entity.data.as_row().expect("row").columns,
1808 [Value::Integer(1)]
1809 );
1810 count += 1;
1811 }
1812 assert_eq!(count, 100);
1813 drop(cursor);
1814 store.persist().expect("reclaim after reader release");
1815 assert!(store.retired_btrees.lock().is_empty());
1816 }
1817
1818 #[test]
1819 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
1820 let directory = tempfile::tempdir().expect("temporary store");
1821 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1822 store.create_collection("rows").expect("collection");
1823 let manager = store.get_collection("rows").expect("rows");
1824 for id in (1..=1200).rev() {
1825 manager
1826 .insert(UnifiedEntity::table_row(
1827 EntityId::new(id),
1828 "rows",
1829 id,
1830 vec![Value::text("x".repeat(4096))],
1831 ))
1832 .expect("seed");
1833 }
1834 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
1835 store.persist().expect("bounded checkpoint");
1836 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
1837 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
1838 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
1839 }
1840
1841 #[test]
1842 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1843 let directory = tempfile::tempdir().expect("temporary store");
1844 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1845 store.create_collection("rows").expect("collection");
1846 for id in 1..=40 {
1847 store
1848 .insert(
1849 "rows",
1850 UnifiedEntity::table_row(
1851 EntityId::new(id),
1852 "rows",
1853 id,
1854 vec![Value::Integer(id as i64)],
1855 ),
1856 )
1857 .expect("seed row");
1858 }
1859 store.persist().expect("initial checkpoint");
1860 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1861 store.persist().expect("unchanged checkpoint");
1862 assert_eq!(
1863 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1864 0,
1865 "unchanged collections must not hold the writer fence while serializing every row"
1866 );
1867 drop(store);
1868 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1869 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1870 reopened.persist().expect("unchanged recovered generation");
1871 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1872 }
1873
1874 #[test]
1875 fn paged_reopen_preserves_stored_sequence_identity() {
1876 let directory = tempfile::tempdir().expect("temporary store");
1877 let path = directory.path().join("data.rdb");
1878 let store = UnifiedStore::open(&path).expect("paged store");
1879 store.create_collection("rows").expect("collection");
1880 for id in [30, 10, 20] {
1881 store.insert("rows", row(id, id as i64)).expect("insert");
1882 }
1883 store.persist().expect("checkpoint");
1884 let sequences = [10, 20, 30].map(|id| {
1885 store
1886 .get("rows", EntityId::new(id))
1887 .expect("row")
1888 .sequence_id
1889 });
1890 drop(store);
1891 let reopened = UnifiedStore::open(&path).expect("reopen");
1892 let recovered = [10, 20, 30].map(|id| {
1893 reopened
1894 .get("rows", EntityId::new(id))
1895 .expect("row")
1896 .sequence_id
1897 });
1898 assert_eq!(
1899 recovered, sequences,
1900 "opening a checkpoint must not rewrite stored row identities"
1901 );
1902 }
1903
1904 #[test]
1905 fn auto_insert_persists_the_assigned_sequence_identity() {
1906 let directory = tempfile::tempdir().expect("temporary store");
1907 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1908 for id in 1u64..=3 {
1909 store
1910 .insert_auto("rows", row(id, id as i64))
1911 .expect("insert");
1912 }
1913 let manager = store.get_collection("rows").expect("rows");
1914 let tree = store
1915 .btree_indices
1916 .read()
1917 .get("rows")
1918 .cloned()
1919 .expect("tree");
1920 for id in 1u64..=3 {
1921 let bytes = tree
1922 .get(&id.to_be_bytes())
1923 .expect("lookup")
1924 .expect("stored row");
1925 let (persisted, _) =
1926 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1927 .expect("decode");
1928 assert_eq!(
1929 persisted.sequence_id,
1930 manager
1931 .get(EntityId::new(id))
1932 .expect("live row")
1933 .sequence_id
1934 );
1935 }
1936 }
1937
1938 #[test]
1939 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
1940 let directory = tempfile::tempdir().expect("temporary store");
1941 let path = directory.path().join("data.rdb");
1942 let store = UnifiedStore::open(&path).expect("paged store");
1943 store.insert_auto("rows", row(1, 1)).expect("seed");
1944 store.persist().expect("checkpoint");
1945 store.insert_auto("rows", row(2, 2)).expect("append");
1946 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1947 store.persist().expect("inline append checkpoint");
1948 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1949 store
1950 .get_collection("rows")
1951 .expect("rows")
1952 .update(row(1, 99))
1953 .expect("untracked edit");
1954 store.insert_auto("rows", row(3, 3)).expect("next append");
1955 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1956 store.persist().expect("untracked edit checkpoint");
1957 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
1958 drop(store);
1959 let reopened = UnifiedStore::open(&path).expect("reopen");
1960 assert_eq!(
1961 reopened
1962 .get("rows", EntityId::new(1))
1963 .expect("row")
1964 .data
1965 .as_row()
1966 .expect("table")
1967 .columns,
1968 vec![Value::Integer(99)]
1969 );
1970 }
1971
1972 #[test]
1973 fn replayed_trees_are_reusable_after_durable_reconciliation() {
1974 let directory = tempfile::tempdir().expect("temporary store");
1975 let path = directory.path().join("data.rdb");
1976 let store = UnifiedStore::open(&path).expect("paged store");
1977 store.insert_auto("rows", row(1, 1)).expect("seed");
1978 store.persist().expect("checkpoint");
1979 store
1980 .insert_auto("rows", row(2, 2))
1981 .expect("WAL-backed append");
1982 drop(store);
1983 let reopened = UnifiedStore::open(&path).expect("replay");
1984 assert!(reopened.get("rows", EntityId::new(2)).is_some());
1985 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1986 reopened.persist().expect("checkpoint reconciled tree");
1987 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1988 }
1989
1990 fn row(id: u64, value: i64) -> UnifiedEntity {
1991 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
1992 }
1993
1994 #[test]
1995 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
1996 let directory = tempfile::tempdir().expect("temporary store");
1997 let path = directory.path().join("data.rdb");
1998 let store = UnifiedStore::open(&path).expect("paged store");
1999 store.create_collection("rows").expect("collection");
2000 store.insert("rows", row(1, 1)).expect("seed");
2001 store.persist().expect("initial checkpoint");
2002 let manager = store.get_collection("rows").expect("rows");
2003 manager.update(row(1, 2)).expect("direct update");
2004 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2005 store.persist().expect("updated row checkpoint");
2006 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2007 let mut metadata = Metadata::new();
2008 metadata.set("verified", MetadataValue::Bool(true));
2009 manager
2010 .set_metadata(EntityId::new(1), metadata)
2011 .expect("metadata");
2012 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2013 store.persist().expect("metadata-only checkpoint");
2014 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2015 manager
2016 .bulk_insert(vec![row(2, 2), row(3, 3)])
2017 .expect("bulk insert");
2018 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2019 store.persist().expect("bulk insert checkpoint");
2020 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
2021 manager.delete(EntityId::new(2)).expect("delete");
2022 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2023 store.persist().expect("changed checkpoint");
2024 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
2025 drop(store);
2026 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
2027 let manager = recovered.get_collection("rows").expect("rows");
2028 assert_eq!(manager.count(), 2);
2029 assert_eq!(
2030 manager
2031 .get(EntityId::new(1))
2032 .expect("row")
2033 .data
2034 .as_row()
2035 .expect("table")
2036 .columns,
2037 vec![Value::Integer(2)]
2038 );
2039 assert!(manager.get(EntityId::new(2)).is_none());
2040 assert!(manager
2041 .get_metadata(EntityId::new(1))
2042 .expect("metadata")
2043 .get("verified")
2044 .is_some());
2045 }
2046
2047 #[test]
2048 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
2049 let directory = tempfile::tempdir().expect("temporary store");
2050 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2051 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2052 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2053 store.create_collection("rows").expect("collection");
2054 for id in 1..=40 {
2055 store.insert("rows", row(id, id as i64)).expect("seed");
2056 }
2057 store.persist().expect("initial checkpoint");
2058 let unrelated = snapshots.begin();
2059 snapshots.rollback(unrelated);
2060 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2061 store.persist().expect("unrelated rollback checkpoint");
2062 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2063
2064 store
2065 .pending_vault_versions
2066 .write()
2067 .insert(EntityId::new(100));
2068 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2069 store.persist().expect("unrelated vault checkpoint");
2070 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2071 store.pending_vault_versions.write().clear();
2072 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2073 store
2074 .persist()
2075 .expect("unrelated vault publication checkpoint");
2076 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2077 }
2078
2079 #[test]
2080 fn rollback_invalidates_reuse_without_a_segment_mutation() {
2081 let directory = tempfile::tempdir().expect("temporary store");
2082 let path = directory.path().join("data.rdb");
2083 let store = UnifiedStore::open(&path).expect("paged store");
2084 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2085 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2086 store.create_collection("rows").expect("collection");
2087 let xid = snapshots.begin();
2088 let mut pending = row(1, 1);
2089 pending.xmin = xid;
2090 store.insert("rows", pending).expect("version");
2091 store.persist().expect("before rollback");
2092 snapshots.rollback(xid);
2093 store.persist().expect("after rollback");
2094 drop(store);
2095 let recovered = UnifiedStore::open(&path).expect("recover");
2096 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2097 }
2098
2099 #[test]
2100 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
2101 let directory = tempfile::tempdir().expect("temporary store");
2102 let path = directory.path().join("data.rdb");
2103 let store = UnifiedStore::open(&path).expect("paged store");
2104 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2105 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2106 store.create_collection("rows").expect("collection");
2107 let xid = snapshots.begin();
2108 let mut deleted = row(1, 1);
2109 deleted.xmax = xid;
2110 store.insert("rows", deleted).expect("version");
2111 store.persist().expect("before rollback");
2112 snapshots.rollback(xid);
2113 store.persist().expect("after rollback");
2114 drop(store);
2115 let recovered = UnifiedStore::open(&path).expect("recover");
2116 let manager = recovered.get_collection("rows").expect("rows");
2117 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
2118 }
2119
2120 #[test]
2121 fn publishing_a_pending_vault_version_invalidates_reuse() {
2122 let directory = tempfile::tempdir().expect("temporary store");
2123 let path = directory.path().join("data.rdb");
2124 let store = UnifiedStore::open(&path).expect("paged store");
2125 store.create_collection("rows").expect("collection");
2126 store.insert("rows", row(1, 1)).expect("row");
2127 store
2128 .pending_vault_versions
2129 .write()
2130 .insert(EntityId::new(1));
2131 store.persist().expect("exclude pending vault version");
2132 store
2133 .pending_vault_versions
2134 .write()
2135 .remove(&EntityId::new(1));
2136 store.persist().expect("include published vault version");
2137 drop(store);
2138 let recovered = UnifiedStore::open(&path).expect("recover");
2139 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
2140 }
2141}