1use super::*;
2use crate::storage::unified::entity_cache::EntityCache;
3use parking_lot::RwLock;
4
5#[cfg(test)]
6thread_local! {
7 static CHECKPOINT_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
8}
9
10const META_PAGE_CONTENT_CAP: usize =
29 crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
30const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
31const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
32const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
33const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
34
35fn tree_matches_records(tree: &BTree, records: &[(Vec<u8>, Vec<u8>)]) -> Result<bool, StoreError> {
36 let mut cursor = tree
37 .cursor_first()
38 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
39 for record in records {
40 if cursor
41 .next()
42 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
43 .as_ref()
44 != Some(record)
45 {
46 return Ok(false);
47 }
48 }
49 Ok(cursor
50 .next()
51 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
52 .is_none())
53}
54
55fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
56 let cs = crate::storage::engine::HEADER_SIZE;
57 let page = match pager.read_page(1) {
58 Ok(p) => p,
59 Err(_) => return Ok(Vec::new()),
60 };
61 let bytes = page.as_bytes();
62 if bytes.len() < cs + META_V3_PAGE1_HEADER {
63 return Ok(Vec::new());
64 }
65 let Some(header) =
66 reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
67 PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
68 })?
69 else {
70 return Ok(Vec::new());
71 };
72 let mut next = header.next_overflow_page_id;
73 let mut pages = Vec::new();
74 let mut seen = std::collections::HashSet::new();
75 while next != 0 {
76 if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
77 return Err(PagerError::InvalidDatabase(
78 "cyclic metadata overflow chain".into(),
79 ));
80 }
81 let ov = match pager.read_page(next) {
82 Ok(p) => p,
83 Err(_) => break,
84 };
85 let ob = ov.as_bytes();
86 let nn = match reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..]) {
87 Ok(header) => header.next_overflow_page_id,
88 Err(_) => 0,
89 };
90 pages.push(next);
91 next = nn;
92 }
93 Ok(pages)
94}
95
96fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
97 let retired = existing_overflow_chain(pager)?;
98 let page = build_meta_page1_with_overflow(pager, meta_data)?;
99 pager.sync()?;
101 pager.write_page(1, page)?;
102 pager.sync()?;
103 for id in retired {
104 pager.free_page(id)?;
105 }
106 Ok(())
107}
108
109fn build_meta_page1_with_overflow(
110 pager: &Pager,
111 meta_data: &[u8],
112) -> Result<crate::storage::engine::Page, PagerError> {
113 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
114
115 let mut page1 = Page::new(PageType::Header, 1);
116 let cs = HEADER_SIZE;
117
118 if meta_data.len() <= META_PAGE_CONTENT_CAP {
119 let buf = page1.as_bytes_mut();
121 buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
122 return Ok(page1);
123 }
124
125 let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
129 let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
130 let mut chunks: Vec<&[u8]> = Vec::new();
131 while !tail.is_empty() {
132 let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
133 chunks.push(&tail[..take]);
134 tail = &tail[take..];
135 }
136
137 let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
138 let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
139 for _ in 0..chunks.len() {
140 let pg = pager.allocate_page(PageType::Overflow)?;
141 overflow_ids.push(pg.page_id());
142 overflow_pages.push(pg);
143 }
144
145 for i in 0..chunks.len() {
146 let next = if i + 1 < chunks.len() {
147 overflow_ids[i + 1]
148 } else {
149 0u32
150 };
151 let len = chunks[i].len() as u32;
152 let buf = overflow_pages[i].as_bytes_mut();
153 reddb_file::encode_native_metadata_overflow_continuation_header(
154 &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
155 reddb_file::NativeMetadataOverflowContinuationHeader {
156 next_overflow_page_id: next,
157 chunk_bytes: len,
158 },
159 )
160 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
161 buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
162 }
163 for (idx, page) in overflow_pages.into_iter().enumerate() {
164 let id = overflow_ids[idx];
165 pager.write_page(id, page)?;
166 }
167
168 let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
170 .ok()
171 .flatten()
172 .map_or(0, |header| header.format_version);
173
174 let buf = page1.as_bytes_mut();
175 reddb_file::encode_native_metadata_overflow_header(
176 &mut buf[cs..cs + META_V3_PAGE1_HEADER],
177 reddb_file::NativeMetadataOverflowHeader {
178 format_version,
179 total_payload_bytes: meta_data.len() as u32,
180 next_overflow_page_id: overflow_ids[0],
181 },
182 )
183 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
184 buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
185 .copy_from_slice(first_chunk);
186
187 Ok(page1)
188}
189
190fn storage_integrity(
191 zone: &str,
192 id: impl Into<String>,
193 collection: Option<String>,
194 detail: impl Into<String>,
195) -> StoreError {
196 StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
197 zone, id, collection, detail,
198 ))
199}
200
201fn page_integrity(
202 page_id: u32,
203 collection: Option<String>,
204 detail: impl Into<String>,
205) -> StoreError {
206 storage_integrity("page", page_id.to_string(), collection, detail)
207}
208
209fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
221 let cs = crate::storage::engine::HEADER_SIZE;
222 let meta_page = pager.read_manifest_page().map_err(|err| {
223 storage_integrity(
224 "manifest",
225 "manifest",
226 None,
227 format!("internal manifest zone read failed ({err})"),
228 )
229 })?;
230 let bytes = meta_page.as_bytes();
231 if bytes.len() < cs + 4 {
232 return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
233 }
234 let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
235 .map_err(|err| page_integrity(1, None, err.to_string()))?
236 {
237 Some(header) => header,
238 None => {
239 return Ok(Some(bytes[cs..].to_vec()));
240 }
241 };
242 if bytes.len() < cs + META_V3_PAGE1_HEADER {
243 return Err(page_integrity(
244 1,
245 None,
246 "metadata overflow header is truncated",
247 ));
248 }
249 let total = header.total_payload_bytes as usize;
250 let mut next = header.next_overflow_page_id;
251 let mut payload: Vec<u8> = Vec::with_capacity(total);
252 let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
253 payload.extend_from_slice(
254 &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
255 );
256 while next != 0 && payload.len() < total {
257 let ov = pager
258 .read_page(next)
259 .map_err(|err| page_integrity(next, None, err.to_string()))?;
260 let ob = ov.as_bytes();
261 if ob.len() < cs + META_V3_OVERFLOW_HEADER {
262 return Err(page_integrity(
263 next,
264 None,
265 "metadata overflow continuation is truncated",
266 ));
267 }
268 let continuation =
269 reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
270 .map_err(|err| page_integrity(next, None, err.to_string()))?;
271 let nn = continuation.next_overflow_page_id;
272 let len = continuation.chunk_bytes as usize;
273 let remaining = total - payload.len();
274 let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
275 payload.extend_from_slice(
276 &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
277 );
278 next = nn;
279 }
280 Ok(Some(payload))
281}
282
283impl UnifiedStore {
284 pub(crate) fn mark_paged_registry_dirty(&self) {
285 self.paged_registry_dirty.store(true, Ordering::Release);
286 }
287
288 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
294 let pager = self.pager.as_ref()?;
295 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
296 return Some(btree);
297 }
298 let mut write = self.btree_indices.write();
299 let btree = write
300 .entry(collection.to_string())
301 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
302 .clone();
303 Some(btree)
304 }
305
306 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
307 let Some(pager) = &self.pager else {
308 return Ok(());
309 };
310
311 if self.paged_registry_dirty.load(Ordering::Acquire) {
312 self.flush_paged_registry()?;
313 self.paged_registry_dirty.store(false, Ordering::Release);
314 return Ok(());
315 }
316
317 pager
318 .flush()
319 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
320 }
321
322 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
323 let Some(pager) = &self.pager else {
324 return Ok(());
325 };
326
327 match pager.read_page(1) {
328 Ok(_) => {}
329 Err(PagerError::PageNotFound(_)) => {
330 let meta_page = pager
331 .allocate_page(crate::storage::engine::PageType::Header)
332 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
333 pager
334 .write_page(meta_page.page_id(), meta_page)
335 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
336 }
337 Err(e) => {
338 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
339 }
340 }
341
342 let format_version = STORE_VERSION_V11;
343 self.set_format_version(format_version);
344
345 let collections = self.collections.read();
346 let btree_indices = self.btree_indices.read();
347 let mut collection_roots = Vec::with_capacity(collections.len());
348 for (name, _) in collections.iter() {
349 let root_page = btree_indices
350 .get(name)
351 .map_or(0, |btree| btree.root_page_id());
352 collection_roots.push((name.clone(), root_page));
353 }
354 drop(btree_indices);
355 drop(collections);
356
357 let mut meta_data = Vec::with_capacity(4096);
358 reddb_file::encode_native_paged_metadata_header(
359 &mut meta_data,
360 reddb_file::NativePagedMetadataHeader {
361 format_version,
362 collection_count: collection_roots.len() as u32,
363 },
364 );
365 for (name, root_page) in &collection_roots {
366 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
367 }
368
369 let cross_refs = self.cross_refs.read();
370 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
371 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
372 for (source_id, refs) in cross_refs.iter() {
373 for (target_id, ref_type, collection) in refs {
374 reddb_file::encode_native_paged_cross_ref(
375 &mut meta_data,
376 source_id.raw(),
377 target_id.raw(),
378 ref_type.to_byte(),
379 collection,
380 );
381 }
382 }
383
384 publish_paged_metadata(pager, &meta_data)
385 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
386
387 Ok(())
388 }
389
390 pub fn pager(&self) -> Option<&Arc<Pager>> {
392 self.pager.as_ref()
393 }
394
395 pub fn page_cache_bytes_in_use(&self) -> u64 {
399 self.pager.as_ref().map_or(0, |pager| {
400 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
401 })
402 }
403
404 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
407 self.commit
408 .as_ref()
409 .map_or(0, |commit| commit.buffered_bytes())
410 }
411
412 pub fn segment_memory_bytes(&self) -> u64 {
416 let managers: Vec<Arc<SegmentManager>> =
417 self.collections.read().values().map(Arc::clone).collect();
418
419 managers
420 .iter()
421 .map(|manager| manager.resident_bytes())
422 .fold(0, u64::saturating_add)
423 }
424
425 pub fn config(&self) -> &UnifiedStoreConfig {
429 &self.config
430 }
431
432 pub fn with_config(config: UnifiedStoreConfig) -> Self {
433 Self {
434 config,
435 format_version: AtomicU32::new(STORE_VERSION_V11),
436 next_entity_id: AtomicU64::new(1),
437 collections: RwLock::new(HashMap::new()),
438 cross_refs: RwLock::new(HashMap::new()),
439 reverse_refs: RwLock::new(HashMap::new()),
440 pager: None,
441 db_path: None,
442 btree_indices: RwLock::new(HashMap::new()),
443 checkpoint_versions: RwLock::new(HashMap::new()),
444 context_index: ContextIndex::new(),
445 entity_cache: EntityCache::new(),
446 graph_label_index: RwLock::new(HashMap::new()),
447 paged_registry_dirty: AtomicBool::new(false),
448 commit: None,
449 unindex_cross_refs_fast_path: AtomicU64::new(0),
450 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
451 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
452 aux_metadata: RwLock::new(Vec::new()),
453 snapshot_manager: std::sync::OnceLock::new(),
454 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
455 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
456 }
457 }
458
459 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
477 Self::open_with_config(path, UnifiedStoreConfig::default())
478 }
479
480 pub fn open_with_config(
481 path: impl AsRef<Path>,
482 config: UnifiedStoreConfig,
483 ) -> Result<Self, StoreError> {
484 let path = path.as_ref();
485 let mut pager_config = PagerConfig::default();
486 if let Some(slots) = config.page_cache_slots {
490 pager_config.cache_size = slots;
491 }
492 if matches!(
500 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
501 Some("0") | Some("false") | Some("off")
502 ) {
503 pager_config.double_write = false;
504 }
505 let pager = Pager::open(path, pager_config)
506 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
507
508 let wal_path = reddb_file::layout::unified_wal_path(path);
509 let commit = Some(Arc::new(
512 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
513 .map_err(StoreError::Io)?,
514 ));
515
516 let store = Self {
517 config,
518 format_version: AtomicU32::new(STORE_VERSION_V11),
519 next_entity_id: AtomicU64::new(1),
520 collections: RwLock::new(HashMap::new()),
521 cross_refs: RwLock::new(HashMap::new()),
522 reverse_refs: RwLock::new(HashMap::new()),
523 pager: Some(Arc::new(pager)),
524 db_path: Some(path.to_path_buf()),
525 btree_indices: RwLock::new(HashMap::new()),
526 checkpoint_versions: RwLock::new(HashMap::new()),
527 context_index: ContextIndex::new(),
528 entity_cache: EntityCache::new(),
529 graph_label_index: RwLock::new(HashMap::new()),
530 paged_registry_dirty: AtomicBool::new(false),
531 commit,
532 unindex_cross_refs_fast_path: AtomicU64::new(0),
533 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
534 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
535 aux_metadata: RwLock::new(Vec::new()),
536 snapshot_manager: std::sync::OnceLock::new(),
537 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
538 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
539 };
540
541 store.load_from_pages()?;
543 if let Some(commit) = &store.commit {
544 commit.replay_into(&store).map_err(StoreError::Io)?;
545 }
546 store.recover_operational_manifest()?;
547
548 Ok(store)
549 }
550
551 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
555 let Some(pager) = &self.pager else {
556 return Ok(());
557 };
558 let collections = self.collections.read();
559 let mut indices = self.btree_indices.write();
560 for (name, manager) in collections.iter() {
561 let mut records: Vec<_> = manager
562 .query_all(|_| true)
563 .into_iter()
564 .map(|entity| {
565 let metadata = manager.get_metadata(entity.id);
566 (
567 entity.id.raw().to_be_bytes().to_vec(),
568 Self::serialize_entity_record(
569 &entity,
570 metadata.as_ref(),
571 self.format_version(),
572 ),
573 )
574 })
575 .collect();
576 records.sort_by(|left, right| left.0.cmp(&right.0));
577 let matches = if let Some(tree) = indices.get(name) {
578 tree_matches_records(tree, &records)?
579 } else {
580 records.is_empty()
581 };
582 if matches {
583 continue;
584 }
585 let tree = Arc::new(BTree::new(Arc::clone(pager)));
586 tree.bulk_insert_sorted(&records).map_err(|e| {
587 StoreError::Io(std::io::Error::other(format!(
588 "WAL tree rebuild for '{name}': {e}"
589 )))
590 })?;
591 indices.insert(name.clone(), tree);
592 self.mark_paged_registry_dirty();
593 }
594 drop(indices);
595 drop(collections);
596 self.flush_paged_state()
597 }
598
599 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
600 let Some(path) = &self.db_path else {
601 return Ok(());
602 };
603 let mut collections = self.list_collections();
604 collections.sort();
605 let pending_drops =
606 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
607 .recover_or_bootstrap(&collections)
608 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
609 for name in pending_drops {
610 if self.get_collection(&name).is_some() {
611 self.drop_collection(&name)?;
612 }
613 }
614 Ok(())
615 }
616
617 pub(crate) fn prepare_operational_collection_create(
618 &self,
619 name: &str,
620 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
621 let Some(path) = &self.db_path else {
622 return Ok(None);
623 };
624 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
625 .prepare_collection_create(name)
626 .map(Some)
627 .map_err(StoreError::Io)
628 }
629
630 pub(crate) fn prepare_operational_collection_drop(
631 &self,
632 name: &str,
633 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
634 let Some(path) = &self.db_path else {
635 return Ok(None);
636 };
637 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
638 .prepare_collection_drop(name)
639 .map(Some)
640 .map_err(StoreError::Io)
641 }
642
643 fn load_from_pages(&self) -> Result<(), StoreError> {
647 let pager = match &self.pager {
648 Some(p) => p,
649 None => return Ok(()), };
651
652 let page_count = pager.page_count().map_err(|e| {
654 StoreError::Io(std::io::Error::other(format!(
655 "failed to read page count: {}",
656 e
657 )))
658 })?;
659 if page_count <= 1 {
660 return Ok(());
662 }
663
664 if let Some(content_vec) = read_meta_payload(pager)? {
669 let content: &[u8] = &content_vec;
670 if content.len() >= 4 {
671 let mut pos = 0;
672 let mut format_version = STORE_VERSION_V1;
673
674 let collection_count = if let Some(header) =
675 reddb_file::decode_native_paged_metadata_header(content)
676 .map_err(|err| StoreError::Serialization(err.to_string()))?
677 {
678 format_version = header.format_version;
679 pos += reddb_file::METADATA_HEADER_BYTES;
680 header.collection_count as usize
681 } else {
682 let count = u32::from_le_bytes([
683 content[pos],
684 content[pos + 1],
685 content[pos + 2],
686 content[pos + 3],
687 ]) as usize;
688 pos += 4;
689 count
690 };
691
692 self.set_format_version(format_version);
693
694 if pos > content.len() {
695 return Ok(());
696 }
697
698 for _ in 0..collection_count {
700 if let Ok(root) =
701 reddb_file::decode_native_paged_collection_root(content, &mut pos)
702 {
703 let root_page = root.root_page;
705 let name = root.collection;
706
707 let _ = self.create_collection_in_memory(&name);
711
712 if root_page > 0 {
714 let btree = BTree::with_root(Arc::clone(pager), root_page);
715
716 let mut cursor = btree.cursor_first().map_err(|err| {
718 page_integrity(root_page, Some(name.clone()), err.to_string())
719 })?;
720 let manager = self.get_collection(&name);
721 loop {
722 let next = cursor.next().map_err(|err| {
723 page_integrity(root_page, Some(name.clone()), err.to_string())
724 })?;
725 let Some((key, value)) = next else {
726 break;
727 };
728 if let Ok((entity, metadata)) =
730 Self::deserialize_entity_record(&value, self.format_version())
731 {
732 if let Some(m) = &manager {
733 let id = entity.id;
734 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
735 m.register_row_id(*row_id);
736 }
737 self.context_index.index_entity(&name, &entity);
738 let _ = m.insert(entity.clone());
739 if let Some(metadata) = metadata {
740 let _ = m.set_metadata(id, metadata);
741 }
742 self.register_entity_id(id);
743 if self.config.auto_index_refs {
744 self.index_cross_refs(&entity, &name)?;
745 }
746 }
747 }
748 }
749
750 self.btree_indices
752 .write()
753 .insert(name.clone(), Arc::new(btree));
754 if format_version == STORE_VERSION_V11 {
755 if let Some(manager) = &manager {
756 self.checkpoint_versions.write().insert(
757 name,
758 CheckpointVersion {
759 manager: Arc::downgrade(manager),
760 segments: manager.checkpoint_revisions(),
761 aborted_revision: 0,
762 pending_vault: Arc::default(),
763 format_version,
764 },
765 );
766 }
767 }
768 }
769 } else {
770 break;
771 }
772 }
773
774 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
775 let cross_ref_count = u32::from_le_bytes([
776 content[pos],
777 content[pos + 1],
778 content[pos + 2],
779 content[pos + 3],
780 ]) as usize;
781 pos += 4;
782
783 for _ in 0..cross_ref_count {
784 let Ok(cross_ref) =
785 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
786 else {
787 break;
788 };
789 let source_id = EntityId::new(cross_ref.source_id);
790 let target_id = EntityId::new(cross_ref.target_id);
791 let ref_type = RefType::from_byte(cross_ref.ref_type);
792 let target_collection = cross_ref.target_collection;
793
794 self.cross_refs.write().entry(source_id).or_default().push((
795 target_id,
796 ref_type,
797 target_collection.clone(),
798 ));
799
800 if let Some((collection, mut entity)) = self.get_any(source_id) {
801 let exists = entity.cross_refs().iter().any(|xref| {
802 xref.target == target_id
803 && xref.ref_type == ref_type
804 && xref.target_collection == target_collection
805 });
806 if !exists {
807 entity.cross_refs_mut().push(CrossRef::new(
808 source_id,
809 target_id,
810 target_collection.clone(),
811 ref_type,
812 ));
813 if let Some(manager) = self.get_collection(&collection) {
814 let _ = manager.update(entity);
815 }
816 }
817 }
818 }
819 }
820 }
821 }
822
823 if self.format_version() < STORE_VERSION_V11 {
824 self.set_format_version(STORE_VERSION_V11);
825 }
826
827 Ok(())
828 }
829
830 pub(crate) fn deserialize_entity(
832 data: &[u8],
833 format_version: u32,
834 ) -> Result<UnifiedEntity, StoreError> {
835 let mut pos = 0;
836 Self::read_entity_binary(data, &mut pos, format_version)
837 .map_err(|e| StoreError::Serialization(e.to_string()))
838 }
839
840 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
842 let mut buf = Vec::with_capacity(256);
847 Self::write_entity_binary(&mut buf, entity, format_version);
848 buf
849 }
850
851 pub(crate) fn serialize_entity_record(
852 entity: &UnifiedEntity,
853 metadata: Option<&Metadata>,
854 format_version: u32,
855 ) -> Vec<u8> {
856 let entity_bytes = Self::serialize_entity(entity, format_version);
857 let has_meta = matches!(metadata, Some(m) if !m.fields.is_empty());
863 if has_meta {
864 let metadata_bytes = serialize_metadata(metadata);
865 reddb_file::encode_native_entity_record_frame(&entity_bytes, Some(&metadata_bytes))
866 } else {
867 reddb_file::encode_native_entity_record_frame(&entity_bytes, None)
868 }
869 }
870
871 pub(crate) fn deserialize_entity_record(
872 data: &[u8],
873 format_version: u32,
874 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
875 let Some(frame) = reddb_file::decode_native_entity_record_frame(data)
876 .map_err(|err| StoreError::Serialization(err.to_string()))?
877 else {
878 return Self::deserialize_entity(data, format_version).map(|entity| (entity, None));
879 };
880
881 let entity = Self::deserialize_entity(frame.entity, format_version)?;
882 let metadata = if frame.metadata.is_empty() {
883 None
884 } else {
885 let metadata = deserialize_metadata(frame.metadata)?;
886 if metadata.is_empty() {
887 None
888 } else {
889 Some(metadata)
890 }
891 };
892
893 Ok((entity, metadata))
894 }
895
896 pub fn persist(&self) -> Result<(), StoreError> {
901 let _vault_guard = self.vault_publication_lock.lock();
902 let pending_vault_versions = self.pending_vault_versions.read();
903 let pager = match &self.pager {
904 Some(p) => p,
905 None => {
906 if let Some(path) = &self.db_path {
908 return self
909 .save_to_file(path)
910 .map_err(|e| StoreError::Serialization(e.to_string()));
911 }
912 return Err(StoreError::Io(std::io::Error::other(
913 "No pager or path configured for persistence",
914 )));
915 }
916 };
917
918 match pager.read_page(1) {
919 Ok(_) => {}
920 Err(PagerError::PageNotFound(_)) => {
921 let meta_page = pager
922 .allocate_page(crate::storage::engine::PageType::Header)
923 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
924 pager
925 .write_page(meta_page.page_id(), meta_page)
926 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
927 }
928 Err(e) => {
929 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
930 }
931 }
932
933 if let Some(commit) = &self.commit {
934 commit.force_sync().map_err(StoreError::Io)?;
935 }
936
937 let collections = self.collections.read();
938 let mut btree_indices = self.btree_indices.write();
939
940 let mut collection_roots: Vec<(String, u32)> = Vec::new();
942
943 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
946 let previous_versions = self.checkpoint_versions.read();
947 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
948 let pending_vault = Arc::new(pending_vault_versions.clone());
949 let aborted_revision = self
950 .snapshot_manager
951 .get()
952 .map_or(0, |manager| manager.checkpoint_aborted_revision());
953 for (name, manager) in collections.iter() {
954 let version = CheckpointVersion {
955 manager: Arc::downgrade(manager),
956 segments: manager.checkpoint_revisions(),
957 aborted_revision,
958 pending_vault: Arc::clone(&pending_vault),
959 format_version: self.format_version(),
960 };
961 if let (Some(previous), Some(tree)) =
962 (previous_versions.get(name), btree_indices.get(name))
963 {
964 if previous.manager.ptr_eq(&version.manager)
965 && previous.segments == version.segments
966 && previous.aborted_revision == version.aborted_revision
967 && previous.pending_vault == version.pending_vault
968 && previous.format_version == version.format_version
969 {
970 collection_roots.push((name.clone(), tree.root_page_id()));
971 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
972 checkpoint_versions.insert(name.clone(), version);
973 continue;
974 }
975 }
976 checkpoint_versions.insert(name.clone(), version);
977 let mut records = Vec::new();
978 manager.for_each_entity_with_metadata(|entity, metadata| {
979 #[cfg(test)]
980 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
981 if pending_vault_versions.contains(&entity.id)
982 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
983 && self
984 .snapshot_manager
985 .get()
986 .is_some_and(|m| m.is_aborted(entity.xmin)))
987 {
988 return true;
989 }
990 let mut normalized = std::borrow::Cow::Borrowed(entity);
993 if matches!(entity.data, EntityData::Row(_))
994 && self
995 .snapshot_manager
996 .get()
997 .is_some_and(|m| m.is_aborted(entity.xmax))
998 {
999 normalized.to_mut().xmax = 0;
1000 }
1001 records.push((
1002 entity.id.raw().to_be_bytes().to_vec(),
1003 Self::serialize_entity_record(
1004 &normalized,
1005 metadata.as_ref(),
1006 self.format_version(),
1007 ),
1008 ));
1009 true
1010 });
1011 records.sort_by(|left, right| left.0.cmp(&right.0));
1012
1013 if let Some(tree) = btree_indices.get(name) {
1014 if tree_matches_records(tree, &records)? {
1015 collection_roots.push((name.clone(), tree.root_page_id()));
1016 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1017 continue;
1018 }
1019 }
1020
1021 let btree = Arc::new(BTree::new(Arc::clone(pager)));
1022
1023 if !records.is_empty() {
1031 btree.bulk_insert_sorted(&records).map_err(|e| {
1032 StoreError::Io(std::io::Error::other(format!(
1033 "B-tree bulk rebuild error for '{name}': {e}"
1034 )))
1035 })?;
1036 }
1037
1038 collection_roots.push((name.clone(), btree.root_page_id()));
1039 rebuilt_indices.insert(name.clone(), btree);
1040 }
1041
1042 let mut meta_data = Vec::with_capacity(4096);
1044
1045 let format_version = STORE_VERSION_V11;
1046 self.set_format_version(format_version);
1047
1048 reddb_file::encode_native_paged_metadata_header(
1049 &mut meta_data,
1050 reddb_file::NativePagedMetadataHeader {
1051 format_version,
1052 collection_count: collection_roots.len() as u32,
1053 },
1054 );
1055
1056 for (name, root_page) in &collection_roots {
1058 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1059 }
1060
1061 let cross_refs = self.cross_refs.read();
1063 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1064 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1065 for (source_id, refs) in cross_refs.iter() {
1066 for (target_id, ref_type, collection) in refs {
1067 reddb_file::encode_native_paged_cross_ref(
1068 &mut meta_data,
1069 source_id.raw(),
1070 target_id.raw(),
1071 ref_type.to_byte(),
1072 collection,
1073 );
1074 }
1075 }
1076
1077 publish_paged_metadata(pager, &meta_data)
1078 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1079
1080 *btree_indices = rebuilt_indices;
1083 drop(previous_versions);
1084 *self.checkpoint_versions.write() = checkpoint_versions;
1085
1086 if let Some(commit) = &self.commit {
1087 commit.truncate().map_err(StoreError::Io)?;
1088 }
1089
1090 Ok(())
1091 }
1092
1093 pub fn is_paged(&self) -> bool {
1095 self.pager.is_some()
1096 }
1097
1098 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1101 self.btree_indices
1102 .read()
1103 .get(collection)
1104 .map(|btree| btree.root_page_id())
1105 .filter(|root| *root != 0)
1106 }
1107
1108 pub fn db_path(&self) -> Option<&Path> {
1110 self.db_path.as_deref()
1111 }
1112}
1113
1114fn serialize_metadata(metadata: Option<&Metadata>) -> Vec<u8> {
1115 let Some(metadata) = metadata else {
1116 return Vec::new();
1117 };
1118 if metadata.is_empty() {
1119 return Vec::new();
1120 }
1121
1122 let mut entries: Vec<_> = metadata.iter().collect();
1123 entries.sort_by_key(|(a, _)| *a);
1124
1125 let mut buf = Vec::new();
1126 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1127 for (key, value) in entries {
1128 write_string(&mut buf, key);
1129 write_metadata_value(&mut buf, value);
1130 }
1131 buf
1132}
1133
1134fn deserialize_metadata(data: &[u8]) -> Result<Metadata, StoreError> {
1135 let mut pos = 0usize;
1136 let count = read_u32(data, &mut pos)? as usize;
1137 let mut metadata = Metadata::new();
1138 for _ in 0..count {
1139 let key = read_string(data, &mut pos)?;
1140 let value = read_metadata_value(data, &mut pos)?;
1141 metadata.set(key, value);
1142 }
1143 Ok(metadata)
1144}
1145
1146fn write_string(buf: &mut Vec<u8>, value: &str) {
1147 reddb_file::encode_native_len_prefixed_str(buf, value);
1148}
1149
1150fn write_bytes(buf: &mut Vec<u8>, value: &[u8]) {
1151 reddb_file::encode_native_len_prefixed_bytes(buf, value);
1152}
1153
1154fn write_ref_target(buf: &mut Vec<u8>, target: &crate::storage::unified::RefTarget) {
1155 use crate::storage::unified::RefTarget;
1156
1157 match target {
1158 RefTarget::TableRow { table, row_id } => {
1159 buf.push(0);
1160 write_string(buf, table);
1161 buf.extend_from_slice(&row_id.to_le_bytes());
1162 }
1163 RefTarget::Node {
1164 collection,
1165 node_id,
1166 } => {
1167 buf.push(1);
1168 write_string(buf, collection);
1169 buf.extend_from_slice(&node_id.raw().to_le_bytes());
1170 }
1171 RefTarget::Edge {
1172 collection,
1173 edge_id,
1174 } => {
1175 buf.push(2);
1176 write_string(buf, collection);
1177 buf.extend_from_slice(&edge_id.raw().to_le_bytes());
1178 }
1179 RefTarget::Vector {
1180 collection,
1181 vector_id,
1182 } => {
1183 buf.push(3);
1184 write_string(buf, collection);
1185 buf.extend_from_slice(&vector_id.raw().to_le_bytes());
1186 }
1187 RefTarget::Entity {
1188 collection,
1189 entity_id,
1190 } => {
1191 buf.push(4);
1192 write_string(buf, collection);
1193 buf.extend_from_slice(&entity_id.raw().to_le_bytes());
1194 }
1195 }
1196}
1197
1198fn write_metadata_value(buf: &mut Vec<u8>, value: &MetadataValue) {
1199 match value {
1200 MetadataValue::Null => buf.push(0),
1201 MetadataValue::Bool(v) => {
1202 buf.push(1);
1203 buf.push(u8::from(*v));
1204 }
1205 MetadataValue::Int(v) => {
1206 buf.push(2);
1207 buf.extend_from_slice(&v.to_le_bytes());
1208 }
1209 MetadataValue::Float(v) => {
1210 buf.push(3);
1211 buf.extend_from_slice(&v.to_le_bytes());
1212 }
1213 MetadataValue::String(v) => {
1214 buf.push(4);
1215 write_string(buf, v);
1216 }
1217 MetadataValue::Bytes(v) => {
1218 buf.push(5);
1219 write_bytes(buf, v);
1220 }
1221 MetadataValue::Array(values) => {
1222 buf.push(6);
1223 buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
1224 for value in values {
1225 write_metadata_value(buf, value);
1226 }
1227 }
1228 MetadataValue::Object(values) => {
1229 buf.push(7);
1230 let mut entries: Vec<_> = values.iter().collect();
1231 entries.sort_by_key(|(a, _)| *a);
1232 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1233 for (key, value) in entries {
1234 write_string(buf, key);
1235 write_metadata_value(buf, value);
1236 }
1237 }
1238 MetadataValue::Timestamp(v) => {
1239 buf.push(8);
1240 buf.extend_from_slice(&v.to_le_bytes());
1241 }
1242 MetadataValue::Geo { lat, lon } => {
1243 buf.push(9);
1244 buf.extend_from_slice(&lat.to_le_bytes());
1245 buf.extend_from_slice(&lon.to_le_bytes());
1246 }
1247 MetadataValue::Reference(target) => {
1248 buf.push(10);
1249 write_ref_target(buf, target);
1250 }
1251 MetadataValue::References(targets) => {
1252 buf.push(11);
1253 buf.extend_from_slice(&(targets.len() as u32).to_le_bytes());
1254 for target in targets {
1255 write_ref_target(buf, target);
1256 }
1257 }
1258 }
1259}
1260
1261fn read_exact_slice<'a>(
1262 data: &'a [u8],
1263 pos: &mut usize,
1264 len: usize,
1265) -> Result<&'a [u8], StoreError> {
1266 if *pos + len > data.len() {
1267 return Err(StoreError::Serialization(
1268 "truncated metadata payload".to_string(),
1269 ));
1270 }
1271 let slice = &data[*pos..*pos + len];
1272 *pos += len;
1273 Ok(slice)
1274}
1275
1276fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, StoreError> {
1277 let bytes = read_exact_slice(data, pos, 4)?;
1278 let mut raw = [0u8; 4];
1279 raw.copy_from_slice(bytes);
1280 Ok(u32::from_le_bytes(raw))
1281}
1282
1283fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, StoreError> {
1284 let bytes = read_exact_slice(data, pos, 8)?;
1285 let mut raw = [0u8; 8];
1286 raw.copy_from_slice(bytes);
1287 Ok(u64::from_le_bytes(raw))
1288}
1289
1290fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, StoreError> {
1291 let bytes = read_exact_slice(data, pos, 8)?;
1292 let mut raw = [0u8; 8];
1293 raw.copy_from_slice(bytes);
1294 Ok(i64::from_le_bytes(raw))
1295}
1296
1297fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, StoreError> {
1298 let bytes = read_exact_slice(data, pos, 8)?;
1299 let mut raw = [0u8; 8];
1300 raw.copy_from_slice(bytes);
1301 Ok(f64::from_le_bytes(raw))
1302}
1303
1304fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, StoreError> {
1305 let bytes = read_exact_slice(data, pos, 1)?;
1306 Ok(bytes[0])
1307}
1308
1309fn read_string(data: &[u8], pos: &mut usize) -> Result<String, StoreError> {
1310 reddb_file::decode_native_len_prefixed_string(data, pos)
1311 .map_err(|err| StoreError::Serialization(err.to_string()))
1312}
1313
1314fn read_bytes(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, StoreError> {
1315 reddb_file::decode_native_len_prefixed_bytes(data, pos)
1316 .map(|bytes| bytes.to_vec())
1317 .map_err(|err| StoreError::Serialization(err.to_string()))
1318}
1319
1320fn read_ref_target(
1321 data: &[u8],
1322 pos: &mut usize,
1323) -> Result<crate::storage::unified::RefTarget, StoreError> {
1324 use crate::storage::unified::RefTarget;
1325
1326 match read_u8(data, pos)? {
1327 0 => Ok(RefTarget::TableRow {
1328 table: read_string(data, pos)?,
1329 row_id: read_u64(data, pos)?,
1330 }),
1331 1 => Ok(RefTarget::Node {
1332 collection: read_string(data, pos)?,
1333 node_id: EntityId::new(read_u64(data, pos)?),
1334 }),
1335 2 => Ok(RefTarget::Edge {
1336 collection: read_string(data, pos)?,
1337 edge_id: EntityId::new(read_u64(data, pos)?),
1338 }),
1339 3 => Ok(RefTarget::Vector {
1340 collection: read_string(data, pos)?,
1341 vector_id: EntityId::new(read_u64(data, pos)?),
1342 }),
1343 4 => Ok(RefTarget::Entity {
1344 collection: read_string(data, pos)?,
1345 entity_id: EntityId::new(read_u64(data, pos)?),
1346 }),
1347 tag => Err(StoreError::Serialization(format!(
1348 "unknown metadata ref target tag {tag}"
1349 ))),
1350 }
1351}
1352
1353fn read_metadata_value(data: &[u8], pos: &mut usize) -> Result<MetadataValue, StoreError> {
1354 match read_u8(data, pos)? {
1355 0 => Ok(MetadataValue::Null),
1356 1 => Ok(MetadataValue::Bool(read_u8(data, pos)? != 0)),
1357 2 => Ok(MetadataValue::Int(read_i64(data, pos)?)),
1358 3 => Ok(MetadataValue::Float(read_f64(data, pos)?)),
1359 4 => Ok(MetadataValue::String(read_string(data, pos)?)),
1360 5 => Ok(MetadataValue::Bytes(read_bytes(data, pos)?)),
1361 6 => {
1362 let count = read_u32(data, pos)? as usize;
1363 let mut values = Vec::with_capacity(count);
1364 for _ in 0..count {
1365 values.push(read_metadata_value(data, pos)?);
1366 }
1367 Ok(MetadataValue::Array(values))
1368 }
1369 7 => {
1370 let count = read_u32(data, pos)? as usize;
1371 let mut values = std::collections::HashMap::with_capacity(count);
1372 for _ in 0..count {
1373 let key = read_string(data, pos)?;
1374 let value = read_metadata_value(data, pos)?;
1375 values.insert(key, value);
1376 }
1377 Ok(MetadataValue::Object(values))
1378 }
1379 8 => Ok(MetadataValue::Timestamp(read_u64(data, pos)?)),
1380 9 => Ok(MetadataValue::Geo {
1381 lat: read_f64(data, pos)?,
1382 lon: read_f64(data, pos)?,
1383 }),
1384 10 => Ok(MetadataValue::Reference(read_ref_target(data, pos)?)),
1385 11 => {
1386 let count = read_u32(data, pos)? as usize;
1387 let mut targets = Vec::with_capacity(count);
1388 for _ in 0..count {
1389 targets.push(read_ref_target(data, pos)?);
1390 }
1391 Ok(MetadataValue::References(targets))
1392 }
1393 tag => Err(StoreError::Serialization(format!(
1394 "unknown metadata value tag {tag}"
1395 ))),
1396 }
1397}
1398
1399#[cfg(test)]
1400mod checkpoint_work_tests {
1401 use super::*;
1402
1403 #[test]
1404 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1405 let directory = tempfile::tempdir().expect("temporary store");
1406 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1407 store.create_collection("rows").expect("collection");
1408 for id in 1..=40 {
1409 store
1410 .insert(
1411 "rows",
1412 UnifiedEntity::table_row(
1413 EntityId::new(id),
1414 "rows",
1415 id,
1416 vec![Value::Integer(id as i64)],
1417 ),
1418 )
1419 .expect("seed row");
1420 }
1421 store.persist().expect("initial checkpoint");
1422 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1423 store.persist().expect("unchanged checkpoint");
1424 assert_eq!(
1425 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1426 0,
1427 "unchanged collections must not hold the writer fence while serializing every row"
1428 );
1429 drop(store);
1430 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1431 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1432 reopened.persist().expect("unchanged recovered generation");
1433 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1434 }
1435
1436 fn row(id: u64, value: i64) -> UnifiedEntity {
1437 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
1438 }
1439
1440 #[test]
1441 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
1442 let directory = tempfile::tempdir().expect("temporary store");
1443 let path = directory.path().join("data.rdb");
1444 let store = UnifiedStore::open(&path).expect("paged store");
1445 store.create_collection("rows").expect("collection");
1446 store.insert("rows", row(1, 1)).expect("seed");
1447 store.persist().expect("initial checkpoint");
1448 let manager = store.get_collection("rows").expect("rows");
1449 manager.update(row(1, 2)).expect("direct update");
1450 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1451 store.persist().expect("updated row checkpoint");
1452 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1453 let mut metadata = Metadata::new();
1454 metadata.set("verified", MetadataValue::Bool(true));
1455 manager
1456 .set_metadata(EntityId::new(1), metadata)
1457 .expect("metadata");
1458 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1459 store.persist().expect("metadata-only checkpoint");
1460 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1461 manager
1462 .bulk_insert(vec![row(2, 2), row(3, 3)])
1463 .expect("bulk insert");
1464 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1465 store.persist().expect("bulk insert checkpoint");
1466 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
1467 manager.delete(EntityId::new(2)).expect("delete");
1468 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1469 store.persist().expect("changed checkpoint");
1470 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
1471 drop(store);
1472 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
1473 let manager = recovered.get_collection("rows").expect("rows");
1474 assert_eq!(manager.count(), 2);
1475 assert_eq!(
1476 manager
1477 .get(EntityId::new(1))
1478 .expect("row")
1479 .data
1480 .as_row()
1481 .expect("table")
1482 .columns,
1483 vec![Value::Integer(2)]
1484 );
1485 assert!(manager.get(EntityId::new(2)).is_none());
1486 assert!(manager
1487 .get_metadata(EntityId::new(1))
1488 .expect("metadata")
1489 .get("verified")
1490 .is_some());
1491 }
1492
1493 #[test]
1494 fn rollback_invalidates_reuse_without_a_segment_mutation() {
1495 let directory = tempfile::tempdir().expect("temporary store");
1496 let path = directory.path().join("data.rdb");
1497 let store = UnifiedStore::open(&path).expect("paged store");
1498 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1499 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1500 store.create_collection("rows").expect("collection");
1501 let xid = snapshots.begin();
1502 let mut pending = row(1, 1);
1503 pending.xmin = xid;
1504 store.insert("rows", pending).expect("version");
1505 store.persist().expect("before rollback");
1506 snapshots.rollback(xid);
1507 store.persist().expect("after rollback");
1508 drop(store);
1509 let recovered = UnifiedStore::open(&path).expect("recover");
1510 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1511 }
1512
1513 #[test]
1514 fn publishing_a_pending_vault_version_invalidates_reuse() {
1515 let directory = tempfile::tempdir().expect("temporary store");
1516 let path = directory.path().join("data.rdb");
1517 let store = UnifiedStore::open(&path).expect("paged store");
1518 store.create_collection("rows").expect("collection");
1519 store.insert("rows", row(1, 1)).expect("row");
1520 store
1521 .pending_vault_versions
1522 .write()
1523 .insert(EntityId::new(1));
1524 store.persist().expect("exclude pending vault version");
1525 store
1526 .pending_vault_versions
1527 .write()
1528 .remove(&EntityId::new(1));
1529 store.persist().expect("include published vault version");
1530 drop(store);
1531 let recovered = UnifiedStore::open(&path).expect("recover");
1532 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
1533 }
1534}