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