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