1use super::*;
2use parking_lot::RwLock;
3
4const CHECKPOINT_BATCH_BYTES: usize = 1024 * 1024;
5const CHECKPOINT_BATCH_ROWS: usize = 512;
6
7#[derive(Debug)]
8struct SystemCollectionCollision {
9 survivor: String,
10 loser: String,
11 canonical: String,
12 shared_physical_state: bool,
13 resume_counter_drift: bool,
14 merged_resume: Option<reddb_file::native_resume::CollectionResume>,
15}
16
17#[derive(Debug, Default)]
18struct CatalogLoadOutcome {
19 migrated: bool,
20 collisions: Vec<SystemCollectionCollision>,
21}
22
23fn merge_shared_alias_resume(
24 legacy: &reddb_file::native_resume::CollectionResume,
25 canonical: &reddb_file::native_resume::CollectionResume,
26) -> Option<reddb_file::native_resume::CollectionResume> {
27 let (data_root_match, key_index_root_match, secondary_index_state_match) =
28 shared_alias_resume_matches(legacy, canonical);
29 if !data_root_match || !key_index_root_match || !secondary_index_state_match {
30 return None;
31 }
32 let mut merged = legacy.clone();
33 merged.entities = legacy.entities.max(canonical.entities);
38 merged.entity_id_next = legacy.entity_id_next.max(canonical.entity_id_next);
39 merged.row_id_next = legacy.row_id_next.max(canonical.row_id_next);
40 merged.sequence_id_next = legacy.sequence_id_next.max(canonical.sequence_id_next);
41 Some(merged)
42}
43
44fn shared_alias_resume_matches(
45 legacy: &reddb_file::native_resume::CollectionResume,
46 canonical: &reddb_file::native_resume::CollectionResume,
47) -> (bool, bool, bool) {
48 (
49 legacy.root_page == canonical.root_page,
50 legacy.key_index_root == canonical.key_index_root,
51 legacy.secondary_indices == canonical.secondary_indices,
52 )
53}
54
55struct CheckpointTreeBuilder {
58 tree: Arc<BTree>,
59 records: Vec<(Vec<u8>, Vec<u8>)>,
60 bytes: usize,
61}
62
63impl CheckpointTreeBuilder {
64 fn new(pager: &Arc<Pager>) -> Self {
65 Self {
66 tree: Arc::new(BTree::new(Arc::clone(pager))),
67 records: Vec::new(),
68 bytes: 0,
69 }
70 }
71
72 fn push(&mut self, record: (Vec<u8>, Vec<u8>)) -> Result<(), StoreError> {
73 let bytes = record.0.len() + record.1.len();
74 if !self.records.is_empty()
75 && (self.bytes.saturating_add(bytes) > CHECKPOINT_BATCH_BYTES
76 || self.records.len() >= CHECKPOINT_BATCH_ROWS)
77 {
78 self.flush()?;
79 }
80 self.bytes += bytes;
81 self.records.push(record);
82 #[cfg(test)]
83 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(peak.get().max(self.bytes)));
84 Ok(())
85 }
86
87 fn flush(&mut self) -> Result<(), StoreError> {
88 self.tree
89 .bulk_insert_sorted(&self.records)
90 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
91 self.records.clear();
92 self.bytes = 0;
93 Ok(())
94 }
95
96 fn copy_prefix(&mut self, previous: &BTree, count: usize) -> Result<(), StoreError> {
97 let mut cursor = previous
98 .cursor_first()
99 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
100 for _ in 0..count {
101 let record = cursor
102 .next()
103 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
104 .ok_or_else(|| StoreError::Serialization("checkpoint prefix disappeared".into()))?;
105 self.push(record)?;
106 }
107 Ok(())
108 }
109}
110
111#[cfg(test)]
112thread_local! {
113 static CHECKPOINT_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
114 static REPLAY_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
115 static CHECKPOINT_BUFFERED_BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
116}
117
118fn checkpoint_visibility_matches(
119 previous: &CheckpointVersion,
120 current: &CheckpointVersion,
121) -> bool {
122 (previous.visibility_revision == current.visibility_revision
123 || previous.excluded_xids == current.excluded_xids)
124 && previous.pending_vault == current.pending_vault
125}
126
127const META_PAGE_CONTENT_CAP: usize =
146 crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
147const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
148const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
149const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
150const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
151
152fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
153 let cs = crate::storage::engine::HEADER_SIZE;
154 let page = pager.read_page(1)?;
155 let bytes = page.as_bytes();
156 if bytes.len() < cs + META_V3_PAGE1_HEADER {
157 return Ok(Vec::new());
158 }
159 let Some(header) =
160 reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
161 PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
162 })?
163 else {
164 return Ok(Vec::new());
165 };
166 let mut next = header.next_overflow_page_id;
167 let mut pages = Vec::new();
168 let mut seen = std::collections::HashSet::new();
169 while next != 0 {
170 if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
171 return Err(PagerError::InvalidDatabase(
172 "cyclic metadata overflow chain".into(),
173 ));
174 }
175 let ov = pager.read_page(next)?;
176 if ov.page_type()? != crate::storage::engine::PageType::Overflow {
177 return Err(PagerError::InvalidDatabase(
178 "invalid metadata overflow page type".into(),
179 ));
180 }
181 let ob = ov.as_bytes();
182 let nn = reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
183 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?
184 .next_overflow_page_id;
185 pages.push(next);
186 next = nn;
187 }
188 Ok(pages)
189}
190
191fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
192 let retired = existing_overflow_chain(pager)?;
193 if !retired.is_empty() {
194 pager.record_retirement_intent(&retired)?;
195 }
196 let page = build_meta_page1_with_overflow(pager, meta_data)?;
197 pager.sync()?;
199 pager.write_page(1, page)?;
200 pager.sync()?;
201 for id in retired {
202 pager.free_page(id)?;
203 }
204 Ok(())
205}
206
207fn build_meta_page1_with_overflow(
208 pager: &Pager,
209 meta_data: &[u8],
210) -> Result<crate::storage::engine::Page, PagerError> {
211 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
212
213 let mut page1 = Page::new(PageType::Header, 1);
214 let cs = HEADER_SIZE;
215
216 if meta_data.len() <= META_PAGE_CONTENT_CAP {
217 let buf = page1.as_bytes_mut();
219 buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
220 return Ok(page1);
221 }
222
223 let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
227 let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
228 let mut chunks: Vec<&[u8]> = Vec::new();
229 while !tail.is_empty() {
230 let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
231 chunks.push(&tail[..take]);
232 tail = &tail[take..];
233 }
234
235 let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
236 let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
237 for _ in 0..chunks.len() {
238 let pg = pager.allocate_page(PageType::Overflow)?;
239 overflow_ids.push(pg.page_id());
240 overflow_pages.push(pg);
241 }
242
243 for i in 0..chunks.len() {
244 let next = if i + 1 < chunks.len() {
245 overflow_ids[i + 1]
246 } else {
247 0u32
248 };
249 let len = chunks[i].len() as u32;
250 let buf = overflow_pages[i].as_bytes_mut();
251 reddb_file::encode_native_metadata_overflow_continuation_header(
252 &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
253 reddb_file::NativeMetadataOverflowContinuationHeader {
254 next_overflow_page_id: next,
255 chunk_bytes: len,
256 },
257 )
258 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
259 buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
260 }
261 for (idx, page) in overflow_pages.into_iter().enumerate() {
262 let id = overflow_ids[idx];
263 pager.write_page(id, page)?;
264 }
265
266 let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
268 .ok()
269 .flatten()
270 .map_or(0, |header| header.format_version);
271
272 let buf = page1.as_bytes_mut();
273 reddb_file::encode_native_metadata_overflow_header(
274 &mut buf[cs..cs + META_V3_PAGE1_HEADER],
275 reddb_file::NativeMetadataOverflowHeader {
276 format_version,
277 total_payload_bytes: meta_data.len() as u32,
278 next_overflow_page_id: overflow_ids[0],
279 },
280 )
281 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
282 buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
283 .copy_from_slice(first_chunk);
284
285 Ok(page1)
286}
287
288fn storage_integrity(
289 zone: &str,
290 id: impl Into<String>,
291 collection: Option<String>,
292 detail: impl Into<String>,
293) -> StoreError {
294 StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
295 zone, id, collection, detail,
296 ))
297}
298
299fn page_integrity(
300 page_id: u32,
301 collection: Option<String>,
302 detail: impl Into<String>,
303) -> StoreError {
304 storage_integrity("page", page_id.to_string(), collection, detail)
305}
306
307fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
319 let cs = crate::storage::engine::HEADER_SIZE;
320 let meta_page = pager.read_manifest_page().map_err(|err| {
321 storage_integrity(
322 "manifest",
323 "manifest",
324 None,
325 format!("internal manifest zone read failed ({err})"),
326 )
327 })?;
328 let bytes = meta_page.as_bytes();
329 if bytes.len() < cs + 4 {
330 return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
331 }
332 let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
333 .map_err(|err| page_integrity(1, None, err.to_string()))?
334 {
335 Some(header) => header,
336 None => {
337 return Ok(Some(bytes[cs..].to_vec()));
338 }
339 };
340 if bytes.len() < cs + META_V3_PAGE1_HEADER {
341 return Err(page_integrity(
342 1,
343 None,
344 "metadata overflow header is truncated",
345 ));
346 }
347 let total = header.total_payload_bytes as usize;
348 let mut next = header.next_overflow_page_id;
349 let mut payload: Vec<u8> = Vec::with_capacity(total);
350 let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
351 payload.extend_from_slice(
352 &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
353 );
354 let mut seen = std::collections::HashSet::new();
355 while next != 0 && payload.len() < total {
356 if !seen.insert(next)
357 || seen.len()
358 >= pager
359 .page_count()
360 .map_err(|err| page_integrity(next, None, err.to_string()))?
361 as usize
362 {
363 return Err(page_integrity(next, None, "cyclic metadata overflow chain"));
364 }
365 let ov = pager
366 .read_page(next)
367 .map_err(|err| page_integrity(next, None, err.to_string()))?;
368 if ov
369 .page_type()
370 .map_err(|err| page_integrity(next, None, err.to_string()))?
371 != crate::storage::engine::PageType::Overflow
372 {
373 return Err(page_integrity(
374 next,
375 None,
376 "invalid metadata overflow page type",
377 ));
378 }
379 let ob = ov.as_bytes();
380 if ob.len() < cs + META_V3_OVERFLOW_HEADER {
381 return Err(page_integrity(
382 next,
383 None,
384 "metadata overflow continuation is truncated",
385 ));
386 }
387 let continuation =
388 reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
389 .map_err(|err| page_integrity(next, None, err.to_string()))?;
390 let nn = continuation.next_overflow_page_id;
391 let len = continuation.chunk_bytes as usize;
392 let remaining = total - payload.len();
393 let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
394 payload.extend_from_slice(
395 &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
396 );
397 next = nn;
398 }
399 if payload.len() != total {
400 return Err(page_integrity(
401 1,
402 None,
403 "metadata overflow chain is incomplete",
404 ));
405 }
406 Ok(Some(payload))
407}
408
409impl UnifiedStore {
410 fn checkpoint_tree(
414 &self,
415 pager: &Arc<Pager>,
416 manager: &SegmentManager,
417 previous: Option<&Arc<BTree>>,
418 mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
419 ) -> Result<Arc<BTree>, StoreError> {
420 if let Some((base, overlay)) = manager.backing_overlay(&mut encode) {
421 use reddb_engine::storage::collection_read::{CollectionRead, MergedCollection};
422 let merged = MergedCollection::new(base.as_ref(), overlay);
423 let mut cursor = merged.cursor()?;
424 let mut builder = CheckpointTreeBuilder::new(pager);
425 while let Some(record) = cursor.next_record(
426 usize::try_from(self.config.resident_memory_bytes_max.unwrap_or(u64::MAX))
427 .unwrap_or(usize::MAX),
428 )? {
429 builder.push((record.id.raw().to_be_bytes().to_vec(), record.bytes))?;
430 }
431 builder.flush()?;
432 return Ok(builder.tree);
433 }
434 let mut cursor = previous
435 .map(|tree| tree.cursor_first())
436 .transpose()
437 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
438 let mut matched = 0;
439 let mut builder = None;
440 let mut failure = None;
441 manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
442 let Some(value) = encode(entity, metadata) else {
443 return true;
444 };
445 let record = (entity.id.raw().to_be_bytes().to_vec(), value);
446 let result = (|| {
447 if builder.is_none() {
448 if let Some(cursor) = cursor.as_mut() {
449 if cursor
450 .next()
451 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
452 .as_ref()
453 == Some(&record)
454 {
455 matched += 1;
456 return Ok(());
457 }
458 }
459 let mut replacement = CheckpointTreeBuilder::new(pager);
460 if let Some(previous) = previous {
461 replacement.copy_prefix(previous, matched)?;
462 }
463 builder = Some(replacement);
464 }
465 builder
466 .as_mut()
467 .expect("replacement initialized")
468 .push(record)
469 })();
470 if let Err(error) = result {
471 failure = Some(error);
472 return false;
473 }
474 true
475 });
476 if let Some(error) = failure {
477 return Err(error);
478 }
479 if builder.is_none() {
480 if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
481 if cursor
482 .next()
483 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
484 .is_none()
485 {
486 return Ok(Arc::clone(previous));
487 }
488 }
489 let mut replacement = CheckpointTreeBuilder::new(pager);
490 if let Some(previous) = previous {
491 replacement.copy_prefix(previous, matched)?;
492 }
493 builder = Some(replacement);
494 }
495 let mut builder = builder.expect("replacement or reused tree");
496 builder.flush()?;
497 Ok(builder.tree)
498 }
499
500 fn publish_reclaimable_trees(
504 &self,
505 pager: &Pager,
506 metadata: &mut Vec<u8>,
507 live: &HashMap<String, Arc<BTree>>,
508 keys: &HashMap<String, PublishedKeyIndex>,
509 ) -> Result<(), StoreError> {
510 let mut next_xid_summary = None;
511 let mut retired = self.retired_btrees.lock();
512 let mut roots = Vec::new();
513 let mut reclaim = Vec::new();
514 let mut remove = std::collections::HashSet::new();
515 for (index, entry) in retired.iter().enumerate() {
516 if live
517 .values()
518 .chain(keys.values().flat_map(PublishedKeyIndex::trees))
519 .any(|tree| Arc::ptr_eq(tree, &entry.tree))
520 {
521 remove.insert(index);
522 continue;
523 }
524 if entry.published
525 && Arc::strong_count(&entry.tree) == 1
526 && !entry.tree.has_retirement_readers()
527 {
528 #[cfg(test)]
532 if std::env::var_os("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT").is_some() {
533 roots.push(entry.tree.root_page_id());
534 }
535 reclaim.extend(
536 entry
537 .tree
538 .retired_pages()
539 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
540 );
541 remove.insert(index);
542 } else if entry.tree.root_page_id() != 0 {
543 roots.push(entry.tree.root_page_id());
544 }
545 }
546 roots.sort_unstable();
547 roots.dedup();
548 reddb_file::encode_native_paged_retired_trees(metadata, &roots)
549 .map_err(|e| StoreError::Serialization(e.to_string()))?;
550 let (pending_pages, published_pages) = pager
551 .deferred_reclamation_pages()
552 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
553 let readers = live
554 .values()
555 .chain(keys.values().flat_map(PublishedKeyIndex::trees))
556 .any(|tree| tree.has_retirement_readers())
557 || retired
558 .iter()
559 .any(|entry| entry.tree.has_retirement_readers());
560 let released_pages = if readers { Vec::new() } else { published_pages };
561 let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
562 let retained_pages: Vec<_> = pending_pages
563 .into_iter()
564 .filter(|id| !released.contains(id))
565 .collect();
566 reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
567 .map_err(|e| StoreError::Serialization(e.to_string()))?;
568 let active_epoch = self.ownership_active_epoch.load(Ordering::Acquire);
569 let catalog_epoch = if active_epoch == 0 {
570 self.ownership_catalog_epoch.load(Ordering::Acquire)
571 } else {
572 active_epoch
573 };
574 if catalog_epoch != 0 {
575 reddb_file::native_store::encode_native_paged_ownership_epoch(metadata, catalog_epoch)
576 .map_err(|e| StoreError::Serialization(e.to_string()))?;
577 }
578 if self.config.resident_memory_bytes_max.is_some() {
579 use reddb_file::native_resume::{
580 encode_catalog_resume, CatalogResume, CollectionResume,
581 };
582 let mut states = std::collections::BTreeMap::new();
583 let mut entity_id_next = self.resident.next_entity_id.load(Ordering::SeqCst);
584 let collections = self.resident.collections.read();
585 let publishes_rows = {
586 let summary = self.recovery_xid_summary.read();
587 live.iter().any(|(name, tree)| {
588 let root = tree.root_page_id();
589 root != 0
590 && summary.as_ref().and_then(|state| state.roots.get(name)) != Some(&root)
591 })
592 };
593 let xid_high_water_mark =
594 self.base_xid_high_water_mark(&collections).map(|mut high| {
595 if publishes_rows {
600 for manager in collections.values() {
601 manager.for_each_entity(|entity| {
602 high = high.max(entity.xmin).max(entity.xmax);
603 true
604 });
605 }
606 }
607 if let Some(snapshots) = self.snapshot_manager.get() {
608 high = high.max(snapshots.peek_next_xid().saturating_sub(1));
609 }
610 high
611 });
612 for (name, manager) in collections.iter() {
613 let tree = live.get(name);
614 let (entity_next, row_id_next) = manager.identity_counters();
615 entity_id_next = entity_id_next.max(entity_next);
616 let root_page = tree.map_or(0, |tree| tree.root_page_id());
617 let entities = if root_page == 0 {
618 0
619 } else if let Some(count) = manager.backing_count_at_root(root_page) {
620 count
621 } else {
622 let mut cursor = tree
625 .expect("invariant: nonzero root has a tree")
626 .cursor_first()
627 .map_err(|error| {
628 StoreError::Io(std::io::Error::other(error.to_string()))
629 })?;
630 let mut count = 0_u64;
631 while cursor
632 .skip_next()
633 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
634 {
635 count += 1;
636 }
637 count
638 };
639 states.insert(
640 name.clone(),
641 CollectionResume {
642 root_page,
643 entities,
644 entity_id_next: entity_next,
645 row_id_next,
646 sequence_id_next: manager.sequence_id_next(),
647 key_index_root: tree.and_then(|tree| {
648 keys.get(name)
649 .filter(|index| {
650 index.indexed && index.source.ptr_eq(&Arc::downgrade(tree))
651 })
652 .map(|index| index.tree.root_page_id())
653 }),
654 secondary_indices: tree
655 .and_then(|tree| {
656 keys.get(name)
657 .filter(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
658 })
659 .map(|index| {
660 index
661 .secondary
662 .iter()
663 .map(|(name, index)| (name.clone(), index.resume()))
664 .collect()
665 })
666 .unwrap_or_default(),
667 },
668 );
669 }
670 let resume = CatalogResume {
671 entity_id_next,
672 xid_high_water_mark,
673 collections: states,
674 };
675 encode_catalog_resume(metadata, &resume)
676 .map_err(|error| StoreError::Serialization(error.to_string()))?;
677 next_xid_summary = xid_high_water_mark.map(|high_water_mark| {
678 super::recovery_xid::RecoveryXidSummary {
679 high_water_mark,
680 roots: resume
681 .collections
682 .into_iter()
683 .map(|(name, state)| (name, state.root_page))
684 .collect(),
685 }
686 });
687 }
688 let mut retirement_intent = reclaim.clone();
691 retirement_intent.extend_from_slice(&released_pages);
692 if !retirement_intent.is_empty() {
693 pager
694 .record_retirement_intent(&retirement_intent)
695 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
696 }
697 publish_paged_metadata(pager, metadata)
700 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
701 *self.recovery_xid_summary.write() = next_xid_summary;
702 self.ownership_catalog_epoch
703 .store(catalog_epoch, Ordering::Release);
704 #[cfg(test)]
705 if !reclaim.is_empty()
706 && std::env::var_os("REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE").is_some()
707 {
708 eprintln!(
709 "experimental exit before retired free pages={}",
710 reclaim.len()
711 );
712 std::process::exit(184);
713 }
714 let mut index = 0;
715 retired.retain_mut(|entry| {
716 let keep = !remove.contains(&index);
717 entry.published = true;
718 index += 1;
719 keep
720 });
721 if !reclaim.is_empty() {
722 pager
723 .free_retired_pages(&reclaim)
724 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
725 #[cfg(test)]
726 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE").is_some() {
727 eprintln!(
728 "experimental exit after retired enqueue pages={}",
729 reclaim.len()
730 );
731 std::process::exit(186);
732 }
733 pager
734 .sync()
735 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
736 #[cfg(test)]
737 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE").is_some() {
738 eprintln!(
739 "experimental exit after retired free pages={}",
740 reclaim.len()
741 );
742 std::process::exit(185);
743 }
744 }
745 pager
746 .complete_deferred_publication(&retained_pages, &released_pages)
747 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
748 if !released_pages.is_empty() {
749 pager
750 .sync()
751 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
752 }
753 Ok(())
754 }
755
756 pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
757 let mut retired = self.retired_btrees.lock();
758 if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
759 {
760 retired.push(RetiredBTree {
761 tree,
762 published: false,
763 });
764 }
765 }
766
767 pub(crate) fn note_paged_insert(
771 &self,
772 collection: &str,
773 manager: &Arc<SegmentManager>,
774 before: &[(u64, u64)],
775 id: EntityId,
776 ) {
777 let after = manager.checkpoint_revisions();
778 if before.len() != after.len() {
779 return;
780 }
781 let mut changed = 0;
782 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
783 if old_id != new_id {
784 return;
785 }
786 if old_revision != new_revision {
787 if old_revision.checked_add(1) != Some(*new_revision) {
788 return;
789 }
790 changed += 1;
791 }
792 }
793 if changed != 1 {
794 return;
795 }
796 let mut versions = self.checkpoint_versions.write();
797 let Some(previous) = versions.get_mut(collection) else {
798 return;
799 };
800 if previous.manager.ptr_eq(&Arc::downgrade(manager))
801 && previous.segments == before
802 && previous.format_version == self.format_version()
803 && !previous.pending_vault.contains(&id)
804 && !previous.excluded_xids.contains(&0)
805 {
806 previous.segments = after;
807 }
808 }
809
810 pub(crate) fn note_paged_delete(
814 &self,
815 collection: &str,
816 manager: &Arc<SegmentManager>,
817 before: &[(u64, u64)],
818 deleted: &[EntityId],
819 ) {
820 let after = manager.checkpoint_revisions();
821 if before.len() != after.len() {
822 return;
823 }
824 let mut changes = 0_u64;
825 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
826 if old_id != new_id {
827 return;
828 }
829 let Some(delta) = new_revision.checked_sub(*old_revision) else {
830 return;
831 };
832 let Some(total) = changes.checked_add(delta) else {
833 return;
834 };
835 changes = total;
836 }
837 let Ok(expected) = u64::try_from(deleted.len()) else {
838 return;
839 };
840 if changes != expected {
841 return;
842 }
843 let mut versions = self.checkpoint_versions.write();
844 let Some(previous) = versions.get_mut(collection) else {
845 return;
846 };
847 if !previous.manager.ptr_eq(&Arc::downgrade(manager))
848 || previous.format_version != self.format_version()
849 || deleted.iter().any(|id| previous.pending_vault.contains(id))
850 || previous.excluded_xids.contains(&0)
851 {
852 return;
853 }
854 if previous.segments != before {
855 let Some(deferred) = previous.deferred_rows.as_ref() else {
856 return;
857 };
858 let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
859 return;
860 };
861 let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
862 let pending: std::collections::BTreeSet<_> = deferred
863 .iter()
864 .copied()
865 .chain(changed)
866 .filter(|id| !removed.contains(id))
867 .collect();
868 if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
869 return;
870 }
871 previous.has_unprojected_rows = !pending.is_empty();
876 previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
877 }
878 previous.segments = after;
879 }
880
881 pub(crate) fn mark_paged_registry_dirty(&self) {
882 self.resident.mark_paged_registry_dirty()
883 }
884
885 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
891 let collection = Self::canonical_collection(collection);
892 let pager = self.pager.as_ref()?;
893 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
894 return Some(btree);
895 }
896 let mut write = self.btree_indices.write();
897 let btree = write
898 .entry(collection.to_string())
899 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
900 .clone();
901 Some(btree)
902 }
903
904 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
905 let Some(pager) = &self.pager else {
906 return Ok(());
907 };
908
909 if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
910 self.flush_paged_registry()?;
911 self.resident
912 .paged_registry_dirty
913 .store(false, Ordering::Release);
914 return Ok(());
915 }
916
917 pager
918 .flush()
919 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
920 }
921
922 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
923 let _vault_guard = self.vault_publication_lock.lock();
924 let Some(pager) = &self.pager else {
925 return Ok(());
926 };
927
928 match pager.read_page(1) {
929 Ok(_) => {}
930 Err(PagerError::PageNotFound(_)) => {
931 let meta_page = pager
932 .allocate_page(crate::storage::engine::PageType::Header)
933 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
934 pager
935 .write_page(meta_page.page_id(), meta_page)
936 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
937 }
938 Err(e) => {
939 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
940 }
941 }
942
943 let format_version = STORE_VERSION_V11;
944 self.set_format_version(format_version);
945
946 let collections = self.resident.collections.read();
947 let btree_indices = self.btree_indices.read();
948 let mut collection_roots = Vec::with_capacity(collections.len());
949 for (name, _) in collections.iter() {
950 let root_page = btree_indices
951 .get(name)
952 .map_or(0, |btree| btree.root_page_id());
953 collection_roots.push((name.clone(), root_page));
954 }
955 drop(btree_indices);
956 drop(collections);
957
958 let mut meta_data = Vec::with_capacity(4096);
959 reddb_file::encode_native_paged_metadata_header(
960 &mut meta_data,
961 reddb_file::NativePagedMetadataHeader {
962 format_version,
963 collection_count: collection_roots.len() as u32,
964 },
965 );
966 for (name, root_page) in &collection_roots {
967 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
968 }
969
970 let cross_refs = self.resident.cross_refs.read();
971 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
972 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
973 for (source_id, refs) in cross_refs.iter() {
974 for (target_id, ref_type, collection) in refs {
975 reddb_file::encode_native_paged_cross_ref(
976 &mut meta_data,
977 source_id.raw(),
978 target_id.raw(),
979 ref_type.to_byte(),
980 reddb_engine::storage::system_collection_names::canonical_or_self(collection),
981 );
982 }
983 }
984
985 self.publish_reclaimable_trees(
986 pager,
987 &mut meta_data,
988 &self.btree_indices.read(),
989 &self.key_indices.read(),
990 )?;
991
992 Ok(())
993 }
994
995 pub fn pager(&self) -> Option<&Arc<Pager>> {
997 self.pager.as_ref()
998 }
999
1000 pub fn page_cache_bytes_in_use(&self) -> u64 {
1004 self.pager.as_ref().map_or(0, |pager| {
1005 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
1006 })
1007 }
1008
1009 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
1012 self.commit
1013 .as_ref()
1014 .map_or(0, |commit| commit.buffered_bytes())
1015 }
1016
1017 pub fn segment_memory_bytes(&self) -> u64 {
1021 let managers: Vec<Arc<SegmentManager>> = self
1022 .resident
1023 .collections
1024 .read()
1025 .values()
1026 .map(Arc::clone)
1027 .collect();
1028
1029 managers
1030 .iter()
1031 .map(|manager| manager.resident_bytes())
1032 .fold(0, u64::saturating_add)
1033 }
1034
1035 pub fn config(&self) -> &UnifiedStoreConfig {
1039 &self.config
1040 }
1041
1042 pub fn with_config(config: UnifiedStoreConfig) -> Self {
1043 Self {
1044 config,
1045 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1046 format_version: AtomicU32::new(STORE_VERSION_V11),
1047 pager: None,
1048 db_path: None,
1049 operational_manifest: parking_lot::Mutex::new(None),
1050 btree_indices: RwLock::new(HashMap::new()),
1051 key_indices: RwLock::new(HashMap::new()),
1052 secondary_index_specs: RwLock::new(HashMap::new()),
1053 recovery_xid_summary: RwLock::new(None),
1054 checkpoint_versions: RwLock::new(HashMap::new()),
1055 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1056 retired_btrees: parking_lot::Mutex::new(Vec::new()),
1057 ownership_catalog_epoch: AtomicU64::new(0),
1058 ownership_active_epoch: AtomicU64::new(0),
1059 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1060 commit: None,
1061 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1062 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1063 snapshot_manager: std::sync::OnceLock::new(),
1064 embedded_commit_session: std::sync::OnceLock::new(),
1065 #[cfg(test)]
1066 test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1067 #[cfg(test)]
1068 test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1069 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1070 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1071 }
1072 }
1073
1074 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
1092 Self::open_with_config(path, UnifiedStoreConfig::default())
1093 }
1094
1095 pub fn open_with_config(
1096 path: impl AsRef<Path>,
1097 config: UnifiedStoreConfig,
1098 ) -> Result<Self, StoreError> {
1099 Self::open_with_config_for_recovery(path, config, false)
1100 }
1101
1102 pub(crate) fn open_with_config_for_recovery(
1105 path: impl AsRef<Path>,
1106 config: UnifiedStoreConfig,
1107 retain_canonical_system_config: bool,
1108 ) -> Result<Self, StoreError> {
1109 let path = path.as_ref();
1110 let mut pager_config = PagerConfig {
1111 disk_residency: config.resident_memory_bytes_max.is_some(),
1112 ..PagerConfig::default()
1113 };
1114 if let Some(slots) = config.page_cache_slots {
1118 pager_config.cache_size = slots;
1119 }
1120 if matches!(
1128 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
1129 Some("0") | Some("false") | Some("off")
1130 ) {
1131 pager_config.double_write = false;
1132 }
1133 let pager_open_started = std::time::Instant::now();
1134 let pager = Pager::open(path, pager_config)
1135 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1136 pager
1137 .enable_deferred_reclamation()
1138 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1139 trace_startup_phase(path, "paged_pager_open", pager_open_started);
1140
1141 if config.resident_memory_bytes_max.is_none()
1142 && pager
1143 .header()
1144 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1145 .version
1146 >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
1147 {
1148 return Err(StoreError::Io(std::io::Error::new(std::io::ErrorKind::Unsupported,
1149 "this database was prepared for disk residency; reopen with the disk-resident storage profile")));
1150 }
1151
1152 let wal_path = reddb_file::layout::unified_wal_path(path);
1153 let commit = Some(Arc::new(
1156 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
1157 .map_err(StoreError::Io)?,
1158 ));
1159
1160 let store = Self {
1161 config,
1162 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1163 format_version: AtomicU32::new(STORE_VERSION_V11),
1164 pager: Some(Arc::new(pager)),
1165 db_path: Some(path.to_path_buf()),
1166 operational_manifest: parking_lot::Mutex::new(None),
1167 btree_indices: RwLock::new(HashMap::new()),
1168 key_indices: RwLock::new(HashMap::new()),
1169 secondary_index_specs: RwLock::new(HashMap::new()),
1170 recovery_xid_summary: RwLock::new(None),
1171 checkpoint_versions: RwLock::new(HashMap::new()),
1172 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1173 retired_btrees: parking_lot::Mutex::new(Vec::new()),
1174 ownership_catalog_epoch: AtomicU64::new(0),
1175 ownership_active_epoch: AtomicU64::new(0),
1176 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1177 commit,
1178 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1179 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1180 snapshot_manager: std::sync::OnceLock::new(),
1181 embedded_commit_session: std::sync::OnceLock::new(),
1182 #[cfg(test)]
1183 test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1184 #[cfg(test)]
1185 test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1186 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1187 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1188 };
1189
1190 let checkpoint_recovery_started = std::time::Instant::now();
1193 if let Some(commit) = &store.commit {
1194 commit
1195 .restore_checkpoint_page_images(&store)
1196 .map_err(StoreError::Io)?;
1197 }
1198 trace_startup_phase(
1199 path,
1200 "paged_checkpoint_recovery",
1201 checkpoint_recovery_started,
1202 );
1203 let page_load_started = std::time::Instant::now();
1205 let catalog_load = store.load_from_pages(retain_canonical_system_config)?;
1206 trace_startup_phase(path, "paged_catalog_and_roots", page_load_started);
1207 store.migrate_operational_manifest(&catalog_load.collisions)?;
1211 let wal_replay_started = std::time::Instant::now();
1212 if let Some(commit) = &store.commit {
1213 commit.replay_into(&store).map_err(StoreError::Io)?;
1214 }
1215 trace_startup_phase(path, "paged_wal_replay", wal_replay_started);
1216 if catalog_load.migrated {
1217 store.mark_paged_registry_dirty();
1220 store.flush_paged_state()?;
1221 }
1222 let manifest_recovery_started = std::time::Instant::now();
1223 store.recover_operational_manifest()?;
1224 if store.config.resident_memory_bytes_max.is_some() {
1225 let missing_key_index = {
1226 let trees = store.btree_indices.read();
1227 let keys = store.key_indices.read();
1228 trees.iter().any(|(name, tree)| {
1229 !keys
1230 .get(name)
1231 .is_some_and(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
1232 })
1233 };
1234 if missing_key_index {
1235 store.persist()?;
1236 }
1237 }
1238 trace_startup_phase(
1239 path,
1240 "paged_manifest_and_derived_roots",
1241 manifest_recovery_started,
1242 );
1243
1244 Ok(store)
1245 }
1246
1247 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
1251 let Some(pager) = &self.pager else {
1252 return Ok(());
1253 };
1254 let collections = self.resident.collections.read();
1255 let mut indices = self.btree_indices.write();
1256 let previous_versions = self.checkpoint_versions.read();
1257 let raw_replay =
1258 self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
1259 for (name, manager) in collections.iter() {
1260 let mut reusable = None;
1261 if raw_replay {
1262 if let (Some(previous), Some(tree)) =
1263 (previous_versions.get(name), indices.get(name))
1264 {
1265 let mut current = CheckpointVersion {
1266 manager: Arc::downgrade(manager),
1267 segments: manager.checkpoint_revisions(),
1268 visibility_revision: 0,
1269 untracked_visibility_revision: 0,
1270 excluded_xids: Arc::default(),
1271 pending_vault: Arc::default(),
1272 format_version: self.format_version(),
1273 deferred_rows: Some(Arc::default()),
1274 has_unprojected_rows: false,
1275 };
1276 if previous.manager.ptr_eq(¤t.manager)
1277 && previous.format_version == current.format_version
1278 {
1279 if previous.segments == current.segments
1280 && !previous.has_unprojected_rows
1281 && checkpoint_visibility_matches(previous, ¤t)
1282 {
1283 reusable = Some(Arc::clone(tree));
1284 } else {
1285 reusable = self.sparse_checkpoint_tree(
1291 name,
1292 manager,
1293 tree,
1294 previous,
1295 &mut current,
1296 )?;
1297 }
1298 }
1299 }
1300 }
1301 let tree = match reusable {
1302 Some(tree) => tree,
1303 None => {
1304 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1305 #[cfg(test)]
1306 REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1307 Some(Self::serialize_entity_record(
1308 entity,
1309 metadata.as_ref(),
1310 self.format_version(),
1311 ))
1312 })?
1313 }
1314 };
1315 if indices
1316 .get(name)
1317 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1318 {
1319 continue;
1320 }
1321 if let Some(previous) = indices.insert(name.clone(), tree) {
1322 self.retire_btree(previous);
1323 }
1324 self.mark_paged_registry_dirty();
1325 #[cfg(test)]
1326 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1327 if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1328 pager.sync().map_err(|error| {
1329 StoreError::Io(std::io::Error::other(error.to_string()))
1330 })?;
1331 }
1332 eprintln!("experimental replay rebuild exit before manifest");
1333 std::process::exit(182);
1334 }
1335 }
1336 drop(previous_versions);
1337 drop(indices);
1338 drop(collections);
1339 self.flush_paged_state()?;
1340 #[cfg(test)]
1341 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1342 eprintln!("experimental replay exit after manifest");
1343 std::process::exit(182);
1344 }
1345 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1348 let collections = self.resident.collections.read();
1349 let mut versions = self.checkpoint_versions.write();
1350 for (name, manager) in collections.iter() {
1351 versions.insert(
1352 name.clone(),
1353 CheckpointVersion {
1354 manager: Arc::downgrade(manager),
1355 segments: manager.checkpoint_revisions(),
1356 visibility_revision: 0,
1357 untracked_visibility_revision: 0,
1358 excluded_xids: Arc::default(),
1359 pending_vault: Arc::default(),
1360 format_version: self.format_version(),
1361 deferred_rows: Some(Arc::default()),
1362 has_unprojected_rows: false,
1363 },
1364 );
1365 }
1366 }
1367 Ok(())
1368 }
1369
1370 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
1371 let Some(path) = &self.db_path else {
1372 return Ok(());
1373 };
1374 let mut collections = self.list_collections();
1375 collections.sort();
1376 let pending_drops = self
1377 .operational_manifest_for_path(path)
1378 .recover_or_bootstrap(&collections)
1379 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1380 for name in pending_drops {
1381 if self.get_collection(&name).is_some() {
1382 self.drop_collection(&name)?;
1383 }
1384 }
1385 Ok(())
1386 }
1387
1388 fn migrate_operational_manifest(
1389 &self,
1390 collisions: &[SystemCollectionCollision],
1391 ) -> Result<(), StoreError> {
1392 let Some(path) = &self.db_path else {
1393 return Ok(());
1394 };
1395 let manifest = self.operational_manifest_for_path(path);
1396 let collision_refs = collisions
1397 .iter()
1398 .map(|collision| {
1399 (
1400 collision.survivor.as_str(),
1401 collision.loser.as_str(),
1402 collision.canonical.as_str(),
1403 )
1404 })
1405 .collect::<Vec<_>>();
1406 manifest
1407 .reconcile_collection_collisions(&collision_refs)
1408 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1409 manifest
1410 .rename_collections(reddb_engine::storage::system_collection_names::LEGACY_NAMES)
1411 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))
1412 }
1413
1414 pub(crate) fn prepare_operational_collection_create(
1415 &self,
1416 name: &str,
1417 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
1418 let Some(path) = &self.db_path else {
1419 return Ok(None);
1420 };
1421 self.operational_manifest_for_path(path)
1422 .prepare_collection_create(name)
1423 .map(Some)
1424 .map_err(StoreError::Io)
1425 }
1426
1427 pub(crate) fn prepare_operational_collection_drop(
1428 &self,
1429 name: &str,
1430 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
1431 let Some(path) = &self.db_path else {
1432 return Ok(None);
1433 };
1434 self.operational_manifest_for_path(path)
1435 .prepare_collection_drop(name)
1436 .map(Some)
1437 .map_err(StoreError::Io)
1438 }
1439
1440 pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1442 let Some(pager) = &self.pager else {
1443 return Ok(0);
1444 };
1445 let Some(content) = read_meta_payload(pager)? else {
1446 return Ok(0);
1447 };
1448 reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1449 .map(|epoch| epoch.unwrap_or(0))
1450 .map_err(|error| StoreError::Serialization(error.to_string()))
1451 }
1452
1453 fn load_from_pages(
1455 &self,
1456 retain_canonical_system_config: bool,
1457 ) -> Result<CatalogLoadOutcome, StoreError> {
1458 let pager = match &self.pager {
1459 Some(p) => p,
1460 None => return Ok(CatalogLoadOutcome::default()), };
1462
1463 let page_count = pager.page_count().map_err(|e| {
1465 StoreError::Io(std::io::Error::other(format!(
1466 "failed to read page count: {}",
1467 e
1468 )))
1469 })?;
1470 if page_count <= 1 {
1471 return Ok(CatalogLoadOutcome::default());
1473 }
1474
1475 if let Some(content_vec) = read_meta_payload(pager)? {
1480 let content: &[u8] = &content_vec;
1481 let file_version = pager
1482 .header()
1483 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1484 .version;
1485 let prepared =
1486 file_version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION;
1487 let resume = if prepared {
1488 reddb_file::native_resume::decode_catalog_resume(content)
1489 .map_err(|error| StoreError::Serialization(error.to_string()))?
1490 } else {
1491 None
1492 };
1493 let mut outcome = CatalogLoadOutcome::default();
1494 let mut ignored_collections = std::collections::HashSet::new();
1495 if retain_canonical_system_config
1496 && !resume.as_ref().is_some_and(|resume| {
1497 resume.collections.contains_key("red_config")
1498 && resume.collections.contains_key("red.config")
1499 })
1500 {
1501 return Err(page_integrity(
1502 1,
1503 None,
1504 "canonical config recovery requires both resume identities",
1505 ));
1506 }
1507 if let Some(resume) = &resume {
1508 for &(legacy, canonical) in
1509 reddb_engine::storage::system_collection_names::LEGACY_NAMES
1510 {
1511 let (Some(legacy_state), Some(canonical_state)) = (
1512 resume.collections.get(legacy),
1513 resume.collections.get(canonical),
1514 ) else {
1515 continue;
1516 };
1517 let merged_resume = merge_shared_alias_resume(legacy_state, canonical_state);
1518 let shared_physical_state = merged_resume.is_some();
1519 let resume_counter_drift = legacy_state != canonical_state;
1520 let (survivor, loser) = match (
1521 legacy_state.entities == 0,
1522 canonical_state.entities == 0,
1523 ) {
1524 _ if retain_canonical_system_config && legacy == "red_config" => {
1525 (canonical, legacy)
1526 }
1527 (false, true) => (legacy, canonical),
1528 (true, false) | (true, true) => (canonical, legacy),
1529 (false, false) if shared_physical_state => (canonical, legacy),
1530 (false, false) => {
1531 let (
1532 data_root_match,
1533 key_index_root_match,
1534 secondary_index_state_match,
1535 ) = shared_alias_resume_matches(legacy_state, canonical_state);
1536 return Err(page_integrity(
1537 1,
1538 None,
1539 format!(
1540 "catalog contains both populated legacy collection '{legacy}' and canonical collection '{canonical}' (data_root_match={data_root_match}, key_index_root_match={key_index_root_match}, secondary_index_state_match={secondary_index_state_match})"
1541 ),
1542 ));
1543 }
1544 };
1545 ignored_collections.insert(loser.to_string());
1546 outcome.collisions.push(SystemCollectionCollision {
1547 survivor: survivor.to_string(),
1548 loser: loser.to_string(),
1549 canonical: canonical.to_string(),
1550 shared_physical_state,
1551 resume_counter_drift,
1552 merged_resume,
1553 });
1554 }
1555 let live_roots = resume
1556 .collections
1557 .iter()
1558 .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1559 .flat_map(|(_, state)| {
1560 std::iter::once(state.root_page)
1561 .chain(state.key_index_root)
1562 .chain(
1563 state
1564 .secondary_indices
1565 .values()
1566 .map(|index| index.root_page),
1567 )
1568 })
1569 .filter(|root| *root != 0)
1570 .collect::<std::collections::HashSet<_>>();
1571 for collision in &outcome.collisions {
1572 if collision.shared_physical_state {
1573 continue;
1574 }
1575 let loser_state = resume
1576 .collections
1577 .get(&collision.loser)
1578 .expect("collision loser is present");
1579 if std::iter::once(loser_state.root_page)
1580 .chain(loser_state.key_index_root)
1581 .chain(
1582 loser_state
1583 .secondary_indices
1584 .values()
1585 .map(|index| index.root_page),
1586 )
1587 .filter(|root| *root != 0)
1588 .any(|root| live_roots.contains(&root))
1589 {
1590 return Err(page_integrity(
1591 1,
1592 None,
1593 format!(
1594 "empty collection '{}' aliases a live collection root",
1595 collision.loser
1596 ),
1597 ));
1598 }
1599 }
1600 reddb_engine::storage::system_collection_names::migration_plan(
1601 resume
1602 .collections
1603 .keys()
1604 .map(String::as_str)
1605 .filter(|name| !ignored_collections.contains(*name)),
1606 )
1607 .map_err(|(legacy, canonical)| {
1608 page_integrity(
1609 1,
1610 None,
1611 format!(
1612 "catalog contains both legacy collection '{legacy}' and canonical collection '{canonical}'"
1613 ),
1614 )
1615 })?;
1616 for state in resume.collections.values() {
1617 if !state.secondary_indices.is_empty()
1618 && file_version < reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
1619 {
1620 return Err(page_integrity(
1621 1,
1622 None,
1623 "secondary roots require the file format fence",
1624 ));
1625 }
1626 for root in std::iter::once(state.root_page)
1627 .chain(state.key_index_root)
1628 .chain(
1629 state
1630 .secondary_indices
1631 .values()
1632 .map(|index| index.root_page),
1633 )
1634 {
1635 if root >= page_count {
1636 return Err(page_integrity(
1637 root,
1638 None,
1639 "published root exceeds the database page count",
1640 ));
1641 }
1642 }
1643 }
1644 self.resident
1645 .next_entity_id
1646 .fetch_max(resume.entity_id_next, Ordering::SeqCst);
1647 }
1648 *self.recovery_xid_summary.write() = resume.as_ref().and_then(|resume| {
1649 resume.xid_high_water_mark.map(|high_water_mark| {
1650 super::recovery_xid::RecoveryXidSummary {
1651 high_water_mark,
1652 roots: resume
1653 .collections
1654 .iter()
1655 .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1656 .map(|(name, state)| {
1657 (
1658 reddb_engine::storage::system_collection_names::canonical_or_self(name)
1659 .to_owned(),
1660 state.root_page,
1661 )
1662 })
1663 .collect(),
1664 }
1665 })
1666 });
1667 if content.len() >= 4 {
1668 let mut pos = 0;
1669 let mut format_version = STORE_VERSION_V1;
1670
1671 let collection_count = if let Some(header) =
1672 reddb_file::decode_native_paged_metadata_header(content)
1673 .map_err(|err| StoreError::Serialization(err.to_string()))?
1674 {
1675 format_version = header.format_version;
1676 pos += reddb_file::METADATA_HEADER_BYTES;
1677 header.collection_count as usize
1678 } else {
1679 let count = u32::from_le_bytes([
1680 content[pos],
1681 content[pos + 1],
1682 content[pos + 2],
1683 content[pos + 3],
1684 ]) as usize;
1685 pos += 4;
1686 count
1687 };
1688
1689 self.set_format_version(format_version);
1690
1691 if pos > content.len() {
1692 return Ok(CatalogLoadOutcome::default());
1693 }
1694
1695 let mut source_names = std::collections::HashMap::<String, String>::new();
1696 let mut migrated_catalog = !outcome.collisions.is_empty();
1697 let mut empty_duplicate_roots = std::collections::HashSet::new();
1698 let mut recovery_config_roots = [0u8; 2];
1699
1700 for _ in 0..collection_count {
1702 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1703 .map_err(|err| page_integrity(1, None, err.to_string()))?;
1704 let root_page = root.root_page;
1706 let stored_name = root.collection;
1707 if retain_canonical_system_config {
1708 let config_index = match stored_name.as_str() {
1709 "red_config" => Some(0),
1710 "red.config" => Some(1),
1711 _ => None,
1712 };
1713 if let Some(index) = config_index {
1714 recovery_config_roots[index] =
1715 recovery_config_roots[index].saturating_add(1);
1716 let state = resume
1717 .as_ref()
1718 .and_then(|resume| resume.collections.get(&stored_name))
1719 .expect("both recovery identities were checked");
1720 if state.root_page != root_page {
1721 return Err(page_integrity(
1722 1,
1723 None,
1724 "config collection root differs from resume catalog",
1725 ));
1726 }
1727 }
1728 }
1729 if ignored_collections.contains(&stored_name) {
1730 let shared_physical_state = outcome
1731 .collisions
1732 .iter()
1733 .find(|collision| collision.loser == stored_name)
1734 .is_some_and(|collision| collision.shared_physical_state);
1735 if shared_physical_state {
1736 continue;
1737 }
1738 let state = resume
1739 .as_ref()
1740 .and_then(|resume| resume.collections.get(&stored_name))
1741 .expect("ignored collection was proven by catalog resume");
1742 for retired_root in std::iter::once(state.root_page)
1743 .chain(state.key_index_root)
1744 .chain(
1745 state
1746 .secondary_indices
1747 .values()
1748 .map(|index| index.root_page),
1749 )
1750 .filter(|root| *root != 0)
1751 {
1752 empty_duplicate_roots.insert(retired_root);
1753 }
1754 continue;
1755 }
1756 let name = reddb_engine::storage::system_collection_names::canonical_or_self(
1757 &stored_name,
1758 )
1759 .to_owned();
1760 if let Some(previous) = source_names.insert(name.clone(), stored_name.clone()) {
1761 if previous != stored_name {
1762 return Err(page_integrity(
1763 1,
1764 None,
1765 format!(
1766 "catalog contains both legacy collection '{previous}' and canonical collection '{name}'"
1767 ),
1768 ));
1769 }
1770 }
1771 migrated_catalog |= name != stored_name;
1772
1773 let _ = self.create_collection_in_memory(&name);
1777
1778 let shared_alias = outcome.collisions.iter().find(|collision| {
1779 collision.survivor == stored_name && collision.shared_physical_state
1780 });
1781 let counters = shared_alias
1782 .and_then(|collision| collision.merged_resume.as_ref())
1783 .or_else(|| {
1784 resume
1785 .as_ref()
1786 .and_then(|resume| resume.collections.get(&stored_name))
1787 });
1788 if let (Some(counters), Some(manager)) = (counters, self.get_collection(&name))
1789 {
1790 manager.restore_identity_counters(
1791 counters.entity_id_next,
1792 counters.row_id_next,
1793 );
1794 manager.restore_sequence_id_next(counters.sequence_id_next);
1795 }
1796 if root_page > 0 || counters.is_some() {
1798 let btree = BTree::with_root(Arc::clone(pager), root_page);
1799
1800 let mut cursor = btree.cursor_first().map_err(|err| {
1802 page_integrity(root_page, Some(name.clone()), err.to_string())
1803 })?;
1804 let manager = self.get_collection(&name);
1805 let mut persisted_entities = counters.map_or(0, |state| state.entities);
1806 while counters.is_none() {
1807 let next = cursor
1808 .next_with_budget(
1809 self.config
1810 .resident_memory_bytes_max
1811 .and_then(|limit| usize::try_from(limit).ok())
1812 .unwrap_or(usize::MAX),
1813 )
1814 .map_err(|err| {
1815 page_integrity(root_page, Some(name.clone()), err.to_string())
1816 })?;
1817 let Some((key, value)) = next else {
1818 break;
1819 };
1820 persisted_entities += 1;
1821 let key_bytes: [u8; 8] = key.as_slice().try_into().map_err(|_| {
1823 page_integrity(
1824 root_page,
1825 Some(name.clone()),
1826 "physical entity key must contain eight bytes",
1827 )
1828 })?;
1829 let record = reddb_engine::storage::collection_read::EncodedRecord {
1830 id: EntityId::new(u64::from_be_bytes(key_bytes)),
1831 bytes: value,
1832 };
1833 let decoded = if let Some(limit) = self.config.resident_memory_bytes_max
1834 {
1835 record.decode_with_budget(
1836 self.format_version(),
1837 usize::try_from(limit).unwrap_or(usize::MAX),
1838 )
1839 } else {
1840 record.decode(self.format_version())
1841 };
1842 let (entity, metadata) = decoded.map_err(|err| {
1843 page_integrity(root_page, Some(name.clone()), err.to_string())
1844 })?;
1845 if let Some(m) = &manager {
1846 let id = entity.id;
1847 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1848 m.register_row_id(*row_id);
1849 }
1850 if self.config.resident_memory_bytes_max.is_some() {
1851 self.register_entity_id(id);
1852 m.restore_sequence_id_next(
1853 entity.sequence_id.saturating_add(1),
1854 );
1855 m.restore_identity_counters(
1856 id.raw().saturating_add(1),
1857 m.identity_counters().1,
1858 );
1859 continue;
1860 }
1861 self.resident.context_index.index_entity(&name, &entity);
1862 m.restore_snapshot_entity(entity.clone())?;
1863 if let Some(metadata) = metadata {
1864 let _ = m.set_metadata(id, metadata);
1865 }
1866 self.register_entity_id(id);
1867 if self.config.auto_index_refs {
1868 self.index_cross_refs(&entity, &name)?;
1869 }
1870 }
1871 }
1872
1873 let btree = Arc::new(btree);
1875 if shared_alias.is_some_and(|collision| collision.resume_counter_drift) {
1876 persisted_entities = u64::try_from(btree.count().map_err(|err| {
1877 page_integrity(root_page, Some(name.clone()), err.to_string())
1878 })?)
1879 .map_err(|_| {
1880 page_integrity(
1881 root_page,
1882 Some(name.clone()),
1883 "shared collection entity count exceeds u64",
1884 )
1885 })?;
1886 }
1887 let key_tree = counters
1888 .and_then(|state| state.key_index_root)
1889 .map(|root| Arc::new(BTree::with_root(Arc::clone(pager), root)));
1890 if let Some(counters) = counters {
1891 self.key_indices.write().insert(
1892 name.clone(),
1893 PublishedKeyIndex {
1894 indexed: key_tree.is_some(),
1895 secondary: counters.secondary_indices.iter().map(|(name, index)| {
1896 (name.clone(), super::secondary_index::PublishedSecondaryIndex {
1897 identity: super::secondary_index::SecondaryIndexIdentity {
1898 codec_version: index.codec_version,
1899 definition: index.definition,
1900 },
1901 tree: Arc::new(BTree::with_root(Arc::clone(pager), index.root_page)),
1902 })
1903 }).collect(),
1904 source: Arc::downgrade(&btree),
1905 tree: key_tree.clone().unwrap_or_else(|| {
1906 Arc::new(BTree::with_root(Arc::clone(pager), 0))
1907 }),
1908 },
1909 );
1910 }
1911 if let (Some(manager), Some(limit)) =
1912 (&manager, self.config.resident_memory_bytes_max)
1913 {
1914 manager.install_backing(
1915 Arc::new(
1916 match key_tree {
1917 Some(keys) => reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(&btree), keys),
1918 None => reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(&btree)),
1919 },
1920 ),
1921 btree.root_page_id(),
1922 format_version,
1923 usize::try_from(limit).unwrap_or(usize::MAX),
1924 persisted_entities,
1925 );
1926 }
1927 self.btree_indices.write().insert(name.clone(), btree);
1928 if format_version == STORE_VERSION_V11 {
1929 if let Some(manager) = &manager {
1930 self.checkpoint_versions.write().insert(
1931 name,
1932 CheckpointVersion {
1933 manager: Arc::downgrade(manager),
1934 segments: manager.checkpoint_revisions(),
1935 visibility_revision: 0,
1936 untracked_visibility_revision: 0,
1937 excluded_xids: Arc::default(),
1938 pending_vault: Arc::default(),
1939 format_version,
1940 deferred_rows: Some(Arc::default()),
1941 has_unprojected_rows: false,
1942 },
1943 );
1944 }
1945 }
1946 }
1947 }
1948
1949 if retain_canonical_system_config && recovery_config_roots != [1, 1] {
1950 return Err(page_integrity(
1951 1,
1952 None,
1953 "canonical config recovery requires exactly one root for each identity",
1954 ));
1955 }
1956 if retain_canonical_system_config
1957 && (format_version < STORE_VERSION_V2 || pos + 4 > content.len())
1958 {
1959 return Err(page_integrity(
1960 1,
1961 None,
1962 "canonical config recovery requires a complete cross-reference catalog",
1963 ));
1964 }
1965 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1966 let cross_ref_count = u32::from_le_bytes([
1967 content[pos],
1968 content[pos + 1],
1969 content[pos + 2],
1970 content[pos + 3],
1971 ]) as usize;
1972 pos += 4;
1973
1974 for _ in 0..cross_ref_count {
1975 let cross_ref =
1976 match reddb_file::decode_native_paged_cross_ref(content, &mut pos) {
1977 Ok(cross_ref) => cross_ref,
1978 Err(error) if retain_canonical_system_config => {
1979 return Err(page_integrity(1, None, error.to_string()));
1980 }
1981 Err(_) => break,
1982 };
1983 if retain_canonical_system_config
1984 && cross_ref.target_collection == "red_config"
1985 {
1986 return Err(page_integrity(
1987 1,
1988 None,
1989 "legacy red_config has cross-references and cannot be retired",
1990 ));
1991 }
1992 let source_id = EntityId::new(cross_ref.source_id);
1993 let target_id = EntityId::new(cross_ref.target_id);
1994 let ref_type = RefType::from_byte(cross_ref.ref_type);
1995 let target_collection =
1996 reddb_engine::storage::system_collection_names::canonical_owned(
1997 cross_ref.target_collection,
1998 );
1999
2000 self.resident
2001 .cross_refs
2002 .write()
2003 .entry(source_id)
2004 .or_default()
2005 .push((target_id, ref_type, target_collection.clone()));
2006
2007 if let Some((collection, mut entity)) = self.get_any(source_id) {
2008 let exists = entity.cross_refs().iter().any(|xref| {
2009 xref.target == target_id
2010 && xref.ref_type == ref_type
2011 && xref.target_collection == target_collection
2012 });
2013 if !exists {
2014 entity.cross_refs_mut().push(CrossRef::new(
2015 source_id,
2016 target_id,
2017 target_collection.clone(),
2018 ref_type,
2019 ));
2020 if let Some(manager) = self.get_collection(&collection) {
2021 let _ = manager.update(entity);
2022 }
2023 }
2024 }
2025 }
2026 }
2027 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
2028 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2029 let already_retired = retired
2030 .iter()
2031 .copied()
2032 .collect::<std::collections::HashSet<_>>();
2033 let mut live_roots: std::collections::HashSet<_> = self
2034 .btree_indices
2035 .read()
2036 .values()
2037 .map(|tree| tree.root_page_id())
2038 .collect();
2039 live_roots.extend(
2040 self.key_indices
2041 .read()
2042 .values()
2043 .flat_map(PublishedKeyIndex::trees)
2044 .map(|tree| tree.root_page_id()),
2045 );
2046 for root in retired {
2047 if live_roots.contains(&root)
2048 || root
2049 >= pager
2050 .page_count()
2051 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
2052 {
2053 return Err(page_integrity(
2054 root,
2055 None,
2056 "retired root aliases a live or missing page",
2057 ));
2058 }
2059 self.retired_btrees.lock().push(RetiredBTree {
2060 tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
2061 published: true,
2062 });
2063 }
2064 for root in empty_duplicate_roots
2065 .iter()
2066 .copied()
2067 .filter(|root| !already_retired.contains(root))
2068 {
2069 self.retire_btree(Arc::new(BTree::with_root(Arc::clone(pager), root)));
2070 }
2071 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
2072 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2073 if deferred
2074 .iter()
2075 .any(|id| live_roots.contains(id) || empty_duplicate_roots.contains(id))
2076 {
2077 return Err(page_integrity(1, None, "deferred page aliases a live root"));
2078 }
2079 pager
2080 .restore_deferred_reclamation(&deferred)
2081 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2082 let ownership_epoch =
2083 reddb_file::native_store::decode_native_paged_ownership_epoch(
2084 content, &mut pos,
2085 )
2086 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2087 self.ownership_catalog_epoch
2088 .store(ownership_epoch.unwrap_or(0), Ordering::Release);
2089 if migrated_catalog {
2090 self.mark_paged_registry_dirty();
2091 }
2092 if self.format_version() < STORE_VERSION_V11 {
2093 self.set_format_version(STORE_VERSION_V11);
2094 }
2095 outcome.migrated = migrated_catalog;
2096 return Ok(outcome);
2097 }
2098 }
2099
2100 if retain_canonical_system_config {
2101 return Err(page_integrity(
2102 1,
2103 None,
2104 "canonical config recovery requires a complete catalog",
2105 ));
2106 }
2107 if self.format_version() < STORE_VERSION_V11 {
2108 self.set_format_version(STORE_VERSION_V11);
2109 }
2110
2111 Ok(CatalogLoadOutcome::default())
2112 }
2113
2114 pub(crate) fn deserialize_entity(
2116 data: &[u8],
2117 format_version: u32,
2118 ) -> Result<UnifiedEntity, StoreError> {
2119 let mut pos = 0;
2120 Self::read_entity_binary(data, &mut pos, format_version)
2121 .map_err(|e| StoreError::Serialization(e.to_string()))
2122 }
2123
2124 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
2126 let mut buf = Vec::with_capacity(256);
2131 Self::write_entity_binary(&mut buf, entity, format_version);
2132 buf
2133 }
2134
2135 pub(crate) fn serialize_entity_record(
2136 entity: &UnifiedEntity,
2137 metadata: Option<&Metadata>,
2138 format_version: u32,
2139 ) -> Vec<u8> {
2140 reddb_engine::storage::metadata_codec::serialize_entity_record(
2141 entity,
2142 metadata,
2143 format_version,
2144 )
2145 }
2146
2147 pub(crate) fn deserialize_entity_record(
2148 data: &[u8],
2149 format_version: u32,
2150 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
2151 reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
2152 .map_err(StoreError::Serialization)
2153 }
2154
2155 pub fn persist(&self) -> Result<(), StoreError> {
2160 self.persist_inner(false)
2161 }
2162
2163 pub(crate) fn persist_after_runtime_snapshots(&self) -> Result<(), StoreError> {
2164 self.persist_inner(true)
2165 }
2166
2167 fn persist_inner(&self, runtime_snapshots_durable: bool) -> Result<(), StoreError> {
2168 let _vault_guard = self.vault_publication_lock.lock();
2169 let pending_vault_versions = self.pending_vault_versions.read();
2170 let pager = match &self.pager {
2171 Some(p) => p,
2172 None => {
2173 if let Some(path) = &self.db_path {
2175 return self
2176 .save_to_file(path)
2177 .map_err(|e| StoreError::Serialization(e.to_string()));
2178 }
2179 return Err(StoreError::Io(std::io::Error::other(
2180 "No pager or path configured for persistence",
2181 )));
2182 }
2183 };
2184 let _ownership_guard = pager.lock_ownership_mutations();
2187
2188 match pager.read_page(1) {
2189 Ok(_) => {}
2190 Err(PagerError::PageNotFound(_)) => {
2191 let meta_page = pager
2192 .allocate_page(crate::storage::engine::PageType::Header)
2193 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2194 pager
2195 .write_page(meta_page.page_id(), meta_page)
2196 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2197 }
2198 Err(e) => {
2199 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
2200 }
2201 }
2202
2203 if let Some(commit) = &self.commit {
2204 commit.force_sync().map_err(StoreError::Io)?;
2205 }
2206
2207 struct InterruptedOwnership<'a> {
2208 pager: &'a Pager,
2209 active: bool,
2210 }
2211 impl Drop for InterruptedOwnership<'_> {
2212 fn drop(&mut self) {
2213 if self.active {
2214 self.pager.fence_interrupted_ownership();
2215 }
2216 }
2217 }
2218 let mut ownership = InterruptedOwnership {
2219 pager,
2220 active: self.commit.is_some(),
2221 };
2222 let epoch = if let Some(commit) = &self.commit {
2223 let epoch = commit
2224 .next_page_ownership_epoch(self)
2225 .map_err(StoreError::Io)?;
2226 commit
2227 .begin_page_ownership_epoch(self, epoch)
2228 .map_err(StoreError::Io)?;
2229 Some(epoch)
2230 } else {
2231 None
2232 };
2233
2234 let collections = self.resident.collections.read();
2235 let mut btree_indices = self.btree_indices.write();
2236
2237 let mut collection_roots: Vec<(String, u32)> = Vec::new();
2239
2240 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
2243 let previous_versions = self.checkpoint_versions.read();
2244 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
2245 let pending_vault = Arc::new(pending_vault_versions.clone());
2246 let (visibility_revision, excluded, untracked_visibility_revision) =
2247 self.snapshot_manager.get().map_or_else(
2248 || (0, Default::default(), 0),
2249 |manager| manager.checkpoint_projection_state(),
2250 );
2251 let excluded = Arc::new(excluded);
2252 for (name, manager) in collections.iter() {
2253 let mut version = CheckpointVersion {
2254 manager: Arc::downgrade(manager),
2255 segments: manager.checkpoint_revisions(),
2256 visibility_revision,
2257 untracked_visibility_revision,
2258 excluded_xids: Arc::clone(&excluded),
2259 pending_vault: Arc::clone(&pending_vault),
2260 format_version: self.format_version(),
2261 deferred_rows: Some(Arc::default()),
2262 has_unprojected_rows: false,
2263 };
2264 if let (Some(previous), Some(tree)) =
2265 (previous_versions.get(name), btree_indices.get(name))
2266 {
2267 if previous.manager.ptr_eq(&version.manager)
2268 && previous.segments == version.segments
2269 && previous.format_version == version.format_version
2270 && !previous.has_unprojected_rows
2271 && checkpoint_visibility_matches(previous, &version)
2272 {
2273 version.deferred_rows = previous.deferred_rows.clone();
2274 collection_roots.push((name.clone(), tree.root_page_id()));
2275 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
2276 checkpoint_versions.insert(name.clone(), version);
2277 continue;
2278 }
2279 }
2280 if let (Some(previous), Some(tree)) =
2281 (previous_versions.get(name), btree_indices.get(name))
2282 {
2283 if previous.manager.ptr_eq(&version.manager)
2284 && previous.format_version == version.format_version
2285 {
2286 if let Some(tree) =
2287 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
2288 {
2289 collection_roots.push((name.clone(), tree.root_page_id()));
2290 rebuilt_indices.insert(name.clone(), tree);
2291 checkpoint_versions.insert(name.clone(), version);
2292 continue;
2293 }
2294 }
2295 }
2296 let btree = self.checkpoint_tree(
2297 pager,
2298 manager,
2299 btree_indices.get(name),
2300 |entity, metadata| {
2301 #[cfg(test)]
2302 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
2303 let hidden = pending_vault_versions.contains(&entity.id)
2304 || (matches!(
2305 entity.data,
2306 EntityData::Row(_)
2307 | EntityData::Vector(_)
2308 | EntityData::QueueMessage(_)
2309 ) && excluded.contains(&entity.xmin));
2310 let normalize_xmax = matches!(
2311 entity.data,
2312 EntityData::Row(_) | EntityData::QueueMessage(_)
2313 ) && excluded.contains(&entity.xmax);
2314 if hidden || normalize_xmax {
2315 version.remember_deferred_row(entity.id);
2316 }
2317 if hidden {
2318 return None;
2319 }
2320 let mut normalized = std::borrow::Cow::Borrowed(entity);
2321 if normalize_xmax {
2322 normalized.to_mut().xmax = 0;
2323 }
2324 Some(Self::serialize_entity_record(
2325 &normalized,
2326 metadata.as_ref(),
2327 self.format_version(),
2328 ))
2329 },
2330 )?;
2331 checkpoint_versions.insert(name.clone(), version);
2332
2333 collection_roots.push((name.clone(), btree.root_page_id()));
2334 rebuilt_indices.insert(name.clone(), btree);
2335 }
2336
2337 let mut meta_data = Vec::with_capacity(4096);
2339
2340 let format_version = STORE_VERSION_V11;
2341 self.set_format_version(format_version);
2342
2343 reddb_file::encode_native_paged_metadata_header(
2344 &mut meta_data,
2345 reddb_file::NativePagedMetadataHeader {
2346 format_version,
2347 collection_count: collection_roots.len() as u32,
2348 },
2349 );
2350
2351 for (name, root_page) in &collection_roots {
2353 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
2354 }
2355
2356 let cross_refs = self.resident.cross_refs.read();
2358 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
2359 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
2360 for (source_id, refs) in cross_refs.iter() {
2361 for (target_id, ref_type, collection) in refs {
2362 reddb_file::encode_native_paged_cross_ref(
2363 &mut meta_data,
2364 source_id.raw(),
2365 target_id.raw(),
2366 ref_type.to_byte(),
2367 collection,
2368 );
2369 }
2370 }
2371
2372 for (name, previous) in btree_indices.iter() {
2373 if !rebuilt_indices
2374 .get(name)
2375 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
2376 {
2377 self.retire_btree(Arc::clone(previous));
2378 }
2379 }
2380 #[cfg(test)]
2381 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
2382 pager
2383 .sync()
2384 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2385 eprintln!("experimental checkpoint exit before manifest");
2386 std::process::exit(183);
2387 }
2388 let rebuilt_keys = self.checkpoint_key_indices(pager, &rebuilt_indices)?;
2389 #[cfg(test)]
2390 if std::env::var_os("REDDB_TEST_EXIT_AFTER_SECONDARY_INDEX_BUILD").is_some() {
2391 pager
2392 .sync()
2393 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2394 std::process::exit(196);
2395 }
2396 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices, &rebuilt_keys)?;
2397
2398 #[cfg(test)]
2399 if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
2400 std::process::exit(195);
2401 }
2402
2403 *btree_indices = rebuilt_indices;
2406 *self.key_indices.write() = rebuilt_keys;
2407 drop(previous_versions);
2408 *self.checkpoint_versions.write() = checkpoint_versions;
2409
2410 if let Some(commit) = &self.commit {
2411 commit
2412 .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
2413 .map_err(StoreError::Io)?;
2414 ownership.active = false;
2415 if runtime_snapshots_durable {
2416 commit
2417 .truncate_after_runtime_checkpoint()
2418 .map_err(StoreError::Io)?;
2419 } else {
2420 commit.truncate().map_err(StoreError::Io)?;
2421 }
2422 }
2423 *self.checkpoint_durable_rows.write() = Default::default();
2424
2425 if let Some(limit) = self.config.resident_memory_bytes_max {
2426 let mut versions = self.checkpoint_versions.write();
2427 for (name, manager) in collections.iter() {
2428 if versions
2429 .get(name)
2430 .is_some_and(|version| !version.has_unprojected_rows)
2431 {
2432 if let Some(tree) = btree_indices.get(name) {
2433 manager.install_backing(
2434 Arc::new(
2435 {
2436 let keys = self.key_indices.read();
2437 let index = keys.get(name).expect("invariant: prepared checkpoint classifies its key index");
2438 if index.indexed {
2439 reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(tree), Arc::clone(&index.tree))
2440 } else {
2441 reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(tree))
2442 }
2443 },
2444 ),
2445 tree.root_page_id(),
2446 self.format_version(),
2447 usize::try_from(limit).unwrap_or(usize::MAX),
2448 manager.count() as u64,
2449 );
2450 if let Some(version) = versions.get_mut(name) {
2451 version.segments = manager.checkpoint_revisions();
2452 }
2453 }
2454 }
2455 }
2456 }
2457
2458 Ok(())
2459 }
2460
2461 pub fn is_paged(&self) -> bool {
2463 self.pager.is_some()
2464 }
2465
2466 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
2469 self.btree_indices
2470 .read()
2471 .get(Self::canonical_collection(collection))
2472 .map(|btree| btree.root_page_id())
2473 .filter(|root| *root != 0)
2474 }
2475
2476 pub(crate) fn operational_manifest_for_path(
2479 &self,
2480 path: &Path,
2481 ) -> reddb_file::OperationalManifest {
2482 let mut cached = self.operational_manifest.lock();
2483 if let Some((current, manifest)) = cached.as_ref() {
2484 if current == path {
2485 return manifest.clone();
2486 }
2487 }
2488 let manifest = reddb_file::OperationalManifest::for_db_path(path);
2489 *cached = Some((path.to_owned(), manifest.clone()));
2490 manifest
2491 }
2492
2493 pub fn db_path(&self) -> Option<&Path> {
2495 self.db_path.as_deref()
2496 }
2497}
2498
2499#[cfg(test)]
2500mod checkpoint_work_tests {
2501 use super::*;
2502
2503 struct SystemNameProjection;
2504
2505 impl super::super::secondary_index::SecondaryIndexProjector for SystemNameProjection {
2506 fn project(
2507 &self,
2508 entity: &UnifiedEntity,
2509 key: &mut [u8; super::super::secondary_index::SECONDARY_KEY_BYTES_MAX],
2510 ) -> Result<Option<usize>, StoreError> {
2511 key[..8].copy_from_slice(&entity.id.raw().to_be_bytes());
2512 Ok(Some(8))
2513 }
2514 }
2515
2516 #[cfg(target_os = "linux")]
2517 #[test]
2518 fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
2519 const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
2520 if let Some(path) = std::env::var_os(CHILD) {
2521 let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
2522 let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
2523 let store = UnifiedStore::open(path).expect("open store");
2524 let initial_count = if multi { 600 } else { 1 };
2525 for id in 1..=initial_count {
2526 store.insert_auto("rows", row(id, 1)).expect("seed");
2527 }
2528 store.persist().expect("baseline");
2529 let manager = store.get_collection("rows").expect("collection");
2530 let ids: Vec<u64> = if multi {
2531 (1..=initial_count).collect()
2532 } else {
2533 vec![if insert { 2 } else { 1 }]
2534 };
2535 let mut actions = Vec::new();
2536 for id in ids {
2537 if insert {
2538 manager.insert(row(id, 9)).expect("insert version");
2539 } else {
2540 manager.update(row(id, 9)).expect("update version");
2541 }
2542 let entity = manager.get(EntityId::new(id)).expect("changed entity");
2543 actions.push(StoreWalAction::upsert_entity(
2544 "rows",
2545 &entity,
2546 None,
2547 store.format_version(),
2548 ));
2549 }
2550 let references = store
2551 .commit
2552 .as_ref()
2553 .expect("coordinator")
2554 .append_checkpoint_actions(&actions)
2555 .expect("durable WAL");
2556 store.remember_checkpoint_rows(actions, references);
2557 store.persist().expect("exit after sparse write");
2558 panic!("inline sparse crash hook was not reached");
2559 }
2560 for (insert, multi) in [(false, false), (true, false), (false, true)] {
2561 let directory = tempfile::tempdir().expect("temporary store");
2562 let path = directory.path().join("data.rdb");
2563 let mut command =
2564 std::process::Command::new(std::env::current_exe().expect("test executable"));
2565 command
2566 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
2567 .env(CHILD, &path)
2568 .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
2569 if insert {
2570 command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
2571 }
2572 if multi {
2573 command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
2574 }
2575 let output = command.output().expect("crash child");
2576 assert_eq!(
2577 output.status.code(),
2578 Some(190),
2579 "insert={insert} multi={multi}: {}",
2580 String::from_utf8_lossy(&output.stderr)
2581 );
2582 let recovered = UnifiedStore::open(&path).expect("recover");
2583 let ids: Vec<u64> = if multi {
2584 (1..=600).collect()
2585 } else {
2586 vec![if insert { 2 } else { 1 }]
2587 };
2588 for id in ids {
2589 assert_eq!(
2590 recovered
2591 .get("rows", EntityId::new(id))
2592 .expect("recovered row")
2593 .data
2594 .as_row()
2595 .expect("row")
2596 .columns,
2597 vec![Value::Integer(9)]
2598 );
2599 }
2600 assert_eq!(
2601 recovered.get_collection("rows").expect("rows").count(),
2602 if multi {
2603 600
2604 } else if insert {
2605 2
2606 } else {
2607 1
2608 }
2609 );
2610 }
2611 }
2612
2613 #[test]
2614 fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
2615 let directory = tempfile::tempdir().expect("temporary store");
2616 let path = directory.path().join("data.rdb");
2617 {
2618 let store = UnifiedStore::open(&path).expect("open store");
2619 store.create_collection("rows").expect("create collection");
2620 store.persist().expect("publish ownership epoch");
2621 assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
2622 store.persist().expect("publish next ownership epoch");
2623 }
2624 let reopened = UnifiedStore::open(&path).expect("reopen store");
2625 assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
2626 }
2627
2628 #[test]
2629 fn metadata_overflow_with_missing_continuation_fails_closed() {
2630 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
2631
2632 let directory = tempfile::tempdir().expect("temporary store");
2633 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
2634 let pager = store.pager.as_ref().unwrap();
2635 let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
2636 let valid_manifest = build_meta_page1_with_overflow(pager, &valid_payload).unwrap();
2637 pager.write_page(1, valid_manifest).unwrap();
2638 assert!(!existing_overflow_chain(pager).unwrap().is_empty());
2639 assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
2640
2641 let missing = pager.page_count().unwrap() + 100;
2642 let mut manifest = Page::new(PageType::Header, 1);
2643 reddb_file::encode_native_metadata_overflow_header(
2644 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2645 reddb_file::NativeMetadataOverflowHeader {
2646 format_version: STORE_VERSION_V11,
2647 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2648 next_overflow_page_id: missing,
2649 },
2650 )
2651 .unwrap();
2652 pager.write_page(1, manifest).unwrap();
2653
2654 assert!(
2655 existing_overflow_chain(pager).is_err(),
2656 "a missing continuation must not be treated as an empty chain"
2657 );
2658
2659 let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
2660 let mut manifest = Page::new(PageType::Header, 1);
2661 reddb_file::encode_native_metadata_overflow_header(
2662 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2663 reddb_file::NativeMetadataOverflowHeader {
2664 format_version: STORE_VERSION_V11,
2665 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2666 next_overflow_page_id: wrong_type.page_id(),
2667 },
2668 )
2669 .unwrap();
2670 pager.write_page(1, manifest).unwrap();
2671 assert!(
2672 existing_overflow_chain(pager).is_err(),
2673 "a live page must not be mistaken for metadata overflow"
2674 );
2675 assert!(
2676 read_meta_payload(pager).is_err(),
2677 "a live page must not be read as metadata overflow"
2678 );
2679
2680 let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
2681 let cycle_id = cycle.page_id();
2682 reddb_file::encode_native_metadata_overflow_continuation_header(
2683 &mut cycle.as_bytes_mut()[HEADER_SIZE..],
2684 reddb_file::NativeMetadataOverflowContinuationHeader {
2685 next_overflow_page_id: cycle_id,
2686 chunk_bytes: 1,
2687 },
2688 )
2689 .unwrap();
2690 pager.write_page(cycle_id, cycle).unwrap();
2691 let mut manifest = Page::new(PageType::Header, 1);
2692 reddb_file::encode_native_metadata_overflow_header(
2693 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2694 reddb_file::NativeMetadataOverflowHeader {
2695 format_version: STORE_VERSION_V11,
2696 total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
2697 next_overflow_page_id: cycle_id,
2698 },
2699 )
2700 .unwrap();
2701 pager.write_page(1, manifest).unwrap();
2702 assert!(
2703 read_meta_payload(pager).is_err(),
2704 "cyclic overflow must not be accepted"
2705 );
2706
2707 let mut manifest = Page::new(PageType::Header, 1);
2708 reddb_file::encode_native_metadata_overflow_header(
2709 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2710 reddb_file::NativeMetadataOverflowHeader {
2711 format_version: STORE_VERSION_V11,
2712 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2713 next_overflow_page_id: 0,
2714 },
2715 )
2716 .unwrap();
2717 pager.write_page(1, manifest).unwrap();
2718 assert!(
2719 read_meta_payload(pager).is_err(),
2720 "a truncated metadata payload must not be accepted"
2721 );
2722 }
2723
2724 #[test]
2725 fn retired_tree_pages_remain_pinned_by_a_cursor() {
2726 let directory = tempfile::tempdir().expect("temporary store");
2727 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2728 for id in 1..=100 {
2729 store.insert_auto("rows", row(id, 1)).expect("seed");
2730 }
2731 store.persist().expect("initial checkpoint");
2732 let tree = store
2733 .btree_indices
2734 .read()
2735 .get("rows")
2736 .cloned()
2737 .expect("tree");
2738 let mut cursor = tree.cursor_first().expect("cursor");
2739 drop(tree);
2740 let manager = store.get_collection("rows").expect("rows");
2741 for value in 2..=6 {
2742 manager.update(row(1, value)).expect("change");
2743 store.persist().expect("checkpoint with old reader");
2744 }
2745 let mut count = 0;
2746 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
2747 let (entity, _) =
2748 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2749 .expect("decode");
2750 assert_eq!(
2751 entity.data.as_row().expect("row").columns,
2752 [Value::Integer(1)]
2753 );
2754 count += 1;
2755 }
2756 assert_eq!(count, 100);
2757 drop(cursor);
2758 store.persist().expect("reclaim after reader release");
2759 assert!(store.retired_btrees.lock().is_empty());
2760 }
2761
2762 #[test]
2763 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
2764 let directory = tempfile::tempdir().expect("temporary store");
2765 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2766 store.create_collection("rows").expect("collection");
2767 let manager = store.get_collection("rows").expect("rows");
2768 for id in (1..=1200).rev() {
2769 manager
2770 .insert(UnifiedEntity::table_row(
2771 EntityId::new(id),
2772 "rows",
2773 id,
2774 vec![Value::text("x".repeat(4096))],
2775 ))
2776 .expect("seed");
2777 }
2778 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
2779 store.persist().expect("bounded checkpoint");
2780 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
2781 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
2782 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
2783 }
2784
2785 #[test]
2786 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
2787 let directory = tempfile::tempdir().expect("temporary store");
2788 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2789 store.create_collection("rows").expect("collection");
2790 for id in 1..=40 {
2791 store
2792 .insert(
2793 "rows",
2794 UnifiedEntity::table_row(
2795 EntityId::new(id),
2796 "rows",
2797 id,
2798 vec![Value::Integer(id as i64)],
2799 ),
2800 )
2801 .expect("seed row");
2802 }
2803 store.persist().expect("initial checkpoint");
2804 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2805 store.persist().expect("unchanged checkpoint");
2806 assert_eq!(
2807 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
2808 0,
2809 "unchanged collections must not hold the writer fence while serializing every row"
2810 );
2811 drop(store);
2812 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
2813 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2814 reopened.persist().expect("unchanged recovered generation");
2815 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2816 }
2817
2818 #[test]
2819 fn sparse_wal_replay_does_not_reencode_complete_collections() {
2820 let directory = tempfile::tempdir().expect("temporary store");
2821 let path = directory.path().join("data.rdb");
2822 let store = UnifiedStore::open(&path).expect("paged store");
2823 for (name, first) in [("rows", 1), ("cold", 129)] {
2824 store.create_collection(name).expect("collection");
2825 for id in first..first + 128 {
2826 store
2827 .insert(
2828 name,
2829 UnifiedEntity::table_row(
2830 EntityId::new(id),
2831 name,
2832 id,
2833 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2834 ),
2835 )
2836 .expect("seed");
2837 }
2838 }
2839 store.persist().expect("durable baseline");
2840 let action = super::super::commit::StoreWalAction::upsert_entity(
2841 "rows",
2842 &row(1, 42),
2843 None,
2844 STORE_VERSION_V11,
2845 );
2846 store
2848 .commit
2849 .as_ref()
2850 .unwrap()
2851 .append_actions(&[action])
2852 .unwrap();
2853 drop(store);
2854
2855 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2856 let recovered = UnifiedStore::open(&path).expect("replay committed change");
2857 assert_eq!(
2858 recovered
2859 .get("rows", EntityId::new(1))
2860 .unwrap()
2861 .data
2862 .as_row()
2863 .unwrap()
2864 .columns,
2865 vec![Value::Integer(42)]
2866 );
2867 for name in ["rows", "cold"] {
2868 assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
2869 }
2870 assert_eq!(
2871 REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
2872 0,
2873 "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
2874 );
2875 for (name, first) in [("rows", 2), ("cold", 129)] {
2876 let end = if name == "rows" { 129 } else { 257 };
2877 for id in first..end {
2878 assert_eq!(
2879 recovered
2880 .get(name, EntityId::new(id))
2881 .unwrap()
2882 .data
2883 .as_row()
2884 .unwrap()
2885 .columns,
2886 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2887 );
2888 }
2889 }
2890
2891 drop(recovered);
2892 let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
2893 assert_eq!(
2894 reopened
2895 .get("rows", EntityId::new(1))
2896 .unwrap()
2897 .data
2898 .as_row()
2899 .unwrap()
2900 .columns,
2901 vec![Value::Integer(42)]
2902 );
2903 }
2904
2905 fn replay_test_payload(bytes: usize) -> String {
2906 let mut state = 0x1234_5678_u32;
2907 (0..bytes)
2908 .map(|_| {
2909 state ^= state << 13;
2910 state ^= state >> 17;
2911 state ^= state << 5;
2912 char::from(b'!' + (state % 90) as u8)
2913 })
2914 .collect()
2915 }
2916
2917 #[test]
2918 fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
2919 let directory = tempfile::tempdir().unwrap();
2920 let path = directory.path().join("data.rdb");
2921 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
2922 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2923 for id in 1..=16 {
2924 store.insert_auto("rows", row(id, 1)).unwrap();
2925 }
2926 store.persist().unwrap();
2927 let payload = replay_test_payload(64 * 1024);
2929 let changed =
2930 UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
2931 let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2932 store
2933 .commit
2934 .as_ref()
2935 .unwrap()
2936 .append_actions(&[action])
2937 .unwrap();
2938 drop(store);
2939 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2940 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
2941 assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
2942 assert_eq!(
2943 recovered
2944 .get("rows", EntityId::new(1))
2945 .unwrap()
2946 .data
2947 .as_row()
2948 .unwrap()
2949 .columns,
2950 changed.data.as_row().unwrap().columns
2951 );
2952 drop(recovered);
2953 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
2954 assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
2955 assert_eq!(
2956 reopened
2957 .get("rows", EntityId::new(1))
2958 .unwrap()
2959 .data
2960 .as_row()
2961 .unwrap()
2962 .columns,
2963 changed.data.as_row().unwrap().columns
2964 );
2965 }
2966
2967 #[cfg(target_os = "linux")]
2968 #[test]
2969 fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
2970 const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
2971 if let Some(path) = std::env::var_os(CHILD) {
2972 let _store = UnifiedStore::open(path).unwrap();
2973 panic!("replay crash hook was not reached");
2974 }
2975 for overflow in [false, true] {
2976 for (hook, code) in [
2977 ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
2978 ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
2979 ] {
2980 let directory = tempfile::tempdir().unwrap();
2981 let path = directory.path().join("data.rdb");
2982 let store = UnifiedStore::open(&path).unwrap();
2983 for id in 1..=128 {
2984 store.insert_auto("rows", row(id, 1)).unwrap();
2985 }
2986 store.persist().unwrap();
2987 let changed = if overflow {
2988 UnifiedEntity::table_row(
2989 EntityId::new(1),
2990 "rows",
2991 1,
2992 vec![Value::text(replay_test_payload(16 * 1024))],
2993 )
2994 } else {
2995 row(1, 42)
2996 };
2997 let action =
2998 StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2999 store
3000 .commit
3001 .as_ref()
3002 .unwrap()
3003 .append_actions(&[action])
3004 .unwrap();
3005 drop(store);
3006 let mut first_size = 0;
3007 for cycle in 0..12 {
3008 let output = std::process::Command::new(std::env::current_exe().unwrap())
3009 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
3010 .env(CHILD, &path).env(hook, "1").output().unwrap();
3011 assert_eq!(
3012 output.status.code(),
3013 Some(code),
3014 "overflow={overflow} hook={hook} cycle={cycle}: {}",
3015 String::from_utf8_lossy(&output.stderr)
3016 );
3017 let size = std::fs::metadata(&path).unwrap().len();
3018 if cycle == 0 {
3019 first_size = size;
3020 }
3021 assert!(
3022 size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
3023 "repeated recovery must reuse reservations: {first_size} -> {size}"
3024 );
3025 }
3026 for _ in 0..2 {
3027 let recovered = UnifiedStore::open(&path).unwrap();
3028 assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
3029 for id in 1..=128 {
3030 let expected = if id == 1 { changed.clone() } else { row(id, 1) };
3031 assert_eq!(
3032 recovered
3033 .get("rows", EntityId::new(id))
3034 .unwrap()
3035 .data
3036 .as_row()
3037 .unwrap()
3038 .columns,
3039 expected.data.as_row().unwrap().columns
3040 );
3041 }
3042 }
3043 }
3044 }
3045 }
3046
3047 #[test]
3048 fn paged_reopen_preserves_stored_sequence_identity() {
3049 let directory = tempfile::tempdir().expect("temporary store");
3050 let path = directory.path().join("data.rdb");
3051 let store = UnifiedStore::open(&path).expect("paged store");
3052 store.create_collection("rows").expect("collection");
3053 for id in [30, 10, 20] {
3054 store.insert("rows", row(id, id as i64)).expect("insert");
3055 }
3056 store.persist().expect("checkpoint");
3057 let sequences = [10, 20, 30].map(|id| {
3058 store
3059 .get("rows", EntityId::new(id))
3060 .expect("row")
3061 .sequence_id
3062 });
3063 drop(store);
3064 let reopened = UnifiedStore::open(&path).expect("reopen");
3065 let recovered = [10, 20, 30].map(|id| {
3066 reopened
3067 .get("rows", EntityId::new(id))
3068 .expect("row")
3069 .sequence_id
3070 });
3071 assert_eq!(
3072 recovered, sequences,
3073 "opening a checkpoint must not rewrite stored row identities"
3074 );
3075 }
3076
3077 #[test]
3078 fn auto_insert_persists_the_assigned_sequence_identity() {
3079 let directory = tempfile::tempdir().expect("temporary store");
3080 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3081 for id in 1u64..=3 {
3082 store
3083 .insert_auto("rows", row(id, id as i64))
3084 .expect("insert");
3085 }
3086 let manager = store.get_collection("rows").expect("rows");
3087 let tree = store
3088 .btree_indices
3089 .read()
3090 .get("rows")
3091 .cloned()
3092 .expect("tree");
3093 for id in 1u64..=3 {
3094 let bytes = tree
3095 .get(&id.to_be_bytes())
3096 .expect("lookup")
3097 .expect("stored row");
3098 let (persisted, _) =
3099 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
3100 .expect("decode");
3101 assert_eq!(
3102 persisted.sequence_id,
3103 manager
3104 .get(EntityId::new(id))
3105 .expect("live row")
3106 .sequence_id
3107 );
3108 }
3109 }
3110
3111 #[test]
3112 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
3113 let directory = tempfile::tempdir().expect("temporary store");
3114 let path = directory.path().join("data.rdb");
3115 let store = UnifiedStore::open(&path).expect("paged store");
3116 store.insert_auto("rows", row(1, 1)).expect("seed");
3117 store.persist().expect("checkpoint");
3118 store.insert_auto("rows", row(2, 2)).expect("append");
3119 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3120 store.persist().expect("inline append checkpoint");
3121 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3122 store
3123 .get_collection("rows")
3124 .expect("rows")
3125 .update(row(1, 99))
3126 .expect("untracked edit");
3127 store.insert_auto("rows", row(3, 3)).expect("next append");
3128 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3129 store.persist().expect("untracked edit checkpoint");
3130 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
3131 drop(store);
3132 let reopened = UnifiedStore::open(&path).expect("reopen");
3133 assert_eq!(
3134 reopened
3135 .get("rows", EntityId::new(1))
3136 .expect("row")
3137 .data
3138 .as_row()
3139 .expect("table")
3140 .columns,
3141 vec![Value::Integer(99)]
3142 );
3143 }
3144
3145 #[test]
3146 fn replayed_trees_are_reusable_after_durable_reconciliation() {
3147 let directory = tempfile::tempdir().expect("temporary store");
3148 let path = directory.path().join("data.rdb");
3149 let store = UnifiedStore::open(&path).expect("paged store");
3150 store.insert_auto("rows", row(1, 1)).expect("seed");
3151 store.persist().expect("checkpoint");
3152 store
3153 .insert_auto("rows", row(2, 2))
3154 .expect("WAL-backed append");
3155 drop(store);
3156 let reopened = UnifiedStore::open(&path).expect("replay");
3157 assert!(reopened.get("rows", EntityId::new(2)).is_some());
3158 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3159 reopened.persist().expect("checkpoint reconciled tree");
3160 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3161 }
3162
3163 #[test]
3164 fn legacy_system_catalog_and_wal_migrate_without_cold_boot_rebuild() {
3165 use crate::storage::wal::{WalRecord, WalWriter};
3166
3167 let directory = tempfile::tempdir().expect("temporary store");
3168 let path = directory.path().join("data.rdb");
3169 let config = UnifiedStoreConfig {
3170 page_cache_slots: Some(32),
3171 resident_memory_bytes_max: Some(2 * 1024 * 1024),
3172 ..UnifiedStoreConfig::default()
3173 };
3174 let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3175 codec_version: 1,
3176 definition: [91; 32],
3177 };
3178 let baseline_index_root;
3179 {
3180 let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3181 store
3182 .create_collection("red_config")
3183 .expect("system collection");
3184 store
3185 .insert_auto(
3186 "red_config",
3187 UnifiedEntity::table_row(
3188 EntityId::new(1),
3189 "red_config",
3190 1,
3191 vec![Value::text("checkpoint")],
3192 ),
3193 )
3194 .expect("legacy-compatible insert");
3195 store
3196 .register_secondary_index(
3197 "red_config",
3198 "by_id",
3199 index_identity.clone(),
3200 Arc::new(SystemNameProjection),
3201 )
3202 .expect("secondary index");
3203 store.persist().expect("baseline checkpoint");
3204 baseline_index_root = store
3205 .secondary_index("red.config", "by_id", &index_identity)
3206 .expect("published secondary index")
3207 .root_page_id();
3208 }
3209
3210 {
3214 let pager = Pager::open(
3215 &path,
3216 PagerConfig {
3217 disk_residency: true,
3218 ..PagerConfig::default()
3219 },
3220 )
3221 .expect("open pager");
3222 let mut page = pager.read_page(1).expect("catalog page");
3223 let bytes = page.as_bytes_mut();
3224 let canonical = b"red.config";
3225 let legacy = b"red_config";
3226 let mut replacements = 0;
3227 for offset in 0..=bytes.len() - canonical.len() {
3228 if &bytes[offset..offset + canonical.len()] == canonical {
3229 bytes[offset..offset + legacy.len()].copy_from_slice(legacy);
3230 replacements += 1;
3231 }
3232 }
3233 assert!(replacements >= 1, "catalog must contain the canonical name");
3234 pager.write_page(1, page).expect("write legacy catalog");
3235 pager.sync().expect("sync legacy catalog");
3236 }
3237 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3238 .rename_collections(&[("red.config", "red_config")])
3239 .expect("restore legacy operational name");
3240
3241 let wal_upsert = reddb_file::encode_store_wal_action_frame(
3244 &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
3245 collection: "red_config".to_string(),
3246 record: UnifiedStore::serialize_entity_record(
3247 &UnifiedEntity::table_row(
3248 EntityId::new(2),
3249 "red_config",
3250 2,
3251 vec![Value::text("legacy WAL")],
3252 ),
3253 None,
3254 STORE_VERSION_CURRENT,
3255 ),
3256 },
3257 )
3258 .expect("legacy WAL action");
3259 let wal_delete = reddb_file::encode_store_wal_action_frame(
3260 &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
3261 collection: "red_config".to_string(),
3262 entity_id: EntityId::new(1).raw(),
3263 },
3264 )
3265 .expect("legacy WAL delete");
3266 let wal_path = reddb_file::layout::unified_wal_path(&path);
3267 {
3268 let mut writer = WalWriter::open(&wal_path).expect("open WAL");
3269 writer
3270 .append(&WalRecord::TxCommitBatch {
3271 tx_id: 77,
3272 actions: vec![wal_upsert, wal_delete],
3273 })
3274 .expect("append legacy transaction");
3275 writer.sync().expect("sync legacy transaction");
3276 }
3277
3278 let (root_after_recovery, pages_after_recovery) = {
3279 let recovered =
3280 UnifiedStore::open_with_config(&path, config.clone()).expect("migrate and recover");
3281 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3282 assert!(
3283 recovered.get("red.config", EntityId::new(1)).is_none(),
3284 "legacy WAL deletes must target the canonical collection"
3285 );
3286 assert_eq!(
3287 recovered
3288 .get("red.config", EntityId::new(2))
3289 .expect("WAL row")
3290 .kind
3291 .collection(),
3292 "red.config"
3293 );
3294 let root = recovered
3295 .collection_root_page("red.config")
3296 .expect("published canonical root");
3297 assert_eq!(
3298 recovered
3299 .secondary_index("red.config", "by_id", &index_identity)
3300 .expect("recovered secondary index")
3301 .root_page_id(),
3302 baseline_index_root,
3303 "name migration must reuse the secondary-index root"
3304 );
3305 let pages = recovered
3306 .pager()
3307 .expect("pager")
3308 .page_count()
3309 .expect("page count");
3310 (root, pages)
3311 };
3312
3313 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3316 .rename_collections(&[("red.config", "red_config")])
3317 .expect("restore stale manifest identity");
3318
3319 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3320 let cold = UnifiedStore::open_with_config(&path, config).expect("second cold boot");
3321 assert_eq!(
3322 cold.collection_root_page("red.config"),
3323 Some(root_after_recovery),
3324 "an idempotent cold boot must reuse the published data root"
3325 );
3326 assert_eq!(
3327 cold.pager()
3328 .expect("pager")
3329 .page_count()
3330 .expect("page count"),
3331 pages_after_recovery,
3332 "cold boot must not allocate replacement index trees"
3333 );
3334 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3335 assert_eq!(
3336 cold.secondary_index("red.config", "by_id", &index_identity)
3337 .expect("secondary index after cold boot")
3338 .root_page_id(),
3339 baseline_index_root
3340 );
3341 assert!(
3342 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3343 .recover_or_bootstrap(&["red.config".to_string()])
3344 .expect("canonical manifest")
3345 .is_empty()
3346 );
3347 }
3348
3349 struct DuplicateSystemCatalogFixture {
3350 _directory: tempfile::TempDir,
3351 path: std::path::PathBuf,
3352 config: UnifiedStoreConfig,
3353 pages_before: u32,
3354 legacy_root: Option<u32>,
3355 legacy_key_root: Option<u32>,
3356 legacy_secondary_root: Option<u32>,
3357 index_identity: super::super::secondary_index::SecondaryIndexIdentity,
3358 }
3359
3360 fn duplicate_system_catalog_fixture(
3361 legacy_populated: bool,
3362 canonical_populated: bool,
3363 index_legacy: bool,
3364 ) -> DuplicateSystemCatalogFixture {
3365 duplicate_system_catalog_fixture_with_ref(
3366 legacy_populated,
3367 canonical_populated,
3368 index_legacy,
3369 false,
3370 )
3371 }
3372
3373 fn duplicate_system_catalog_fixture_with_ref(
3374 legacy_populated: bool,
3375 canonical_populated: bool,
3376 index_legacy: bool,
3377 cross_reference_legacy: bool,
3378 ) -> DuplicateSystemCatalogFixture {
3379 let directory = tempfile::tempdir().expect("temporary store");
3380 let path = directory.path().join("data.rdb");
3381 let config = UnifiedStoreConfig {
3382 page_cache_slots: Some(32),
3383 resident_memory_bytes_max: Some(2 * 1024 * 1024),
3384 ..UnifiedStoreConfig::default()
3385 };
3386 let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3387 codec_version: 1,
3388 definition: [73; 32],
3389 };
3390 let (legacy_root, legacy_key_root, legacy_secondary_root);
3391 {
3392 let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3393 store
3394 .create_collection("cfg_legacy")
3395 .expect("legacy source collection");
3396 if legacy_populated {
3397 store
3398 .insert_auto(
3399 "cfg_legacy",
3400 UnifiedEntity::table_row(
3401 EntityId::new(1),
3402 "red_config",
3403 1,
3404 vec![Value::text("legacy")],
3405 ),
3406 )
3407 .expect("legacy row");
3408 }
3409 store
3410 .create_collection("cfg.canon_")
3411 .expect("canonical source collection");
3412 if canonical_populated {
3413 store
3414 .insert_auto(
3415 "cfg.canon_",
3416 UnifiedEntity::table_row(
3417 EntityId::new(2),
3418 "red.config",
3419 2,
3420 vec![Value::text("canonical")],
3421 ),
3422 )
3423 .expect("canonical row");
3424 }
3425 if cross_reference_legacy {
3426 store
3427 .add_cross_ref(
3428 "cfg.canon_",
3429 EntityId::new(2),
3430 "cfg_legacy",
3431 EntityId::new(1),
3432 RefType::RowToNode,
3433 1.0,
3434 )
3435 .expect("reference to legacy config");
3436 }
3437 if index_legacy {
3438 store
3439 .register_secondary_index(
3440 "cfg_legacy",
3441 "by_id",
3442 index_identity.clone(),
3443 Arc::new(SystemNameProjection),
3444 )
3445 .expect("legacy secondary index");
3446 }
3447 store.persist().expect("checkpoint duplicate catalog");
3448 legacy_root = store.collection_root_page("cfg_legacy");
3449 legacy_key_root = store
3450 .key_indices
3451 .read()
3452 .get("cfg_legacy")
3453 .map(|index| index.tree.root_page_id());
3454 legacy_secondary_root = index_legacy.then(|| {
3455 store
3456 .secondary_index("cfg_legacy", "by_id", &index_identity)
3457 .expect("legacy secondary root")
3458 .root_page_id()
3459 });
3460 }
3461
3462 {
3467 let pager = Pager::open(
3468 &path,
3469 PagerConfig {
3470 disk_residency: true,
3471 ..PagerConfig::default()
3472 },
3473 )
3474 .expect("open pager");
3475 let mut page = pager.read_page(1).expect("catalog page");
3476 let bytes = page.as_bytes_mut();
3477 for (source, destination) in [
3478 (b"cfg_legacy".as_slice(), b"red_config".as_slice()),
3479 (b"cfg.canon_".as_slice(), b"red.config".as_slice()),
3480 ] {
3481 let mut replacements = 0;
3482 for offset in 0..=bytes.len() - source.len() {
3483 if &bytes[offset..offset + source.len()] == source {
3484 bytes[offset..offset + destination.len()].copy_from_slice(destination);
3485 replacements += 1;
3486 }
3487 }
3488 assert!(replacements >= 1, "catalog must contain the source name");
3489 }
3490 pager.write_page(1, page).expect("write duplicate catalog");
3491 pager.sync().expect("sync duplicate catalog");
3492 }
3493 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3494 .rename_collections(&[("cfg_legacy", "red_config"), ("cfg.canon_", "red.config")])
3495 .expect("publish duplicate operational names");
3496
3497 let pages_before = Pager::open(
3498 &path,
3499 PagerConfig {
3500 disk_residency: true,
3501 ..PagerConfig::default()
3502 },
3503 )
3504 .expect("inspect pager")
3505 .page_count()
3506 .expect("page count");
3507 DuplicateSystemCatalogFixture {
3508 _directory: directory,
3509 path,
3510 config,
3511 pages_before,
3512 legacy_root,
3513 legacy_key_root,
3514 legacy_secondary_root,
3515 index_identity,
3516 }
3517 }
3518
3519 fn alias_duplicate_system_catalog_state_with(
3520 fixture: &DuplicateSystemCatalogFixture,
3521 mutate_canonical: impl FnOnce(&mut reddb_file::native_resume::CollectionResume),
3522 ) {
3523 let pager = Pager::open(
3524 &fixture.path,
3525 PagerConfig {
3526 disk_residency: true,
3527 ..PagerConfig::default()
3528 },
3529 )
3530 .expect("open duplicate catalog pager");
3531 let mut metadata = read_meta_payload(&pager)
3532 .expect("read duplicate catalog")
3533 .expect("published duplicate catalog");
3534 let mut resume = reddb_file::native_resume::decode_catalog_resume(&metadata)
3535 .expect("decode duplicate resume")
3536 .expect("prepared duplicate resume");
3537 let legacy_state = resume
3538 .collections
3539 .get("red_config")
3540 .expect("legacy resume state")
3541 .clone();
3542 let mut canonical_state = legacy_state.clone();
3543 mutate_canonical(&mut canonical_state);
3544 resume
3545 .collections
3546 .insert("red.config".to_string(), canonical_state);
3547
3548 let header = reddb_file::decode_native_paged_metadata_header(&metadata)
3549 .expect("decode metadata header")
3550 .expect("native metadata header");
3551 let mut position = reddb_file::METADATA_HEADER_BYTES;
3552 let mut replaced_root = false;
3553 for _ in 0..header.collection_count {
3554 let root = reddb_file::decode_native_paged_collection_root(&metadata, &mut position)
3555 .expect("decode collection root");
3556 if root.collection == "red.config" {
3557 metadata[position - 4..position]
3558 .copy_from_slice(&legacy_state.root_page.to_le_bytes());
3559 replaced_root = true;
3560 }
3561 }
3562 assert!(replaced_root, "canonical catalog root must be present");
3563
3564 let trailer = metadata
3565 .windows(4)
3566 .rposition(|bytes| bytes == b"RDRS")
3567 .expect("catalog resume trailer");
3568 metadata.truncate(trailer);
3569 reddb_file::native_resume::encode_catalog_resume(&mut metadata, &resume)
3570 .expect("encode aliased resume");
3571 publish_paged_metadata(&pager, &metadata).expect("publish aliased catalog");
3572 }
3573
3574 fn alias_duplicate_system_catalog_state(fixture: &DuplicateSystemCatalogFixture) {
3575 alias_duplicate_system_catalog_state_with(fixture, |_| {});
3576 }
3577
3578 #[test]
3579 fn populated_legacy_system_collection_replaces_empty_canonical_duplicate() {
3580 let fixture = duplicate_system_catalog_fixture(true, false, true);
3581 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
3582 .expect("resolve empty duplicate");
3583 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3584 assert_eq!(
3585 recovered
3586 .get("red.config", EntityId::new(1))
3587 .expect("preserved legacy row")
3588 .kind
3589 .collection(),
3590 "red.config"
3591 );
3592 assert_eq!(
3593 recovered.collection_root_page("red.config"),
3594 fixture.legacy_root,
3595 "the populated data root must be reused"
3596 );
3597 assert_eq!(
3598 recovered
3599 .key_indices
3600 .read()
3601 .get("red.config")
3602 .map(|index| index.tree.root_page_id()),
3603 fixture.legacy_key_root,
3604 "the populated key-index root must be reused"
3605 );
3606 assert_eq!(
3607 recovered
3608 .secondary_index("red.config", "by_id", &fixture.index_identity)
3609 .expect("surviving secondary index")
3610 .root_page_id(),
3611 fixture
3612 .legacy_secondary_root
3613 .expect("fixture secondary root")
3614 );
3615 drop(recovered);
3616
3617 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3618 .expect("idempotent cold boot");
3619 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
3620 assert_eq!(
3621 cold.pager()
3622 .expect("pager")
3623 .page_count()
3624 .expect("page count"),
3625 fixture.pages_before,
3626 "resolving an empty duplicate must not extend the file"
3627 );
3628 }
3629
3630 #[test]
3631 fn populated_canonical_system_collection_replaces_empty_legacy_duplicate() {
3632 let fixture = duplicate_system_catalog_fixture(false, true, false);
3633 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3634 .expect("retain populated canonical duplicate");
3635 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3636 assert_eq!(
3637 recovered
3638 .get("red.config", EntityId::new(2))
3639 .expect("preserved canonical row")
3640 .kind
3641 .collection(),
3642 "red.config"
3643 );
3644 assert!(recovered.get("red.config", EntityId::new(1)).is_none());
3645 }
3646
3647 #[test]
3648 fn empty_canonical_system_collection_replaces_empty_legacy_duplicate() {
3649 let fixture = duplicate_system_catalog_fixture(false, false, false);
3650 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3651 .expect("prefer canonical empty duplicate");
3652 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3653 assert_eq!(
3654 recovered
3655 .get_collection("red.config")
3656 .expect("canonical collection")
3657 .count(),
3658 0
3659 );
3660 }
3661
3662 #[test]
3663 fn populated_system_collection_duplicates_still_fail_closed() {
3664 let fixture = duplicate_system_catalog_fixture(true, true, false);
3665 let error = match UnifiedStore::open_with_config(&fixture.path, fixture.config) {
3666 Ok(_) => panic!("two populated identities are ambiguous"),
3667 Err(error) => error,
3668 };
3669 assert!(
3670 error
3671 .to_string()
3672 .contains("both populated legacy collection 'red_config' and canonical collection 'red.config'"),
3673 "unexpected error: {error}"
3674 );
3675 assert!(
3676 error.to_string().contains(
3677 "data_root_match=false, key_index_root_match=true, secondary_index_state_match=true"
3678 ),
3679 "the error must identify which physical state diverged without exposing page IDs: {error}"
3680 );
3681 }
3682
3683 #[test]
3684 fn explicit_compaction_retires_populated_legacy_system_config() {
3685 let fixture = duplicate_system_catalog_fixture(true, true, false);
3686 let destination = fixture._directory.path().join("canonical-system-config");
3687
3688 let report = crate::storage::compact::compact_paged_canonical_system_config(
3689 &fixture.path,
3690 &destination,
3691 )
3692 .expect("rewrite duplicate system config");
3693 assert!(report.records > 0);
3694
3695 let rewritten_path = destination.join("data.rdb");
3696 let rewritten = UnifiedStore::open_with_config(&rewritten_path, fixture.config.clone())
3697 .expect("open canonical rewrite");
3698 assert_eq!(rewritten.list_collections(), vec!["red.config".to_string()]);
3699 assert!(
3700 rewritten.get("red.config", EntityId::new(1)).is_none(),
3701 "the obsolete legacy tree must not leak into the canonical collection"
3702 );
3703 assert_eq!(
3704 rewritten
3705 .get("red.config", EntityId::new(2))
3706 .expect("canonical row survives")
3707 .kind
3708 .collection(),
3709 "red.config"
3710 );
3711 drop(rewritten);
3712
3713 let cold = UnifiedStore::open_with_config(&rewritten_path, fixture.config)
3714 .expect("cold reopen canonical rewrite");
3715 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
3716 assert_eq!(
3717 cold.get_collection("red.config")
3718 .expect("canonical collection")
3719 .count(),
3720 1
3721 );
3722 assert!(
3723 crate::storage::operational_manifest::OperationalManifest::for_db_path(&rewritten_path)
3724 .recover_or_bootstrap(&["red.config".to_string()])
3725 .expect("canonical operational manifest")
3726 .is_empty()
3727 );
3728 }
3729
3730 #[test]
3731 fn canonical_system_config_recovery_replays_pending_unrelated_wal() {
3732 use crate::storage::wal::{WalRecord, WalWriter};
3733
3734 let fixture = duplicate_system_catalog_fixture(true, true, true);
3735 let wal = reddb_file::unified_wal_path(&fixture.path);
3736 let action = reddb_file::encode_store_wal_action_frame(
3737 &reddb_file::StoreWalActionFrame::CreateCollection {
3738 name: "pending_collection".into(),
3739 },
3740 )
3741 .expect("encode pending action");
3742 let canonical_update = reddb_file::encode_store_wal_action_frame(
3743 &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
3744 collection: "red.config".into(),
3745 record: UnifiedStore::serialize_entity_record(
3746 &UnifiedEntity::table_row(
3747 EntityId::new(3),
3748 "red.config",
3749 3,
3750 vec![Value::text("canonical WAL")],
3751 ),
3752 None,
3753 STORE_VERSION_CURRENT,
3754 ),
3755 },
3756 )
3757 .expect("encode canonical update");
3758 {
3759 let mut writer = WalWriter::open(&wal).expect("open WAL");
3760 writer
3761 .append(&WalRecord::TxCommitBatch {
3762 tx_id: 999,
3763 actions: vec![action, canonical_update],
3764 })
3765 .expect("append committed WAL");
3766 writer.sync().expect("sync WAL");
3767 }
3768 let original = std::fs::read(&fixture.path).expect("source bytes");
3769 let original_wal = std::fs::read(&wal).expect("WAL bytes");
3770 let destination = fixture._directory.path().join("recovered-canonical-config");
3771
3772 let default_destination = fixture._directory.path().join("default-recovery");
3773 let error = crate::storage::compact::recover_paged_wal(&fixture.path, &default_destination)
3774 .expect_err("ordinary recovery must reject divergent identities");
3775 assert!(error
3776 .to_string()
3777 .contains("both populated legacy collection"));
3778
3779 crate::storage::compact::recover_paged_wal_canonical_system_config(
3780 &fixture.path,
3781 &destination,
3782 )
3783 .expect("recover pending WAL and retain canonical config");
3784
3785 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
3786 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
3787 for _ in 0..2 {
3788 let recovered = UnifiedStore::open_with_config(
3789 destination.join("data.rdb"),
3790 fixture.config.clone(),
3791 )
3792 .expect("open recovered store");
3793 assert!(recovered.get_collection("pending_collection").is_some());
3794 assert!(recovered.get("red.config", EntityId::new(1)).is_none());
3795 assert!(recovered.get("red.config", EntityId::new(2)).is_some());
3796 assert!(recovered.get("red.config", EntityId::new(3)).is_some());
3797 }
3798 }
3799
3800 #[test]
3801 fn canonical_system_config_recovery_rejects_legacy_wal_actions() {
3802 use crate::storage::wal::{WalRecord, WalWriter};
3803
3804 let fixture = duplicate_system_catalog_fixture(true, true, false);
3805 let wal = reddb_file::unified_wal_path(&fixture.path);
3806 let action = reddb_file::encode_store_wal_action_frame(
3807 &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
3808 collection: "red_config".into(),
3809 entity_id: 1,
3810 },
3811 )
3812 .unwrap();
3813 {
3814 let mut writer = WalWriter::open(&wal).unwrap();
3815 writer
3816 .append(&WalRecord::TxCommitBatch {
3817 tx_id: 999,
3818 actions: vec![action],
3819 })
3820 .unwrap();
3821 writer.sync().unwrap();
3822 }
3823 let original = std::fs::read(&fixture.path).unwrap();
3824 let original_wal = std::fs::read(&wal).unwrap();
3825 let destination = fixture._directory.path().join("rejected-legacy-wal");
3826 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
3827 &fixture.path,
3828 &destination,
3829 )
3830 .expect_err("legacy WAL must not be replayed against the canonical tree");
3831 assert!(error
3832 .to_string()
3833 .contains("WAL references divergent legacy red_config"));
3834 assert!(!destination.exists());
3835 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
3836 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
3837 }
3838
3839 #[test]
3840 fn canonical_system_config_recovery_rejects_legacy_cross_references() {
3841 use crate::storage::wal::{WalRecord, WalWriter};
3842
3843 let fixture = duplicate_system_catalog_fixture_with_ref(true, true, false, true);
3844 let wal = reddb_file::unified_wal_path(&fixture.path);
3845 {
3846 let mut writer = WalWriter::open(&wal).unwrap();
3847 writer.append(&WalRecord::Begin { tx_id: 999 }).unwrap();
3848 writer.sync().unwrap();
3849 }
3850 let original = std::fs::read(&fixture.path).unwrap();
3851 let original_wal = std::fs::read(&wal).unwrap();
3852 let destination = fixture._directory.path().join("rejected-legacy-reference");
3853 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
3854 &fixture.path,
3855 &destination,
3856 )
3857 .expect_err("legacy references must not be retargeted silently");
3858 assert!(error
3859 .to_string()
3860 .contains("legacy red_config has cross-references"));
3861 assert!(!destination.join("data.rdb").exists());
3862 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
3863 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
3864 }
3865
3866 #[test]
3867 fn populated_system_collection_aliases_reconcile_without_rebuild() {
3868 let fixture = duplicate_system_catalog_fixture(true, false, true);
3869 alias_duplicate_system_catalog_state(&fixture);
3870
3871 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
3872 .expect("reconcile identical populated aliases");
3873 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3874 assert_eq!(
3875 recovered
3876 .get("red.config", EntityId::new(1))
3877 .expect("preserved aliased row")
3878 .kind
3879 .collection(),
3880 "red.config"
3881 );
3882 assert_eq!(
3883 recovered.collection_root_page("red.config"),
3884 fixture.legacy_root,
3885 "the shared data root must be reused"
3886 );
3887 assert_eq!(
3888 recovered
3889 .key_indices
3890 .read()
3891 .get("red.config")
3892 .map(|index| index.tree.root_page_id()),
3893 fixture.legacy_key_root,
3894 "the shared key-index root must be reused"
3895 );
3896 assert_eq!(
3897 recovered
3898 .secondary_index("red.config", "by_id", &fixture.index_identity)
3899 .expect("shared secondary index")
3900 .root_page_id(),
3901 fixture
3902 .legacy_secondary_root
3903 .expect("fixture secondary root")
3904 );
3905 drop(recovered);
3906
3907 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3908 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3909 .expect("idempotent cold boot");
3910 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
3911 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3912 assert_eq!(
3913 cold.pager()
3914 .expect("pager")
3915 .page_count()
3916 .expect("page count"),
3917 fixture.pages_before,
3918 "reconciling a physical alias must not extend the file"
3919 );
3920 }
3921
3922 #[test]
3923 fn populated_system_collection_aliases_merge_resume_counter_drift() {
3924 let fixture = duplicate_system_catalog_fixture(true, false, false);
3925 alias_duplicate_system_catalog_state_with(&fixture, |canonical| {
3926 canonical.entities += 7;
3927 canonical.entity_id_next += 100;
3928 canonical.row_id_next += 200;
3929 canonical.sequence_id_next += 300;
3930 });
3931
3932 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
3933 .expect("reconcile shared roots with conservative counters");
3934 let manager = recovered
3935 .get_collection("red.config")
3936 .expect("canonical collection");
3937 assert_eq!(manager.count(), 1, "the shared tree owns the exact count");
3938 assert_eq!(manager.identity_counters(), (102, 202));
3939 assert_eq!(manager.sequence_id_next(), 301);
3940 assert_eq!(
3941 recovered.collection_root_page("red.config"),
3942 fixture.legacy_root,
3943 "counter reconciliation must keep the shared data root"
3944 );
3945 drop(manager);
3946 drop(recovered);
3947
3948 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3949 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3950 .expect("idempotent cold boot after counter reconciliation");
3951 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
3952 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3953 assert_eq!(
3954 cold.pager()
3955 .expect("pager")
3956 .page_count()
3957 .expect("page count"),
3958 fixture.pages_before,
3959 "counter reconciliation must not extend the file"
3960 );
3961 }
3962
3963 fn row(id: u64, value: i64) -> UnifiedEntity {
3964 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
3965 }
3966
3967 #[test]
3968 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
3969 let directory = tempfile::tempdir().expect("temporary store");
3970 let path = directory.path().join("data.rdb");
3971 let store = UnifiedStore::open(&path).expect("paged store");
3972 store.create_collection("rows").expect("collection");
3973 store.insert("rows", row(1, 1)).expect("seed");
3974 store.persist().expect("initial checkpoint");
3975 let manager = store.get_collection("rows").expect("rows");
3976 manager.update(row(1, 2)).expect("direct update");
3977 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3978 store.persist().expect("updated row checkpoint");
3979 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
3980 let mut metadata = Metadata::new();
3981 metadata.set("verified", MetadataValue::Bool(true));
3982 manager
3983 .set_metadata(EntityId::new(1), metadata)
3984 .expect("metadata");
3985 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3986 store.persist().expect("metadata-only checkpoint");
3987 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
3988 manager
3989 .bulk_insert(vec![row(2, 2), row(3, 3)])
3990 .expect("bulk insert");
3991 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3992 store.persist().expect("bulk insert checkpoint");
3993 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
3994 manager.delete(EntityId::new(2)).expect("delete");
3995 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3996 store.persist().expect("changed checkpoint");
3997 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
3998 drop(store);
3999 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
4000 let manager = recovered.get_collection("rows").expect("rows");
4001 assert_eq!(manager.count(), 2);
4002 assert_eq!(
4003 manager
4004 .get(EntityId::new(1))
4005 .expect("row")
4006 .data
4007 .as_row()
4008 .expect("table")
4009 .columns,
4010 vec![Value::Integer(2)]
4011 );
4012 assert!(manager.get(EntityId::new(2)).is_none());
4013 assert!(manager
4014 .get_metadata(EntityId::new(1))
4015 .expect("metadata")
4016 .get("verified")
4017 .is_some());
4018 }
4019
4020 #[test]
4021 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
4022 let directory = tempfile::tempdir().expect("temporary store");
4023 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
4024 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
4025 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
4026 store.create_collection("rows").expect("collection");
4027 for id in 1..=40 {
4028 store.insert("rows", row(id, id as i64)).expect("seed");
4029 }
4030 store.persist().expect("initial checkpoint");
4031 let unrelated = snapshots.begin();
4032 snapshots.rollback(unrelated);
4033 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4034 store.persist().expect("unrelated rollback checkpoint");
4035 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
4036
4037 store
4038 .pending_vault_versions
4039 .write()
4040 .insert(EntityId::new(100));
4041 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4042 store.persist().expect("unrelated vault checkpoint");
4043 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
4044 store.pending_vault_versions.write().clear();
4045 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
4046 store
4047 .persist()
4048 .expect("unrelated vault publication checkpoint");
4049 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
4050 }
4051
4052 #[test]
4053 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
4054 let directory = tempfile::tempdir().expect("temporary store");
4055 let path = directory.path().join("data.rdb");
4056 let store = UnifiedStore::open(&path).expect("paged store");
4057 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
4058 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
4059 store.create_collection("rows").expect("collection");
4060 let xid = snapshots.begin();
4061 let mut entity = row(1, 1);
4062 entity.xmin = xid;
4063 store.insert("rows", entity).expect("committed row");
4064 snapshots.commit(xid);
4065 store.persist().expect("committed baseline");
4066 snapshots.rollback(xid);
4067 store
4068 .persist()
4069 .expect("untracked outcome requires validation");
4070 drop(store);
4071 let recovered = UnifiedStore::open(&path).expect("recover");
4072 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
4073 }
4074
4075 #[test]
4076 fn exhausted_deferred_row_cache_keeps_full_validation() {
4077 let directory = tempfile::tempdir().expect("temporary store");
4078 let path = directory.path().join("data.rdb");
4079 let store = UnifiedStore::open(&path).expect("paged store");
4080 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
4081 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
4082 store.create_collection("rows").expect("collection");
4083 let xid = snapshots.begin();
4084 for id in 1..=1025 {
4085 let mut entity = row(id, id as i64);
4086 entity.xmin = xid;
4087 store.insert("rows", entity).expect("pending row");
4088 }
4089 store.persist().expect("exclude pending rows");
4090 assert!(store.checkpoint_versions.read()["rows"]
4091 .deferred_rows
4092 .is_none());
4093 snapshots.commit(xid);
4094 store
4095 .persist()
4096 .expect("publish committed rows conservatively");
4097 drop(store);
4098 let recovered = UnifiedStore::open(&path).expect("recover");
4099 assert_eq!(
4100 recovered.get_collection("rows").expect("rows").count(),
4101 1025
4102 );
4103 }
4104
4105 #[test]
4106 fn rollback_invalidates_reuse_without_a_segment_mutation() {
4107 let directory = tempfile::tempdir().expect("temporary store");
4108 let path = directory.path().join("data.rdb");
4109 let store = UnifiedStore::open(&path).expect("paged store");
4110 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
4111 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
4112 store.create_collection("rows").expect("collection");
4113 let xid = snapshots.begin();
4114 let mut pending = row(1, 1);
4115 pending.xmin = xid;
4116 store.insert("rows", pending).expect("version");
4117 store.persist().expect("before rollback");
4118 snapshots.rollback(xid);
4119 store.persist().expect("after rollback");
4120 drop(store);
4121 let recovered = UnifiedStore::open(&path).expect("recover");
4122 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
4123 }
4124
4125 #[test]
4126 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
4127 let directory = tempfile::tempdir().expect("temporary store");
4128 let path = directory.path().join("data.rdb");
4129 let store = UnifiedStore::open(&path).expect("paged store");
4130 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
4131 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
4132 store.create_collection("rows").expect("collection");
4133 let xid = snapshots.begin();
4134 let mut deleted = row(1, 1);
4135 deleted.xmax = xid;
4136 store.insert("rows", deleted).expect("version");
4137 store.persist().expect("before rollback");
4138 snapshots.rollback(xid);
4139 store.persist().expect("after rollback");
4140 drop(store);
4141 let recovered = UnifiedStore::open(&path).expect("recover");
4142 let manager = recovered.get_collection("rows").expect("rows");
4143 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
4144 }
4145
4146 #[test]
4147 fn publishing_a_pending_vault_version_invalidates_reuse() {
4148 let directory = tempfile::tempdir().expect("temporary store");
4149 let path = directory.path().join("data.rdb");
4150 let store = UnifiedStore::open(&path).expect("paged store");
4151 store.create_collection("rows").expect("collection");
4152 store.insert("rows", row(1, 1)).expect("row");
4153 store
4154 .pending_vault_versions
4155 .write()
4156 .insert(EntityId::new(1));
4157 store.persist().expect("exclude pending vault version");
4158 store
4159 .pending_vault_versions
4160 .write()
4161 .remove(&EntityId::new(1));
4162 store.persist().expect("include published vault version");
4163 drop(store);
4164 let recovered = UnifiedStore::open(&path).expect("recover");
4165 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
4166 }
4167}