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 let path = path.as_ref();
1100 let mut pager_config = PagerConfig {
1101 disk_residency: config.resident_memory_bytes_max.is_some(),
1102 ..PagerConfig::default()
1103 };
1104 if let Some(slots) = config.page_cache_slots {
1108 pager_config.cache_size = slots;
1109 }
1110 if matches!(
1118 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
1119 Some("0") | Some("false") | Some("off")
1120 ) {
1121 pager_config.double_write = false;
1122 }
1123 let pager_open_started = std::time::Instant::now();
1124 let pager = Pager::open(path, pager_config)
1125 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1126 pager
1127 .enable_deferred_reclamation()
1128 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1129 trace_startup_phase(path, "paged_pager_open", pager_open_started);
1130
1131 if config.resident_memory_bytes_max.is_none()
1132 && pager
1133 .header()
1134 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1135 .version
1136 >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
1137 {
1138 return Err(StoreError::Io(std::io::Error::new(std::io::ErrorKind::Unsupported,
1139 "this database was prepared for disk residency; reopen with the disk-resident storage profile")));
1140 }
1141
1142 let wal_path = reddb_file::layout::unified_wal_path(path);
1143 let commit = Some(Arc::new(
1146 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
1147 .map_err(StoreError::Io)?,
1148 ));
1149
1150 let store = Self {
1151 config,
1152 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1153 format_version: AtomicU32::new(STORE_VERSION_V11),
1154 pager: Some(Arc::new(pager)),
1155 db_path: Some(path.to_path_buf()),
1156 operational_manifest: parking_lot::Mutex::new(None),
1157 btree_indices: RwLock::new(HashMap::new()),
1158 key_indices: RwLock::new(HashMap::new()),
1159 secondary_index_specs: RwLock::new(HashMap::new()),
1160 recovery_xid_summary: RwLock::new(None),
1161 checkpoint_versions: RwLock::new(HashMap::new()),
1162 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1163 retired_btrees: parking_lot::Mutex::new(Vec::new()),
1164 ownership_catalog_epoch: AtomicU64::new(0),
1165 ownership_active_epoch: AtomicU64::new(0),
1166 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1167 commit,
1168 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1169 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1170 snapshot_manager: std::sync::OnceLock::new(),
1171 embedded_commit_session: std::sync::OnceLock::new(),
1172 #[cfg(test)]
1173 test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1174 #[cfg(test)]
1175 test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1176 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1177 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1178 };
1179
1180 let checkpoint_recovery_started = std::time::Instant::now();
1183 if let Some(commit) = &store.commit {
1184 commit
1185 .restore_checkpoint_page_images(&store)
1186 .map_err(StoreError::Io)?;
1187 }
1188 trace_startup_phase(
1189 path,
1190 "paged_checkpoint_recovery",
1191 checkpoint_recovery_started,
1192 );
1193 let page_load_started = std::time::Instant::now();
1195 let catalog_load = store.load_from_pages()?;
1196 trace_startup_phase(path, "paged_catalog_and_roots", page_load_started);
1197 store.migrate_operational_manifest(&catalog_load.collisions)?;
1201 let wal_replay_started = std::time::Instant::now();
1202 if let Some(commit) = &store.commit {
1203 commit.replay_into(&store).map_err(StoreError::Io)?;
1204 }
1205 trace_startup_phase(path, "paged_wal_replay", wal_replay_started);
1206 if catalog_load.migrated {
1207 store.mark_paged_registry_dirty();
1210 store.flush_paged_state()?;
1211 }
1212 let manifest_recovery_started = std::time::Instant::now();
1213 store.recover_operational_manifest()?;
1214 if store.config.resident_memory_bytes_max.is_some() {
1215 let missing_key_index = {
1216 let trees = store.btree_indices.read();
1217 let keys = store.key_indices.read();
1218 trees.iter().any(|(name, tree)| {
1219 !keys
1220 .get(name)
1221 .is_some_and(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
1222 })
1223 };
1224 if missing_key_index {
1225 store.persist()?;
1226 }
1227 }
1228 trace_startup_phase(
1229 path,
1230 "paged_manifest_and_derived_roots",
1231 manifest_recovery_started,
1232 );
1233
1234 Ok(store)
1235 }
1236
1237 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
1241 let Some(pager) = &self.pager else {
1242 return Ok(());
1243 };
1244 let collections = self.resident.collections.read();
1245 let mut indices = self.btree_indices.write();
1246 let previous_versions = self.checkpoint_versions.read();
1247 let raw_replay =
1248 self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
1249 for (name, manager) in collections.iter() {
1250 let mut reusable = None;
1251 if raw_replay {
1252 if let (Some(previous), Some(tree)) =
1253 (previous_versions.get(name), indices.get(name))
1254 {
1255 let mut current = CheckpointVersion {
1256 manager: Arc::downgrade(manager),
1257 segments: manager.checkpoint_revisions(),
1258 visibility_revision: 0,
1259 untracked_visibility_revision: 0,
1260 excluded_xids: Arc::default(),
1261 pending_vault: Arc::default(),
1262 format_version: self.format_version(),
1263 deferred_rows: Some(Arc::default()),
1264 has_unprojected_rows: false,
1265 };
1266 if previous.manager.ptr_eq(¤t.manager)
1267 && previous.format_version == current.format_version
1268 {
1269 if previous.segments == current.segments
1270 && !previous.has_unprojected_rows
1271 && checkpoint_visibility_matches(previous, ¤t)
1272 {
1273 reusable = Some(Arc::clone(tree));
1274 } else {
1275 reusable = self.sparse_checkpoint_tree(
1281 name,
1282 manager,
1283 tree,
1284 previous,
1285 &mut current,
1286 )?;
1287 }
1288 }
1289 }
1290 }
1291 let tree = match reusable {
1292 Some(tree) => tree,
1293 None => {
1294 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1295 #[cfg(test)]
1296 REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1297 Some(Self::serialize_entity_record(
1298 entity,
1299 metadata.as_ref(),
1300 self.format_version(),
1301 ))
1302 })?
1303 }
1304 };
1305 if indices
1306 .get(name)
1307 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1308 {
1309 continue;
1310 }
1311 if let Some(previous) = indices.insert(name.clone(), tree) {
1312 self.retire_btree(previous);
1313 }
1314 self.mark_paged_registry_dirty();
1315 #[cfg(test)]
1316 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1317 if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1318 pager.sync().map_err(|error| {
1319 StoreError::Io(std::io::Error::other(error.to_string()))
1320 })?;
1321 }
1322 eprintln!("experimental replay rebuild exit before manifest");
1323 std::process::exit(182);
1324 }
1325 }
1326 drop(previous_versions);
1327 drop(indices);
1328 drop(collections);
1329 self.flush_paged_state()?;
1330 #[cfg(test)]
1331 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1332 eprintln!("experimental replay exit after manifest");
1333 std::process::exit(182);
1334 }
1335 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1338 let collections = self.resident.collections.read();
1339 let mut versions = self.checkpoint_versions.write();
1340 for (name, manager) in collections.iter() {
1341 versions.insert(
1342 name.clone(),
1343 CheckpointVersion {
1344 manager: Arc::downgrade(manager),
1345 segments: manager.checkpoint_revisions(),
1346 visibility_revision: 0,
1347 untracked_visibility_revision: 0,
1348 excluded_xids: Arc::default(),
1349 pending_vault: Arc::default(),
1350 format_version: self.format_version(),
1351 deferred_rows: Some(Arc::default()),
1352 has_unprojected_rows: false,
1353 },
1354 );
1355 }
1356 }
1357 Ok(())
1358 }
1359
1360 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
1361 let Some(path) = &self.db_path else {
1362 return Ok(());
1363 };
1364 let mut collections = self.list_collections();
1365 collections.sort();
1366 let pending_drops = self
1367 .operational_manifest_for_path(path)
1368 .recover_or_bootstrap(&collections)
1369 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1370 for name in pending_drops {
1371 if self.get_collection(&name).is_some() {
1372 self.drop_collection(&name)?;
1373 }
1374 }
1375 Ok(())
1376 }
1377
1378 fn migrate_operational_manifest(
1379 &self,
1380 collisions: &[SystemCollectionCollision],
1381 ) -> Result<(), StoreError> {
1382 let Some(path) = &self.db_path else {
1383 return Ok(());
1384 };
1385 let manifest = self.operational_manifest_for_path(path);
1386 let collision_refs = collisions
1387 .iter()
1388 .map(|collision| {
1389 (
1390 collision.survivor.as_str(),
1391 collision.loser.as_str(),
1392 collision.canonical.as_str(),
1393 )
1394 })
1395 .collect::<Vec<_>>();
1396 manifest
1397 .reconcile_collection_collisions(&collision_refs)
1398 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1399 manifest
1400 .rename_collections(reddb_engine::storage::system_collection_names::LEGACY_NAMES)
1401 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))
1402 }
1403
1404 pub(crate) fn prepare_operational_collection_create(
1405 &self,
1406 name: &str,
1407 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
1408 let Some(path) = &self.db_path else {
1409 return Ok(None);
1410 };
1411 self.operational_manifest_for_path(path)
1412 .prepare_collection_create(name)
1413 .map(Some)
1414 .map_err(StoreError::Io)
1415 }
1416
1417 pub(crate) fn prepare_operational_collection_drop(
1418 &self,
1419 name: &str,
1420 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
1421 let Some(path) = &self.db_path else {
1422 return Ok(None);
1423 };
1424 self.operational_manifest_for_path(path)
1425 .prepare_collection_drop(name)
1426 .map(Some)
1427 .map_err(StoreError::Io)
1428 }
1429
1430 pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1432 let Some(pager) = &self.pager else {
1433 return Ok(0);
1434 };
1435 let Some(content) = read_meta_payload(pager)? else {
1436 return Ok(0);
1437 };
1438 reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1439 .map(|epoch| epoch.unwrap_or(0))
1440 .map_err(|error| StoreError::Serialization(error.to_string()))
1441 }
1442
1443 fn load_from_pages(&self) -> Result<CatalogLoadOutcome, StoreError> {
1445 let pager = match &self.pager {
1446 Some(p) => p,
1447 None => return Ok(CatalogLoadOutcome::default()), };
1449
1450 let page_count = pager.page_count().map_err(|e| {
1452 StoreError::Io(std::io::Error::other(format!(
1453 "failed to read page count: {}",
1454 e
1455 )))
1456 })?;
1457 if page_count <= 1 {
1458 return Ok(CatalogLoadOutcome::default());
1460 }
1461
1462 if let Some(content_vec) = read_meta_payload(pager)? {
1467 let content: &[u8] = &content_vec;
1468 let file_version = pager
1469 .header()
1470 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1471 .version;
1472 let prepared =
1473 file_version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION;
1474 let resume = if prepared {
1475 reddb_file::native_resume::decode_catalog_resume(content)
1476 .map_err(|error| StoreError::Serialization(error.to_string()))?
1477 } else {
1478 None
1479 };
1480 let mut outcome = CatalogLoadOutcome::default();
1481 let mut ignored_collections = std::collections::HashSet::new();
1482 if let Some(resume) = &resume {
1483 for &(legacy, canonical) in
1484 reddb_engine::storage::system_collection_names::LEGACY_NAMES
1485 {
1486 let (Some(legacy_state), Some(canonical_state)) = (
1487 resume.collections.get(legacy),
1488 resume.collections.get(canonical),
1489 ) else {
1490 continue;
1491 };
1492 let merged_resume = merge_shared_alias_resume(legacy_state, canonical_state);
1493 let shared_physical_state = merged_resume.is_some();
1494 let resume_counter_drift = legacy_state != canonical_state;
1495 let (survivor, loser) = match (
1496 legacy_state.entities == 0,
1497 canonical_state.entities == 0,
1498 ) {
1499 (false, true) => (legacy, canonical),
1500 (true, false) | (true, true) => (canonical, legacy),
1501 (false, false) if shared_physical_state => (canonical, legacy),
1502 (false, false) => {
1503 let (
1504 data_root_match,
1505 key_index_root_match,
1506 secondary_index_state_match,
1507 ) = shared_alias_resume_matches(legacy_state, canonical_state);
1508 return Err(page_integrity(
1509 1,
1510 None,
1511 format!(
1512 "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})"
1513 ),
1514 ));
1515 }
1516 };
1517 ignored_collections.insert(loser.to_string());
1518 outcome.collisions.push(SystemCollectionCollision {
1519 survivor: survivor.to_string(),
1520 loser: loser.to_string(),
1521 canonical: canonical.to_string(),
1522 shared_physical_state,
1523 resume_counter_drift,
1524 merged_resume,
1525 });
1526 }
1527 let live_roots = resume
1528 .collections
1529 .iter()
1530 .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1531 .flat_map(|(_, state)| {
1532 std::iter::once(state.root_page)
1533 .chain(state.key_index_root)
1534 .chain(
1535 state
1536 .secondary_indices
1537 .values()
1538 .map(|index| index.root_page),
1539 )
1540 })
1541 .filter(|root| *root != 0)
1542 .collect::<std::collections::HashSet<_>>();
1543 for collision in &outcome.collisions {
1544 if collision.shared_physical_state {
1545 continue;
1546 }
1547 let loser_state = resume
1548 .collections
1549 .get(&collision.loser)
1550 .expect("collision loser is present");
1551 if std::iter::once(loser_state.root_page)
1552 .chain(loser_state.key_index_root)
1553 .chain(
1554 loser_state
1555 .secondary_indices
1556 .values()
1557 .map(|index| index.root_page),
1558 )
1559 .filter(|root| *root != 0)
1560 .any(|root| live_roots.contains(&root))
1561 {
1562 return Err(page_integrity(
1563 1,
1564 None,
1565 format!(
1566 "empty collection '{}' aliases a live collection root",
1567 collision.loser
1568 ),
1569 ));
1570 }
1571 }
1572 reddb_engine::storage::system_collection_names::migration_plan(
1573 resume
1574 .collections
1575 .keys()
1576 .map(String::as_str)
1577 .filter(|name| !ignored_collections.contains(*name)),
1578 )
1579 .map_err(|(legacy, canonical)| {
1580 page_integrity(
1581 1,
1582 None,
1583 format!(
1584 "catalog contains both legacy collection '{legacy}' and canonical collection '{canonical}'"
1585 ),
1586 )
1587 })?;
1588 for state in resume.collections.values() {
1589 if !state.secondary_indices.is_empty()
1590 && file_version < reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
1591 {
1592 return Err(page_integrity(
1593 1,
1594 None,
1595 "secondary roots require the file format fence",
1596 ));
1597 }
1598 for root in std::iter::once(state.root_page)
1599 .chain(state.key_index_root)
1600 .chain(
1601 state
1602 .secondary_indices
1603 .values()
1604 .map(|index| index.root_page),
1605 )
1606 {
1607 if root >= page_count {
1608 return Err(page_integrity(
1609 root,
1610 None,
1611 "published root exceeds the database page count",
1612 ));
1613 }
1614 }
1615 }
1616 self.resident
1617 .next_entity_id
1618 .fetch_max(resume.entity_id_next, Ordering::SeqCst);
1619 }
1620 *self.recovery_xid_summary.write() = resume.as_ref().and_then(|resume| {
1621 resume.xid_high_water_mark.map(|high_water_mark| {
1622 super::recovery_xid::RecoveryXidSummary {
1623 high_water_mark,
1624 roots: resume
1625 .collections
1626 .iter()
1627 .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1628 .map(|(name, state)| {
1629 (
1630 reddb_engine::storage::system_collection_names::canonical_or_self(name)
1631 .to_owned(),
1632 state.root_page,
1633 )
1634 })
1635 .collect(),
1636 }
1637 })
1638 });
1639 if content.len() >= 4 {
1640 let mut pos = 0;
1641 let mut format_version = STORE_VERSION_V1;
1642
1643 let collection_count = if let Some(header) =
1644 reddb_file::decode_native_paged_metadata_header(content)
1645 .map_err(|err| StoreError::Serialization(err.to_string()))?
1646 {
1647 format_version = header.format_version;
1648 pos += reddb_file::METADATA_HEADER_BYTES;
1649 header.collection_count as usize
1650 } else {
1651 let count = u32::from_le_bytes([
1652 content[pos],
1653 content[pos + 1],
1654 content[pos + 2],
1655 content[pos + 3],
1656 ]) as usize;
1657 pos += 4;
1658 count
1659 };
1660
1661 self.set_format_version(format_version);
1662
1663 if pos > content.len() {
1664 return Ok(CatalogLoadOutcome::default());
1665 }
1666
1667 let mut source_names = std::collections::HashMap::<String, String>::new();
1668 let mut migrated_catalog = !outcome.collisions.is_empty();
1669 let mut empty_duplicate_roots = std::collections::HashSet::new();
1670
1671 for _ in 0..collection_count {
1673 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1674 .map_err(|err| page_integrity(1, None, err.to_string()))?;
1675 let root_page = root.root_page;
1677 let stored_name = root.collection;
1678 if ignored_collections.contains(&stored_name) {
1679 let shared_physical_state = outcome
1680 .collisions
1681 .iter()
1682 .find(|collision| collision.loser == stored_name)
1683 .is_some_and(|collision| collision.shared_physical_state);
1684 if shared_physical_state {
1685 continue;
1686 }
1687 let state = resume
1688 .as_ref()
1689 .and_then(|resume| resume.collections.get(&stored_name))
1690 .expect("ignored collection was proven by catalog resume");
1691 for retired_root in std::iter::once(state.root_page)
1692 .chain(state.key_index_root)
1693 .chain(
1694 state
1695 .secondary_indices
1696 .values()
1697 .map(|index| index.root_page),
1698 )
1699 .filter(|root| *root != 0)
1700 {
1701 empty_duplicate_roots.insert(retired_root);
1702 }
1703 continue;
1704 }
1705 let name = reddb_engine::storage::system_collection_names::canonical_or_self(
1706 &stored_name,
1707 )
1708 .to_owned();
1709 if let Some(previous) = source_names.insert(name.clone(), stored_name.clone()) {
1710 if previous != stored_name {
1711 return Err(page_integrity(
1712 1,
1713 None,
1714 format!(
1715 "catalog contains both legacy collection '{previous}' and canonical collection '{name}'"
1716 ),
1717 ));
1718 }
1719 }
1720 migrated_catalog |= name != stored_name;
1721
1722 let _ = self.create_collection_in_memory(&name);
1726
1727 let shared_alias = outcome.collisions.iter().find(|collision| {
1728 collision.survivor == stored_name && collision.shared_physical_state
1729 });
1730 let counters = shared_alias
1731 .and_then(|collision| collision.merged_resume.as_ref())
1732 .or_else(|| {
1733 resume
1734 .as_ref()
1735 .and_then(|resume| resume.collections.get(&stored_name))
1736 });
1737 if let (Some(counters), Some(manager)) = (counters, self.get_collection(&name))
1738 {
1739 manager.restore_identity_counters(
1740 counters.entity_id_next,
1741 counters.row_id_next,
1742 );
1743 manager.restore_sequence_id_next(counters.sequence_id_next);
1744 }
1745 if root_page > 0 || counters.is_some() {
1747 let btree = BTree::with_root(Arc::clone(pager), root_page);
1748
1749 let mut cursor = btree.cursor_first().map_err(|err| {
1751 page_integrity(root_page, Some(name.clone()), err.to_string())
1752 })?;
1753 let manager = self.get_collection(&name);
1754 let mut persisted_entities = counters.map_or(0, |state| state.entities);
1755 while counters.is_none() {
1756 let next = cursor
1757 .next_with_budget(
1758 self.config
1759 .resident_memory_bytes_max
1760 .and_then(|limit| usize::try_from(limit).ok())
1761 .unwrap_or(usize::MAX),
1762 )
1763 .map_err(|err| {
1764 page_integrity(root_page, Some(name.clone()), err.to_string())
1765 })?;
1766 let Some((key, value)) = next else {
1767 break;
1768 };
1769 persisted_entities += 1;
1770 let key_bytes: [u8; 8] = key.as_slice().try_into().map_err(|_| {
1772 page_integrity(
1773 root_page,
1774 Some(name.clone()),
1775 "physical entity key must contain eight bytes",
1776 )
1777 })?;
1778 let record = reddb_engine::storage::collection_read::EncodedRecord {
1779 id: EntityId::new(u64::from_be_bytes(key_bytes)),
1780 bytes: value,
1781 };
1782 let decoded = if let Some(limit) = self.config.resident_memory_bytes_max
1783 {
1784 record.decode_with_budget(
1785 self.format_version(),
1786 usize::try_from(limit).unwrap_or(usize::MAX),
1787 )
1788 } else {
1789 record.decode(self.format_version())
1790 };
1791 let (entity, metadata) = decoded.map_err(|err| {
1792 page_integrity(root_page, Some(name.clone()), err.to_string())
1793 })?;
1794 if let Some(m) = &manager {
1795 let id = entity.id;
1796 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1797 m.register_row_id(*row_id);
1798 }
1799 if self.config.resident_memory_bytes_max.is_some() {
1800 self.register_entity_id(id);
1801 m.restore_sequence_id_next(
1802 entity.sequence_id.saturating_add(1),
1803 );
1804 m.restore_identity_counters(
1805 id.raw().saturating_add(1),
1806 m.identity_counters().1,
1807 );
1808 continue;
1809 }
1810 self.resident.context_index.index_entity(&name, &entity);
1811 m.restore_snapshot_entity(entity.clone())?;
1812 if let Some(metadata) = metadata {
1813 let _ = m.set_metadata(id, metadata);
1814 }
1815 self.register_entity_id(id);
1816 if self.config.auto_index_refs {
1817 self.index_cross_refs(&entity, &name)?;
1818 }
1819 }
1820 }
1821
1822 let btree = Arc::new(btree);
1824 if shared_alias.is_some_and(|collision| collision.resume_counter_drift) {
1825 persisted_entities = u64::try_from(btree.count().map_err(|err| {
1826 page_integrity(root_page, Some(name.clone()), err.to_string())
1827 })?)
1828 .map_err(|_| {
1829 page_integrity(
1830 root_page,
1831 Some(name.clone()),
1832 "shared collection entity count exceeds u64",
1833 )
1834 })?;
1835 }
1836 let key_tree = counters
1837 .and_then(|state| state.key_index_root)
1838 .map(|root| Arc::new(BTree::with_root(Arc::clone(pager), root)));
1839 if let Some(counters) = counters {
1840 self.key_indices.write().insert(
1841 name.clone(),
1842 PublishedKeyIndex {
1843 indexed: key_tree.is_some(),
1844 secondary: counters.secondary_indices.iter().map(|(name, index)| {
1845 (name.clone(), super::secondary_index::PublishedSecondaryIndex {
1846 identity: super::secondary_index::SecondaryIndexIdentity {
1847 codec_version: index.codec_version,
1848 definition: index.definition,
1849 },
1850 tree: Arc::new(BTree::with_root(Arc::clone(pager), index.root_page)),
1851 })
1852 }).collect(),
1853 source: Arc::downgrade(&btree),
1854 tree: key_tree.clone().unwrap_or_else(|| {
1855 Arc::new(BTree::with_root(Arc::clone(pager), 0))
1856 }),
1857 },
1858 );
1859 }
1860 if let (Some(manager), Some(limit)) =
1861 (&manager, self.config.resident_memory_bytes_max)
1862 {
1863 manager.install_backing(
1864 Arc::new(
1865 match key_tree {
1866 Some(keys) => reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(&btree), keys),
1867 None => reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(&btree)),
1868 },
1869 ),
1870 btree.root_page_id(),
1871 format_version,
1872 usize::try_from(limit).unwrap_or(usize::MAX),
1873 persisted_entities,
1874 );
1875 }
1876 self.btree_indices.write().insert(name.clone(), btree);
1877 if format_version == STORE_VERSION_V11 {
1878 if let Some(manager) = &manager {
1879 self.checkpoint_versions.write().insert(
1880 name,
1881 CheckpointVersion {
1882 manager: Arc::downgrade(manager),
1883 segments: manager.checkpoint_revisions(),
1884 visibility_revision: 0,
1885 untracked_visibility_revision: 0,
1886 excluded_xids: Arc::default(),
1887 pending_vault: Arc::default(),
1888 format_version,
1889 deferred_rows: Some(Arc::default()),
1890 has_unprojected_rows: false,
1891 },
1892 );
1893 }
1894 }
1895 }
1896 }
1897
1898 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1899 let cross_ref_count = u32::from_le_bytes([
1900 content[pos],
1901 content[pos + 1],
1902 content[pos + 2],
1903 content[pos + 3],
1904 ]) as usize;
1905 pos += 4;
1906
1907 for _ in 0..cross_ref_count {
1908 let Ok(cross_ref) =
1909 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1910 else {
1911 break;
1912 };
1913 let source_id = EntityId::new(cross_ref.source_id);
1914 let target_id = EntityId::new(cross_ref.target_id);
1915 let ref_type = RefType::from_byte(cross_ref.ref_type);
1916 let target_collection =
1917 reddb_engine::storage::system_collection_names::canonical_owned(
1918 cross_ref.target_collection,
1919 );
1920
1921 self.resident
1922 .cross_refs
1923 .write()
1924 .entry(source_id)
1925 .or_default()
1926 .push((target_id, ref_type, target_collection.clone()));
1927
1928 if let Some((collection, mut entity)) = self.get_any(source_id) {
1929 let exists = entity.cross_refs().iter().any(|xref| {
1930 xref.target == target_id
1931 && xref.ref_type == ref_type
1932 && xref.target_collection == target_collection
1933 });
1934 if !exists {
1935 entity.cross_refs_mut().push(CrossRef::new(
1936 source_id,
1937 target_id,
1938 target_collection.clone(),
1939 ref_type,
1940 ));
1941 if let Some(manager) = self.get_collection(&collection) {
1942 let _ = manager.update(entity);
1943 }
1944 }
1945 }
1946 }
1947 }
1948 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1949 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1950 let already_retired = retired
1951 .iter()
1952 .copied()
1953 .collect::<std::collections::HashSet<_>>();
1954 let mut live_roots: std::collections::HashSet<_> = self
1955 .btree_indices
1956 .read()
1957 .values()
1958 .map(|tree| tree.root_page_id())
1959 .collect();
1960 live_roots.extend(
1961 self.key_indices
1962 .read()
1963 .values()
1964 .flat_map(PublishedKeyIndex::trees)
1965 .map(|tree| tree.root_page_id()),
1966 );
1967 for root in retired {
1968 if live_roots.contains(&root)
1969 || root
1970 >= pager
1971 .page_count()
1972 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1973 {
1974 return Err(page_integrity(
1975 root,
1976 None,
1977 "retired root aliases a live or missing page",
1978 ));
1979 }
1980 self.retired_btrees.lock().push(RetiredBTree {
1981 tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1982 published: true,
1983 });
1984 }
1985 for root in empty_duplicate_roots
1986 .iter()
1987 .copied()
1988 .filter(|root| !already_retired.contains(root))
1989 {
1990 self.retire_btree(Arc::new(BTree::with_root(Arc::clone(pager), root)));
1991 }
1992 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1993 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1994 if deferred
1995 .iter()
1996 .any(|id| live_roots.contains(id) || empty_duplicate_roots.contains(id))
1997 {
1998 return Err(page_integrity(1, None, "deferred page aliases a live root"));
1999 }
2000 pager
2001 .restore_deferred_reclamation(&deferred)
2002 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2003 let ownership_epoch =
2004 reddb_file::native_store::decode_native_paged_ownership_epoch(
2005 content, &mut pos,
2006 )
2007 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2008 self.ownership_catalog_epoch
2009 .store(ownership_epoch.unwrap_or(0), Ordering::Release);
2010 if migrated_catalog {
2011 self.mark_paged_registry_dirty();
2012 }
2013 if self.format_version() < STORE_VERSION_V11 {
2014 self.set_format_version(STORE_VERSION_V11);
2015 }
2016 outcome.migrated = migrated_catalog;
2017 return Ok(outcome);
2018 }
2019 }
2020
2021 if self.format_version() < STORE_VERSION_V11 {
2022 self.set_format_version(STORE_VERSION_V11);
2023 }
2024
2025 Ok(CatalogLoadOutcome::default())
2026 }
2027
2028 pub(crate) fn deserialize_entity(
2030 data: &[u8],
2031 format_version: u32,
2032 ) -> Result<UnifiedEntity, StoreError> {
2033 let mut pos = 0;
2034 Self::read_entity_binary(data, &mut pos, format_version)
2035 .map_err(|e| StoreError::Serialization(e.to_string()))
2036 }
2037
2038 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
2040 let mut buf = Vec::with_capacity(256);
2045 Self::write_entity_binary(&mut buf, entity, format_version);
2046 buf
2047 }
2048
2049 pub(crate) fn serialize_entity_record(
2050 entity: &UnifiedEntity,
2051 metadata: Option<&Metadata>,
2052 format_version: u32,
2053 ) -> Vec<u8> {
2054 reddb_engine::storage::metadata_codec::serialize_entity_record(
2055 entity,
2056 metadata,
2057 format_version,
2058 )
2059 }
2060
2061 pub(crate) fn deserialize_entity_record(
2062 data: &[u8],
2063 format_version: u32,
2064 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
2065 reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
2066 .map_err(StoreError::Serialization)
2067 }
2068
2069 pub fn persist(&self) -> Result<(), StoreError> {
2074 self.persist_inner(false)
2075 }
2076
2077 pub(crate) fn persist_after_runtime_snapshots(&self) -> Result<(), StoreError> {
2078 self.persist_inner(true)
2079 }
2080
2081 fn persist_inner(&self, runtime_snapshots_durable: bool) -> Result<(), StoreError> {
2082 let _vault_guard = self.vault_publication_lock.lock();
2083 let pending_vault_versions = self.pending_vault_versions.read();
2084 let pager = match &self.pager {
2085 Some(p) => p,
2086 None => {
2087 if let Some(path) = &self.db_path {
2089 return self
2090 .save_to_file(path)
2091 .map_err(|e| StoreError::Serialization(e.to_string()));
2092 }
2093 return Err(StoreError::Io(std::io::Error::other(
2094 "No pager or path configured for persistence",
2095 )));
2096 }
2097 };
2098 let _ownership_guard = pager.lock_ownership_mutations();
2101
2102 match pager.read_page(1) {
2103 Ok(_) => {}
2104 Err(PagerError::PageNotFound(_)) => {
2105 let meta_page = pager
2106 .allocate_page(crate::storage::engine::PageType::Header)
2107 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2108 pager
2109 .write_page(meta_page.page_id(), meta_page)
2110 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2111 }
2112 Err(e) => {
2113 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
2114 }
2115 }
2116
2117 if let Some(commit) = &self.commit {
2118 commit.force_sync().map_err(StoreError::Io)?;
2119 }
2120
2121 struct InterruptedOwnership<'a> {
2122 pager: &'a Pager,
2123 active: bool,
2124 }
2125 impl Drop for InterruptedOwnership<'_> {
2126 fn drop(&mut self) {
2127 if self.active {
2128 self.pager.fence_interrupted_ownership();
2129 }
2130 }
2131 }
2132 let mut ownership = InterruptedOwnership {
2133 pager,
2134 active: self.commit.is_some(),
2135 };
2136 let epoch = if let Some(commit) = &self.commit {
2137 let epoch = commit
2138 .next_page_ownership_epoch(self)
2139 .map_err(StoreError::Io)?;
2140 commit
2141 .begin_page_ownership_epoch(self, epoch)
2142 .map_err(StoreError::Io)?;
2143 Some(epoch)
2144 } else {
2145 None
2146 };
2147
2148 let collections = self.resident.collections.read();
2149 let mut btree_indices = self.btree_indices.write();
2150
2151 let mut collection_roots: Vec<(String, u32)> = Vec::new();
2153
2154 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
2157 let previous_versions = self.checkpoint_versions.read();
2158 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
2159 let pending_vault = Arc::new(pending_vault_versions.clone());
2160 let (visibility_revision, excluded, untracked_visibility_revision) =
2161 self.snapshot_manager.get().map_or_else(
2162 || (0, Default::default(), 0),
2163 |manager| manager.checkpoint_projection_state(),
2164 );
2165 let excluded = Arc::new(excluded);
2166 for (name, manager) in collections.iter() {
2167 let mut version = CheckpointVersion {
2168 manager: Arc::downgrade(manager),
2169 segments: manager.checkpoint_revisions(),
2170 visibility_revision,
2171 untracked_visibility_revision,
2172 excluded_xids: Arc::clone(&excluded),
2173 pending_vault: Arc::clone(&pending_vault),
2174 format_version: self.format_version(),
2175 deferred_rows: Some(Arc::default()),
2176 has_unprojected_rows: false,
2177 };
2178 if let (Some(previous), Some(tree)) =
2179 (previous_versions.get(name), btree_indices.get(name))
2180 {
2181 if previous.manager.ptr_eq(&version.manager)
2182 && previous.segments == version.segments
2183 && previous.format_version == version.format_version
2184 && !previous.has_unprojected_rows
2185 && checkpoint_visibility_matches(previous, &version)
2186 {
2187 version.deferred_rows = previous.deferred_rows.clone();
2188 collection_roots.push((name.clone(), tree.root_page_id()));
2189 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
2190 checkpoint_versions.insert(name.clone(), version);
2191 continue;
2192 }
2193 }
2194 if let (Some(previous), Some(tree)) =
2195 (previous_versions.get(name), btree_indices.get(name))
2196 {
2197 if previous.manager.ptr_eq(&version.manager)
2198 && previous.format_version == version.format_version
2199 {
2200 if let Some(tree) =
2201 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
2202 {
2203 collection_roots.push((name.clone(), tree.root_page_id()));
2204 rebuilt_indices.insert(name.clone(), tree);
2205 checkpoint_versions.insert(name.clone(), version);
2206 continue;
2207 }
2208 }
2209 }
2210 let btree = self.checkpoint_tree(
2211 pager,
2212 manager,
2213 btree_indices.get(name),
2214 |entity, metadata| {
2215 #[cfg(test)]
2216 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
2217 let hidden = pending_vault_versions.contains(&entity.id)
2218 || (matches!(
2219 entity.data,
2220 EntityData::Row(_)
2221 | EntityData::Vector(_)
2222 | EntityData::QueueMessage(_)
2223 ) && excluded.contains(&entity.xmin));
2224 let normalize_xmax = matches!(
2225 entity.data,
2226 EntityData::Row(_) | EntityData::QueueMessage(_)
2227 ) && excluded.contains(&entity.xmax);
2228 if hidden || normalize_xmax {
2229 version.remember_deferred_row(entity.id);
2230 }
2231 if hidden {
2232 return None;
2233 }
2234 let mut normalized = std::borrow::Cow::Borrowed(entity);
2235 if normalize_xmax {
2236 normalized.to_mut().xmax = 0;
2237 }
2238 Some(Self::serialize_entity_record(
2239 &normalized,
2240 metadata.as_ref(),
2241 self.format_version(),
2242 ))
2243 },
2244 )?;
2245 checkpoint_versions.insert(name.clone(), version);
2246
2247 collection_roots.push((name.clone(), btree.root_page_id()));
2248 rebuilt_indices.insert(name.clone(), btree);
2249 }
2250
2251 let mut meta_data = Vec::with_capacity(4096);
2253
2254 let format_version = STORE_VERSION_V11;
2255 self.set_format_version(format_version);
2256
2257 reddb_file::encode_native_paged_metadata_header(
2258 &mut meta_data,
2259 reddb_file::NativePagedMetadataHeader {
2260 format_version,
2261 collection_count: collection_roots.len() as u32,
2262 },
2263 );
2264
2265 for (name, root_page) in &collection_roots {
2267 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
2268 }
2269
2270 let cross_refs = self.resident.cross_refs.read();
2272 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
2273 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
2274 for (source_id, refs) in cross_refs.iter() {
2275 for (target_id, ref_type, collection) in refs {
2276 reddb_file::encode_native_paged_cross_ref(
2277 &mut meta_data,
2278 source_id.raw(),
2279 target_id.raw(),
2280 ref_type.to_byte(),
2281 collection,
2282 );
2283 }
2284 }
2285
2286 for (name, previous) in btree_indices.iter() {
2287 if !rebuilt_indices
2288 .get(name)
2289 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
2290 {
2291 self.retire_btree(Arc::clone(previous));
2292 }
2293 }
2294 #[cfg(test)]
2295 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
2296 pager
2297 .sync()
2298 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2299 eprintln!("experimental checkpoint exit before manifest");
2300 std::process::exit(183);
2301 }
2302 let rebuilt_keys = self.checkpoint_key_indices(pager, &rebuilt_indices)?;
2303 #[cfg(test)]
2304 if std::env::var_os("REDDB_TEST_EXIT_AFTER_SECONDARY_INDEX_BUILD").is_some() {
2305 pager
2306 .sync()
2307 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2308 std::process::exit(196);
2309 }
2310 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices, &rebuilt_keys)?;
2311
2312 #[cfg(test)]
2313 if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
2314 std::process::exit(195);
2315 }
2316
2317 *btree_indices = rebuilt_indices;
2320 *self.key_indices.write() = rebuilt_keys;
2321 drop(previous_versions);
2322 *self.checkpoint_versions.write() = checkpoint_versions;
2323
2324 if let Some(commit) = &self.commit {
2325 commit
2326 .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
2327 .map_err(StoreError::Io)?;
2328 ownership.active = false;
2329 if runtime_snapshots_durable {
2330 commit
2331 .truncate_after_runtime_checkpoint()
2332 .map_err(StoreError::Io)?;
2333 } else {
2334 commit.truncate().map_err(StoreError::Io)?;
2335 }
2336 }
2337 *self.checkpoint_durable_rows.write() = Default::default();
2338
2339 if let Some(limit) = self.config.resident_memory_bytes_max {
2340 let mut versions = self.checkpoint_versions.write();
2341 for (name, manager) in collections.iter() {
2342 if versions
2343 .get(name)
2344 .is_some_and(|version| !version.has_unprojected_rows)
2345 {
2346 if let Some(tree) = btree_indices.get(name) {
2347 manager.install_backing(
2348 Arc::new(
2349 {
2350 let keys = self.key_indices.read();
2351 let index = keys.get(name).expect("invariant: prepared checkpoint classifies its key index");
2352 if index.indexed {
2353 reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(tree), Arc::clone(&index.tree))
2354 } else {
2355 reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(tree))
2356 }
2357 },
2358 ),
2359 tree.root_page_id(),
2360 self.format_version(),
2361 usize::try_from(limit).unwrap_or(usize::MAX),
2362 manager.count() as u64,
2363 );
2364 if let Some(version) = versions.get_mut(name) {
2365 version.segments = manager.checkpoint_revisions();
2366 }
2367 }
2368 }
2369 }
2370 }
2371
2372 Ok(())
2373 }
2374
2375 pub fn is_paged(&self) -> bool {
2377 self.pager.is_some()
2378 }
2379
2380 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
2383 self.btree_indices
2384 .read()
2385 .get(Self::canonical_collection(collection))
2386 .map(|btree| btree.root_page_id())
2387 .filter(|root| *root != 0)
2388 }
2389
2390 pub(crate) fn operational_manifest_for_path(
2393 &self,
2394 path: &Path,
2395 ) -> reddb_file::OperationalManifest {
2396 let mut cached = self.operational_manifest.lock();
2397 if let Some((current, manifest)) = cached.as_ref() {
2398 if current == path {
2399 return manifest.clone();
2400 }
2401 }
2402 let manifest = reddb_file::OperationalManifest::for_db_path(path);
2403 *cached = Some((path.to_owned(), manifest.clone()));
2404 manifest
2405 }
2406
2407 pub fn db_path(&self) -> Option<&Path> {
2409 self.db_path.as_deref()
2410 }
2411}
2412
2413#[cfg(test)]
2414mod checkpoint_work_tests {
2415 use super::*;
2416
2417 struct SystemNameProjection;
2418
2419 impl super::super::secondary_index::SecondaryIndexProjector for SystemNameProjection {
2420 fn project(
2421 &self,
2422 entity: &UnifiedEntity,
2423 key: &mut [u8; super::super::secondary_index::SECONDARY_KEY_BYTES_MAX],
2424 ) -> Result<Option<usize>, StoreError> {
2425 key[..8].copy_from_slice(&entity.id.raw().to_be_bytes());
2426 Ok(Some(8))
2427 }
2428 }
2429
2430 #[cfg(target_os = "linux")]
2431 #[test]
2432 fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
2433 const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
2434 if let Some(path) = std::env::var_os(CHILD) {
2435 let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
2436 let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
2437 let store = UnifiedStore::open(path).expect("open store");
2438 let initial_count = if multi { 600 } else { 1 };
2439 for id in 1..=initial_count {
2440 store.insert_auto("rows", row(id, 1)).expect("seed");
2441 }
2442 store.persist().expect("baseline");
2443 let manager = store.get_collection("rows").expect("collection");
2444 let ids: Vec<u64> = if multi {
2445 (1..=initial_count).collect()
2446 } else {
2447 vec![if insert { 2 } else { 1 }]
2448 };
2449 let mut actions = Vec::new();
2450 for id in ids {
2451 if insert {
2452 manager.insert(row(id, 9)).expect("insert version");
2453 } else {
2454 manager.update(row(id, 9)).expect("update version");
2455 }
2456 let entity = manager.get(EntityId::new(id)).expect("changed entity");
2457 actions.push(StoreWalAction::upsert_entity(
2458 "rows",
2459 &entity,
2460 None,
2461 store.format_version(),
2462 ));
2463 }
2464 let references = store
2465 .commit
2466 .as_ref()
2467 .expect("coordinator")
2468 .append_checkpoint_actions(&actions)
2469 .expect("durable WAL");
2470 store.remember_checkpoint_rows(actions, references);
2471 store.persist().expect("exit after sparse write");
2472 panic!("inline sparse crash hook was not reached");
2473 }
2474 for (insert, multi) in [(false, false), (true, false), (false, true)] {
2475 let directory = tempfile::tempdir().expect("temporary store");
2476 let path = directory.path().join("data.rdb");
2477 let mut command =
2478 std::process::Command::new(std::env::current_exe().expect("test executable"));
2479 command
2480 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
2481 .env(CHILD, &path)
2482 .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
2483 if insert {
2484 command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
2485 }
2486 if multi {
2487 command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
2488 }
2489 let output = command.output().expect("crash child");
2490 assert_eq!(
2491 output.status.code(),
2492 Some(190),
2493 "insert={insert} multi={multi}: {}",
2494 String::from_utf8_lossy(&output.stderr)
2495 );
2496 let recovered = UnifiedStore::open(&path).expect("recover");
2497 let ids: Vec<u64> = if multi {
2498 (1..=600).collect()
2499 } else {
2500 vec![if insert { 2 } else { 1 }]
2501 };
2502 for id in ids {
2503 assert_eq!(
2504 recovered
2505 .get("rows", EntityId::new(id))
2506 .expect("recovered row")
2507 .data
2508 .as_row()
2509 .expect("row")
2510 .columns,
2511 vec![Value::Integer(9)]
2512 );
2513 }
2514 assert_eq!(
2515 recovered.get_collection("rows").expect("rows").count(),
2516 if multi {
2517 600
2518 } else if insert {
2519 2
2520 } else {
2521 1
2522 }
2523 );
2524 }
2525 }
2526
2527 #[test]
2528 fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
2529 let directory = tempfile::tempdir().expect("temporary store");
2530 let path = directory.path().join("data.rdb");
2531 {
2532 let store = UnifiedStore::open(&path).expect("open store");
2533 store.create_collection("rows").expect("create collection");
2534 store.persist().expect("publish ownership epoch");
2535 assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
2536 store.persist().expect("publish next ownership epoch");
2537 }
2538 let reopened = UnifiedStore::open(&path).expect("reopen store");
2539 assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
2540 }
2541
2542 #[test]
2543 fn metadata_overflow_with_missing_continuation_fails_closed() {
2544 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
2545
2546 let directory = tempfile::tempdir().expect("temporary store");
2547 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
2548 let pager = store.pager.as_ref().unwrap();
2549 let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
2550 let valid_manifest = build_meta_page1_with_overflow(pager, &valid_payload).unwrap();
2551 pager.write_page(1, valid_manifest).unwrap();
2552 assert!(!existing_overflow_chain(pager).unwrap().is_empty());
2553 assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
2554
2555 let missing = pager.page_count().unwrap() + 100;
2556 let mut manifest = Page::new(PageType::Header, 1);
2557 reddb_file::encode_native_metadata_overflow_header(
2558 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2559 reddb_file::NativeMetadataOverflowHeader {
2560 format_version: STORE_VERSION_V11,
2561 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2562 next_overflow_page_id: missing,
2563 },
2564 )
2565 .unwrap();
2566 pager.write_page(1, manifest).unwrap();
2567
2568 assert!(
2569 existing_overflow_chain(pager).is_err(),
2570 "a missing continuation must not be treated as an empty chain"
2571 );
2572
2573 let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
2574 let mut manifest = Page::new(PageType::Header, 1);
2575 reddb_file::encode_native_metadata_overflow_header(
2576 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2577 reddb_file::NativeMetadataOverflowHeader {
2578 format_version: STORE_VERSION_V11,
2579 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2580 next_overflow_page_id: wrong_type.page_id(),
2581 },
2582 )
2583 .unwrap();
2584 pager.write_page(1, manifest).unwrap();
2585 assert!(
2586 existing_overflow_chain(pager).is_err(),
2587 "a live page must not be mistaken for metadata overflow"
2588 );
2589 assert!(
2590 read_meta_payload(pager).is_err(),
2591 "a live page must not be read as metadata overflow"
2592 );
2593
2594 let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
2595 let cycle_id = cycle.page_id();
2596 reddb_file::encode_native_metadata_overflow_continuation_header(
2597 &mut cycle.as_bytes_mut()[HEADER_SIZE..],
2598 reddb_file::NativeMetadataOverflowContinuationHeader {
2599 next_overflow_page_id: cycle_id,
2600 chunk_bytes: 1,
2601 },
2602 )
2603 .unwrap();
2604 pager.write_page(cycle_id, cycle).unwrap();
2605 let mut manifest = Page::new(PageType::Header, 1);
2606 reddb_file::encode_native_metadata_overflow_header(
2607 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2608 reddb_file::NativeMetadataOverflowHeader {
2609 format_version: STORE_VERSION_V11,
2610 total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
2611 next_overflow_page_id: cycle_id,
2612 },
2613 )
2614 .unwrap();
2615 pager.write_page(1, manifest).unwrap();
2616 assert!(
2617 read_meta_payload(pager).is_err(),
2618 "cyclic overflow must not be accepted"
2619 );
2620
2621 let mut manifest = Page::new(PageType::Header, 1);
2622 reddb_file::encode_native_metadata_overflow_header(
2623 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2624 reddb_file::NativeMetadataOverflowHeader {
2625 format_version: STORE_VERSION_V11,
2626 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2627 next_overflow_page_id: 0,
2628 },
2629 )
2630 .unwrap();
2631 pager.write_page(1, manifest).unwrap();
2632 assert!(
2633 read_meta_payload(pager).is_err(),
2634 "a truncated metadata payload must not be accepted"
2635 );
2636 }
2637
2638 #[test]
2639 fn retired_tree_pages_remain_pinned_by_a_cursor() {
2640 let directory = tempfile::tempdir().expect("temporary store");
2641 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2642 for id in 1..=100 {
2643 store.insert_auto("rows", row(id, 1)).expect("seed");
2644 }
2645 store.persist().expect("initial checkpoint");
2646 let tree = store
2647 .btree_indices
2648 .read()
2649 .get("rows")
2650 .cloned()
2651 .expect("tree");
2652 let mut cursor = tree.cursor_first().expect("cursor");
2653 drop(tree);
2654 let manager = store.get_collection("rows").expect("rows");
2655 for value in 2..=6 {
2656 manager.update(row(1, value)).expect("change");
2657 store.persist().expect("checkpoint with old reader");
2658 }
2659 let mut count = 0;
2660 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
2661 let (entity, _) =
2662 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2663 .expect("decode");
2664 assert_eq!(
2665 entity.data.as_row().expect("row").columns,
2666 [Value::Integer(1)]
2667 );
2668 count += 1;
2669 }
2670 assert_eq!(count, 100);
2671 drop(cursor);
2672 store.persist().expect("reclaim after reader release");
2673 assert!(store.retired_btrees.lock().is_empty());
2674 }
2675
2676 #[test]
2677 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
2678 let directory = tempfile::tempdir().expect("temporary store");
2679 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2680 store.create_collection("rows").expect("collection");
2681 let manager = store.get_collection("rows").expect("rows");
2682 for id in (1..=1200).rev() {
2683 manager
2684 .insert(UnifiedEntity::table_row(
2685 EntityId::new(id),
2686 "rows",
2687 id,
2688 vec![Value::text("x".repeat(4096))],
2689 ))
2690 .expect("seed");
2691 }
2692 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
2693 store.persist().expect("bounded checkpoint");
2694 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
2695 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
2696 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
2697 }
2698
2699 #[test]
2700 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
2701 let directory = tempfile::tempdir().expect("temporary store");
2702 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2703 store.create_collection("rows").expect("collection");
2704 for id in 1..=40 {
2705 store
2706 .insert(
2707 "rows",
2708 UnifiedEntity::table_row(
2709 EntityId::new(id),
2710 "rows",
2711 id,
2712 vec![Value::Integer(id as i64)],
2713 ),
2714 )
2715 .expect("seed row");
2716 }
2717 store.persist().expect("initial checkpoint");
2718 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2719 store.persist().expect("unchanged checkpoint");
2720 assert_eq!(
2721 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
2722 0,
2723 "unchanged collections must not hold the writer fence while serializing every row"
2724 );
2725 drop(store);
2726 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
2727 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2728 reopened.persist().expect("unchanged recovered generation");
2729 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2730 }
2731
2732 #[test]
2733 fn sparse_wal_replay_does_not_reencode_complete_collections() {
2734 let directory = tempfile::tempdir().expect("temporary store");
2735 let path = directory.path().join("data.rdb");
2736 let store = UnifiedStore::open(&path).expect("paged store");
2737 for (name, first) in [("rows", 1), ("cold", 129)] {
2738 store.create_collection(name).expect("collection");
2739 for id in first..first + 128 {
2740 store
2741 .insert(
2742 name,
2743 UnifiedEntity::table_row(
2744 EntityId::new(id),
2745 name,
2746 id,
2747 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2748 ),
2749 )
2750 .expect("seed");
2751 }
2752 }
2753 store.persist().expect("durable baseline");
2754 let action = super::super::commit::StoreWalAction::upsert_entity(
2755 "rows",
2756 &row(1, 42),
2757 None,
2758 STORE_VERSION_V11,
2759 );
2760 store
2762 .commit
2763 .as_ref()
2764 .unwrap()
2765 .append_actions(&[action])
2766 .unwrap();
2767 drop(store);
2768
2769 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2770 let recovered = UnifiedStore::open(&path).expect("replay committed change");
2771 assert_eq!(
2772 recovered
2773 .get("rows", EntityId::new(1))
2774 .unwrap()
2775 .data
2776 .as_row()
2777 .unwrap()
2778 .columns,
2779 vec![Value::Integer(42)]
2780 );
2781 for name in ["rows", "cold"] {
2782 assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
2783 }
2784 assert_eq!(
2785 REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
2786 0,
2787 "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
2788 );
2789 for (name, first) in [("rows", 2), ("cold", 129)] {
2790 let end = if name == "rows" { 129 } else { 257 };
2791 for id in first..end {
2792 assert_eq!(
2793 recovered
2794 .get(name, EntityId::new(id))
2795 .unwrap()
2796 .data
2797 .as_row()
2798 .unwrap()
2799 .columns,
2800 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2801 );
2802 }
2803 }
2804
2805 drop(recovered);
2806 let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
2807 assert_eq!(
2808 reopened
2809 .get("rows", EntityId::new(1))
2810 .unwrap()
2811 .data
2812 .as_row()
2813 .unwrap()
2814 .columns,
2815 vec![Value::Integer(42)]
2816 );
2817 }
2818
2819 fn replay_test_payload(bytes: usize) -> String {
2820 let mut state = 0x1234_5678_u32;
2821 (0..bytes)
2822 .map(|_| {
2823 state ^= state << 13;
2824 state ^= state >> 17;
2825 state ^= state << 5;
2826 char::from(b'!' + (state % 90) as u8)
2827 })
2828 .collect()
2829 }
2830
2831 #[test]
2832 fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
2833 let directory = tempfile::tempdir().unwrap();
2834 let path = directory.path().join("data.rdb");
2835 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
2836 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2837 for id in 1..=16 {
2838 store.insert_auto("rows", row(id, 1)).unwrap();
2839 }
2840 store.persist().unwrap();
2841 let payload = replay_test_payload(64 * 1024);
2843 let changed =
2844 UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
2845 let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2846 store
2847 .commit
2848 .as_ref()
2849 .unwrap()
2850 .append_actions(&[action])
2851 .unwrap();
2852 drop(store);
2853 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2854 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
2855 assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
2856 assert_eq!(
2857 recovered
2858 .get("rows", EntityId::new(1))
2859 .unwrap()
2860 .data
2861 .as_row()
2862 .unwrap()
2863 .columns,
2864 changed.data.as_row().unwrap().columns
2865 );
2866 drop(recovered);
2867 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
2868 assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
2869 assert_eq!(
2870 reopened
2871 .get("rows", EntityId::new(1))
2872 .unwrap()
2873 .data
2874 .as_row()
2875 .unwrap()
2876 .columns,
2877 changed.data.as_row().unwrap().columns
2878 );
2879 }
2880
2881 #[cfg(target_os = "linux")]
2882 #[test]
2883 fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
2884 const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
2885 if let Some(path) = std::env::var_os(CHILD) {
2886 let _store = UnifiedStore::open(path).unwrap();
2887 panic!("replay crash hook was not reached");
2888 }
2889 for overflow in [false, true] {
2890 for (hook, code) in [
2891 ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
2892 ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
2893 ] {
2894 let directory = tempfile::tempdir().unwrap();
2895 let path = directory.path().join("data.rdb");
2896 let store = UnifiedStore::open(&path).unwrap();
2897 for id in 1..=128 {
2898 store.insert_auto("rows", row(id, 1)).unwrap();
2899 }
2900 store.persist().unwrap();
2901 let changed = if overflow {
2902 UnifiedEntity::table_row(
2903 EntityId::new(1),
2904 "rows",
2905 1,
2906 vec![Value::text(replay_test_payload(16 * 1024))],
2907 )
2908 } else {
2909 row(1, 42)
2910 };
2911 let action =
2912 StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2913 store
2914 .commit
2915 .as_ref()
2916 .unwrap()
2917 .append_actions(&[action])
2918 .unwrap();
2919 drop(store);
2920 let mut first_size = 0;
2921 for cycle in 0..12 {
2922 let output = std::process::Command::new(std::env::current_exe().unwrap())
2923 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
2924 .env(CHILD, &path).env(hook, "1").output().unwrap();
2925 assert_eq!(
2926 output.status.code(),
2927 Some(code),
2928 "overflow={overflow} hook={hook} cycle={cycle}: {}",
2929 String::from_utf8_lossy(&output.stderr)
2930 );
2931 let size = std::fs::metadata(&path).unwrap().len();
2932 if cycle == 0 {
2933 first_size = size;
2934 }
2935 assert!(
2936 size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
2937 "repeated recovery must reuse reservations: {first_size} -> {size}"
2938 );
2939 }
2940 for _ in 0..2 {
2941 let recovered = UnifiedStore::open(&path).unwrap();
2942 assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
2943 for id in 1..=128 {
2944 let expected = if id == 1 { changed.clone() } else { row(id, 1) };
2945 assert_eq!(
2946 recovered
2947 .get("rows", EntityId::new(id))
2948 .unwrap()
2949 .data
2950 .as_row()
2951 .unwrap()
2952 .columns,
2953 expected.data.as_row().unwrap().columns
2954 );
2955 }
2956 }
2957 }
2958 }
2959 }
2960
2961 #[test]
2962 fn paged_reopen_preserves_stored_sequence_identity() {
2963 let directory = tempfile::tempdir().expect("temporary store");
2964 let path = directory.path().join("data.rdb");
2965 let store = UnifiedStore::open(&path).expect("paged store");
2966 store.create_collection("rows").expect("collection");
2967 for id in [30, 10, 20] {
2968 store.insert("rows", row(id, id as i64)).expect("insert");
2969 }
2970 store.persist().expect("checkpoint");
2971 let sequences = [10, 20, 30].map(|id| {
2972 store
2973 .get("rows", EntityId::new(id))
2974 .expect("row")
2975 .sequence_id
2976 });
2977 drop(store);
2978 let reopened = UnifiedStore::open(&path).expect("reopen");
2979 let recovered = [10, 20, 30].map(|id| {
2980 reopened
2981 .get("rows", EntityId::new(id))
2982 .expect("row")
2983 .sequence_id
2984 });
2985 assert_eq!(
2986 recovered, sequences,
2987 "opening a checkpoint must not rewrite stored row identities"
2988 );
2989 }
2990
2991 #[test]
2992 fn auto_insert_persists_the_assigned_sequence_identity() {
2993 let directory = tempfile::tempdir().expect("temporary store");
2994 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2995 for id in 1u64..=3 {
2996 store
2997 .insert_auto("rows", row(id, id as i64))
2998 .expect("insert");
2999 }
3000 let manager = store.get_collection("rows").expect("rows");
3001 let tree = store
3002 .btree_indices
3003 .read()
3004 .get("rows")
3005 .cloned()
3006 .expect("tree");
3007 for id in 1u64..=3 {
3008 let bytes = tree
3009 .get(&id.to_be_bytes())
3010 .expect("lookup")
3011 .expect("stored row");
3012 let (persisted, _) =
3013 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
3014 .expect("decode");
3015 assert_eq!(
3016 persisted.sequence_id,
3017 manager
3018 .get(EntityId::new(id))
3019 .expect("live row")
3020 .sequence_id
3021 );
3022 }
3023 }
3024
3025 #[test]
3026 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
3027 let directory = tempfile::tempdir().expect("temporary store");
3028 let path = directory.path().join("data.rdb");
3029 let store = UnifiedStore::open(&path).expect("paged store");
3030 store.insert_auto("rows", row(1, 1)).expect("seed");
3031 store.persist().expect("checkpoint");
3032 store.insert_auto("rows", row(2, 2)).expect("append");
3033 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3034 store.persist().expect("inline append checkpoint");
3035 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3036 store
3037 .get_collection("rows")
3038 .expect("rows")
3039 .update(row(1, 99))
3040 .expect("untracked edit");
3041 store.insert_auto("rows", row(3, 3)).expect("next append");
3042 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3043 store.persist().expect("untracked edit checkpoint");
3044 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
3045 drop(store);
3046 let reopened = UnifiedStore::open(&path).expect("reopen");
3047 assert_eq!(
3048 reopened
3049 .get("rows", EntityId::new(1))
3050 .expect("row")
3051 .data
3052 .as_row()
3053 .expect("table")
3054 .columns,
3055 vec![Value::Integer(99)]
3056 );
3057 }
3058
3059 #[test]
3060 fn replayed_trees_are_reusable_after_durable_reconciliation() {
3061 let directory = tempfile::tempdir().expect("temporary store");
3062 let path = directory.path().join("data.rdb");
3063 let store = UnifiedStore::open(&path).expect("paged store");
3064 store.insert_auto("rows", row(1, 1)).expect("seed");
3065 store.persist().expect("checkpoint");
3066 store
3067 .insert_auto("rows", row(2, 2))
3068 .expect("WAL-backed append");
3069 drop(store);
3070 let reopened = UnifiedStore::open(&path).expect("replay");
3071 assert!(reopened.get("rows", EntityId::new(2)).is_some());
3072 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3073 reopened.persist().expect("checkpoint reconciled tree");
3074 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3075 }
3076
3077 #[test]
3078 fn legacy_system_catalog_and_wal_migrate_without_cold_boot_rebuild() {
3079 use crate::storage::wal::{WalRecord, WalWriter};
3080
3081 let directory = tempfile::tempdir().expect("temporary store");
3082 let path = directory.path().join("data.rdb");
3083 let config = UnifiedStoreConfig {
3084 page_cache_slots: Some(32),
3085 resident_memory_bytes_max: Some(2 * 1024 * 1024),
3086 ..UnifiedStoreConfig::default()
3087 };
3088 let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3089 codec_version: 1,
3090 definition: [91; 32],
3091 };
3092 let baseline_index_root;
3093 {
3094 let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3095 store
3096 .create_collection("red_config")
3097 .expect("system collection");
3098 store
3099 .insert_auto(
3100 "red_config",
3101 UnifiedEntity::table_row(
3102 EntityId::new(1),
3103 "red_config",
3104 1,
3105 vec![Value::text("checkpoint")],
3106 ),
3107 )
3108 .expect("legacy-compatible insert");
3109 store
3110 .register_secondary_index(
3111 "red_config",
3112 "by_id",
3113 index_identity.clone(),
3114 Arc::new(SystemNameProjection),
3115 )
3116 .expect("secondary index");
3117 store.persist().expect("baseline checkpoint");
3118 baseline_index_root = store
3119 .secondary_index("red.config", "by_id", &index_identity)
3120 .expect("published secondary index")
3121 .root_page_id();
3122 }
3123
3124 {
3128 let pager = Pager::open(
3129 &path,
3130 PagerConfig {
3131 disk_residency: true,
3132 ..PagerConfig::default()
3133 },
3134 )
3135 .expect("open pager");
3136 let mut page = pager.read_page(1).expect("catalog page");
3137 let bytes = page.as_bytes_mut();
3138 let canonical = b"red.config";
3139 let legacy = b"red_config";
3140 let mut replacements = 0;
3141 for offset in 0..=bytes.len() - canonical.len() {
3142 if &bytes[offset..offset + canonical.len()] == canonical {
3143 bytes[offset..offset + legacy.len()].copy_from_slice(legacy);
3144 replacements += 1;
3145 }
3146 }
3147 assert!(replacements >= 1, "catalog must contain the canonical name");
3148 pager.write_page(1, page).expect("write legacy catalog");
3149 pager.sync().expect("sync legacy catalog");
3150 }
3151 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3152 .rename_collections(&[("red.config", "red_config")])
3153 .expect("restore legacy operational name");
3154
3155 let wal_upsert = reddb_file::encode_store_wal_action_frame(
3158 &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
3159 collection: "red_config".to_string(),
3160 record: UnifiedStore::serialize_entity_record(
3161 &UnifiedEntity::table_row(
3162 EntityId::new(2),
3163 "red_config",
3164 2,
3165 vec![Value::text("legacy WAL")],
3166 ),
3167 None,
3168 STORE_VERSION_CURRENT,
3169 ),
3170 },
3171 )
3172 .expect("legacy WAL action");
3173 let wal_delete = reddb_file::encode_store_wal_action_frame(
3174 &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
3175 collection: "red_config".to_string(),
3176 entity_id: EntityId::new(1).raw(),
3177 },
3178 )
3179 .expect("legacy WAL delete");
3180 let wal_path = reddb_file::layout::unified_wal_path(&path);
3181 {
3182 let mut writer = WalWriter::open(&wal_path).expect("open WAL");
3183 writer
3184 .append(&WalRecord::TxCommitBatch {
3185 tx_id: 77,
3186 actions: vec![wal_upsert, wal_delete],
3187 })
3188 .expect("append legacy transaction");
3189 writer.sync().expect("sync legacy transaction");
3190 }
3191
3192 let (root_after_recovery, pages_after_recovery) = {
3193 let recovered =
3194 UnifiedStore::open_with_config(&path, config.clone()).expect("migrate and recover");
3195 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3196 assert!(
3197 recovered.get("red.config", EntityId::new(1)).is_none(),
3198 "legacy WAL deletes must target the canonical collection"
3199 );
3200 assert_eq!(
3201 recovered
3202 .get("red.config", EntityId::new(2))
3203 .expect("WAL row")
3204 .kind
3205 .collection(),
3206 "red.config"
3207 );
3208 let root = recovered
3209 .collection_root_page("red.config")
3210 .expect("published canonical root");
3211 assert_eq!(
3212 recovered
3213 .secondary_index("red.config", "by_id", &index_identity)
3214 .expect("recovered secondary index")
3215 .root_page_id(),
3216 baseline_index_root,
3217 "name migration must reuse the secondary-index root"
3218 );
3219 let pages = recovered
3220 .pager()
3221 .expect("pager")
3222 .page_count()
3223 .expect("page count");
3224 (root, pages)
3225 };
3226
3227 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3230 .rename_collections(&[("red.config", "red_config")])
3231 .expect("restore stale manifest identity");
3232
3233 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3234 let cold = UnifiedStore::open_with_config(&path, config).expect("second cold boot");
3235 assert_eq!(
3236 cold.collection_root_page("red.config"),
3237 Some(root_after_recovery),
3238 "an idempotent cold boot must reuse the published data root"
3239 );
3240 assert_eq!(
3241 cold.pager()
3242 .expect("pager")
3243 .page_count()
3244 .expect("page count"),
3245 pages_after_recovery,
3246 "cold boot must not allocate replacement index trees"
3247 );
3248 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3249 assert_eq!(
3250 cold.secondary_index("red.config", "by_id", &index_identity)
3251 .expect("secondary index after cold boot")
3252 .root_page_id(),
3253 baseline_index_root
3254 );
3255 assert!(
3256 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3257 .recover_or_bootstrap(&["red.config".to_string()])
3258 .expect("canonical manifest")
3259 .is_empty()
3260 );
3261 }
3262
3263 struct DuplicateSystemCatalogFixture {
3264 _directory: tempfile::TempDir,
3265 path: std::path::PathBuf,
3266 config: UnifiedStoreConfig,
3267 pages_before: u32,
3268 legacy_root: Option<u32>,
3269 legacy_key_root: Option<u32>,
3270 legacy_secondary_root: Option<u32>,
3271 index_identity: super::super::secondary_index::SecondaryIndexIdentity,
3272 }
3273
3274 fn duplicate_system_catalog_fixture(
3275 legacy_populated: bool,
3276 canonical_populated: bool,
3277 index_legacy: bool,
3278 ) -> DuplicateSystemCatalogFixture {
3279 let directory = tempfile::tempdir().expect("temporary store");
3280 let path = directory.path().join("data.rdb");
3281 let config = UnifiedStoreConfig {
3282 page_cache_slots: Some(32),
3283 resident_memory_bytes_max: Some(2 * 1024 * 1024),
3284 ..UnifiedStoreConfig::default()
3285 };
3286 let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3287 codec_version: 1,
3288 definition: [73; 32],
3289 };
3290 let (legacy_root, legacy_key_root, legacy_secondary_root);
3291 {
3292 let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3293 store
3294 .create_collection("cfg_legacy")
3295 .expect("legacy source collection");
3296 if legacy_populated {
3297 store
3298 .insert_auto(
3299 "cfg_legacy",
3300 UnifiedEntity::table_row(
3301 EntityId::new(1),
3302 "red_config",
3303 1,
3304 vec![Value::text("legacy")],
3305 ),
3306 )
3307 .expect("legacy row");
3308 }
3309 store
3310 .create_collection("cfg.canon_")
3311 .expect("canonical source collection");
3312 if canonical_populated {
3313 store
3314 .insert_auto(
3315 "cfg.canon_",
3316 UnifiedEntity::table_row(
3317 EntityId::new(2),
3318 "red.config",
3319 2,
3320 vec![Value::text("canonical")],
3321 ),
3322 )
3323 .expect("canonical row");
3324 }
3325 if index_legacy {
3326 store
3327 .register_secondary_index(
3328 "cfg_legacy",
3329 "by_id",
3330 index_identity.clone(),
3331 Arc::new(SystemNameProjection),
3332 )
3333 .expect("legacy secondary index");
3334 }
3335 store.persist().expect("checkpoint duplicate catalog");
3336 legacy_root = store.collection_root_page("cfg_legacy");
3337 legacy_key_root = store
3338 .key_indices
3339 .read()
3340 .get("cfg_legacy")
3341 .map(|index| index.tree.root_page_id());
3342 legacy_secondary_root = index_legacy.then(|| {
3343 store
3344 .secondary_index("cfg_legacy", "by_id", &index_identity)
3345 .expect("legacy secondary root")
3346 .root_page_id()
3347 });
3348 }
3349
3350 {
3355 let pager = Pager::open(
3356 &path,
3357 PagerConfig {
3358 disk_residency: true,
3359 ..PagerConfig::default()
3360 },
3361 )
3362 .expect("open pager");
3363 let mut page = pager.read_page(1).expect("catalog page");
3364 let bytes = page.as_bytes_mut();
3365 for (source, destination) in [
3366 (b"cfg_legacy".as_slice(), b"red_config".as_slice()),
3367 (b"cfg.canon_".as_slice(), b"red.config".as_slice()),
3368 ] {
3369 let mut replacements = 0;
3370 for offset in 0..=bytes.len() - source.len() {
3371 if &bytes[offset..offset + source.len()] == source {
3372 bytes[offset..offset + destination.len()].copy_from_slice(destination);
3373 replacements += 1;
3374 }
3375 }
3376 assert!(replacements >= 1, "catalog must contain the source name");
3377 }
3378 pager.write_page(1, page).expect("write duplicate catalog");
3379 pager.sync().expect("sync duplicate catalog");
3380 }
3381 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3382 .rename_collections(&[("cfg_legacy", "red_config"), ("cfg.canon_", "red.config")])
3383 .expect("publish duplicate operational names");
3384
3385 let pages_before = Pager::open(
3386 &path,
3387 PagerConfig {
3388 disk_residency: true,
3389 ..PagerConfig::default()
3390 },
3391 )
3392 .expect("inspect pager")
3393 .page_count()
3394 .expect("page count");
3395 DuplicateSystemCatalogFixture {
3396 _directory: directory,
3397 path,
3398 config,
3399 pages_before,
3400 legacy_root,
3401 legacy_key_root,
3402 legacy_secondary_root,
3403 index_identity,
3404 }
3405 }
3406
3407 fn alias_duplicate_system_catalog_state_with(
3408 fixture: &DuplicateSystemCatalogFixture,
3409 mutate_canonical: impl FnOnce(&mut reddb_file::native_resume::CollectionResume),
3410 ) {
3411 let pager = Pager::open(
3412 &fixture.path,
3413 PagerConfig {
3414 disk_residency: true,
3415 ..PagerConfig::default()
3416 },
3417 )
3418 .expect("open duplicate catalog pager");
3419 let mut metadata = read_meta_payload(&pager)
3420 .expect("read duplicate catalog")
3421 .expect("published duplicate catalog");
3422 let mut resume = reddb_file::native_resume::decode_catalog_resume(&metadata)
3423 .expect("decode duplicate resume")
3424 .expect("prepared duplicate resume");
3425 let legacy_state = resume
3426 .collections
3427 .get("red_config")
3428 .expect("legacy resume state")
3429 .clone();
3430 let mut canonical_state = legacy_state.clone();
3431 mutate_canonical(&mut canonical_state);
3432 resume
3433 .collections
3434 .insert("red.config".to_string(), canonical_state);
3435
3436 let header = reddb_file::decode_native_paged_metadata_header(&metadata)
3437 .expect("decode metadata header")
3438 .expect("native metadata header");
3439 let mut position = reddb_file::METADATA_HEADER_BYTES;
3440 let mut replaced_root = false;
3441 for _ in 0..header.collection_count {
3442 let root = reddb_file::decode_native_paged_collection_root(&metadata, &mut position)
3443 .expect("decode collection root");
3444 if root.collection == "red.config" {
3445 metadata[position - 4..position]
3446 .copy_from_slice(&legacy_state.root_page.to_le_bytes());
3447 replaced_root = true;
3448 }
3449 }
3450 assert!(replaced_root, "canonical catalog root must be present");
3451
3452 let trailer = metadata
3453 .windows(4)
3454 .rposition(|bytes| bytes == b"RDRS")
3455 .expect("catalog resume trailer");
3456 metadata.truncate(trailer);
3457 reddb_file::native_resume::encode_catalog_resume(&mut metadata, &resume)
3458 .expect("encode aliased resume");
3459 publish_paged_metadata(&pager, &metadata).expect("publish aliased catalog");
3460 }
3461
3462 fn alias_duplicate_system_catalog_state(fixture: &DuplicateSystemCatalogFixture) {
3463 alias_duplicate_system_catalog_state_with(fixture, |_| {});
3464 }
3465
3466 #[test]
3467 fn populated_legacy_system_collection_replaces_empty_canonical_duplicate() {
3468 let fixture = duplicate_system_catalog_fixture(true, false, true);
3469 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
3470 .expect("resolve empty duplicate");
3471 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3472 assert_eq!(
3473 recovered
3474 .get("red.config", EntityId::new(1))
3475 .expect("preserved legacy row")
3476 .kind
3477 .collection(),
3478 "red.config"
3479 );
3480 assert_eq!(
3481 recovered.collection_root_page("red.config"),
3482 fixture.legacy_root,
3483 "the populated data root must be reused"
3484 );
3485 assert_eq!(
3486 recovered
3487 .key_indices
3488 .read()
3489 .get("red.config")
3490 .map(|index| index.tree.root_page_id()),
3491 fixture.legacy_key_root,
3492 "the populated key-index root must be reused"
3493 );
3494 assert_eq!(
3495 recovered
3496 .secondary_index("red.config", "by_id", &fixture.index_identity)
3497 .expect("surviving secondary index")
3498 .root_page_id(),
3499 fixture
3500 .legacy_secondary_root
3501 .expect("fixture secondary root")
3502 );
3503 drop(recovered);
3504
3505 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3506 .expect("idempotent cold boot");
3507 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
3508 assert_eq!(
3509 cold.pager()
3510 .expect("pager")
3511 .page_count()
3512 .expect("page count"),
3513 fixture.pages_before,
3514 "resolving an empty duplicate must not extend the file"
3515 );
3516 }
3517
3518 #[test]
3519 fn populated_canonical_system_collection_replaces_empty_legacy_duplicate() {
3520 let fixture = duplicate_system_catalog_fixture(false, true, false);
3521 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3522 .expect("retain populated canonical duplicate");
3523 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3524 assert_eq!(
3525 recovered
3526 .get("red.config", EntityId::new(2))
3527 .expect("preserved canonical row")
3528 .kind
3529 .collection(),
3530 "red.config"
3531 );
3532 assert!(recovered.get("red.config", EntityId::new(1)).is_none());
3533 }
3534
3535 #[test]
3536 fn empty_canonical_system_collection_replaces_empty_legacy_duplicate() {
3537 let fixture = duplicate_system_catalog_fixture(false, false, false);
3538 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3539 .expect("prefer canonical empty duplicate");
3540 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3541 assert_eq!(
3542 recovered
3543 .get_collection("red.config")
3544 .expect("canonical collection")
3545 .count(),
3546 0
3547 );
3548 }
3549
3550 #[test]
3551 fn populated_system_collection_duplicates_still_fail_closed() {
3552 let fixture = duplicate_system_catalog_fixture(true, true, false);
3553 let error = match UnifiedStore::open_with_config(&fixture.path, fixture.config) {
3554 Ok(_) => panic!("two populated identities are ambiguous"),
3555 Err(error) => error,
3556 };
3557 assert!(
3558 error
3559 .to_string()
3560 .contains("both populated legacy collection 'red_config' and canonical collection 'red.config'"),
3561 "unexpected error: {error}"
3562 );
3563 assert!(
3564 error.to_string().contains(
3565 "data_root_match=false, key_index_root_match=true, secondary_index_state_match=true"
3566 ),
3567 "the error must identify which physical state diverged without exposing page IDs: {error}"
3568 );
3569 }
3570
3571 #[test]
3572 fn explicit_compaction_retires_populated_legacy_system_config() {
3573 let fixture = duplicate_system_catalog_fixture(true, true, false);
3574 let destination = fixture._directory.path().join("canonical-system-config");
3575
3576 let report = crate::storage::compact::compact_paged_canonical_system_config(
3577 &fixture.path,
3578 &destination,
3579 )
3580 .expect("rewrite duplicate system config");
3581 assert!(report.records > 0);
3582
3583 let rewritten_path = destination.join("data.rdb");
3584 let rewritten = UnifiedStore::open_with_config(&rewritten_path, fixture.config.clone())
3585 .expect("open canonical rewrite");
3586 assert_eq!(rewritten.list_collections(), vec!["red.config".to_string()]);
3587 assert!(
3588 rewritten.get("red.config", EntityId::new(1)).is_none(),
3589 "the obsolete legacy tree must not leak into the canonical collection"
3590 );
3591 assert_eq!(
3592 rewritten
3593 .get("red.config", EntityId::new(2))
3594 .expect("canonical row survives")
3595 .kind
3596 .collection(),
3597 "red.config"
3598 );
3599 drop(rewritten);
3600
3601 let cold = UnifiedStore::open_with_config(&rewritten_path, fixture.config)
3602 .expect("cold reopen canonical rewrite");
3603 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
3604 assert_eq!(
3605 cold.get_collection("red.config")
3606 .expect("canonical collection")
3607 .count(),
3608 1
3609 );
3610 assert!(
3611 crate::storage::operational_manifest::OperationalManifest::for_db_path(&rewritten_path)
3612 .recover_or_bootstrap(&["red.config".to_string()])
3613 .expect("canonical operational manifest")
3614 .is_empty()
3615 );
3616 }
3617
3618 #[test]
3619 fn populated_system_collection_aliases_reconcile_without_rebuild() {
3620 let fixture = duplicate_system_catalog_fixture(true, false, true);
3621 alias_duplicate_system_catalog_state(&fixture);
3622
3623 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
3624 .expect("reconcile identical populated aliases");
3625 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3626 assert_eq!(
3627 recovered
3628 .get("red.config", EntityId::new(1))
3629 .expect("preserved aliased row")
3630 .kind
3631 .collection(),
3632 "red.config"
3633 );
3634 assert_eq!(
3635 recovered.collection_root_page("red.config"),
3636 fixture.legacy_root,
3637 "the shared data root must be reused"
3638 );
3639 assert_eq!(
3640 recovered
3641 .key_indices
3642 .read()
3643 .get("red.config")
3644 .map(|index| index.tree.root_page_id()),
3645 fixture.legacy_key_root,
3646 "the shared key-index root must be reused"
3647 );
3648 assert_eq!(
3649 recovered
3650 .secondary_index("red.config", "by_id", &fixture.index_identity)
3651 .expect("shared secondary index")
3652 .root_page_id(),
3653 fixture
3654 .legacy_secondary_root
3655 .expect("fixture secondary root")
3656 );
3657 drop(recovered);
3658
3659 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3660 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3661 .expect("idempotent cold boot");
3662 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
3663 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3664 assert_eq!(
3665 cold.pager()
3666 .expect("pager")
3667 .page_count()
3668 .expect("page count"),
3669 fixture.pages_before,
3670 "reconciling a physical alias must not extend the file"
3671 );
3672 }
3673
3674 #[test]
3675 fn populated_system_collection_aliases_merge_resume_counter_drift() {
3676 let fixture = duplicate_system_catalog_fixture(true, false, false);
3677 alias_duplicate_system_catalog_state_with(&fixture, |canonical| {
3678 canonical.entities += 7;
3679 canonical.entity_id_next += 100;
3680 canonical.row_id_next += 200;
3681 canonical.sequence_id_next += 300;
3682 });
3683
3684 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
3685 .expect("reconcile shared roots with conservative counters");
3686 let manager = recovered
3687 .get_collection("red.config")
3688 .expect("canonical collection");
3689 assert_eq!(manager.count(), 1, "the shared tree owns the exact count");
3690 assert_eq!(manager.identity_counters(), (102, 202));
3691 assert_eq!(manager.sequence_id_next(), 301);
3692 assert_eq!(
3693 recovered.collection_root_page("red.config"),
3694 fixture.legacy_root,
3695 "counter reconciliation must keep the shared data root"
3696 );
3697 drop(manager);
3698 drop(recovered);
3699
3700 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3701 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
3702 .expect("idempotent cold boot after counter reconciliation");
3703 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
3704 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3705 assert_eq!(
3706 cold.pager()
3707 .expect("pager")
3708 .page_count()
3709 .expect("page count"),
3710 fixture.pages_before,
3711 "counter reconciliation must not extend the file"
3712 );
3713 }
3714
3715 fn row(id: u64, value: i64) -> UnifiedEntity {
3716 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
3717 }
3718
3719 #[test]
3720 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
3721 let directory = tempfile::tempdir().expect("temporary store");
3722 let path = directory.path().join("data.rdb");
3723 let store = UnifiedStore::open(&path).expect("paged store");
3724 store.create_collection("rows").expect("collection");
3725 store.insert("rows", row(1, 1)).expect("seed");
3726 store.persist().expect("initial checkpoint");
3727 let manager = store.get_collection("rows").expect("rows");
3728 manager.update(row(1, 2)).expect("direct update");
3729 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3730 store.persist().expect("updated row checkpoint");
3731 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
3732 let mut metadata = Metadata::new();
3733 metadata.set("verified", MetadataValue::Bool(true));
3734 manager
3735 .set_metadata(EntityId::new(1), metadata)
3736 .expect("metadata");
3737 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3738 store.persist().expect("metadata-only checkpoint");
3739 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
3740 manager
3741 .bulk_insert(vec![row(2, 2), row(3, 3)])
3742 .expect("bulk insert");
3743 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3744 store.persist().expect("bulk insert checkpoint");
3745 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
3746 manager.delete(EntityId::new(2)).expect("delete");
3747 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3748 store.persist().expect("changed checkpoint");
3749 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
3750 drop(store);
3751 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
3752 let manager = recovered.get_collection("rows").expect("rows");
3753 assert_eq!(manager.count(), 2);
3754 assert_eq!(
3755 manager
3756 .get(EntityId::new(1))
3757 .expect("row")
3758 .data
3759 .as_row()
3760 .expect("table")
3761 .columns,
3762 vec![Value::Integer(2)]
3763 );
3764 assert!(manager.get(EntityId::new(2)).is_none());
3765 assert!(manager
3766 .get_metadata(EntityId::new(1))
3767 .expect("metadata")
3768 .get("verified")
3769 .is_some());
3770 }
3771
3772 #[test]
3773 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
3774 let directory = tempfile::tempdir().expect("temporary store");
3775 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3776 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3777 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3778 store.create_collection("rows").expect("collection");
3779 for id in 1..=40 {
3780 store.insert("rows", row(id, id as i64)).expect("seed");
3781 }
3782 store.persist().expect("initial checkpoint");
3783 let unrelated = snapshots.begin();
3784 snapshots.rollback(unrelated);
3785 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3786 store.persist().expect("unrelated rollback checkpoint");
3787 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3788
3789 store
3790 .pending_vault_versions
3791 .write()
3792 .insert(EntityId::new(100));
3793 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3794 store.persist().expect("unrelated vault checkpoint");
3795 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3796 store.pending_vault_versions.write().clear();
3797 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3798 store
3799 .persist()
3800 .expect("unrelated vault publication checkpoint");
3801 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3802 }
3803
3804 #[test]
3805 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
3806 let directory = tempfile::tempdir().expect("temporary store");
3807 let path = directory.path().join("data.rdb");
3808 let store = UnifiedStore::open(&path).expect("paged store");
3809 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3810 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3811 store.create_collection("rows").expect("collection");
3812 let xid = snapshots.begin();
3813 let mut entity = row(1, 1);
3814 entity.xmin = xid;
3815 store.insert("rows", entity).expect("committed row");
3816 snapshots.commit(xid);
3817 store.persist().expect("committed baseline");
3818 snapshots.rollback(xid);
3819 store
3820 .persist()
3821 .expect("untracked outcome requires validation");
3822 drop(store);
3823 let recovered = UnifiedStore::open(&path).expect("recover");
3824 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
3825 }
3826
3827 #[test]
3828 fn exhausted_deferred_row_cache_keeps_full_validation() {
3829 let directory = tempfile::tempdir().expect("temporary store");
3830 let path = directory.path().join("data.rdb");
3831 let store = UnifiedStore::open(&path).expect("paged store");
3832 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3833 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3834 store.create_collection("rows").expect("collection");
3835 let xid = snapshots.begin();
3836 for id in 1..=1025 {
3837 let mut entity = row(id, id as i64);
3838 entity.xmin = xid;
3839 store.insert("rows", entity).expect("pending row");
3840 }
3841 store.persist().expect("exclude pending rows");
3842 assert!(store.checkpoint_versions.read()["rows"]
3843 .deferred_rows
3844 .is_none());
3845 snapshots.commit(xid);
3846 store
3847 .persist()
3848 .expect("publish committed rows conservatively");
3849 drop(store);
3850 let recovered = UnifiedStore::open(&path).expect("recover");
3851 assert_eq!(
3852 recovered.get_collection("rows").expect("rows").count(),
3853 1025
3854 );
3855 }
3856
3857 #[test]
3858 fn rollback_invalidates_reuse_without_a_segment_mutation() {
3859 let directory = tempfile::tempdir().expect("temporary store");
3860 let path = directory.path().join("data.rdb");
3861 let store = UnifiedStore::open(&path).expect("paged store");
3862 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3863 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3864 store.create_collection("rows").expect("collection");
3865 let xid = snapshots.begin();
3866 let mut pending = row(1, 1);
3867 pending.xmin = xid;
3868 store.insert("rows", pending).expect("version");
3869 store.persist().expect("before rollback");
3870 snapshots.rollback(xid);
3871 store.persist().expect("after rollback");
3872 drop(store);
3873 let recovered = UnifiedStore::open(&path).expect("recover");
3874 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
3875 }
3876
3877 #[test]
3878 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
3879 let directory = tempfile::tempdir().expect("temporary store");
3880 let path = directory.path().join("data.rdb");
3881 let store = UnifiedStore::open(&path).expect("paged store");
3882 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
3883 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
3884 store.create_collection("rows").expect("collection");
3885 let xid = snapshots.begin();
3886 let mut deleted = row(1, 1);
3887 deleted.xmax = xid;
3888 store.insert("rows", deleted).expect("version");
3889 store.persist().expect("before rollback");
3890 snapshots.rollback(xid);
3891 store.persist().expect("after rollback");
3892 drop(store);
3893 let recovered = UnifiedStore::open(&path).expect("recover");
3894 let manager = recovered.get_collection("rows").expect("rows");
3895 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
3896 }
3897
3898 #[test]
3899 fn publishing_a_pending_vault_version_invalidates_reuse() {
3900 let directory = tempfile::tempdir().expect("temporary store");
3901 let path = directory.path().join("data.rdb");
3902 let store = UnifiedStore::open(&path).expect("paged store");
3903 store.create_collection("rows").expect("collection");
3904 store.insert("rows", row(1, 1)).expect("row");
3905 store
3906 .pending_vault_versions
3907 .write()
3908 .insert(EntityId::new(1));
3909 store.persist().expect("exclude pending vault version");
3910 store
3911 .pending_vault_versions
3912 .write()
3913 .remove(&EntityId::new(1));
3914 store.persist().expect("include published vault version");
3915 drop(store);
3916 let recovered = UnifiedStore::open(&path).expect("recover");
3917 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
3918 }
3919}