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 note_paged_insert(
319 &self,
320 collection: &str,
321 manager: &Arc<SegmentManager>,
322 before: &[(u64, u64)],
323 id: EntityId,
324 ) {
325 let after = manager.checkpoint_revisions();
326 if before.len() != after.len() {
327 return;
328 }
329 let mut changed = 0;
330 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
331 if old_id != new_id {
332 return;
333 }
334 if old_revision != new_revision {
335 if old_revision.checked_add(1) != Some(*new_revision) {
336 return;
337 }
338 changed += 1;
339 }
340 }
341 if changed != 1 {
342 return;
343 }
344 let mut versions = self.checkpoint_versions.write();
345 let Some(previous) = versions.get_mut(collection) else {
346 return;
347 };
348 if previous.manager.ptr_eq(&Arc::downgrade(manager))
349 && previous.segments == before
350 && previous.format_version == self.format_version()
351 && !previous.pending_vault.contains(&id)
352 && !previous.aborted.contains(&0)
353 {
354 previous.segments = after;
355 }
356 }
357
358 pub(crate) fn mark_paged_registry_dirty(&self) {
359 self.paged_registry_dirty.store(true, Ordering::Release);
360 }
361
362 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
368 let pager = self.pager.as_ref()?;
369 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
370 return Some(btree);
371 }
372 let mut write = self.btree_indices.write();
373 let btree = write
374 .entry(collection.to_string())
375 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
376 .clone();
377 Some(btree)
378 }
379
380 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
381 let Some(pager) = &self.pager else {
382 return Ok(());
383 };
384
385 if self.paged_registry_dirty.load(Ordering::Acquire) {
386 self.flush_paged_registry()?;
387 self.paged_registry_dirty.store(false, Ordering::Release);
388 return Ok(());
389 }
390
391 pager
392 .flush()
393 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
394 }
395
396 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
397 let Some(pager) = &self.pager else {
398 return Ok(());
399 };
400
401 match pager.read_page(1) {
402 Ok(_) => {}
403 Err(PagerError::PageNotFound(_)) => {
404 let meta_page = pager
405 .allocate_page(crate::storage::engine::PageType::Header)
406 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
407 pager
408 .write_page(meta_page.page_id(), meta_page)
409 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
410 }
411 Err(e) => {
412 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
413 }
414 }
415
416 let format_version = STORE_VERSION_V11;
417 self.set_format_version(format_version);
418
419 let collections = self.collections.read();
420 let btree_indices = self.btree_indices.read();
421 let mut collection_roots = Vec::with_capacity(collections.len());
422 for (name, _) in collections.iter() {
423 let root_page = btree_indices
424 .get(name)
425 .map_or(0, |btree| btree.root_page_id());
426 collection_roots.push((name.clone(), root_page));
427 }
428 drop(btree_indices);
429 drop(collections);
430
431 let mut meta_data = Vec::with_capacity(4096);
432 reddb_file::encode_native_paged_metadata_header(
433 &mut meta_data,
434 reddb_file::NativePagedMetadataHeader {
435 format_version,
436 collection_count: collection_roots.len() as u32,
437 },
438 );
439 for (name, root_page) in &collection_roots {
440 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
441 }
442
443 let cross_refs = self.cross_refs.read();
444 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
445 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
446 for (source_id, refs) in cross_refs.iter() {
447 for (target_id, ref_type, collection) in refs {
448 reddb_file::encode_native_paged_cross_ref(
449 &mut meta_data,
450 source_id.raw(),
451 target_id.raw(),
452 ref_type.to_byte(),
453 collection,
454 );
455 }
456 }
457
458 publish_paged_metadata(pager, &meta_data)
459 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
460
461 Ok(())
462 }
463
464 pub fn pager(&self) -> Option<&Arc<Pager>> {
466 self.pager.as_ref()
467 }
468
469 pub fn page_cache_bytes_in_use(&self) -> u64 {
473 self.pager.as_ref().map_or(0, |pager| {
474 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
475 })
476 }
477
478 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
481 self.commit
482 .as_ref()
483 .map_or(0, |commit| commit.buffered_bytes())
484 }
485
486 pub fn segment_memory_bytes(&self) -> u64 {
490 let managers: Vec<Arc<SegmentManager>> =
491 self.collections.read().values().map(Arc::clone).collect();
492
493 managers
494 .iter()
495 .map(|manager| manager.resident_bytes())
496 .fold(0, u64::saturating_add)
497 }
498
499 pub fn config(&self) -> &UnifiedStoreConfig {
503 &self.config
504 }
505
506 pub fn with_config(config: UnifiedStoreConfig) -> Self {
507 Self {
508 config,
509 format_version: AtomicU32::new(STORE_VERSION_V11),
510 next_entity_id: AtomicU64::new(1),
511 collections: RwLock::new(HashMap::new()),
512 cross_refs: RwLock::new(HashMap::new()),
513 reverse_refs: RwLock::new(HashMap::new()),
514 pager: None,
515 db_path: None,
516 btree_indices: RwLock::new(HashMap::new()),
517 checkpoint_versions: RwLock::new(HashMap::new()),
518 context_index: ContextIndex::new(),
519 entity_cache: EntityCache::new(),
520 graph_label_index: RwLock::new(HashMap::new()),
521 paged_registry_dirty: AtomicBool::new(false),
522 commit: None,
523 unindex_cross_refs_fast_path: AtomicU64::new(0),
524 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
525 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
526 aux_metadata: RwLock::new(Vec::new()),
527 snapshot_manager: std::sync::OnceLock::new(),
528 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
529 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
530 }
531 }
532
533 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
551 Self::open_with_config(path, UnifiedStoreConfig::default())
552 }
553
554 pub fn open_with_config(
555 path: impl AsRef<Path>,
556 config: UnifiedStoreConfig,
557 ) -> Result<Self, StoreError> {
558 let path = path.as_ref();
559 let mut pager_config = PagerConfig::default();
560 if let Some(slots) = config.page_cache_slots {
564 pager_config.cache_size = slots;
565 }
566 if matches!(
574 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
575 Some("0") | Some("false") | Some("off")
576 ) {
577 pager_config.double_write = false;
578 }
579 let pager = Pager::open(path, pager_config)
580 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
581
582 let wal_path = reddb_file::layout::unified_wal_path(path);
583 let commit = Some(Arc::new(
586 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
587 .map_err(StoreError::Io)?,
588 ));
589
590 let store = Self {
591 config,
592 format_version: AtomicU32::new(STORE_VERSION_V11),
593 next_entity_id: AtomicU64::new(1),
594 collections: RwLock::new(HashMap::new()),
595 cross_refs: RwLock::new(HashMap::new()),
596 reverse_refs: RwLock::new(HashMap::new()),
597 pager: Some(Arc::new(pager)),
598 db_path: Some(path.to_path_buf()),
599 btree_indices: RwLock::new(HashMap::new()),
600 checkpoint_versions: RwLock::new(HashMap::new()),
601 context_index: ContextIndex::new(),
602 entity_cache: EntityCache::new(),
603 graph_label_index: RwLock::new(HashMap::new()),
604 paged_registry_dirty: AtomicBool::new(false),
605 commit,
606 unindex_cross_refs_fast_path: AtomicU64::new(0),
607 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
608 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
609 aux_metadata: RwLock::new(Vec::new()),
610 snapshot_manager: std::sync::OnceLock::new(),
611 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
612 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
613 };
614
615 store.load_from_pages()?;
617 if let Some(commit) = &store.commit {
618 commit.replay_into(&store).map_err(StoreError::Io)?;
619 }
620 store.recover_operational_manifest()?;
621
622 Ok(store)
623 }
624
625 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
629 let Some(pager) = &self.pager else {
630 return Ok(());
631 };
632 let collections = self.collections.read();
633 let mut indices = self.btree_indices.write();
634 for (name, manager) in collections.iter() {
635 let mut records: Vec<_> = manager
636 .query_all(|_| true)
637 .into_iter()
638 .map(|entity| {
639 let metadata = manager.get_metadata(entity.id);
640 (
641 entity.id.raw().to_be_bytes().to_vec(),
642 Self::serialize_entity_record(
643 &entity,
644 metadata.as_ref(),
645 self.format_version(),
646 ),
647 )
648 })
649 .collect();
650 records.sort_by(|left, right| left.0.cmp(&right.0));
651 let matches = if let Some(tree) = indices.get(name) {
652 tree_matches_records(tree, &records)?
653 } else {
654 records.is_empty()
655 };
656 if matches {
657 continue;
658 }
659 let tree = Arc::new(BTree::new(Arc::clone(pager)));
660 tree.bulk_insert_sorted(&records).map_err(|e| {
661 StoreError::Io(std::io::Error::other(format!(
662 "WAL tree rebuild for '{name}': {e}"
663 )))
664 })?;
665 indices.insert(name.clone(), tree);
666 self.mark_paged_registry_dirty();
667 }
668 drop(indices);
669 drop(collections);
670 self.flush_paged_state()?;
671 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
674 let collections = self.collections.read();
675 let mut versions = self.checkpoint_versions.write();
676 for (name, manager) in collections.iter() {
677 versions.insert(
678 name.clone(),
679 CheckpointVersion {
680 manager: Arc::downgrade(manager),
681 segments: manager.checkpoint_revisions(),
682 aborted_revision: 0,
683 aborted: Arc::default(),
684 pending_vault: Arc::default(),
685 format_version: self.format_version(),
686 },
687 );
688 }
689 }
690 Ok(())
691 }
692
693 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
694 let Some(path) = &self.db_path else {
695 return Ok(());
696 };
697 let mut collections = self.list_collections();
698 collections.sort();
699 let pending_drops =
700 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
701 .recover_or_bootstrap(&collections)
702 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
703 for name in pending_drops {
704 if self.get_collection(&name).is_some() {
705 self.drop_collection(&name)?;
706 }
707 }
708 Ok(())
709 }
710
711 pub(crate) fn prepare_operational_collection_create(
712 &self,
713 name: &str,
714 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
715 let Some(path) = &self.db_path else {
716 return Ok(None);
717 };
718 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
719 .prepare_collection_create(name)
720 .map(Some)
721 .map_err(StoreError::Io)
722 }
723
724 pub(crate) fn prepare_operational_collection_drop(
725 &self,
726 name: &str,
727 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
728 let Some(path) = &self.db_path else {
729 return Ok(None);
730 };
731 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
732 .prepare_collection_drop(name)
733 .map(Some)
734 .map_err(StoreError::Io)
735 }
736
737 fn load_from_pages(&self) -> Result<(), StoreError> {
741 let pager = match &self.pager {
742 Some(p) => p,
743 None => return Ok(()), };
745
746 let page_count = pager.page_count().map_err(|e| {
748 StoreError::Io(std::io::Error::other(format!(
749 "failed to read page count: {}",
750 e
751 )))
752 })?;
753 if page_count <= 1 {
754 return Ok(());
756 }
757
758 if let Some(content_vec) = read_meta_payload(pager)? {
763 let content: &[u8] = &content_vec;
764 if content.len() >= 4 {
765 let mut pos = 0;
766 let mut format_version = STORE_VERSION_V1;
767
768 let collection_count = if let Some(header) =
769 reddb_file::decode_native_paged_metadata_header(content)
770 .map_err(|err| StoreError::Serialization(err.to_string()))?
771 {
772 format_version = header.format_version;
773 pos += reddb_file::METADATA_HEADER_BYTES;
774 header.collection_count as usize
775 } else {
776 let count = u32::from_le_bytes([
777 content[pos],
778 content[pos + 1],
779 content[pos + 2],
780 content[pos + 3],
781 ]) as usize;
782 pos += 4;
783 count
784 };
785
786 self.set_format_version(format_version);
787
788 if pos > content.len() {
789 return Ok(());
790 }
791
792 for _ in 0..collection_count {
794 if let Ok(root) =
795 reddb_file::decode_native_paged_collection_root(content, &mut pos)
796 {
797 let root_page = root.root_page;
799 let name = root.collection;
800
801 let _ = self.create_collection_in_memory(&name);
805
806 if root_page > 0 {
808 let btree = BTree::with_root(Arc::clone(pager), root_page);
809
810 let mut cursor = btree.cursor_first().map_err(|err| {
812 page_integrity(root_page, Some(name.clone()), err.to_string())
813 })?;
814 let manager = self.get_collection(&name);
815 loop {
816 let next = cursor.next().map_err(|err| {
817 page_integrity(root_page, Some(name.clone()), err.to_string())
818 })?;
819 let Some((key, value)) = next else {
820 break;
821 };
822 if let Ok((entity, metadata)) =
824 Self::deserialize_entity_record(&value, self.format_version())
825 {
826 if let Some(m) = &manager {
827 let id = entity.id;
828 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
829 m.register_row_id(*row_id);
830 }
831 self.context_index.index_entity(&name, &entity);
832 m.restore_snapshot_entity(entity.clone())?;
833 if let Some(metadata) = metadata {
834 let _ = m.set_metadata(id, metadata);
835 }
836 self.register_entity_id(id);
837 if self.config.auto_index_refs {
838 self.index_cross_refs(&entity, &name)?;
839 }
840 }
841 }
842 }
843
844 self.btree_indices
846 .write()
847 .insert(name.clone(), Arc::new(btree));
848 if format_version == STORE_VERSION_V11 {
849 if let Some(manager) = &manager {
850 self.checkpoint_versions.write().insert(
851 name,
852 CheckpointVersion {
853 manager: Arc::downgrade(manager),
854 segments: manager.checkpoint_revisions(),
855 aborted_revision: 0,
856 aborted: Arc::default(),
857 pending_vault: Arc::default(),
858 format_version,
859 },
860 );
861 }
862 }
863 }
864 } else {
865 break;
866 }
867 }
868
869 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
870 let cross_ref_count = u32::from_le_bytes([
871 content[pos],
872 content[pos + 1],
873 content[pos + 2],
874 content[pos + 3],
875 ]) as usize;
876 pos += 4;
877
878 for _ in 0..cross_ref_count {
879 let Ok(cross_ref) =
880 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
881 else {
882 break;
883 };
884 let source_id = EntityId::new(cross_ref.source_id);
885 let target_id = EntityId::new(cross_ref.target_id);
886 let ref_type = RefType::from_byte(cross_ref.ref_type);
887 let target_collection = cross_ref.target_collection;
888
889 self.cross_refs.write().entry(source_id).or_default().push((
890 target_id,
891 ref_type,
892 target_collection.clone(),
893 ));
894
895 if let Some((collection, mut entity)) = self.get_any(source_id) {
896 let exists = entity.cross_refs().iter().any(|xref| {
897 xref.target == target_id
898 && xref.ref_type == ref_type
899 && xref.target_collection == target_collection
900 });
901 if !exists {
902 entity.cross_refs_mut().push(CrossRef::new(
903 source_id,
904 target_id,
905 target_collection.clone(),
906 ref_type,
907 ));
908 if let Some(manager) = self.get_collection(&collection) {
909 let _ = manager.update(entity);
910 }
911 }
912 }
913 }
914 }
915 }
916 }
917
918 if self.format_version() < STORE_VERSION_V11 {
919 self.set_format_version(STORE_VERSION_V11);
920 }
921
922 Ok(())
923 }
924
925 pub(crate) fn deserialize_entity(
927 data: &[u8],
928 format_version: u32,
929 ) -> Result<UnifiedEntity, StoreError> {
930 let mut pos = 0;
931 Self::read_entity_binary(data, &mut pos, format_version)
932 .map_err(|e| StoreError::Serialization(e.to_string()))
933 }
934
935 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
937 let mut buf = Vec::with_capacity(256);
942 Self::write_entity_binary(&mut buf, entity, format_version);
943 buf
944 }
945
946 pub(crate) fn serialize_entity_record(
947 entity: &UnifiedEntity,
948 metadata: Option<&Metadata>,
949 format_version: u32,
950 ) -> Vec<u8> {
951 let entity_bytes = Self::serialize_entity(entity, format_version);
952 let has_meta = matches!(metadata, Some(m) if !m.fields.is_empty());
958 if has_meta {
959 let metadata_bytes = serialize_metadata(metadata);
960 reddb_file::encode_native_entity_record_frame(&entity_bytes, Some(&metadata_bytes))
961 } else {
962 reddb_file::encode_native_entity_record_frame(&entity_bytes, None)
963 }
964 }
965
966 pub(crate) fn deserialize_entity_record(
967 data: &[u8],
968 format_version: u32,
969 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
970 let Some(frame) = reddb_file::decode_native_entity_record_frame(data)
971 .map_err(|err| StoreError::Serialization(err.to_string()))?
972 else {
973 return Self::deserialize_entity(data, format_version).map(|entity| (entity, None));
974 };
975
976 let entity = Self::deserialize_entity(frame.entity, format_version)?;
977 let metadata = if frame.metadata.is_empty() {
978 None
979 } else {
980 let metadata = deserialize_metadata(frame.metadata)?;
981 if metadata.is_empty() {
982 None
983 } else {
984 Some(metadata)
985 }
986 };
987
988 Ok((entity, metadata))
989 }
990
991 pub fn persist(&self) -> Result<(), StoreError> {
996 let _vault_guard = self.vault_publication_lock.lock();
997 let pending_vault_versions = self.pending_vault_versions.read();
998 let pager = match &self.pager {
999 Some(p) => p,
1000 None => {
1001 if let Some(path) = &self.db_path {
1003 return self
1004 .save_to_file(path)
1005 .map_err(|e| StoreError::Serialization(e.to_string()));
1006 }
1007 return Err(StoreError::Io(std::io::Error::other(
1008 "No pager or path configured for persistence",
1009 )));
1010 }
1011 };
1012
1013 match pager.read_page(1) {
1014 Ok(_) => {}
1015 Err(PagerError::PageNotFound(_)) => {
1016 let meta_page = pager
1017 .allocate_page(crate::storage::engine::PageType::Header)
1018 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1019 pager
1020 .write_page(meta_page.page_id(), meta_page)
1021 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1022 }
1023 Err(e) => {
1024 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1025 }
1026 }
1027
1028 if let Some(commit) = &self.commit {
1029 commit.force_sync().map_err(StoreError::Io)?;
1030 }
1031
1032 let collections = self.collections.read();
1033 let mut btree_indices = self.btree_indices.write();
1034
1035 let mut collection_roots: Vec<(String, u32)> = Vec::new();
1037
1038 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1041 let previous_versions = self.checkpoint_versions.read();
1042 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1043 let pending_vault = Arc::new(pending_vault_versions.clone());
1044 let (aborted_revision, aborted) = self.snapshot_manager.get().map_or_else(
1045 || (0, Default::default()),
1046 |manager| manager.checkpoint_aborted_state(),
1047 );
1048 let aborted = Arc::new(aborted);
1049 for (name, manager) in collections.iter() {
1050 let version = CheckpointVersion {
1051 manager: Arc::downgrade(manager),
1052 segments: manager.checkpoint_revisions(),
1053 aborted_revision,
1054 aborted: Arc::clone(&aborted),
1055 pending_vault: Arc::clone(&pending_vault),
1056 format_version: self.format_version(),
1057 };
1058 if let (Some(previous), Some(tree)) =
1059 (previous_versions.get(name), btree_indices.get(name))
1060 {
1061 if previous.manager.ptr_eq(&version.manager)
1062 && previous.segments == version.segments
1063 && previous.format_version == version.format_version
1064 && checkpoint_visibility_matches(previous, &version, manager)
1065 {
1066 collection_roots.push((name.clone(), tree.root_page_id()));
1067 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1068 checkpoint_versions.insert(name.clone(), version);
1069 continue;
1070 }
1071 }
1072 checkpoint_versions.insert(name.clone(), version);
1073 let mut records = Vec::new();
1074 manager.for_each_entity_with_metadata(|entity, metadata| {
1075 #[cfg(test)]
1076 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1077 if pending_vault_versions.contains(&entity.id)
1078 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1079 && aborted.contains(&entity.xmin))
1080 {
1081 return true;
1082 }
1083 let mut normalized = std::borrow::Cow::Borrowed(entity);
1086 if matches!(entity.data, EntityData::Row(_)) && aborted.contains(&entity.xmax) {
1087 normalized.to_mut().xmax = 0;
1088 }
1089 records.push((
1090 entity.id.raw().to_be_bytes().to_vec(),
1091 Self::serialize_entity_record(
1092 &normalized,
1093 metadata.as_ref(),
1094 self.format_version(),
1095 ),
1096 ));
1097 true
1098 });
1099 records.sort_by(|left, right| left.0.cmp(&right.0));
1100
1101 if let Some(tree) = btree_indices.get(name) {
1102 if tree_matches_records(tree, &records)? {
1103 collection_roots.push((name.clone(), tree.root_page_id()));
1104 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1105 continue;
1106 }
1107 }
1108
1109 let btree = Arc::new(BTree::new(Arc::clone(pager)));
1110
1111 if !records.is_empty() {
1119 btree.bulk_insert_sorted(&records).map_err(|e| {
1120 StoreError::Io(std::io::Error::other(format!(
1121 "B-tree bulk rebuild error for '{name}': {e}"
1122 )))
1123 })?;
1124 }
1125
1126 collection_roots.push((name.clone(), btree.root_page_id()));
1127 rebuilt_indices.insert(name.clone(), btree);
1128 }
1129
1130 let mut meta_data = Vec::with_capacity(4096);
1132
1133 let format_version = STORE_VERSION_V11;
1134 self.set_format_version(format_version);
1135
1136 reddb_file::encode_native_paged_metadata_header(
1137 &mut meta_data,
1138 reddb_file::NativePagedMetadataHeader {
1139 format_version,
1140 collection_count: collection_roots.len() as u32,
1141 },
1142 );
1143
1144 for (name, root_page) in &collection_roots {
1146 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1147 }
1148
1149 let cross_refs = self.cross_refs.read();
1151 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1152 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1153 for (source_id, refs) in cross_refs.iter() {
1154 for (target_id, ref_type, collection) in refs {
1155 reddb_file::encode_native_paged_cross_ref(
1156 &mut meta_data,
1157 source_id.raw(),
1158 target_id.raw(),
1159 ref_type.to_byte(),
1160 collection,
1161 );
1162 }
1163 }
1164
1165 publish_paged_metadata(pager, &meta_data)
1166 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1167
1168 *btree_indices = rebuilt_indices;
1171 drop(previous_versions);
1172 *self.checkpoint_versions.write() = checkpoint_versions;
1173
1174 if let Some(commit) = &self.commit {
1175 commit.truncate().map_err(StoreError::Io)?;
1176 }
1177
1178 Ok(())
1179 }
1180
1181 pub fn is_paged(&self) -> bool {
1183 self.pager.is_some()
1184 }
1185
1186 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1189 self.btree_indices
1190 .read()
1191 .get(collection)
1192 .map(|btree| btree.root_page_id())
1193 .filter(|root| *root != 0)
1194 }
1195
1196 pub fn db_path(&self) -> Option<&Path> {
1198 self.db_path.as_deref()
1199 }
1200}
1201
1202fn serialize_metadata(metadata: Option<&Metadata>) -> Vec<u8> {
1203 let Some(metadata) = metadata else {
1204 return Vec::new();
1205 };
1206 if metadata.is_empty() {
1207 return Vec::new();
1208 }
1209
1210 let mut entries: Vec<_> = metadata.iter().collect();
1211 entries.sort_by_key(|(a, _)| *a);
1212
1213 let mut buf = Vec::new();
1214 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1215 for (key, value) in entries {
1216 write_string(&mut buf, key);
1217 write_metadata_value(&mut buf, value);
1218 }
1219 buf
1220}
1221
1222fn deserialize_metadata(data: &[u8]) -> Result<Metadata, StoreError> {
1223 let mut pos = 0usize;
1224 let count = read_u32(data, &mut pos)? as usize;
1225 let mut metadata = Metadata::new();
1226 for _ in 0..count {
1227 let key = read_string(data, &mut pos)?;
1228 let value = read_metadata_value(data, &mut pos)?;
1229 metadata.set(key, value);
1230 }
1231 Ok(metadata)
1232}
1233
1234fn write_string(buf: &mut Vec<u8>, value: &str) {
1235 reddb_file::encode_native_len_prefixed_str(buf, value);
1236}
1237
1238fn write_bytes(buf: &mut Vec<u8>, value: &[u8]) {
1239 reddb_file::encode_native_len_prefixed_bytes(buf, value);
1240}
1241
1242fn write_ref_target(buf: &mut Vec<u8>, target: &crate::storage::unified::RefTarget) {
1243 use crate::storage::unified::RefTarget;
1244
1245 match target {
1246 RefTarget::TableRow { table, row_id } => {
1247 buf.push(0);
1248 write_string(buf, table);
1249 buf.extend_from_slice(&row_id.to_le_bytes());
1250 }
1251 RefTarget::Node {
1252 collection,
1253 node_id,
1254 } => {
1255 buf.push(1);
1256 write_string(buf, collection);
1257 buf.extend_from_slice(&node_id.raw().to_le_bytes());
1258 }
1259 RefTarget::Edge {
1260 collection,
1261 edge_id,
1262 } => {
1263 buf.push(2);
1264 write_string(buf, collection);
1265 buf.extend_from_slice(&edge_id.raw().to_le_bytes());
1266 }
1267 RefTarget::Vector {
1268 collection,
1269 vector_id,
1270 } => {
1271 buf.push(3);
1272 write_string(buf, collection);
1273 buf.extend_from_slice(&vector_id.raw().to_le_bytes());
1274 }
1275 RefTarget::Entity {
1276 collection,
1277 entity_id,
1278 } => {
1279 buf.push(4);
1280 write_string(buf, collection);
1281 buf.extend_from_slice(&entity_id.raw().to_le_bytes());
1282 }
1283 }
1284}
1285
1286fn write_metadata_value(buf: &mut Vec<u8>, value: &MetadataValue) {
1287 match value {
1288 MetadataValue::Null => buf.push(0),
1289 MetadataValue::Bool(v) => {
1290 buf.push(1);
1291 buf.push(u8::from(*v));
1292 }
1293 MetadataValue::Int(v) => {
1294 buf.push(2);
1295 buf.extend_from_slice(&v.to_le_bytes());
1296 }
1297 MetadataValue::Float(v) => {
1298 buf.push(3);
1299 buf.extend_from_slice(&v.to_le_bytes());
1300 }
1301 MetadataValue::String(v) => {
1302 buf.push(4);
1303 write_string(buf, v);
1304 }
1305 MetadataValue::Bytes(v) => {
1306 buf.push(5);
1307 write_bytes(buf, v);
1308 }
1309 MetadataValue::Array(values) => {
1310 buf.push(6);
1311 buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
1312 for value in values {
1313 write_metadata_value(buf, value);
1314 }
1315 }
1316 MetadataValue::Object(values) => {
1317 buf.push(7);
1318 let mut entries: Vec<_> = values.iter().collect();
1319 entries.sort_by_key(|(a, _)| *a);
1320 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1321 for (key, value) in entries {
1322 write_string(buf, key);
1323 write_metadata_value(buf, value);
1324 }
1325 }
1326 MetadataValue::Timestamp(v) => {
1327 buf.push(8);
1328 buf.extend_from_slice(&v.to_le_bytes());
1329 }
1330 MetadataValue::Geo { lat, lon } => {
1331 buf.push(9);
1332 buf.extend_from_slice(&lat.to_le_bytes());
1333 buf.extend_from_slice(&lon.to_le_bytes());
1334 }
1335 MetadataValue::Reference(target) => {
1336 buf.push(10);
1337 write_ref_target(buf, target);
1338 }
1339 MetadataValue::References(targets) => {
1340 buf.push(11);
1341 buf.extend_from_slice(&(targets.len() as u32).to_le_bytes());
1342 for target in targets {
1343 write_ref_target(buf, target);
1344 }
1345 }
1346 }
1347}
1348
1349fn read_exact_slice<'a>(
1350 data: &'a [u8],
1351 pos: &mut usize,
1352 len: usize,
1353) -> Result<&'a [u8], StoreError> {
1354 if *pos + len > data.len() {
1355 return Err(StoreError::Serialization(
1356 "truncated metadata payload".to_string(),
1357 ));
1358 }
1359 let slice = &data[*pos..*pos + len];
1360 *pos += len;
1361 Ok(slice)
1362}
1363
1364fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, StoreError> {
1365 let bytes = read_exact_slice(data, pos, 4)?;
1366 let mut raw = [0u8; 4];
1367 raw.copy_from_slice(bytes);
1368 Ok(u32::from_le_bytes(raw))
1369}
1370
1371fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, StoreError> {
1372 let bytes = read_exact_slice(data, pos, 8)?;
1373 let mut raw = [0u8; 8];
1374 raw.copy_from_slice(bytes);
1375 Ok(u64::from_le_bytes(raw))
1376}
1377
1378fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, StoreError> {
1379 let bytes = read_exact_slice(data, pos, 8)?;
1380 let mut raw = [0u8; 8];
1381 raw.copy_from_slice(bytes);
1382 Ok(i64::from_le_bytes(raw))
1383}
1384
1385fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, StoreError> {
1386 let bytes = read_exact_slice(data, pos, 8)?;
1387 let mut raw = [0u8; 8];
1388 raw.copy_from_slice(bytes);
1389 Ok(f64::from_le_bytes(raw))
1390}
1391
1392fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, StoreError> {
1393 let bytes = read_exact_slice(data, pos, 1)?;
1394 Ok(bytes[0])
1395}
1396
1397fn read_string(data: &[u8], pos: &mut usize) -> Result<String, StoreError> {
1398 reddb_file::decode_native_len_prefixed_string(data, pos)
1399 .map_err(|err| StoreError::Serialization(err.to_string()))
1400}
1401
1402fn read_bytes(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, StoreError> {
1403 reddb_file::decode_native_len_prefixed_bytes(data, pos)
1404 .map(|bytes| bytes.to_vec())
1405 .map_err(|err| StoreError::Serialization(err.to_string()))
1406}
1407
1408fn read_ref_target(
1409 data: &[u8],
1410 pos: &mut usize,
1411) -> Result<crate::storage::unified::RefTarget, StoreError> {
1412 use crate::storage::unified::RefTarget;
1413
1414 match read_u8(data, pos)? {
1415 0 => Ok(RefTarget::TableRow {
1416 table: read_string(data, pos)?,
1417 row_id: read_u64(data, pos)?,
1418 }),
1419 1 => Ok(RefTarget::Node {
1420 collection: read_string(data, pos)?,
1421 node_id: EntityId::new(read_u64(data, pos)?),
1422 }),
1423 2 => Ok(RefTarget::Edge {
1424 collection: read_string(data, pos)?,
1425 edge_id: EntityId::new(read_u64(data, pos)?),
1426 }),
1427 3 => Ok(RefTarget::Vector {
1428 collection: read_string(data, pos)?,
1429 vector_id: EntityId::new(read_u64(data, pos)?),
1430 }),
1431 4 => Ok(RefTarget::Entity {
1432 collection: read_string(data, pos)?,
1433 entity_id: EntityId::new(read_u64(data, pos)?),
1434 }),
1435 tag => Err(StoreError::Serialization(format!(
1436 "unknown metadata ref target tag {tag}"
1437 ))),
1438 }
1439}
1440
1441fn read_metadata_value(data: &[u8], pos: &mut usize) -> Result<MetadataValue, StoreError> {
1442 match read_u8(data, pos)? {
1443 0 => Ok(MetadataValue::Null),
1444 1 => Ok(MetadataValue::Bool(read_u8(data, pos)? != 0)),
1445 2 => Ok(MetadataValue::Int(read_i64(data, pos)?)),
1446 3 => Ok(MetadataValue::Float(read_f64(data, pos)?)),
1447 4 => Ok(MetadataValue::String(read_string(data, pos)?)),
1448 5 => Ok(MetadataValue::Bytes(read_bytes(data, pos)?)),
1449 6 => {
1450 let count = read_u32(data, pos)? as usize;
1451 let mut values = Vec::with_capacity(count);
1452 for _ in 0..count {
1453 values.push(read_metadata_value(data, pos)?);
1454 }
1455 Ok(MetadataValue::Array(values))
1456 }
1457 7 => {
1458 let count = read_u32(data, pos)? as usize;
1459 let mut values = std::collections::HashMap::with_capacity(count);
1460 for _ in 0..count {
1461 let key = read_string(data, pos)?;
1462 let value = read_metadata_value(data, pos)?;
1463 values.insert(key, value);
1464 }
1465 Ok(MetadataValue::Object(values))
1466 }
1467 8 => Ok(MetadataValue::Timestamp(read_u64(data, pos)?)),
1468 9 => Ok(MetadataValue::Geo {
1469 lat: read_f64(data, pos)?,
1470 lon: read_f64(data, pos)?,
1471 }),
1472 10 => Ok(MetadataValue::Reference(read_ref_target(data, pos)?)),
1473 11 => {
1474 let count = read_u32(data, pos)? as usize;
1475 let mut targets = Vec::with_capacity(count);
1476 for _ in 0..count {
1477 targets.push(read_ref_target(data, pos)?);
1478 }
1479 Ok(MetadataValue::References(targets))
1480 }
1481 tag => Err(StoreError::Serialization(format!(
1482 "unknown metadata value tag {tag}"
1483 ))),
1484 }
1485}
1486
1487#[cfg(test)]
1488mod checkpoint_work_tests {
1489 use super::*;
1490
1491 #[test]
1492 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1493 let directory = tempfile::tempdir().expect("temporary store");
1494 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1495 store.create_collection("rows").expect("collection");
1496 for id in 1..=40 {
1497 store
1498 .insert(
1499 "rows",
1500 UnifiedEntity::table_row(
1501 EntityId::new(id),
1502 "rows",
1503 id,
1504 vec![Value::Integer(id as i64)],
1505 ),
1506 )
1507 .expect("seed row");
1508 }
1509 store.persist().expect("initial checkpoint");
1510 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1511 store.persist().expect("unchanged checkpoint");
1512 assert_eq!(
1513 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1514 0,
1515 "unchanged collections must not hold the writer fence while serializing every row"
1516 );
1517 drop(store);
1518 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1519 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1520 reopened.persist().expect("unchanged recovered generation");
1521 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1522 }
1523
1524 #[test]
1525 fn paged_reopen_preserves_stored_sequence_identity() {
1526 let directory = tempfile::tempdir().expect("temporary store");
1527 let path = directory.path().join("data.rdb");
1528 let store = UnifiedStore::open(&path).expect("paged store");
1529 store.create_collection("rows").expect("collection");
1530 for id in [30, 10, 20] {
1531 store.insert("rows", row(id, id as i64)).expect("insert");
1532 }
1533 store.persist().expect("checkpoint");
1534 let sequences = [10, 20, 30].map(|id| {
1535 store
1536 .get("rows", EntityId::new(id))
1537 .expect("row")
1538 .sequence_id
1539 });
1540 drop(store);
1541 let reopened = UnifiedStore::open(&path).expect("reopen");
1542 let recovered = [10, 20, 30].map(|id| {
1543 reopened
1544 .get("rows", EntityId::new(id))
1545 .expect("row")
1546 .sequence_id
1547 });
1548 assert_eq!(
1549 recovered, sequences,
1550 "opening a checkpoint must not rewrite stored row identities"
1551 );
1552 }
1553
1554 #[test]
1555 fn auto_insert_persists_the_assigned_sequence_identity() {
1556 let directory = tempfile::tempdir().expect("temporary store");
1557 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1558 for id in 1u64..=3 {
1559 store
1560 .insert_auto("rows", row(id, id as i64))
1561 .expect("insert");
1562 }
1563 let manager = store.get_collection("rows").expect("rows");
1564 let tree = store
1565 .btree_indices
1566 .read()
1567 .get("rows")
1568 .cloned()
1569 .expect("tree");
1570 for id in 1u64..=3 {
1571 let bytes = tree
1572 .get(&id.to_be_bytes())
1573 .expect("lookup")
1574 .expect("stored row");
1575 let (persisted, _) =
1576 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1577 .expect("decode");
1578 assert_eq!(
1579 persisted.sequence_id,
1580 manager
1581 .get(EntityId::new(id))
1582 .expect("live row")
1583 .sequence_id
1584 );
1585 }
1586 }
1587
1588 #[test]
1589 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
1590 let directory = tempfile::tempdir().expect("temporary store");
1591 let path = directory.path().join("data.rdb");
1592 let store = UnifiedStore::open(&path).expect("paged store");
1593 store.insert_auto("rows", row(1, 1)).expect("seed");
1594 store.persist().expect("checkpoint");
1595 store.insert_auto("rows", row(2, 2)).expect("append");
1596 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1597 store.persist().expect("inline append checkpoint");
1598 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1599 store
1600 .get_collection("rows")
1601 .expect("rows")
1602 .update(row(1, 99))
1603 .expect("untracked edit");
1604 store.insert_auto("rows", row(3, 3)).expect("next append");
1605 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1606 store.persist().expect("untracked edit checkpoint");
1607 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
1608 drop(store);
1609 let reopened = UnifiedStore::open(&path).expect("reopen");
1610 assert_eq!(
1611 reopened
1612 .get("rows", EntityId::new(1))
1613 .expect("row")
1614 .data
1615 .as_row()
1616 .expect("table")
1617 .columns,
1618 vec![Value::Integer(99)]
1619 );
1620 }
1621
1622 #[test]
1623 fn replayed_trees_are_reusable_after_durable_reconciliation() {
1624 let directory = tempfile::tempdir().expect("temporary store");
1625 let path = directory.path().join("data.rdb");
1626 let store = UnifiedStore::open(&path).expect("paged store");
1627 store.insert_auto("rows", row(1, 1)).expect("seed");
1628 store.persist().expect("checkpoint");
1629 store
1630 .insert_auto("rows", row(2, 2))
1631 .expect("WAL-backed append");
1632 drop(store);
1633 let reopened = UnifiedStore::open(&path).expect("replay");
1634 assert!(reopened.get("rows", EntityId::new(2)).is_some());
1635 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1636 reopened.persist().expect("checkpoint reconciled tree");
1637 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1638 }
1639
1640 fn row(id: u64, value: i64) -> UnifiedEntity {
1641 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
1642 }
1643
1644 #[test]
1645 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
1646 let directory = tempfile::tempdir().expect("temporary store");
1647 let path = directory.path().join("data.rdb");
1648 let store = UnifiedStore::open(&path).expect("paged store");
1649 store.create_collection("rows").expect("collection");
1650 store.insert("rows", row(1, 1)).expect("seed");
1651 store.persist().expect("initial checkpoint");
1652 let manager = store.get_collection("rows").expect("rows");
1653 manager.update(row(1, 2)).expect("direct update");
1654 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1655 store.persist().expect("updated row checkpoint");
1656 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1657 let mut metadata = Metadata::new();
1658 metadata.set("verified", MetadataValue::Bool(true));
1659 manager
1660 .set_metadata(EntityId::new(1), metadata)
1661 .expect("metadata");
1662 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1663 store.persist().expect("metadata-only checkpoint");
1664 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1665 manager
1666 .bulk_insert(vec![row(2, 2), row(3, 3)])
1667 .expect("bulk insert");
1668 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1669 store.persist().expect("bulk insert checkpoint");
1670 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
1671 manager.delete(EntityId::new(2)).expect("delete");
1672 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1673 store.persist().expect("changed checkpoint");
1674 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
1675 drop(store);
1676 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
1677 let manager = recovered.get_collection("rows").expect("rows");
1678 assert_eq!(manager.count(), 2);
1679 assert_eq!(
1680 manager
1681 .get(EntityId::new(1))
1682 .expect("row")
1683 .data
1684 .as_row()
1685 .expect("table")
1686 .columns,
1687 vec![Value::Integer(2)]
1688 );
1689 assert!(manager.get(EntityId::new(2)).is_none());
1690 assert!(manager
1691 .get_metadata(EntityId::new(1))
1692 .expect("metadata")
1693 .get("verified")
1694 .is_some());
1695 }
1696
1697 #[test]
1698 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
1699 let directory = tempfile::tempdir().expect("temporary store");
1700 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1701 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1702 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1703 store.create_collection("rows").expect("collection");
1704 for id in 1..=40 {
1705 store.insert("rows", row(id, id as i64)).expect("seed");
1706 }
1707 store.persist().expect("initial checkpoint");
1708 let unrelated = snapshots.begin();
1709 snapshots.rollback(unrelated);
1710 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1711 store.persist().expect("unrelated rollback checkpoint");
1712 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1713
1714 store
1715 .pending_vault_versions
1716 .write()
1717 .insert(EntityId::new(100));
1718 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1719 store.persist().expect("unrelated vault checkpoint");
1720 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1721 store.pending_vault_versions.write().clear();
1722 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1723 store
1724 .persist()
1725 .expect("unrelated vault publication checkpoint");
1726 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1727 }
1728
1729 #[test]
1730 fn rollback_invalidates_reuse_without_a_segment_mutation() {
1731 let directory = tempfile::tempdir().expect("temporary store");
1732 let path = directory.path().join("data.rdb");
1733 let store = UnifiedStore::open(&path).expect("paged store");
1734 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1735 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1736 store.create_collection("rows").expect("collection");
1737 let xid = snapshots.begin();
1738 let mut pending = row(1, 1);
1739 pending.xmin = xid;
1740 store.insert("rows", pending).expect("version");
1741 store.persist().expect("before rollback");
1742 snapshots.rollback(xid);
1743 store.persist().expect("after rollback");
1744 drop(store);
1745 let recovered = UnifiedStore::open(&path).expect("recover");
1746 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1747 }
1748
1749 #[test]
1750 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
1751 let directory = tempfile::tempdir().expect("temporary store");
1752 let path = directory.path().join("data.rdb");
1753 let store = UnifiedStore::open(&path).expect("paged store");
1754 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1755 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1756 store.create_collection("rows").expect("collection");
1757 let xid = snapshots.begin();
1758 let mut deleted = row(1, 1);
1759 deleted.xmax = xid;
1760 store.insert("rows", deleted).expect("version");
1761 store.persist().expect("before rollback");
1762 snapshots.rollback(xid);
1763 store.persist().expect("after rollback");
1764 drop(store);
1765 let recovered = UnifiedStore::open(&path).expect("recover");
1766 let manager = recovered.get_collection("rows").expect("rows");
1767 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
1768 }
1769
1770 #[test]
1771 fn publishing_a_pending_vault_version_invalidates_reuse() {
1772 let directory = tempfile::tempdir().expect("temporary store");
1773 let path = directory.path().join("data.rdb");
1774 let store = UnifiedStore::open(&path).expect("paged store");
1775 store.create_collection("rows").expect("collection");
1776 store.insert("rows", row(1, 1)).expect("row");
1777 store
1778 .pending_vault_versions
1779 .write()
1780 .insert(EntityId::new(1));
1781 store.persist().expect("exclude pending vault version");
1782 store
1783 .pending_vault_versions
1784 .write()
1785 .remove(&EntityId::new(1));
1786 store.persist().expect("include published vault version");
1787 drop(store);
1788 let recovered = UnifiedStore::open(&path).expect("recover");
1789 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
1790 }
1791}