1use super::*;
2use parking_lot::RwLock;
3
4const CHECKPOINT_BATCH_BYTES: usize = 1024 * 1024;
5const CHECKPOINT_BATCH_ROWS: usize = 512;
6
7struct CheckpointTreeBuilder {
10 tree: Arc<BTree>,
11 records: Vec<(Vec<u8>, Vec<u8>)>,
12 bytes: usize,
13}
14
15impl CheckpointTreeBuilder {
16 fn new(pager: &Arc<Pager>) -> Self {
17 Self {
18 tree: Arc::new(BTree::new(Arc::clone(pager))),
19 records: Vec::new(),
20 bytes: 0,
21 }
22 }
23
24 fn push(&mut self, record: (Vec<u8>, Vec<u8>)) -> Result<(), StoreError> {
25 let bytes = record.0.len() + record.1.len();
26 if !self.records.is_empty()
27 && (self.bytes.saturating_add(bytes) > CHECKPOINT_BATCH_BYTES
28 || self.records.len() >= CHECKPOINT_BATCH_ROWS)
29 {
30 self.flush()?;
31 }
32 self.bytes += bytes;
33 self.records.push(record);
34 #[cfg(test)]
35 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(peak.get().max(self.bytes)));
36 Ok(())
37 }
38
39 fn flush(&mut self) -> Result<(), StoreError> {
40 self.tree
41 .bulk_insert_sorted(&self.records)
42 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
43 self.records.clear();
44 self.bytes = 0;
45 Ok(())
46 }
47
48 fn copy_prefix(&mut self, previous: &BTree, count: usize) -> Result<(), StoreError> {
49 let mut cursor = previous
50 .cursor_first()
51 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
52 for _ in 0..count {
53 let record = cursor
54 .next()
55 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
56 .ok_or_else(|| StoreError::Serialization("checkpoint prefix disappeared".into()))?;
57 self.push(record)?;
58 }
59 Ok(())
60 }
61}
62
63#[cfg(test)]
64thread_local! {
65 static CHECKPOINT_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
66 static CHECKPOINT_BUFFERED_BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
67}
68
69fn checkpoint_visibility_matches(
70 previous: &CheckpointVersion,
71 current: &CheckpointVersion,
72) -> bool {
73 (previous.visibility_revision == current.visibility_revision
74 || previous.excluded_xids == current.excluded_xids)
75 && previous.pending_vault == current.pending_vault
76}
77
78const META_PAGE_CONTENT_CAP: usize =
97 crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
98const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
99const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
100const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
101const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
102
103fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
104 let cs = crate::storage::engine::HEADER_SIZE;
105 let page = pager.read_page(1)?;
106 let bytes = page.as_bytes();
107 if bytes.len() < cs + META_V3_PAGE1_HEADER {
108 return Ok(Vec::new());
109 }
110 let Some(header) =
111 reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
112 PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
113 })?
114 else {
115 return Ok(Vec::new());
116 };
117 let mut next = header.next_overflow_page_id;
118 let mut pages = Vec::new();
119 let mut seen = std::collections::HashSet::new();
120 while next != 0 {
121 if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
122 return Err(PagerError::InvalidDatabase(
123 "cyclic metadata overflow chain".into(),
124 ));
125 }
126 let ov = pager.read_page(next)?;
127 if ov.page_type()? != crate::storage::engine::PageType::Overflow {
128 return Err(PagerError::InvalidDatabase(
129 "invalid metadata overflow page type".into(),
130 ));
131 }
132 let ob = ov.as_bytes();
133 let nn = reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
134 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?
135 .next_overflow_page_id;
136 pages.push(next);
137 next = nn;
138 }
139 Ok(pages)
140}
141
142fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
143 let retired = existing_overflow_chain(pager)?;
144 if !retired.is_empty() {
145 pager.record_retirement_intent(&retired)?;
146 }
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 let mut seen = std::collections::HashSet::new();
306 while next != 0 && payload.len() < total {
307 if !seen.insert(next)
308 || seen.len()
309 >= pager
310 .page_count()
311 .map_err(|err| page_integrity(next, None, err.to_string()))?
312 as usize
313 {
314 return Err(page_integrity(next, None, "cyclic metadata overflow chain"));
315 }
316 let ov = pager
317 .read_page(next)
318 .map_err(|err| page_integrity(next, None, err.to_string()))?;
319 if ov
320 .page_type()
321 .map_err(|err| page_integrity(next, None, err.to_string()))?
322 != crate::storage::engine::PageType::Overflow
323 {
324 return Err(page_integrity(
325 next,
326 None,
327 "invalid metadata overflow page type",
328 ));
329 }
330 let ob = ov.as_bytes();
331 if ob.len() < cs + META_V3_OVERFLOW_HEADER {
332 return Err(page_integrity(
333 next,
334 None,
335 "metadata overflow continuation is truncated",
336 ));
337 }
338 let continuation =
339 reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
340 .map_err(|err| page_integrity(next, None, err.to_string()))?;
341 let nn = continuation.next_overflow_page_id;
342 let len = continuation.chunk_bytes as usize;
343 let remaining = total - payload.len();
344 let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
345 payload.extend_from_slice(
346 &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
347 );
348 next = nn;
349 }
350 if payload.len() != total {
351 return Err(page_integrity(
352 1,
353 None,
354 "metadata overflow chain is incomplete",
355 ));
356 }
357 Ok(Some(payload))
358}
359
360impl UnifiedStore {
361 fn checkpoint_tree(
365 &self,
366 pager: &Arc<Pager>,
367 manager: &SegmentManager,
368 previous: Option<&Arc<BTree>>,
369 mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
370 ) -> Result<Arc<BTree>, StoreError> {
371 let mut cursor = previous
372 .map(|tree| tree.cursor_first())
373 .transpose()
374 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
375 let mut matched = 0;
376 let mut builder = None;
377 let mut failure = None;
378 manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
379 let Some(value) = encode(entity, metadata) else {
380 return true;
381 };
382 let record = (entity.id.raw().to_be_bytes().to_vec(), value);
383 let result = (|| {
384 if builder.is_none() {
385 if let Some(cursor) = cursor.as_mut() {
386 if cursor
387 .next()
388 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
389 .as_ref()
390 == Some(&record)
391 {
392 matched += 1;
393 return Ok(());
394 }
395 }
396 let mut replacement = CheckpointTreeBuilder::new(pager);
397 if let Some(previous) = previous {
398 replacement.copy_prefix(previous, matched)?;
399 }
400 builder = Some(replacement);
401 }
402 builder
403 .as_mut()
404 .expect("replacement initialized")
405 .push(record)
406 })();
407 if let Err(error) = result {
408 failure = Some(error);
409 return false;
410 }
411 true
412 });
413 if let Some(error) = failure {
414 return Err(error);
415 }
416 if builder.is_none() {
417 if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
418 if cursor
419 .next()
420 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
421 .is_none()
422 {
423 return Ok(Arc::clone(previous));
424 }
425 }
426 let mut replacement = CheckpointTreeBuilder::new(pager);
427 if let Some(previous) = previous {
428 replacement.copy_prefix(previous, matched)?;
429 }
430 builder = Some(replacement);
431 }
432 let mut builder = builder.expect("replacement or reused tree");
433 builder.flush()?;
434 Ok(builder.tree)
435 }
436
437 fn publish_reclaimable_trees(
441 &self,
442 pager: &Pager,
443 metadata: &mut Vec<u8>,
444 live: &HashMap<String, Arc<BTree>>,
445 ) -> Result<(), StoreError> {
446 let mut retired = self.retired_btrees.lock();
447 let mut roots = Vec::new();
448 let mut reclaim = Vec::new();
449 let mut remove = std::collections::HashSet::new();
450 for (index, entry) in retired.iter().enumerate() {
451 if live.values().any(|tree| Arc::ptr_eq(tree, &entry.tree)) {
452 remove.insert(index);
453 continue;
454 }
455 if entry.published
456 && Arc::strong_count(&entry.tree) == 1
457 && !entry.tree.has_retirement_readers()
458 {
459 #[cfg(test)]
463 if std::env::var_os("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT").is_some() {
464 roots.push(entry.tree.root_page_id());
465 }
466 reclaim.extend(
467 entry
468 .tree
469 .retired_pages()
470 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
471 );
472 remove.insert(index);
473 } else if entry.tree.root_page_id() != 0 {
474 roots.push(entry.tree.root_page_id());
475 }
476 }
477 roots.sort_unstable();
478 roots.dedup();
479 reddb_file::encode_native_paged_retired_trees(metadata, &roots)
480 .map_err(|e| StoreError::Serialization(e.to_string()))?;
481 let (pending_pages, published_pages) = pager
482 .deferred_reclamation_pages()
483 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
484 let readers = live.values().any(|tree| tree.has_retirement_readers())
485 || retired
486 .iter()
487 .any(|entry| entry.tree.has_retirement_readers());
488 let released_pages = if readers { Vec::new() } else { published_pages };
489 let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
490 let retained_pages: Vec<_> = pending_pages
491 .into_iter()
492 .filter(|id| !released.contains(id))
493 .collect();
494 reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
495 .map_err(|e| StoreError::Serialization(e.to_string()))?;
496 let active_epoch = self.ownership_active_epoch.load(Ordering::Acquire);
497 let catalog_epoch = if active_epoch == 0 {
498 self.ownership_catalog_epoch.load(Ordering::Acquire)
499 } else {
500 active_epoch
501 };
502 if catalog_epoch != 0 {
503 reddb_file::native_store::encode_native_paged_ownership_epoch(metadata, catalog_epoch)
504 .map_err(|e| StoreError::Serialization(e.to_string()))?;
505 }
506 let mut retirement_intent = reclaim.clone();
509 retirement_intent.extend_from_slice(&released_pages);
510 if !retirement_intent.is_empty() {
511 pager
512 .record_retirement_intent(&retirement_intent)
513 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
514 }
515 publish_paged_metadata(pager, metadata)
518 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
519 self.ownership_catalog_epoch
520 .store(catalog_epoch, Ordering::Release);
521 #[cfg(test)]
522 if !reclaim.is_empty()
523 && std::env::var_os("REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE").is_some()
524 {
525 eprintln!(
526 "experimental exit before retired free pages={}",
527 reclaim.len()
528 );
529 std::process::exit(184);
530 }
531 let mut index = 0;
532 retired.retain_mut(|entry| {
533 let keep = !remove.contains(&index);
534 entry.published = true;
535 index += 1;
536 keep
537 });
538 if !reclaim.is_empty() {
539 pager
540 .free_retired_pages(&reclaim)
541 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
542 #[cfg(test)]
543 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE").is_some() {
544 eprintln!(
545 "experimental exit after retired enqueue pages={}",
546 reclaim.len()
547 );
548 std::process::exit(186);
549 }
550 pager
551 .sync()
552 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
553 #[cfg(test)]
554 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE").is_some() {
555 eprintln!(
556 "experimental exit after retired free pages={}",
557 reclaim.len()
558 );
559 std::process::exit(185);
560 }
561 }
562 pager
563 .complete_deferred_publication(&retained_pages, &released_pages)
564 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
565 if !released_pages.is_empty() {
566 pager
567 .sync()
568 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
569 }
570 Ok(())
571 }
572
573 pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
574 let mut retired = self.retired_btrees.lock();
575 if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
576 {
577 retired.push(RetiredBTree {
578 tree,
579 published: false,
580 });
581 }
582 }
583
584 pub(crate) fn note_paged_insert(
588 &self,
589 collection: &str,
590 manager: &Arc<SegmentManager>,
591 before: &[(u64, u64)],
592 id: EntityId,
593 ) {
594 let after = manager.checkpoint_revisions();
595 if before.len() != after.len() {
596 return;
597 }
598 let mut changed = 0;
599 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
600 if old_id != new_id {
601 return;
602 }
603 if old_revision != new_revision {
604 if old_revision.checked_add(1) != Some(*new_revision) {
605 return;
606 }
607 changed += 1;
608 }
609 }
610 if changed != 1 {
611 return;
612 }
613 let mut versions = self.checkpoint_versions.write();
614 let Some(previous) = versions.get_mut(collection) else {
615 return;
616 };
617 if previous.manager.ptr_eq(&Arc::downgrade(manager))
618 && previous.segments == before
619 && previous.format_version == self.format_version()
620 && !previous.pending_vault.contains(&id)
621 && !previous.excluded_xids.contains(&0)
622 {
623 previous.segments = after;
624 }
625 }
626
627 pub(crate) fn note_paged_delete(
631 &self,
632 collection: &str,
633 manager: &Arc<SegmentManager>,
634 before: &[(u64, u64)],
635 deleted: &[EntityId],
636 ) {
637 let after = manager.checkpoint_revisions();
638 if before.len() != after.len() {
639 return;
640 }
641 let mut changes = 0_u64;
642 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
643 if old_id != new_id {
644 return;
645 }
646 let Some(delta) = new_revision.checked_sub(*old_revision) else {
647 return;
648 };
649 let Some(total) = changes.checked_add(delta) else {
650 return;
651 };
652 changes = total;
653 }
654 let Ok(expected) = u64::try_from(deleted.len()) else {
655 return;
656 };
657 if changes != expected {
658 return;
659 }
660 let mut versions = self.checkpoint_versions.write();
661 let Some(previous) = versions.get_mut(collection) else {
662 return;
663 };
664 if !previous.manager.ptr_eq(&Arc::downgrade(manager))
665 || previous.format_version != self.format_version()
666 || deleted.iter().any(|id| previous.pending_vault.contains(id))
667 || previous.excluded_xids.contains(&0)
668 {
669 return;
670 }
671 if previous.segments != before {
672 let Some(deferred) = previous.deferred_rows.as_ref() else {
673 return;
674 };
675 let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
676 return;
677 };
678 let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
679 let pending: std::collections::BTreeSet<_> = deferred
680 .iter()
681 .copied()
682 .chain(changed)
683 .filter(|id| !removed.contains(id))
684 .collect();
685 if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
686 return;
687 }
688 previous.has_unprojected_rows = !pending.is_empty();
693 previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
694 }
695 previous.segments = after;
696 }
697
698 pub(crate) fn mark_paged_registry_dirty(&self) {
699 self.resident.mark_paged_registry_dirty()
700 }
701
702 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
708 let pager = self.pager.as_ref()?;
709 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
710 return Some(btree);
711 }
712 let mut write = self.btree_indices.write();
713 let btree = write
714 .entry(collection.to_string())
715 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
716 .clone();
717 Some(btree)
718 }
719
720 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
721 let Some(pager) = &self.pager else {
722 return Ok(());
723 };
724
725 if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
726 self.flush_paged_registry()?;
727 self.resident
728 .paged_registry_dirty
729 .store(false, Ordering::Release);
730 return Ok(());
731 }
732
733 pager
734 .flush()
735 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
736 }
737
738 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
739 let _vault_guard = self.vault_publication_lock.lock();
740 let Some(pager) = &self.pager else {
741 return Ok(());
742 };
743
744 match pager.read_page(1) {
745 Ok(_) => {}
746 Err(PagerError::PageNotFound(_)) => {
747 let meta_page = pager
748 .allocate_page(crate::storage::engine::PageType::Header)
749 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
750 pager
751 .write_page(meta_page.page_id(), meta_page)
752 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
753 }
754 Err(e) => {
755 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
756 }
757 }
758
759 let format_version = STORE_VERSION_V11;
760 self.set_format_version(format_version);
761
762 let collections = self.resident.collections.read();
763 let btree_indices = self.btree_indices.read();
764 let mut collection_roots = Vec::with_capacity(collections.len());
765 for (name, _) in collections.iter() {
766 let root_page = btree_indices
767 .get(name)
768 .map_or(0, |btree| btree.root_page_id());
769 collection_roots.push((name.clone(), root_page));
770 }
771 drop(btree_indices);
772 drop(collections);
773
774 let mut meta_data = Vec::with_capacity(4096);
775 reddb_file::encode_native_paged_metadata_header(
776 &mut meta_data,
777 reddb_file::NativePagedMetadataHeader {
778 format_version,
779 collection_count: collection_roots.len() as u32,
780 },
781 );
782 for (name, root_page) in &collection_roots {
783 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
784 }
785
786 let cross_refs = self.resident.cross_refs.read();
787 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
788 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
789 for (source_id, refs) in cross_refs.iter() {
790 for (target_id, ref_type, collection) in refs {
791 reddb_file::encode_native_paged_cross_ref(
792 &mut meta_data,
793 source_id.raw(),
794 target_id.raw(),
795 ref_type.to_byte(),
796 collection,
797 );
798 }
799 }
800
801 self.publish_reclaimable_trees(pager, &mut meta_data, &self.btree_indices.read())?;
802
803 Ok(())
804 }
805
806 pub fn pager(&self) -> Option<&Arc<Pager>> {
808 self.pager.as_ref()
809 }
810
811 pub fn page_cache_bytes_in_use(&self) -> u64 {
815 self.pager.as_ref().map_or(0, |pager| {
816 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
817 })
818 }
819
820 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
823 self.commit
824 .as_ref()
825 .map_or(0, |commit| commit.buffered_bytes())
826 }
827
828 pub fn segment_memory_bytes(&self) -> u64 {
832 let managers: Vec<Arc<SegmentManager>> = self
833 .resident
834 .collections
835 .read()
836 .values()
837 .map(Arc::clone)
838 .collect();
839
840 managers
841 .iter()
842 .map(|manager| manager.resident_bytes())
843 .fold(0, u64::saturating_add)
844 }
845
846 pub fn config(&self) -> &UnifiedStoreConfig {
850 &self.config
851 }
852
853 pub fn with_config(config: UnifiedStoreConfig) -> Self {
854 Self {
855 config,
856 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
857 format_version: AtomicU32::new(STORE_VERSION_V11),
858 pager: None,
859 db_path: None,
860 btree_indices: RwLock::new(HashMap::new()),
861 checkpoint_versions: RwLock::new(HashMap::new()),
862 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
863 retired_btrees: parking_lot::Mutex::new(Vec::new()),
864 ownership_catalog_epoch: AtomicU64::new(0),
865 ownership_active_epoch: AtomicU64::new(0),
866 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
867 commit: None,
868 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
869 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
870 snapshot_manager: std::sync::OnceLock::new(),
871 embedded_commit_session: std::sync::OnceLock::new(),
872 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
873 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
874 }
875 }
876
877 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
895 Self::open_with_config(path, UnifiedStoreConfig::default())
896 }
897
898 pub fn open_with_config(
899 path: impl AsRef<Path>,
900 config: UnifiedStoreConfig,
901 ) -> Result<Self, StoreError> {
902 let path = path.as_ref();
903 let mut pager_config = PagerConfig::default();
904 if let Some(slots) = config.page_cache_slots {
908 pager_config.cache_size = slots;
909 }
910 if matches!(
918 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
919 Some("0") | Some("false") | Some("off")
920 ) {
921 pager_config.double_write = false;
922 }
923 let pager = Pager::open(path, pager_config)
924 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
925 pager
926 .enable_deferred_reclamation()
927 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
928
929 let wal_path = reddb_file::layout::unified_wal_path(path);
930 let commit = Some(Arc::new(
933 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
934 .map_err(StoreError::Io)?,
935 ));
936
937 let store = Self {
938 config,
939 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
940 format_version: AtomicU32::new(STORE_VERSION_V11),
941 pager: Some(Arc::new(pager)),
942 db_path: Some(path.to_path_buf()),
943 btree_indices: RwLock::new(HashMap::new()),
944 checkpoint_versions: RwLock::new(HashMap::new()),
945 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
946 retired_btrees: parking_lot::Mutex::new(Vec::new()),
947 ownership_catalog_epoch: AtomicU64::new(0),
948 ownership_active_epoch: AtomicU64::new(0),
949 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
950 commit,
951 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
952 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
953 snapshot_manager: std::sync::OnceLock::new(),
954 embedded_commit_session: std::sync::OnceLock::new(),
955 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
956 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
957 };
958
959 if let Some(commit) = &store.commit {
962 commit
963 .restore_checkpoint_page_images(&store)
964 .map_err(StoreError::Io)?;
965 }
966 store.load_from_pages()?;
968 if let Some(commit) = &store.commit {
969 commit.replay_into(&store).map_err(StoreError::Io)?;
970 }
971 store.recover_operational_manifest()?;
972
973 Ok(store)
974 }
975
976 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
980 let Some(pager) = &self.pager else {
981 return Ok(());
982 };
983 let collections = self.resident.collections.read();
984 let mut indices = self.btree_indices.write();
985 for (name, manager) in collections.iter() {
986 let tree =
987 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
988 Some(Self::serialize_entity_record(
989 entity,
990 metadata.as_ref(),
991 self.format_version(),
992 ))
993 })?;
994 if indices
995 .get(name)
996 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
997 {
998 continue;
999 }
1000 if let Some(previous) = indices.insert(name.clone(), tree) {
1001 self.retire_btree(previous);
1002 }
1003 self.mark_paged_registry_dirty();
1004 #[cfg(test)]
1005 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1006 if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1007 pager.sync().map_err(|error| {
1008 StoreError::Io(std::io::Error::other(error.to_string()))
1009 })?;
1010 }
1011 eprintln!("experimental replay rebuild exit before manifest");
1012 std::process::exit(182);
1013 }
1014 }
1015 drop(indices);
1016 drop(collections);
1017 self.flush_paged_state()?;
1018 #[cfg(test)]
1019 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1020 eprintln!("experimental replay exit after manifest");
1021 std::process::exit(182);
1022 }
1023 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1026 let collections = self.resident.collections.read();
1027 let mut versions = self.checkpoint_versions.write();
1028 for (name, manager) in collections.iter() {
1029 versions.insert(
1030 name.clone(),
1031 CheckpointVersion {
1032 manager: Arc::downgrade(manager),
1033 segments: manager.checkpoint_revisions(),
1034 visibility_revision: 0,
1035 untracked_visibility_revision: 0,
1036 excluded_xids: Arc::default(),
1037 pending_vault: Arc::default(),
1038 format_version: self.format_version(),
1039 deferred_rows: Some(Arc::default()),
1040 has_unprojected_rows: false,
1041 },
1042 );
1043 }
1044 }
1045 Ok(())
1046 }
1047
1048 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
1049 let Some(path) = &self.db_path else {
1050 return Ok(());
1051 };
1052 let mut collections = self.list_collections();
1053 collections.sort();
1054 let pending_drops =
1055 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1056 .recover_or_bootstrap(&collections)
1057 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1058 for name in pending_drops {
1059 if self.get_collection(&name).is_some() {
1060 self.drop_collection(&name)?;
1061 }
1062 }
1063 Ok(())
1064 }
1065
1066 pub(crate) fn prepare_operational_collection_create(
1067 &self,
1068 name: &str,
1069 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
1070 let Some(path) = &self.db_path else {
1071 return Ok(None);
1072 };
1073 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1074 .prepare_collection_create(name)
1075 .map(Some)
1076 .map_err(StoreError::Io)
1077 }
1078
1079 pub(crate) fn prepare_operational_collection_drop(
1080 &self,
1081 name: &str,
1082 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
1083 let Some(path) = &self.db_path else {
1084 return Ok(None);
1085 };
1086 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1087 .prepare_collection_drop(name)
1088 .map(Some)
1089 .map_err(StoreError::Io)
1090 }
1091
1092 pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1094 let Some(pager) = &self.pager else {
1095 return Ok(0);
1096 };
1097 let Some(content) = read_meta_payload(pager)? else {
1098 return Ok(0);
1099 };
1100 reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1101 .map(|epoch| epoch.unwrap_or(0))
1102 .map_err(|error| StoreError::Serialization(error.to_string()))
1103 }
1104
1105 fn load_from_pages(&self) -> Result<(), StoreError> {
1107 let pager = match &self.pager {
1108 Some(p) => p,
1109 None => return Ok(()), };
1111
1112 let page_count = pager.page_count().map_err(|e| {
1114 StoreError::Io(std::io::Error::other(format!(
1115 "failed to read page count: {}",
1116 e
1117 )))
1118 })?;
1119 if page_count <= 1 {
1120 return Ok(());
1122 }
1123
1124 if let Some(content_vec) = read_meta_payload(pager)? {
1129 let content: &[u8] = &content_vec;
1130 if content.len() >= 4 {
1131 let mut pos = 0;
1132 let mut format_version = STORE_VERSION_V1;
1133
1134 let collection_count = if let Some(header) =
1135 reddb_file::decode_native_paged_metadata_header(content)
1136 .map_err(|err| StoreError::Serialization(err.to_string()))?
1137 {
1138 format_version = header.format_version;
1139 pos += reddb_file::METADATA_HEADER_BYTES;
1140 header.collection_count as usize
1141 } else {
1142 let count = u32::from_le_bytes([
1143 content[pos],
1144 content[pos + 1],
1145 content[pos + 2],
1146 content[pos + 3],
1147 ]) as usize;
1148 pos += 4;
1149 count
1150 };
1151
1152 self.set_format_version(format_version);
1153
1154 if pos > content.len() {
1155 return Ok(());
1156 }
1157
1158 for _ in 0..collection_count {
1160 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1161 .map_err(|err| page_integrity(1, None, err.to_string()))?;
1162 let root_page = root.root_page;
1164 let name = root.collection;
1165
1166 let _ = self.create_collection_in_memory(&name);
1170
1171 if root_page > 0 {
1173 let btree = BTree::with_root(Arc::clone(pager), root_page);
1174
1175 let mut cursor = btree.cursor_first().map_err(|err| {
1177 page_integrity(root_page, Some(name.clone()), err.to_string())
1178 })?;
1179 let manager = self.get_collection(&name);
1180 loop {
1181 let next = cursor.next().map_err(|err| {
1182 page_integrity(root_page, Some(name.clone()), err.to_string())
1183 })?;
1184 let Some((key, value)) = next else {
1185 break;
1186 };
1187 let (entity, metadata) =
1189 Self::deserialize_entity_record(&value, self.format_version())
1190 .map_err(|err| {
1191 page_integrity(
1192 root_page,
1193 Some(name.clone()),
1194 err.to_string(),
1195 )
1196 })?;
1197 if let Some(m) = &manager {
1198 let id = entity.id;
1199 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1200 m.register_row_id(*row_id);
1201 }
1202 self.resident.context_index.index_entity(&name, &entity);
1203 m.restore_snapshot_entity(entity.clone())?;
1204 if let Some(metadata) = metadata {
1205 let _ = m.set_metadata(id, metadata);
1206 }
1207 self.register_entity_id(id);
1208 if self.config.auto_index_refs {
1209 self.index_cross_refs(&entity, &name)?;
1210 }
1211 }
1212 }
1213
1214 self.btree_indices
1216 .write()
1217 .insert(name.clone(), Arc::new(btree));
1218 if format_version == STORE_VERSION_V11 {
1219 if let Some(manager) = &manager {
1220 self.checkpoint_versions.write().insert(
1221 name,
1222 CheckpointVersion {
1223 manager: Arc::downgrade(manager),
1224 segments: manager.checkpoint_revisions(),
1225 visibility_revision: 0,
1226 untracked_visibility_revision: 0,
1227 excluded_xids: Arc::default(),
1228 pending_vault: Arc::default(),
1229 format_version,
1230 deferred_rows: Some(Arc::default()),
1231 has_unprojected_rows: false,
1232 },
1233 );
1234 }
1235 }
1236 }
1237 }
1238
1239 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1240 let cross_ref_count = u32::from_le_bytes([
1241 content[pos],
1242 content[pos + 1],
1243 content[pos + 2],
1244 content[pos + 3],
1245 ]) as usize;
1246 pos += 4;
1247
1248 for _ in 0..cross_ref_count {
1249 let Ok(cross_ref) =
1250 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1251 else {
1252 break;
1253 };
1254 let source_id = EntityId::new(cross_ref.source_id);
1255 let target_id = EntityId::new(cross_ref.target_id);
1256 let ref_type = RefType::from_byte(cross_ref.ref_type);
1257 let target_collection = cross_ref.target_collection;
1258
1259 self.resident
1260 .cross_refs
1261 .write()
1262 .entry(source_id)
1263 .or_default()
1264 .push((target_id, ref_type, target_collection.clone()));
1265
1266 if let Some((collection, mut entity)) = self.get_any(source_id) {
1267 let exists = entity.cross_refs().iter().any(|xref| {
1268 xref.target == target_id
1269 && xref.ref_type == ref_type
1270 && xref.target_collection == target_collection
1271 });
1272 if !exists {
1273 entity.cross_refs_mut().push(CrossRef::new(
1274 source_id,
1275 target_id,
1276 target_collection.clone(),
1277 ref_type,
1278 ));
1279 if let Some(manager) = self.get_collection(&collection) {
1280 let _ = manager.update(entity);
1281 }
1282 }
1283 }
1284 }
1285 }
1286 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1287 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1288 let live_roots: std::collections::HashSet<_> = self
1289 .btree_indices
1290 .read()
1291 .values()
1292 .map(|tree| tree.root_page_id())
1293 .collect();
1294 for root in retired {
1295 if live_roots.contains(&root)
1296 || root
1297 >= pager
1298 .page_count()
1299 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1300 {
1301 return Err(page_integrity(
1302 root,
1303 None,
1304 "retired root aliases a live or missing page",
1305 ));
1306 }
1307 self.retired_btrees.lock().push(RetiredBTree {
1308 tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1309 published: true,
1310 });
1311 }
1312 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1313 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1314 if deferred.iter().any(|id| live_roots.contains(id)) {
1315 return Err(page_integrity(1, None, "deferred page aliases a live root"));
1316 }
1317 pager
1318 .restore_deferred_reclamation(&deferred)
1319 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1320 let ownership_epoch =
1321 reddb_file::native_store::decode_native_paged_ownership_epoch(
1322 content, &mut pos,
1323 )
1324 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1325 self.ownership_catalog_epoch
1326 .store(ownership_epoch.unwrap_or(0), Ordering::Release);
1327 }
1328 }
1329
1330 if self.format_version() < STORE_VERSION_V11 {
1331 self.set_format_version(STORE_VERSION_V11);
1332 }
1333
1334 Ok(())
1335 }
1336
1337 pub(crate) fn deserialize_entity(
1339 data: &[u8],
1340 format_version: u32,
1341 ) -> Result<UnifiedEntity, StoreError> {
1342 let mut pos = 0;
1343 Self::read_entity_binary(data, &mut pos, format_version)
1344 .map_err(|e| StoreError::Serialization(e.to_string()))
1345 }
1346
1347 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1349 let mut buf = Vec::with_capacity(256);
1354 Self::write_entity_binary(&mut buf, entity, format_version);
1355 buf
1356 }
1357
1358 pub(crate) fn serialize_entity_record(
1359 entity: &UnifiedEntity,
1360 metadata: Option<&Metadata>,
1361 format_version: u32,
1362 ) -> Vec<u8> {
1363 reddb_engine::storage::metadata_codec::serialize_entity_record(
1364 entity,
1365 metadata,
1366 format_version,
1367 )
1368 }
1369
1370 pub(crate) fn deserialize_entity_record(
1371 data: &[u8],
1372 format_version: u32,
1373 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1374 reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
1375 .map_err(StoreError::Serialization)
1376 }
1377
1378 pub fn persist(&self) -> Result<(), StoreError> {
1383 let _vault_guard = self.vault_publication_lock.lock();
1384 let pending_vault_versions = self.pending_vault_versions.read();
1385 let pager = match &self.pager {
1386 Some(p) => p,
1387 None => {
1388 if let Some(path) = &self.db_path {
1390 return self
1391 .save_to_file(path)
1392 .map_err(|e| StoreError::Serialization(e.to_string()));
1393 }
1394 return Err(StoreError::Io(std::io::Error::other(
1395 "No pager or path configured for persistence",
1396 )));
1397 }
1398 };
1399 let _ownership_guard = pager.lock_ownership_mutations();
1402
1403 match pager.read_page(1) {
1404 Ok(_) => {}
1405 Err(PagerError::PageNotFound(_)) => {
1406 let meta_page = pager
1407 .allocate_page(crate::storage::engine::PageType::Header)
1408 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1409 pager
1410 .write_page(meta_page.page_id(), meta_page)
1411 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1412 }
1413 Err(e) => {
1414 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1415 }
1416 }
1417
1418 if let Some(commit) = &self.commit {
1419 commit.force_sync().map_err(StoreError::Io)?;
1420 }
1421
1422 struct InterruptedOwnership<'a> {
1423 pager: &'a Pager,
1424 active: bool,
1425 }
1426 impl Drop for InterruptedOwnership<'_> {
1427 fn drop(&mut self) {
1428 if self.active {
1429 self.pager.fence_interrupted_ownership();
1430 }
1431 }
1432 }
1433 let mut ownership = InterruptedOwnership {
1434 pager,
1435 active: self.commit.is_some(),
1436 };
1437 let epoch = if let Some(commit) = &self.commit {
1438 let epoch = commit
1439 .next_page_ownership_epoch(self)
1440 .map_err(StoreError::Io)?;
1441 commit
1442 .begin_page_ownership_epoch(self, epoch)
1443 .map_err(StoreError::Io)?;
1444 Some(epoch)
1445 } else {
1446 None
1447 };
1448
1449 let collections = self.resident.collections.read();
1450 let mut btree_indices = self.btree_indices.write();
1451
1452 let mut collection_roots: Vec<(String, u32)> = Vec::new();
1454
1455 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1458 let previous_versions = self.checkpoint_versions.read();
1459 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1460 let pending_vault = Arc::new(pending_vault_versions.clone());
1461 let (visibility_revision, excluded, untracked_visibility_revision) =
1462 self.snapshot_manager.get().map_or_else(
1463 || (0, Default::default(), 0),
1464 |manager| manager.checkpoint_projection_state(),
1465 );
1466 let excluded = Arc::new(excluded);
1467 for (name, manager) in collections.iter() {
1468 let mut version = CheckpointVersion {
1469 manager: Arc::downgrade(manager),
1470 segments: manager.checkpoint_revisions(),
1471 visibility_revision,
1472 untracked_visibility_revision,
1473 excluded_xids: Arc::clone(&excluded),
1474 pending_vault: Arc::clone(&pending_vault),
1475 format_version: self.format_version(),
1476 deferred_rows: Some(Arc::default()),
1477 has_unprojected_rows: false,
1478 };
1479 if let (Some(previous), Some(tree)) =
1480 (previous_versions.get(name), btree_indices.get(name))
1481 {
1482 if previous.manager.ptr_eq(&version.manager)
1483 && previous.segments == version.segments
1484 && previous.format_version == version.format_version
1485 && !previous.has_unprojected_rows
1486 && checkpoint_visibility_matches(previous, &version)
1487 {
1488 version.deferred_rows = previous.deferred_rows.clone();
1489 collection_roots.push((name.clone(), tree.root_page_id()));
1490 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1491 checkpoint_versions.insert(name.clone(), version);
1492 continue;
1493 }
1494 }
1495 if let (Some(previous), Some(tree)) =
1496 (previous_versions.get(name), btree_indices.get(name))
1497 {
1498 if previous.manager.ptr_eq(&version.manager)
1499 && previous.format_version == version.format_version
1500 {
1501 if let Some(tree) =
1502 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
1503 {
1504 collection_roots.push((name.clone(), tree.root_page_id()));
1505 rebuilt_indices.insert(name.clone(), tree);
1506 checkpoint_versions.insert(name.clone(), version);
1507 continue;
1508 }
1509 }
1510 }
1511 let btree = self.checkpoint_tree(
1512 pager,
1513 manager,
1514 btree_indices.get(name),
1515 |entity, metadata| {
1516 #[cfg(test)]
1517 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1518 let hidden = pending_vault_versions.contains(&entity.id)
1519 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1520 && excluded.contains(&entity.xmin));
1521 let normalize_xmax = matches!(entity.data, EntityData::Row(_))
1522 && excluded.contains(&entity.xmax);
1523 if hidden || normalize_xmax {
1524 version.remember_deferred_row(entity.id);
1525 }
1526 if hidden {
1527 return None;
1528 }
1529 let mut normalized = std::borrow::Cow::Borrowed(entity);
1530 if normalize_xmax {
1531 normalized.to_mut().xmax = 0;
1532 }
1533 Some(Self::serialize_entity_record(
1534 &normalized,
1535 metadata.as_ref(),
1536 self.format_version(),
1537 ))
1538 },
1539 )?;
1540 checkpoint_versions.insert(name.clone(), version);
1541
1542 collection_roots.push((name.clone(), btree.root_page_id()));
1543 rebuilt_indices.insert(name.clone(), btree);
1544 }
1545
1546 let mut meta_data = Vec::with_capacity(4096);
1548
1549 let format_version = STORE_VERSION_V11;
1550 self.set_format_version(format_version);
1551
1552 reddb_file::encode_native_paged_metadata_header(
1553 &mut meta_data,
1554 reddb_file::NativePagedMetadataHeader {
1555 format_version,
1556 collection_count: collection_roots.len() as u32,
1557 },
1558 );
1559
1560 for (name, root_page) in &collection_roots {
1562 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1563 }
1564
1565 let cross_refs = self.resident.cross_refs.read();
1567 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1568 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1569 for (source_id, refs) in cross_refs.iter() {
1570 for (target_id, ref_type, collection) in refs {
1571 reddb_file::encode_native_paged_cross_ref(
1572 &mut meta_data,
1573 source_id.raw(),
1574 target_id.raw(),
1575 ref_type.to_byte(),
1576 collection,
1577 );
1578 }
1579 }
1580
1581 for (name, previous) in btree_indices.iter() {
1582 if !rebuilt_indices
1583 .get(name)
1584 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1585 {
1586 self.retire_btree(Arc::clone(previous));
1587 }
1588 }
1589 #[cfg(test)]
1590 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
1591 pager
1592 .sync()
1593 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1594 eprintln!("experimental checkpoint exit before manifest");
1595 std::process::exit(183);
1596 }
1597 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices)?;
1598
1599 #[cfg(test)]
1600 if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
1601 std::process::exit(195);
1602 }
1603
1604 *btree_indices = rebuilt_indices;
1607 drop(previous_versions);
1608 *self.checkpoint_versions.write() = checkpoint_versions;
1609
1610 if let Some(commit) = &self.commit {
1611 commit
1612 .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
1613 .map_err(StoreError::Io)?;
1614 ownership.active = false;
1615 commit.truncate().map_err(StoreError::Io)?;
1616 }
1617 *self.checkpoint_durable_rows.write() = Default::default();
1618
1619 Ok(())
1620 }
1621
1622 pub fn is_paged(&self) -> bool {
1624 self.pager.is_some()
1625 }
1626
1627 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1630 self.btree_indices
1631 .read()
1632 .get(collection)
1633 .map(|btree| btree.root_page_id())
1634 .filter(|root| *root != 0)
1635 }
1636
1637 pub fn db_path(&self) -> Option<&Path> {
1639 self.db_path.as_deref()
1640 }
1641}
1642
1643#[cfg(test)]
1644mod checkpoint_work_tests {
1645 use super::*;
1646
1647 #[cfg(target_os = "linux")]
1648 #[test]
1649 fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
1650 const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
1651 if let Some(path) = std::env::var_os(CHILD) {
1652 let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
1653 let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
1654 let store = UnifiedStore::open(path).expect("open store");
1655 let initial_count = if multi { 600 } else { 1 };
1656 for id in 1..=initial_count {
1657 store.insert_auto("rows", row(id, 1)).expect("seed");
1658 }
1659 store.persist().expect("baseline");
1660 let manager = store.get_collection("rows").expect("collection");
1661 let ids: Vec<u64> = if multi {
1662 (1..=initial_count).collect()
1663 } else {
1664 vec![if insert { 2 } else { 1 }]
1665 };
1666 let mut actions = Vec::new();
1667 for id in ids {
1668 if insert {
1669 manager.insert(row(id, 9)).expect("insert version");
1670 } else {
1671 manager.update(row(id, 9)).expect("update version");
1672 }
1673 let entity = manager.get(EntityId::new(id)).expect("changed entity");
1674 actions.push(StoreWalAction::upsert_entity(
1675 "rows",
1676 &entity,
1677 None,
1678 store.format_version(),
1679 ));
1680 }
1681 let references = store
1682 .commit
1683 .as_ref()
1684 .expect("coordinator")
1685 .append_checkpoint_actions(&actions)
1686 .expect("durable WAL");
1687 store.remember_checkpoint_rows(actions, references);
1688 store.persist().expect("exit after sparse write");
1689 panic!("inline sparse crash hook was not reached");
1690 }
1691 for (insert, multi) in [(false, false), (true, false), (false, true)] {
1692 let directory = tempfile::tempdir().expect("temporary store");
1693 let path = directory.path().join("data.rdb");
1694 let mut command =
1695 std::process::Command::new(std::env::current_exe().expect("test executable"));
1696 command
1697 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
1698 .env(CHILD, &path)
1699 .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
1700 if insert {
1701 command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
1702 }
1703 if multi {
1704 command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
1705 }
1706 let output = command.output().expect("crash child");
1707 assert_eq!(
1708 output.status.code(),
1709 Some(190),
1710 "insert={insert} multi={multi}: {}",
1711 String::from_utf8_lossy(&output.stderr)
1712 );
1713 let recovered = UnifiedStore::open(&path).expect("recover");
1714 let ids: Vec<u64> = if multi {
1715 (1..=600).collect()
1716 } else {
1717 vec![if insert { 2 } else { 1 }]
1718 };
1719 for id in ids {
1720 assert_eq!(
1721 recovered
1722 .get("rows", EntityId::new(id))
1723 .expect("recovered row")
1724 .data
1725 .as_row()
1726 .expect("row")
1727 .columns,
1728 vec![Value::Integer(9)]
1729 );
1730 }
1731 assert_eq!(
1732 recovered.get_collection("rows").expect("rows").count(),
1733 if multi {
1734 600
1735 } else if insert {
1736 2
1737 } else {
1738 1
1739 }
1740 );
1741 }
1742 }
1743
1744 #[test]
1745 fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
1746 let directory = tempfile::tempdir().expect("temporary store");
1747 let path = directory.path().join("data.rdb");
1748 {
1749 let store = UnifiedStore::open(&path).expect("open store");
1750 store.create_collection("rows").expect("create collection");
1751 store.persist().expect("publish ownership epoch");
1752 assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
1753 store.persist().expect("publish next ownership epoch");
1754 }
1755 let reopened = UnifiedStore::open(&path).expect("reopen store");
1756 assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
1757 }
1758
1759 #[test]
1760 fn metadata_overflow_with_missing_continuation_fails_closed() {
1761 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
1762
1763 let directory = tempfile::tempdir().expect("temporary store");
1764 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
1765 let pager = store.pager.as_ref().unwrap();
1766 let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
1767 let valid_manifest = build_meta_page1_with_overflow(pager, &valid_payload).unwrap();
1768 pager.write_page(1, valid_manifest).unwrap();
1769 assert!(!existing_overflow_chain(pager).unwrap().is_empty());
1770 assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
1771
1772 let missing = pager.page_count().unwrap() + 100;
1773 let mut manifest = Page::new(PageType::Header, 1);
1774 reddb_file::encode_native_metadata_overflow_header(
1775 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1776 reddb_file::NativeMetadataOverflowHeader {
1777 format_version: STORE_VERSION_V11,
1778 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
1779 next_overflow_page_id: missing,
1780 },
1781 )
1782 .unwrap();
1783 pager.write_page(1, manifest).unwrap();
1784
1785 assert!(
1786 existing_overflow_chain(pager).is_err(),
1787 "a missing continuation must not be treated as an empty chain"
1788 );
1789
1790 let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
1791 let mut manifest = Page::new(PageType::Header, 1);
1792 reddb_file::encode_native_metadata_overflow_header(
1793 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1794 reddb_file::NativeMetadataOverflowHeader {
1795 format_version: STORE_VERSION_V11,
1796 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
1797 next_overflow_page_id: wrong_type.page_id(),
1798 },
1799 )
1800 .unwrap();
1801 pager.write_page(1, manifest).unwrap();
1802 assert!(
1803 existing_overflow_chain(pager).is_err(),
1804 "a live page must not be mistaken for metadata overflow"
1805 );
1806 assert!(
1807 read_meta_payload(pager).is_err(),
1808 "a live page must not be read as metadata overflow"
1809 );
1810
1811 let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
1812 let cycle_id = cycle.page_id();
1813 reddb_file::encode_native_metadata_overflow_continuation_header(
1814 &mut cycle.as_bytes_mut()[HEADER_SIZE..],
1815 reddb_file::NativeMetadataOverflowContinuationHeader {
1816 next_overflow_page_id: cycle_id,
1817 chunk_bytes: 1,
1818 },
1819 )
1820 .unwrap();
1821 pager.write_page(cycle_id, cycle).unwrap();
1822 let mut manifest = Page::new(PageType::Header, 1);
1823 reddb_file::encode_native_metadata_overflow_header(
1824 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1825 reddb_file::NativeMetadataOverflowHeader {
1826 format_version: STORE_VERSION_V11,
1827 total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
1828 next_overflow_page_id: cycle_id,
1829 },
1830 )
1831 .unwrap();
1832 pager.write_page(1, manifest).unwrap();
1833 assert!(
1834 read_meta_payload(pager).is_err(),
1835 "cyclic overflow must not be accepted"
1836 );
1837
1838 let mut manifest = Page::new(PageType::Header, 1);
1839 reddb_file::encode_native_metadata_overflow_header(
1840 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1841 reddb_file::NativeMetadataOverflowHeader {
1842 format_version: STORE_VERSION_V11,
1843 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
1844 next_overflow_page_id: 0,
1845 },
1846 )
1847 .unwrap();
1848 pager.write_page(1, manifest).unwrap();
1849 assert!(
1850 read_meta_payload(pager).is_err(),
1851 "a truncated metadata payload must not be accepted"
1852 );
1853 }
1854
1855 #[test]
1856 fn retired_tree_pages_remain_pinned_by_a_cursor() {
1857 let directory = tempfile::tempdir().expect("temporary store");
1858 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1859 for id in 1..=100 {
1860 store.insert_auto("rows", row(id, 1)).expect("seed");
1861 }
1862 store.persist().expect("initial checkpoint");
1863 let tree = store
1864 .btree_indices
1865 .read()
1866 .get("rows")
1867 .cloned()
1868 .expect("tree");
1869 let mut cursor = tree.cursor_first().expect("cursor");
1870 drop(tree);
1871 let manager = store.get_collection("rows").expect("rows");
1872 for value in 2..=6 {
1873 manager.update(row(1, value)).expect("change");
1874 store.persist().expect("checkpoint with old reader");
1875 }
1876 let mut count = 0;
1877 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
1878 let (entity, _) =
1879 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1880 .expect("decode");
1881 assert_eq!(
1882 entity.data.as_row().expect("row").columns,
1883 [Value::Integer(1)]
1884 );
1885 count += 1;
1886 }
1887 assert_eq!(count, 100);
1888 drop(cursor);
1889 store.persist().expect("reclaim after reader release");
1890 assert!(store.retired_btrees.lock().is_empty());
1891 }
1892
1893 #[test]
1894 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
1895 let directory = tempfile::tempdir().expect("temporary store");
1896 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1897 store.create_collection("rows").expect("collection");
1898 let manager = store.get_collection("rows").expect("rows");
1899 for id in (1..=1200).rev() {
1900 manager
1901 .insert(UnifiedEntity::table_row(
1902 EntityId::new(id),
1903 "rows",
1904 id,
1905 vec![Value::text("x".repeat(4096))],
1906 ))
1907 .expect("seed");
1908 }
1909 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
1910 store.persist().expect("bounded checkpoint");
1911 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
1912 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
1913 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
1914 }
1915
1916 #[test]
1917 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1918 let directory = tempfile::tempdir().expect("temporary store");
1919 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1920 store.create_collection("rows").expect("collection");
1921 for id in 1..=40 {
1922 store
1923 .insert(
1924 "rows",
1925 UnifiedEntity::table_row(
1926 EntityId::new(id),
1927 "rows",
1928 id,
1929 vec![Value::Integer(id as i64)],
1930 ),
1931 )
1932 .expect("seed row");
1933 }
1934 store.persist().expect("initial checkpoint");
1935 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1936 store.persist().expect("unchanged checkpoint");
1937 assert_eq!(
1938 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1939 0,
1940 "unchanged collections must not hold the writer fence while serializing every row"
1941 );
1942 drop(store);
1943 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1944 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1945 reopened.persist().expect("unchanged recovered generation");
1946 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1947 }
1948
1949 #[test]
1950 fn paged_reopen_preserves_stored_sequence_identity() {
1951 let directory = tempfile::tempdir().expect("temporary store");
1952 let path = directory.path().join("data.rdb");
1953 let store = UnifiedStore::open(&path).expect("paged store");
1954 store.create_collection("rows").expect("collection");
1955 for id in [30, 10, 20] {
1956 store.insert("rows", row(id, id as i64)).expect("insert");
1957 }
1958 store.persist().expect("checkpoint");
1959 let sequences = [10, 20, 30].map(|id| {
1960 store
1961 .get("rows", EntityId::new(id))
1962 .expect("row")
1963 .sequence_id
1964 });
1965 drop(store);
1966 let reopened = UnifiedStore::open(&path).expect("reopen");
1967 let recovered = [10, 20, 30].map(|id| {
1968 reopened
1969 .get("rows", EntityId::new(id))
1970 .expect("row")
1971 .sequence_id
1972 });
1973 assert_eq!(
1974 recovered, sequences,
1975 "opening a checkpoint must not rewrite stored row identities"
1976 );
1977 }
1978
1979 #[test]
1980 fn auto_insert_persists_the_assigned_sequence_identity() {
1981 let directory = tempfile::tempdir().expect("temporary store");
1982 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1983 for id in 1u64..=3 {
1984 store
1985 .insert_auto("rows", row(id, id as i64))
1986 .expect("insert");
1987 }
1988 let manager = store.get_collection("rows").expect("rows");
1989 let tree = store
1990 .btree_indices
1991 .read()
1992 .get("rows")
1993 .cloned()
1994 .expect("tree");
1995 for id in 1u64..=3 {
1996 let bytes = tree
1997 .get(&id.to_be_bytes())
1998 .expect("lookup")
1999 .expect("stored row");
2000 let (persisted, _) =
2001 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2002 .expect("decode");
2003 assert_eq!(
2004 persisted.sequence_id,
2005 manager
2006 .get(EntityId::new(id))
2007 .expect("live row")
2008 .sequence_id
2009 );
2010 }
2011 }
2012
2013 #[test]
2014 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
2015 let directory = tempfile::tempdir().expect("temporary store");
2016 let path = directory.path().join("data.rdb");
2017 let store = UnifiedStore::open(&path).expect("paged store");
2018 store.insert_auto("rows", row(1, 1)).expect("seed");
2019 store.persist().expect("checkpoint");
2020 store.insert_auto("rows", row(2, 2)).expect("append");
2021 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2022 store.persist().expect("inline append checkpoint");
2023 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2024 store
2025 .get_collection("rows")
2026 .expect("rows")
2027 .update(row(1, 99))
2028 .expect("untracked edit");
2029 store.insert_auto("rows", row(3, 3)).expect("next append");
2030 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2031 store.persist().expect("untracked edit checkpoint");
2032 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
2033 drop(store);
2034 let reopened = UnifiedStore::open(&path).expect("reopen");
2035 assert_eq!(
2036 reopened
2037 .get("rows", EntityId::new(1))
2038 .expect("row")
2039 .data
2040 .as_row()
2041 .expect("table")
2042 .columns,
2043 vec![Value::Integer(99)]
2044 );
2045 }
2046
2047 #[test]
2048 fn replayed_trees_are_reusable_after_durable_reconciliation() {
2049 let directory = tempfile::tempdir().expect("temporary store");
2050 let path = directory.path().join("data.rdb");
2051 let store = UnifiedStore::open(&path).expect("paged store");
2052 store.insert_auto("rows", row(1, 1)).expect("seed");
2053 store.persist().expect("checkpoint");
2054 store
2055 .insert_auto("rows", row(2, 2))
2056 .expect("WAL-backed append");
2057 drop(store);
2058 let reopened = UnifiedStore::open(&path).expect("replay");
2059 assert!(reopened.get("rows", EntityId::new(2)).is_some());
2060 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2061 reopened.persist().expect("checkpoint reconciled tree");
2062 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2063 }
2064
2065 fn row(id: u64, value: i64) -> UnifiedEntity {
2066 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
2067 }
2068
2069 #[test]
2070 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
2071 let directory = tempfile::tempdir().expect("temporary store");
2072 let path = directory.path().join("data.rdb");
2073 let store = UnifiedStore::open(&path).expect("paged store");
2074 store.create_collection("rows").expect("collection");
2075 store.insert("rows", row(1, 1)).expect("seed");
2076 store.persist().expect("initial checkpoint");
2077 let manager = store.get_collection("rows").expect("rows");
2078 manager.update(row(1, 2)).expect("direct update");
2079 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2080 store.persist().expect("updated row checkpoint");
2081 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2082 let mut metadata = Metadata::new();
2083 metadata.set("verified", MetadataValue::Bool(true));
2084 manager
2085 .set_metadata(EntityId::new(1), metadata)
2086 .expect("metadata");
2087 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2088 store.persist().expect("metadata-only checkpoint");
2089 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2090 manager
2091 .bulk_insert(vec![row(2, 2), row(3, 3)])
2092 .expect("bulk insert");
2093 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2094 store.persist().expect("bulk insert checkpoint");
2095 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
2096 manager.delete(EntityId::new(2)).expect("delete");
2097 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2098 store.persist().expect("changed checkpoint");
2099 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
2100 drop(store);
2101 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
2102 let manager = recovered.get_collection("rows").expect("rows");
2103 assert_eq!(manager.count(), 2);
2104 assert_eq!(
2105 manager
2106 .get(EntityId::new(1))
2107 .expect("row")
2108 .data
2109 .as_row()
2110 .expect("table")
2111 .columns,
2112 vec![Value::Integer(2)]
2113 );
2114 assert!(manager.get(EntityId::new(2)).is_none());
2115 assert!(manager
2116 .get_metadata(EntityId::new(1))
2117 .expect("metadata")
2118 .get("verified")
2119 .is_some());
2120 }
2121
2122 #[test]
2123 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
2124 let directory = tempfile::tempdir().expect("temporary store");
2125 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2126 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2127 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2128 store.create_collection("rows").expect("collection");
2129 for id in 1..=40 {
2130 store.insert("rows", row(id, id as i64)).expect("seed");
2131 }
2132 store.persist().expect("initial checkpoint");
2133 let unrelated = snapshots.begin();
2134 snapshots.rollback(unrelated);
2135 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2136 store.persist().expect("unrelated rollback checkpoint");
2137 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2138
2139 store
2140 .pending_vault_versions
2141 .write()
2142 .insert(EntityId::new(100));
2143 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2144 store.persist().expect("unrelated vault checkpoint");
2145 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2146 store.pending_vault_versions.write().clear();
2147 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2148 store
2149 .persist()
2150 .expect("unrelated vault publication checkpoint");
2151 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2152 }
2153
2154 #[test]
2155 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
2156 let directory = tempfile::tempdir().expect("temporary store");
2157 let path = directory.path().join("data.rdb");
2158 let store = UnifiedStore::open(&path).expect("paged store");
2159 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2160 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2161 store.create_collection("rows").expect("collection");
2162 let xid = snapshots.begin();
2163 let mut entity = row(1, 1);
2164 entity.xmin = xid;
2165 store.insert("rows", entity).expect("committed row");
2166 snapshots.commit(xid);
2167 store.persist().expect("committed baseline");
2168 snapshots.rollback(xid);
2169 store
2170 .persist()
2171 .expect("untracked outcome requires validation");
2172 drop(store);
2173 let recovered = UnifiedStore::open(&path).expect("recover");
2174 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2175 }
2176
2177 #[test]
2178 fn exhausted_deferred_row_cache_keeps_full_validation() {
2179 let directory = tempfile::tempdir().expect("temporary store");
2180 let path = directory.path().join("data.rdb");
2181 let store = UnifiedStore::open(&path).expect("paged store");
2182 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2183 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2184 store.create_collection("rows").expect("collection");
2185 let xid = snapshots.begin();
2186 for id in 1..=1025 {
2187 let mut entity = row(id, id as i64);
2188 entity.xmin = xid;
2189 store.insert("rows", entity).expect("pending row");
2190 }
2191 store.persist().expect("exclude pending rows");
2192 assert!(store.checkpoint_versions.read()["rows"]
2193 .deferred_rows
2194 .is_none());
2195 snapshots.commit(xid);
2196 store
2197 .persist()
2198 .expect("publish committed rows conservatively");
2199 drop(store);
2200 let recovered = UnifiedStore::open(&path).expect("recover");
2201 assert_eq!(
2202 recovered.get_collection("rows").expect("rows").count(),
2203 1025
2204 );
2205 }
2206
2207 #[test]
2208 fn rollback_invalidates_reuse_without_a_segment_mutation() {
2209 let directory = tempfile::tempdir().expect("temporary store");
2210 let path = directory.path().join("data.rdb");
2211 let store = UnifiedStore::open(&path).expect("paged store");
2212 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2213 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2214 store.create_collection("rows").expect("collection");
2215 let xid = snapshots.begin();
2216 let mut pending = row(1, 1);
2217 pending.xmin = xid;
2218 store.insert("rows", pending).expect("version");
2219 store.persist().expect("before rollback");
2220 snapshots.rollback(xid);
2221 store.persist().expect("after rollback");
2222 drop(store);
2223 let recovered = UnifiedStore::open(&path).expect("recover");
2224 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2225 }
2226
2227 #[test]
2228 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
2229 let directory = tempfile::tempdir().expect("temporary store");
2230 let path = directory.path().join("data.rdb");
2231 let store = UnifiedStore::open(&path).expect("paged store");
2232 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2233 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2234 store.create_collection("rows").expect("collection");
2235 let xid = snapshots.begin();
2236 let mut deleted = row(1, 1);
2237 deleted.xmax = xid;
2238 store.insert("rows", deleted).expect("version");
2239 store.persist().expect("before rollback");
2240 snapshots.rollback(xid);
2241 store.persist().expect("after rollback");
2242 drop(store);
2243 let recovered = UnifiedStore::open(&path).expect("recover");
2244 let manager = recovered.get_collection("rows").expect("rows");
2245 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
2246 }
2247
2248 #[test]
2249 fn publishing_a_pending_vault_version_invalidates_reuse() {
2250 let directory = tempfile::tempdir().expect("temporary store");
2251 let path = directory.path().join("data.rdb");
2252 let store = UnifiedStore::open(&path).expect("paged store");
2253 store.create_collection("rows").expect("collection");
2254 store.insert("rows", row(1, 1)).expect("row");
2255 store
2256 .pending_vault_versions
2257 .write()
2258 .insert(EntityId::new(1));
2259 store.persist().expect("exclude pending vault version");
2260 store
2261 .pending_vault_versions
2262 .write()
2263 .remove(&EntityId::new(1));
2264 store.persist().expect("include published vault version");
2265 drop(store);
2266 let recovered = UnifiedStore::open(&path).expect("recover");
2267 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
2268 }
2269}