1use super::*;
2use parking_lot::RwLock;
3
4const CHECKPOINT_BATCH_BYTES: usize = 1024 * 1024;
5const CHECKPOINT_BATCH_ROWS: usize = 512;
6
7fn restore_published_tree(
8 pager: &Arc<Pager>,
9 root: u32,
10 proofs: Option<&reddb_file::page_proof::CatalogProofs>,
11) -> Result<BTree, StoreError> {
12 let proof = proofs.and_then(|proofs| proofs.get(&root)).copied();
13 let tree = match (root, proof) {
14 (0, _) if proofs.is_some() => BTree::new_verified(Arc::clone(pager)),
15 (_, Some(proof)) => BTree::with_verified_root(Arc::clone(pager), root, proof),
16 (_, None) if proofs.is_some() => {
17 return Err(page_integrity(
18 root,
19 None,
20 "missing published tree generation proof",
21 ));
22 }
23 _ => return Ok(BTree::with_root(Arc::clone(pager), root)),
24 };
25 tree.map_err(|error| page_integrity(root, None, error.to_string()))
26}
27
28#[derive(Debug)]
29struct SystemCollectionCollision {
30 survivor: String,
31 loser: String,
32 canonical: String,
33 shared_physical_state: bool,
34 resume_counter_drift: bool,
35 merged_resume: Option<reddb_file::native_resume::CollectionResume>,
36}
37
38#[derive(Debug, Default)]
39struct CatalogLoadOutcome {
40 migrated: bool,
41 collisions: Vec<SystemCollectionCollision>,
42}
43
44fn merge_shared_alias_resume(
45 legacy: &reddb_file::native_resume::CollectionResume,
46 canonical: &reddb_file::native_resume::CollectionResume,
47) -> Option<reddb_file::native_resume::CollectionResume> {
48 let (data_root_match, key_index_root_match, secondary_index_state_match) =
49 shared_alias_resume_matches(legacy, canonical);
50 if !data_root_match || !key_index_root_match || !secondary_index_state_match {
51 return None;
52 }
53 let mut merged = legacy.clone();
54 merged.entities = legacy.entities.max(canonical.entities);
59 merged.entity_id_next = legacy.entity_id_next.max(canonical.entity_id_next);
60 merged.row_id_next = legacy.row_id_next.max(canonical.row_id_next);
61 merged.sequence_id_next = legacy.sequence_id_next.max(canonical.sequence_id_next);
62 Some(merged)
63}
64
65fn shared_alias_resume_matches(
66 legacy: &reddb_file::native_resume::CollectionResume,
67 canonical: &reddb_file::native_resume::CollectionResume,
68) -> (bool, bool, bool) {
69 (
70 legacy.root_page == canonical.root_page,
71 legacy.key_index_root == canonical.key_index_root,
72 legacy.secondary_indices == canonical.secondary_indices
73 && legacy.metadata_expiries == canonical.metadata_expiries,
74 )
75}
76
77struct CheckpointTreeBuilder {
80 tree: Arc<BTree>,
81 records: Vec<(Vec<u8>, Vec<u8>)>,
82 bytes: usize,
83}
84
85impl CheckpointTreeBuilder {
86 fn new(pager: &Arc<Pager>) -> Result<Self, StoreError> {
87 pager
88 .fence_required_capabilities(
89 reddb_file::file_format::FILE_CAPABILITY_TREE_PROOFS
90 | reddb_file::file_format::FILE_CAPABILITY_DWB_PUBLICATION,
91 )
92 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
93 Ok(Self {
94 tree: UnifiedStore::new_derived_btree(pager)?,
95 records: Vec::new(),
96 bytes: 0,
97 })
98 }
99
100 fn push(&mut self, record: (Vec<u8>, Vec<u8>)) -> Result<(), StoreError> {
101 let bytes = record.0.len() + record.1.len();
102 if !self.records.is_empty()
103 && (self.bytes.saturating_add(bytes) > CHECKPOINT_BATCH_BYTES
104 || self.records.len() >= CHECKPOINT_BATCH_ROWS)
105 {
106 self.flush()?;
107 }
108 self.bytes += bytes;
109 self.records.push(record);
110 #[cfg(test)]
111 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(peak.get().max(self.bytes)));
112 Ok(())
113 }
114
115 fn flush(&mut self) -> Result<(), StoreError> {
116 self.tree
117 .bulk_insert_sorted(&self.records)
118 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
119 self.records.clear();
120 self.bytes = 0;
121 Ok(())
122 }
123
124 fn copy_prefix(&mut self, previous: &BTree, count: usize) -> Result<(), StoreError> {
125 let mut cursor = previous
126 .cursor_first()
127 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
128 for _ in 0..count {
129 let record = cursor
130 .next()
131 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
132 .ok_or_else(|| StoreError::Serialization("checkpoint prefix disappeared".into()))?;
133 self.push(record)?;
134 }
135 Ok(())
136 }
137}
138
139#[cfg(test)]
140thread_local! {
141 static CHECKPOINT_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
142 static REPLAY_SERIALIZED_ROWS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
143 static CHECKPOINT_BUFFERED_BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
144}
145
146fn checkpoint_visibility_matches(
147 previous: &CheckpointVersion,
148 current: &CheckpointVersion,
149) -> bool {
150 (previous.visibility_revision == current.visibility_revision
151 || previous.excluded_xids == current.excluded_xids)
152 && previous.pending_vault == current.pending_vault
153}
154
155const META_PAGE_CONTENT_CAP: usize =
174 crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
175const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
176const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
177const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
178const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
179
180fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
181 let cs = crate::storage::engine::HEADER_SIZE;
182 let page = pager.read_manifest_page()?;
183 let mut digest = reddb_file::page_proof::CatalogDigest::default();
184 digest.add_page(page.as_bytes());
185 let bytes = page.as_bytes();
186 if bytes.len() < cs + META_V3_PAGE1_HEADER {
187 return Ok(Vec::new());
188 }
189 let Some(header) =
190 reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
191 PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
192 })?
193 else {
194 verify_catalog_digest(pager, digest.finish())?;
195 return Ok(Vec::new());
196 };
197 let mut next = header.next_overflow_page_id;
198 let mut pages = Vec::new();
199 let mut seen = std::collections::HashSet::new();
200 while next != 0 {
201 if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
202 return Err(PagerError::InvalidDatabase(
203 "cyclic metadata overflow chain".into(),
204 ));
205 }
206 let ov = pager.read_page(next)?;
207 digest.add_page(ov.as_bytes());
208 if ov.page_type()? != crate::storage::engine::PageType::Overflow {
209 return Err(PagerError::InvalidDatabase(
210 "invalid metadata overflow page type".into(),
211 ));
212 }
213 let ob = ov.as_bytes();
214 let nn = reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
215 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?
216 .next_overflow_page_id;
217 pages.push(next);
218 next = nn;
219 }
220 verify_catalog_digest(pager, digest.finish())?;
221 Ok(pages)
222}
223
224fn verify_catalog_digest(pager: &Pager, digest: [u8; 32]) -> Result<(), PagerError> {
225 let reference = pager.manifest_reference()?;
226 if reference.page_id != 0 && reference.digest != digest {
227 return Err(PagerError::InvalidDatabase(format!(
228 "catalog page {} generation proof does not match the superblock",
229 reference.page_id
230 )));
231 }
232 Ok(())
233}
234
235fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
236 let _ownership = pager.lock_ownership_mutations();
237 let mut retired = existing_overflow_chain(pager)?;
238 let previous_root = pager.manifest_page_id()?;
239 if previous_root >= reddb_engine::storage::pager_io::FIRST_ALLOCATABLE_PAGE {
240 retired.push(previous_root);
241 }
242 if !retired.is_empty() {
243 pager.record_retirement_intent(&retired)?;
244 }
245 pager.fence_required_capabilities(reddb_file::FILE_CAPABILITY_CATALOG_ROOT)?;
246 let root = pager
247 .allocate_page(crate::storage::engine::PageType::Header)?
248 .page_id();
249 let (page, digest) = build_meta_page1_with_overflow(pager, meta_data, root)?;
250 pager.write_page(root, page)?;
251 #[cfg(test)]
252 if let Ok(prefix) = std::env::var("REDDB_TEST_CATALOG_HOME_IO_PREFIX") {
253 crate::storage::engine::pager::test_fail_next_pager_write(
254 Some(u64::from(root) * crate::storage::engine::PAGE_SIZE as u64),
255 prefix.parse().expect("catalog home prefix"),
256 );
257 }
258 pager.sync()?;
260 #[cfg(test)]
261 if std::env::var_os("REDDB_TEST_EXIT_BEFORE_CATALOG_ROOT_PUBLICATION").is_some() {
262 std::process::exit(197);
263 }
264 pager.publish_manifest_reference(reddb_file::page_proof::PageReference {
265 page_id: root,
266 digest,
267 })?;
268 #[cfg(test)]
269 if std::env::var_os("REDDB_TEST_EXIT_AFTER_CATALOG_ROOT_PUBLICATION").is_some() {
270 std::process::exit(198);
271 }
272 if !retired.is_empty() {
273 pager.free_retired_pages(&retired)?;
276 pager.sync()?;
277 }
278 Ok(())
279}
280
281fn build_meta_page1_with_overflow(
282 pager: &Pager,
283 meta_data: &[u8],
284 root_page_id: u32,
285) -> Result<(crate::storage::engine::Page, [u8; 32]), PagerError> {
286 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
287
288 let mut page1 = Page::new(PageType::Header, root_page_id);
289 let cs = HEADER_SIZE;
290
291 if meta_data.len() <= META_PAGE_CONTENT_CAP {
292 let buf = page1.as_bytes_mut();
294 buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
295 page1.update_checksum();
296 let digest = reddb_file::page_proof::page_digest(page1.as_bytes());
297 return Ok((page1, digest));
298 }
299
300 let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
304 let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
305 let mut chunks: Vec<&[u8]> = Vec::new();
306 while !tail.is_empty() {
307 let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
308 chunks.push(&tail[..take]);
309 tail = &tail[take..];
310 }
311
312 let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
313 let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
314 for _ in 0..chunks.len() {
315 let pg = pager.allocate_page(PageType::Overflow)?;
316 overflow_ids.push(pg.page_id());
317 overflow_pages.push(pg);
318 }
319
320 for i in 0..chunks.len() {
321 let next = if i + 1 < chunks.len() {
322 overflow_ids[i + 1]
323 } else {
324 0u32
325 };
326 let len = chunks[i].len() as u32;
327 let buf = overflow_pages[i].as_bytes_mut();
328 reddb_file::encode_native_metadata_overflow_continuation_header(
329 &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
330 reddb_file::NativeMetadataOverflowContinuationHeader {
331 next_overflow_page_id: next,
332 chunk_bytes: len,
333 },
334 )
335 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
336 buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
337 }
338
339 let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
341 .ok()
342 .flatten()
343 .map_or(0, |header| header.format_version);
344
345 let buf = page1.as_bytes_mut();
346 reddb_file::encode_native_metadata_overflow_header(
347 &mut buf[cs..cs + META_V3_PAGE1_HEADER],
348 reddb_file::NativeMetadataOverflowHeader {
349 format_version,
350 total_payload_bytes: meta_data.len() as u32,
351 next_overflow_page_id: overflow_ids[0],
352 },
353 )
354 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
355 buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
356 .copy_from_slice(first_chunk);
357
358 page1.update_checksum();
359 let mut digest = reddb_file::page_proof::CatalogDigest::default();
360 digest.add_page(page1.as_bytes());
361 for mut page in overflow_pages {
362 page.update_checksum();
363 digest.add_page(page.as_bytes());
364 pager.write_page(page.page_id(), page)?;
365 }
366 Ok((page1, digest.finish()))
367}
368
369fn storage_integrity(
370 zone: &str,
371 id: impl Into<String>,
372 collection: Option<String>,
373 detail: impl Into<String>,
374) -> StoreError {
375 StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
376 zone, id, collection, detail,
377 ))
378}
379
380fn page_integrity(
381 page_id: u32,
382 collection: Option<String>,
383 detail: impl Into<String>,
384) -> StoreError {
385 storage_integrity("page", page_id.to_string(), collection, detail)
386}
387
388fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
400 let _ownership = pager.lock_ownership_mutations();
401 let cs = crate::storage::engine::HEADER_SIZE;
402 let meta_page = pager.read_manifest_page().map_err(|err| {
403 storage_integrity(
404 "manifest",
405 "manifest",
406 None,
407 format!("internal manifest zone read failed ({err})"),
408 )
409 })?;
410 let mut digest = reddb_file::page_proof::CatalogDigest::default();
411 digest.add_page(meta_page.as_bytes());
412 let bytes = meta_page.as_bytes();
413 if bytes.len() < cs + 4 {
414 return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
415 }
416 let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
417 .map_err(|err| page_integrity(1, None, err.to_string()))?
418 {
419 Some(header) => header,
420 None => {
421 verify_catalog_digest(pager, digest.finish())
422 .map_err(|error| page_integrity(meta_page.page_id(), None, error.to_string()))?;
423 return Ok(Some(bytes[cs..].to_vec()));
424 }
425 };
426 if bytes.len() < cs + META_V3_PAGE1_HEADER {
427 return Err(page_integrity(
428 1,
429 None,
430 "metadata overflow header is truncated",
431 ));
432 }
433 let total = header.total_payload_bytes as usize;
434 if total == 0 || total > 64 * 1024 * 1024 {
435 return Err(page_integrity(
436 meta_page.page_id(),
437 None,
438 "catalog exceeds metadata budget",
439 ));
440 }
441 let mut next = header.next_overflow_page_id;
442 let mut payload: Vec<u8> = Vec::with_capacity(total);
443 let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
444 payload.extend_from_slice(
445 &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
446 );
447 let mut seen = std::collections::HashSet::new();
448 while next != 0 && payload.len() < total {
449 if !seen.insert(next)
450 || seen.len()
451 >= pager
452 .page_count()
453 .map_err(|err| page_integrity(next, None, err.to_string()))?
454 as usize
455 {
456 return Err(page_integrity(next, None, "cyclic metadata overflow chain"));
457 }
458 let ov = pager
459 .read_page(next)
460 .map_err(|err| page_integrity(next, None, err.to_string()))?;
461 digest.add_page(ov.as_bytes());
462 if ov
463 .page_type()
464 .map_err(|err| page_integrity(next, None, err.to_string()))?
465 != crate::storage::engine::PageType::Overflow
466 {
467 return Err(page_integrity(
468 next,
469 None,
470 "invalid metadata overflow page type",
471 ));
472 }
473 let ob = ov.as_bytes();
474 if ob.len() < cs + META_V3_OVERFLOW_HEADER {
475 return Err(page_integrity(
476 next,
477 None,
478 "metadata overflow continuation is truncated",
479 ));
480 }
481 let continuation =
482 reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
483 .map_err(|err| page_integrity(next, None, err.to_string()))?;
484 let nn = continuation.next_overflow_page_id;
485 let len = continuation.chunk_bytes as usize;
486 let remaining = total - payload.len();
487 let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
488 payload.extend_from_slice(
489 &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
490 );
491 next = nn;
492 }
493 if payload.len() != total || next != 0 {
494 return Err(page_integrity(
495 1,
496 None,
497 "metadata overflow chain is incomplete",
498 ));
499 }
500 verify_catalog_digest(pager, digest.finish())
501 .map_err(|error| page_integrity(meta_page.page_id(), None, error.to_string()))?;
502 Ok(Some(payload))
503}
504
505impl UnifiedStore {
506 pub(super) fn new_derived_btree(pager: &Arc<Pager>) -> Result<Arc<BTree>, StoreError> {
507 let header = pager
508 .header()
509 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
510 let tree = if header.required_capabilities
511 & reddb_file::file_format::FILE_CAPABILITY_TREE_PROOFS
512 != 0
513 {
514 BTree::new_verified(Arc::clone(pager))
515 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
516 } else {
517 BTree::new(Arc::clone(pager))
518 };
519 Ok(Arc::new(tree))
520 }
521
522 pub(super) fn immutable_paged_base(&self) -> Result<bool, StoreError> {
523 if self.config.resident_memory_bytes_max.is_some() {
524 return Ok(true);
525 }
526 let Some(pager) = &self.pager else {
527 return Ok(false);
528 };
529 let header = pager
530 .header()
531 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
532 Ok(
533 header.required_capabilities & reddb_file::file_format::FILE_CAPABILITY_TREE_PROOFS
534 != 0,
535 )
536 }
537
538 pub(crate) fn publish_native_catalog(pager: &Pager, metadata: &[u8]) -> Result<(), PagerError> {
541 publish_paged_metadata(pager, metadata)
542 }
543
544 pub(crate) fn fence_required_capabilities(&self, capabilities: u64) -> Result<(), StoreError> {
545 if capabilities == 0 {
546 return Ok(());
547 }
548 let Some(pager) = &self.pager else {
549 return Ok(());
550 };
551 pager
552 .fence_required_capabilities(capabilities)
553 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))
554 }
555
556 fn checkpoint_tree(
560 &self,
561 pager: &Arc<Pager>,
562 manager: &SegmentManager,
563 previous: Option<&Arc<BTree>>,
564 mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
565 ) -> Result<Arc<BTree>, StoreError> {
566 let previous = previous.filter(|tree| tree.has_page_proofs());
567 if let Some((base, overlay)) = manager.backing_overlay(&mut encode) {
568 use reddb_engine::storage::collection_read::{CollectionRead, MergedCollection};
569 let merged = MergedCollection::new(base.as_ref(), overlay);
570 let mut cursor = merged.cursor()?;
571 let mut builder = CheckpointTreeBuilder::new(pager)?;
572 while let Some(record) = cursor.next_record(
573 usize::try_from(self.config.resident_memory_bytes_max.unwrap_or(u64::MAX))
574 .unwrap_or(usize::MAX),
575 )? {
576 builder.push((record.id.raw().to_be_bytes().to_vec(), record.bytes))?;
577 }
578 builder.flush()?;
579 return Ok(builder.tree);
580 }
581 let mut cursor = previous
582 .map(|tree| tree.cursor_first())
583 .transpose()
584 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
585 let mut matched = 0;
586 let mut builder = None;
587 let mut failure = None;
588 manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
589 let Some(value) = encode(entity, metadata) else {
590 return true;
591 };
592 let record = (entity.id.raw().to_be_bytes().to_vec(), value);
593 let result = (|| {
594 if builder.is_none() {
595 if let Some(cursor) = cursor.as_mut() {
596 if cursor
597 .next()
598 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
599 .as_ref()
600 == Some(&record)
601 {
602 matched += 1;
603 return Ok(());
604 }
605 }
606 let mut replacement = CheckpointTreeBuilder::new(pager)?;
607 if let Some(previous) = previous {
608 replacement.copy_prefix(previous, matched)?;
609 }
610 builder = Some(replacement);
611 }
612 builder
613 .as_mut()
614 .expect("replacement initialized")
615 .push(record)
616 })();
617 if let Err(error) = result {
618 failure = Some(error);
619 return false;
620 }
621 true
622 });
623 if let Some(error) = failure {
624 return Err(error);
625 }
626 if builder.is_none() {
627 if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
628 if cursor
629 .next()
630 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
631 .is_none()
632 {
633 return Ok(Arc::clone(previous));
634 }
635 }
636 let mut replacement = CheckpointTreeBuilder::new(pager)?;
637 if let Some(previous) = previous {
638 replacement.copy_prefix(previous, matched)?;
639 }
640 builder = Some(replacement);
641 }
642 let mut builder = builder.expect("replacement or reused tree");
643 builder.flush()?;
644 Ok(builder.tree)
645 }
646
647 fn publish_reclaimable_trees(
651 &self,
652 pager: &Pager,
653 metadata: &mut Vec<u8>,
654 live: &HashMap<String, Arc<BTree>>,
655 keys: &HashMap<String, PublishedKeyIndex>,
656 ) -> Result<(), StoreError> {
657 let mut next_xid_summary = None;
658 let publishes_proofs = pager
659 .header()
660 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
661 .required_capabilities
662 & reddb_file::file_format::FILE_CAPABILITY_TREE_PROOFS
663 != 0;
664 if publishes_proofs {
665 pager
666 .fence_required_capabilities(
667 reddb_file::file_format::FILE_CAPABILITY_DWB_PUBLICATION,
668 )
669 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
670 }
671 let mut retired = self.retired_btrees.lock();
672 let mut roots = Vec::new();
673 let mut reclaim = Vec::new();
674 let mut remove = std::collections::HashSet::new();
675 for (index, entry) in retired.iter_mut().enumerate() {
676 if live
677 .values()
678 .chain(keys.values().flat_map(PublishedKeyIndex::trees))
679 .any(|tree| Arc::ptr_eq(tree, &entry.tree))
680 {
681 remove.insert(index);
682 continue;
683 }
684 if entry.published
685 && Arc::strong_count(&entry.tree) == 1
686 && !entry.tree.has_retirement_readers()
687 {
688 #[cfg(test)]
692 if std::env::var_os("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT").is_some() {
693 roots.push(entry.tree.root_page_id());
694 }
695 reclaim.extend(
696 entry
697 .tree
698 .retired_pages()
699 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
700 );
701 remove.insert(index);
702 } else if entry.tree.root_page_id() != 0 {
703 if publishes_proofs && !entry.tree.has_page_proofs() {
704 let prepared = entry.tree.prepare_legacy_page_proofs().map_err(|error| {
705 page_integrity(entry.tree.root_page_id(), None, error.to_string())
706 })?;
707 entry.tree = Arc::new(prepared);
708 }
709 roots.push(entry.tree.root_page_id());
710 }
711 }
712 roots.sort_unstable();
713 roots.dedup();
714 reddb_file::encode_native_paged_retired_trees(metadata, &roots)
715 .map_err(|e| StoreError::Serialization(e.to_string()))?;
716 let (pending_pages, published_pages) = pager
717 .deferred_reclamation_pages()
718 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
719 let readers = live
720 .values()
721 .chain(keys.values().flat_map(PublishedKeyIndex::trees))
722 .any(|tree| tree.has_retirement_readers())
723 || retired
724 .iter()
725 .any(|entry| entry.tree.has_retirement_readers());
726 let released_pages = if readers { Vec::new() } else { published_pages };
727 let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
728 let retained_pages: Vec<_> = pending_pages
729 .into_iter()
730 .filter(|id| !released.contains(id))
731 .collect();
732 reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
733 .map_err(|e| StoreError::Serialization(e.to_string()))?;
734 let active_epoch = self.ownership_active_epoch.load(Ordering::Acquire);
735 let catalog_epoch = if active_epoch == 0 {
736 self.ownership_catalog_epoch.load(Ordering::Acquire)
737 } else {
738 active_epoch
739 };
740 if catalog_epoch != 0 {
741 reddb_file::native_store::encode_native_paged_ownership_epoch(metadata, catalog_epoch)
742 .map_err(|e| StoreError::Serialization(e.to_string()))?;
743 }
744 if publishes_proofs {
745 let mut proofs = reddb_file::page_proof::CatalogProofs::new();
746 for tree in live
747 .values()
748 .chain(keys.values().flat_map(PublishedKeyIndex::trees))
749 .chain(
750 retired
751 .iter()
752 .enumerate()
753 .filter(|(index, _)| !remove.contains(index))
754 .map(|(_, entry)| &entry.tree),
755 )
756 {
757 if let Some((root, proof)) = tree
758 .seal_page_proofs()
759 .map_err(|error| page_integrity(tree.root_page_id(), None, error.to_string()))?
760 {
761 if root != 0 {
762 if proofs.insert(root, proof).is_some_and(|old| old != proof) {
763 return Err(page_integrity(
764 root,
765 None,
766 "conflicting tree generation proofs",
767 ));
768 }
769 }
770 }
771 }
772 if live
773 .values()
774 .chain(keys.values().flat_map(PublishedKeyIndex::trees))
775 .any(|tree| tree.root_page_id() != 0 && !proofs.contains_key(&tree.root_page_id()))
776 {
777 return Err(page_integrity(1, None, "live tree has no generation proof"));
778 }
779 reddb_file::page_proof::encode_catalog(metadata, &proofs)
780 .map_err(|error| StoreError::Serialization(error.to_string()))?;
781 }
782 if self.config.resident_memory_bytes_max.is_some() {
783 pager
784 .fence_required_capabilities(reddb_file::FILE_CAPABILITY_DISK_RESIDENCY)
785 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
786 use reddb_file::native_resume::{
787 encode_catalog_resume, CatalogResume, CollectionResume,
788 };
789 let mut states = std::collections::BTreeMap::new();
790 let mut entity_id_next = self.resident.next_entity_id.load(Ordering::SeqCst);
791 let collections = self.resident.collections.read();
792 let metadata_expiry_prepared = collections
793 .values()
794 .all(|manager| manager.metadata_expiry_projection_ready());
795 let publishes_rows = {
796 let summary = self.recovery_xid_summary.read();
797 live.iter().any(|(name, tree)| {
798 let root = tree.root_page_id();
799 root != 0
800 && summary.as_ref().and_then(|state| state.roots.get(name)) != Some(&root)
801 })
802 };
803 let xid_high_water_mark =
804 self.base_xid_high_water_mark(&collections).map(|mut high| {
805 if publishes_rows {
810 for manager in collections.values() {
811 manager.for_each_entity(|entity| {
812 high = high.max(entity.xmin).max(entity.xmax);
813 true
814 });
815 }
816 }
817 if let Some(snapshots) = self.snapshot_manager.get() {
818 high = high.max(snapshots.peek_next_xid().saturating_sub(1));
819 }
820 high
821 });
822 for (name, manager) in collections.iter() {
823 let tree = live.get(name);
824 let (entity_next, row_id_next) = manager.identity_counters();
825 entity_id_next = entity_id_next.max(entity_next);
826 let root_page = tree.map_or(0, |tree| tree.root_page_id());
827 let entities = if root_page == 0 {
828 0
829 } else if let Some(count) = manager.backing_count_at_root(root_page) {
830 count
831 } else {
832 let mut cursor = tree
835 .expect("invariant: nonzero root has a tree")
836 .cursor_first()
837 .map_err(|error| {
838 StoreError::Io(std::io::Error::other(error.to_string()))
839 })?;
840 let mut count = 0_u64;
841 while cursor
842 .skip_next()
843 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
844 {
845 count += 1;
846 }
847 count
848 };
849 let metadata_expiries = if metadata_expiry_prepared {
850 let mut expiries = Vec::new();
851 if let Some(tree) = tree {
852 for (deadline_ms, entity_id) in manager.metadata_expiry_entries() {
853 if tree
854 .get(&entity_id.raw().to_be_bytes())
855 .map_err(|error| {
856 StoreError::Io(std::io::Error::other(error.to_string()))
857 })?
858 .is_some()
859 {
860 expiries.push(reddb_file::native_resume::MetadataExpiryResume {
861 deadline_ms,
862 entity_id: entity_id.raw(),
863 });
864 }
865 }
866 }
867 expiries
868 } else {
869 Vec::new()
870 };
871 states.insert(
872 name.clone(),
873 CollectionResume {
874 root_page,
875 entities,
876 entity_id_next: entity_next,
877 row_id_next,
878 sequence_id_next: manager.sequence_id_next(),
879 key_index_root: tree.and_then(|tree| {
880 keys.get(name)
881 .filter(|index| {
882 index.indexed && index.source.ptr_eq(&Arc::downgrade(tree))
883 })
884 .map(|index| index.tree.root_page_id())
885 }),
886 secondary_indices: tree
887 .and_then(|tree| {
888 keys.get(name)
889 .filter(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
890 })
891 .map(|index| {
892 index
893 .secondary
894 .iter()
895 .map(|(name, index)| (name.clone(), index.resume()))
896 .collect()
897 })
898 .unwrap_or_default(),
899 metadata_expiries,
900 },
901 );
902 }
903 let resume = CatalogResume {
904 entity_id_next,
905 xid_high_water_mark,
906 metadata_expiry_prepared,
907 collections: states,
908 };
909 encode_catalog_resume(metadata, &resume)
910 .map_err(|error| StoreError::Serialization(error.to_string()))?;
911 next_xid_summary = xid_high_water_mark.map(|high_water_mark| {
912 super::recovery_xid::RecoveryXidSummary {
913 high_water_mark,
914 roots: resume
915 .collections
916 .into_iter()
917 .map(|(name, state)| (name, state.root_page))
918 .collect(),
919 }
920 });
921 }
922 let mut retirement_intent = reclaim.clone();
925 retirement_intent.extend_from_slice(&released_pages);
926 if !retirement_intent.is_empty() {
927 pager
928 .record_retirement_intent(&retirement_intent)
929 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
930 }
931 publish_paged_metadata(pager, metadata)
934 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
935 if self.config.resident_memory_bytes_max.is_some()
939 && pager
940 .header()
941 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
942 .version
943 == reddb_file::file_format::PAGE_RESULT_CACHE_FILE_VERSION
944 {
945 pager
946 .fence_file_format_version(
947 reddb_file::file_format::PAGE_RESULT_CACHE_DISK_FILE_VERSION,
948 )
949 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
950 }
951 *self.recovery_xid_summary.write() = next_xid_summary;
952 self.ownership_catalog_epoch
953 .store(catalog_epoch, Ordering::Release);
954 #[cfg(test)]
955 if !reclaim.is_empty()
956 && std::env::var_os("REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE").is_some()
957 {
958 eprintln!(
959 "experimental exit before retired free pages={}",
960 reclaim.len()
961 );
962 std::process::exit(184);
963 }
964 let mut index = 0;
965 retired.retain_mut(|entry| {
966 let keep = !remove.contains(&index);
967 entry.published = true;
968 index += 1;
969 keep
970 });
971 if !reclaim.is_empty() {
972 pager
973 .free_retired_pages(&reclaim)
974 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
975 #[cfg(test)]
976 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE").is_some() {
977 eprintln!(
978 "experimental exit after retired enqueue pages={}",
979 reclaim.len()
980 );
981 std::process::exit(186);
982 }
983 pager
984 .sync()
985 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
986 #[cfg(test)]
987 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE").is_some() {
988 eprintln!(
989 "experimental exit after retired free pages={}",
990 reclaim.len()
991 );
992 std::process::exit(185);
993 }
994 }
995 pager
996 .complete_deferred_publication(&retained_pages, &released_pages)
997 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
998 if !released_pages.is_empty() {
999 pager
1000 .sync()
1001 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1002 }
1003 Ok(())
1004 }
1005
1006 pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
1007 let mut retired = self.retired_btrees.lock();
1008 if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
1009 {
1010 retired.push(RetiredBTree {
1011 tree,
1012 published: false,
1013 });
1014 }
1015 }
1016
1017 pub(crate) fn note_paged_insert(
1021 &self,
1022 collection: &str,
1023 manager: &Arc<SegmentManager>,
1024 before: &[(u64, u64)],
1025 id: EntityId,
1026 ) {
1027 let after = manager.checkpoint_revisions();
1028 if before.len() != after.len() {
1029 return;
1030 }
1031 let mut changed = 0;
1032 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
1033 if old_id != new_id {
1034 return;
1035 }
1036 if old_revision != new_revision {
1037 if old_revision.checked_add(1) != Some(*new_revision) {
1038 return;
1039 }
1040 changed += 1;
1041 }
1042 }
1043 if changed != 1 {
1044 return;
1045 }
1046 let mut versions = self.checkpoint_versions.write();
1047 let Some(previous) = versions.get_mut(collection) else {
1048 return;
1049 };
1050 if previous.manager.ptr_eq(&Arc::downgrade(manager))
1051 && previous.segments == before
1052 && previous.format_version == self.format_version()
1053 && !previous.pending_vault.contains(&id)
1054 && !previous.excluded_xids.contains(&0)
1055 {
1056 previous.segments = after;
1057 }
1058 }
1059
1060 pub(crate) fn note_paged_delete(
1064 &self,
1065 collection: &str,
1066 manager: &Arc<SegmentManager>,
1067 before: &[(u64, u64)],
1068 deleted: &[EntityId],
1069 ) {
1070 let after = manager.checkpoint_revisions();
1071 if before.len() != after.len() {
1072 return;
1073 }
1074 let mut changes = 0_u64;
1075 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
1076 if old_id != new_id {
1077 return;
1078 }
1079 let Some(delta) = new_revision.checked_sub(*old_revision) else {
1080 return;
1081 };
1082 let Some(total) = changes.checked_add(delta) else {
1083 return;
1084 };
1085 changes = total;
1086 }
1087 let Ok(expected) = u64::try_from(deleted.len()) else {
1088 return;
1089 };
1090 if changes != expected {
1091 return;
1092 }
1093 let mut versions = self.checkpoint_versions.write();
1094 let Some(previous) = versions.get_mut(collection) else {
1095 return;
1096 };
1097 if !previous.manager.ptr_eq(&Arc::downgrade(manager))
1098 || previous.format_version != self.format_version()
1099 || deleted.iter().any(|id| previous.pending_vault.contains(id))
1100 || previous.excluded_xids.contains(&0)
1101 {
1102 return;
1103 }
1104 if previous.segments != before {
1105 let Some(deferred) = previous.deferred_rows.as_ref() else {
1106 return;
1107 };
1108 let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
1109 return;
1110 };
1111 let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
1112 let pending: std::collections::BTreeSet<_> = deferred
1113 .iter()
1114 .copied()
1115 .chain(changed)
1116 .filter(|id| !removed.contains(id))
1117 .collect();
1118 if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
1119 return;
1120 }
1121 previous.has_unprojected_rows = !pending.is_empty();
1126 previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
1127 }
1128 previous.segments = after;
1129 }
1130
1131 pub(crate) fn mark_paged_registry_dirty(&self) {
1132 self.resident.mark_paged_registry_dirty()
1133 }
1134
1135 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
1141 let collection = Self::canonical_collection(collection);
1142 let pager = self.pager.as_ref()?;
1143 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
1144 return Some(btree);
1145 }
1146 let mut write = self.btree_indices.write();
1147 let btree = write
1148 .entry(collection.to_string())
1149 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
1150 .clone();
1151 Some(btree)
1152 }
1153
1154 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
1155 if self.is_read_only() {
1156 return Ok(());
1157 }
1158 let Some(pager) = &self.pager else {
1159 return Ok(());
1160 };
1161
1162 if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
1163 self.flush_paged_registry()?;
1164 self.resident
1165 .paged_registry_dirty
1166 .store(false, Ordering::Release);
1167 return Ok(());
1168 }
1169
1170 pager
1171 .flush()
1172 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
1173 }
1174
1175 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
1176 if self.is_read_only() {
1177 return Ok(());
1178 }
1179 let _vault_guard = self.vault_publication_lock.lock();
1180 let Some(pager) = &self.pager else {
1181 return Ok(());
1182 };
1183 let _ownership_guard = pager.lock_ownership_mutations();
1184 struct RegistryPublication<'a> {
1185 pager: &'a Pager,
1186 active: bool,
1187 }
1188 impl Drop for RegistryPublication<'_> {
1189 fn drop(&mut self) {
1190 if self.active {
1191 self.pager.fence_interrupted_ownership();
1192 }
1193 }
1194 }
1195 let epoch = if let Some(commit) = &self.commit {
1196 if self.ownership_active_epoch.load(Ordering::Acquire) == 0 {
1197 commit.force_sync().map_err(StoreError::Io)?;
1198 let epoch = commit
1199 .next_page_ownership_epoch(self)
1200 .map_err(StoreError::Io)?;
1201 commit
1202 .begin_page_ownership_epoch(self, epoch)
1203 .map_err(StoreError::Io)?;
1204 Some(epoch)
1205 } else {
1206 None
1207 }
1208 } else {
1209 None
1210 };
1211 let mut publication = RegistryPublication {
1212 pager,
1213 active: epoch.is_some(),
1214 };
1215
1216 match pager.read_manifest_page() {
1217 Ok(_) => {}
1218 Err(PagerError::PageNotFound(_)) => {
1219 let meta_page = pager
1220 .allocate_page(crate::storage::engine::PageType::Header)
1221 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1222 pager
1223 .write_page(meta_page.page_id(), meta_page)
1224 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1225 }
1226 Err(e) => {
1227 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1228 }
1229 }
1230
1231 let format_version = STORE_VERSION_V11;
1232 self.set_format_version(format_version);
1233
1234 let collections = self.resident.collections.read();
1235 let btree_indices = self.btree_indices.read();
1236 let mut collection_roots = Vec::with_capacity(collections.len());
1237 for (name, _) in collections.iter() {
1238 let root_page = btree_indices
1239 .get(name)
1240 .map_or(0, |btree| btree.root_page_id());
1241 collection_roots.push((name.clone(), root_page));
1242 }
1243 drop(btree_indices);
1244 drop(collections);
1245
1246 let mut meta_data = Vec::with_capacity(4096);
1247 reddb_file::encode_native_paged_metadata_header(
1248 &mut meta_data,
1249 reddb_file::NativePagedMetadataHeader {
1250 format_version,
1251 collection_count: collection_roots.len() as u32,
1252 },
1253 );
1254 for (name, root_page) in &collection_roots {
1255 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1256 }
1257
1258 let cross_refs = self.resident.cross_refs.read();
1259 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1260 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1261 for (source_id, refs) in cross_refs.iter() {
1262 for (target_id, ref_type, collection) in refs {
1263 reddb_file::encode_native_paged_cross_ref(
1264 &mut meta_data,
1265 source_id.raw(),
1266 target_id.raw(),
1267 ref_type.to_byte(),
1268 reddb_engine::storage::system_collection_names::canonical_or_self(collection),
1269 );
1270 }
1271 }
1272
1273 self.publish_reclaimable_trees(
1274 pager,
1275 &mut meta_data,
1276 &self.btree_indices.read(),
1277 &self.key_indices.read(),
1278 )?;
1279 if let Some(epoch) = epoch {
1280 self.commit
1281 .as_ref()
1282 .expect("registry epoch has a coordinator")
1283 .finish_count_batch_ownership_epoch(self, epoch)
1284 .map_err(StoreError::Io)?;
1285 publication.active = false;
1286 }
1287 Ok(())
1288 }
1289
1290 pub fn pager(&self) -> Option<&Arc<Pager>> {
1292 self.pager.as_ref()
1293 }
1294
1295 pub(crate) fn ensure_pager_writeback_available(&self) -> Result<(), StoreError> {
1296 self.pager.as_ref().map_or(Ok(()), |pager| {
1297 pager
1298 .check_writeback_available()
1299 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))
1300 })
1301 }
1302
1303 pub(crate) fn is_read_only(&self) -> bool {
1304 self.pager
1305 .as_ref()
1306 .is_some_and(|pager| pager.is_read_only())
1307 }
1308
1309 pub fn page_cache_bytes_in_use(&self) -> u64 {
1313 self.pager.as_ref().map_or(0, |pager| {
1314 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
1315 })
1316 }
1317
1318 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
1321 self.commit
1322 .as_ref()
1323 .map_or(0, |commit| commit.buffered_bytes())
1324 }
1325
1326 pub fn segment_memory_bytes(&self) -> u64 {
1330 let managers: Vec<Arc<SegmentManager>> = self
1331 .resident
1332 .collections
1333 .read()
1334 .values()
1335 .map(Arc::clone)
1336 .collect();
1337
1338 managers
1339 .iter()
1340 .map(|manager| manager.resident_bytes())
1341 .fold(0, u64::saturating_add)
1342 }
1343
1344 pub fn config(&self) -> &UnifiedStoreConfig {
1348 &self.config
1349 }
1350
1351 pub fn with_config(config: UnifiedStoreConfig) -> Self {
1352 Self {
1353 config,
1354 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1355 format_version: AtomicU32::new(STORE_VERSION_V11),
1356 pager: None,
1357 db_path: None,
1358 operational_manifest: parking_lot::Mutex::new(None),
1359 btree_indices: RwLock::new(HashMap::new()),
1360 key_indices: RwLock::new(HashMap::new()),
1361 secondary_index_specs: RwLock::new(HashMap::new()),
1362 count_builds: RwLock::new(HashMap::new()),
1363 count_projection_seeds: parking_lot::Mutex::new(HashMap::new()),
1364 queue_dedup_cursors: parking_lot::Mutex::new(HashMap::new()),
1365 recovery_xid_summary: RwLock::new(None),
1366 checkpoint_versions: RwLock::new(HashMap::new()),
1367 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1368 retired_btrees: parking_lot::Mutex::new(Vec::new()),
1369 ownership_catalog_epoch: AtomicU64::new(0),
1370 ownership_active_epoch: AtomicU64::new(0),
1371 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1372 commit: None,
1373 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1374 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1375 result_cache_epoch: Arc::new(result_cache_epoch::ResultCacheEpochState::default()),
1376 snapshot_manager: std::sync::OnceLock::new(),
1377 embedded_commit_session: std::sync::OnceLock::new(),
1378 #[cfg(test)]
1379 test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1380 #[cfg(test)]
1381 test_versioned_update_after_old: parking_lot::Mutex::new(None),
1382 #[cfg(test)]
1383 test_fail_versioned_update_phase: std::sync::atomic::AtomicU8::new(0),
1384 #[cfg(test)]
1385 test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1386 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1387 row_publications: Default::default(),
1388 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1389 }
1390 }
1391
1392 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
1410 Self::open_with_config(path, UnifiedStoreConfig::default())
1411 }
1412
1413 pub fn open_with_config(
1414 path: impl AsRef<Path>,
1415 config: UnifiedStoreConfig,
1416 ) -> Result<Self, StoreError> {
1417 Self::open_with_config_for_recovery(path, config, false, None, None)
1418 }
1419
1420 pub fn open_read_only_with_config(
1423 path: impl AsRef<Path>,
1424 mut config: UnifiedStoreConfig,
1425 ) -> Result<Self, StoreError> {
1426 let path = path.as_ref();
1427 config.embedded_wal_path = None;
1428 let pager = Pager::open(
1429 path,
1430 PagerConfig {
1431 read_only: true,
1432 create: false,
1433 disk_residency: config.resident_memory_bytes_max.is_some(),
1434 cache_size: config
1435 .page_cache_slots
1436 .unwrap_or_else(|| PagerConfig::default().cache_size),
1437 ..PagerConfig::default()
1438 },
1439 )
1440 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1441 pager
1442 .enable_deferred_reclamation()
1443 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1444 let mut store = Self::with_config(config);
1445 store.pager = Some(Arc::new(pager));
1446 store.db_path = Some(path.to_path_buf());
1447 let wal_path = reddb_file::layout::unified_wal_path(path);
1448 StoreCommitCoordinator::restore_checkpoint_page_images_from_path(&store, &wal_path)
1449 .map_err(StoreError::Io)?;
1450 let catalog = store.load_from_pages(false, None)?;
1451 StoreCommitCoordinator::replay_read_only(&store, &wal_path).map_err(StoreError::Io)?;
1452 store.load_result_cache_epoch()?;
1453 store.require_normalized_red_config_keys()?;
1454 let pending_drops = store.legacy_pending_collection_drops()?.unwrap_or_default();
1455 for name in pending_drops {
1456 if !catalog
1457 .collisions
1458 .iter()
1459 .any(|collision| collision.loser == name)
1460 && store.get_collection(&name).is_some()
1461 {
1462 store.drop_collection_in_memory(&name)?;
1463 }
1464 }
1465 Ok(store)
1466 }
1467
1468 pub(crate) fn open_with_config_for_recovery(
1471 path: impl AsRef<Path>,
1472 config: UnifiedStoreConfig,
1473 retain_canonical_system_config: bool,
1474 boot_wal: Option<&crate::storage::compact::boot_system_wal::BootSystemWalPlan>,
1475 catalog_plan: Option<&crate::storage::compact::catalog_aliases::CanonicalCatalogPlan>,
1476 ) -> Result<Self, StoreError> {
1477 let path = path.as_ref();
1478 let mut pager_config = PagerConfig {
1479 disk_residency: config.resident_memory_bytes_max.is_some(),
1480 ..PagerConfig::default()
1481 };
1482 if let Some(slots) = config.page_cache_slots {
1486 pager_config.cache_size = slots;
1487 }
1488 if matches!(
1496 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
1497 Some("0") | Some("false") | Some("off")
1498 ) {
1499 pager_config.double_write = false;
1500 }
1501 let pager_open_started = std::time::Instant::now();
1502 let pager = Pager::open(path, pager_config)
1503 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1504 pager
1505 .enable_deferred_reclamation()
1506 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1507 trace_startup_phase(path, "paged_pager_open", pager_open_started);
1508
1509 let header = pager
1510 .header()
1511 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1512 if config.resident_memory_bytes_max.is_none() && header.requires_disk_residency() {
1513 return Err(StoreError::Io(std::io::Error::new(std::io::ErrorKind::Unsupported,
1514 "this database was prepared for disk residency; reopen with the disk-resident storage profile")));
1515 }
1516
1517 let wal_path = reddb_file::layout::unified_wal_path(path);
1518 let commit = Some(Arc::new(
1521 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
1522 .map_err(StoreError::Io)?,
1523 ));
1524
1525 let store = Self {
1526 config,
1527 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1528 format_version: AtomicU32::new(STORE_VERSION_V11),
1529 pager: Some(Arc::new(pager)),
1530 db_path: Some(path.to_path_buf()),
1531 operational_manifest: parking_lot::Mutex::new(None),
1532 btree_indices: RwLock::new(HashMap::new()),
1533 key_indices: RwLock::new(HashMap::new()),
1534 secondary_index_specs: RwLock::new(HashMap::new()),
1535 count_builds: RwLock::new(HashMap::new()),
1536 count_projection_seeds: parking_lot::Mutex::new(HashMap::new()),
1537 queue_dedup_cursors: parking_lot::Mutex::new(HashMap::new()),
1538 recovery_xid_summary: RwLock::new(None),
1539 checkpoint_versions: RwLock::new(HashMap::new()),
1540 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1541 retired_btrees: parking_lot::Mutex::new(Vec::new()),
1542 ownership_catalog_epoch: AtomicU64::new(0),
1543 ownership_active_epoch: AtomicU64::new(0),
1544 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1545 commit,
1546 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1547 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1548 result_cache_epoch: Arc::new(result_cache_epoch::ResultCacheEpochState::default()),
1549 snapshot_manager: std::sync::OnceLock::new(),
1550 embedded_commit_session: std::sync::OnceLock::new(),
1551 #[cfg(test)]
1552 test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1553 #[cfg(test)]
1554 test_versioned_update_after_old: parking_lot::Mutex::new(None),
1555 #[cfg(test)]
1556 test_fail_versioned_update_phase: std::sync::atomic::AtomicU8::new(0),
1557 #[cfg(test)]
1558 test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1559 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1560 row_publications: Default::default(),
1561 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1562 };
1563
1564 let checkpoint_recovery_started = std::time::Instant::now();
1567 if let Some(commit) = &store.commit {
1568 commit
1569 .restore_checkpoint_page_images(&store)
1570 .map_err(StoreError::Io)?;
1571 }
1572 trace_startup_phase(
1573 path,
1574 "paged_checkpoint_recovery",
1575 checkpoint_recovery_started,
1576 );
1577 let page_load_started = std::time::Instant::now();
1579 let catalog_load = store.load_from_pages(retain_canonical_system_config, catalog_plan)?;
1580 trace_startup_phase(path, "paged_catalog_and_roots", page_load_started);
1581 let wal_replay_started = std::time::Instant::now();
1582 if let Some(commit) = &store.commit {
1583 commit
1584 .replay_into(&store, boot_wal)
1585 .map_err(StoreError::Io)?;
1586 }
1587 store.load_result_cache_epoch()?;
1588 let normalized_config_keys = store.normalize_red_config_keys()?;
1589 if normalized_config_keys > 0 {
1590 tracing::warn!(
1591 migrated = normalized_config_keys,
1592 "normalized legacy red.config keys before boot config consumers"
1593 );
1594 }
1595 trace_startup_phase(path, "paged_wal_replay", wal_replay_started);
1596 if catalog_load.migrated {
1597 store.mark_paged_registry_dirty();
1600 store.flush_paged_state()?;
1601 }
1602 let manifest_recovery_started = std::time::Instant::now();
1603 store.recover_operational_manifest(&catalog_load.collisions)?;
1604 if store.config.resident_memory_bytes_max.is_some() {
1605 let missing_key_index = {
1606 let trees = store.btree_indices.read();
1607 let keys = store.key_indices.read();
1608 trees.iter().any(|(name, tree)| {
1609 !keys
1610 .get(name)
1611 .is_some_and(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
1612 })
1613 };
1614 if missing_key_index {
1615 store.persist()?;
1616 }
1617 }
1618 trace_startup_phase(
1619 path,
1620 "paged_manifest_and_derived_roots",
1621 manifest_recovery_started,
1622 );
1623
1624 Ok(store)
1625 }
1626
1627 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
1631 let Some(pager) = &self.pager else {
1632 return Ok(());
1633 };
1634 let collections = self.resident.collections.read();
1635 let mut indices = self.btree_indices.write();
1636 let previous_versions = self.checkpoint_versions.read();
1637 let raw_replay =
1638 self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
1639 for (name, manager) in collections.iter() {
1640 let mut reusable = None;
1641 if raw_replay {
1642 if let (Some(previous), Some(tree)) = (
1643 previous_versions.get(name),
1644 indices.get(name).filter(|tree| tree.has_page_proofs()),
1645 ) {
1646 let mut current = CheckpointVersion {
1647 manager: Arc::downgrade(manager),
1648 segments: manager.checkpoint_revisions(),
1649 visibility_revision: 0,
1650 untracked_visibility_revision: 0,
1651 excluded_xids: Arc::default(),
1652 pending_vault: Arc::default(),
1653 format_version: self.format_version(),
1654 deferred_rows: Some(Arc::default()),
1655 has_unprojected_rows: false,
1656 };
1657 if previous.manager.ptr_eq(¤t.manager)
1658 && previous.format_version == current.format_version
1659 {
1660 if previous.segments == current.segments
1661 && !previous.has_unprojected_rows
1662 && checkpoint_visibility_matches(previous, ¤t)
1663 {
1664 reusable = Some(Arc::clone(tree));
1665 } else {
1666 reusable = self.sparse_checkpoint_tree(
1672 name,
1673 manager,
1674 tree,
1675 previous,
1676 &mut current,
1677 )?;
1678 }
1679 }
1680 }
1681 }
1682 let tree = match reusable {
1683 Some(tree) => tree,
1684 None => {
1685 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1686 #[cfg(test)]
1687 REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1688 Some(Self::serialize_entity_record(
1689 entity,
1690 metadata.as_ref(),
1691 self.format_version(),
1692 ))
1693 })?
1694 }
1695 };
1696 if indices
1697 .get(name)
1698 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1699 {
1700 continue;
1701 }
1702 if let Some(previous) = indices.insert(name.clone(), tree) {
1703 self.retire_btree(previous);
1704 }
1705 self.mark_paged_registry_dirty();
1706 #[cfg(test)]
1707 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1708 if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1709 pager.sync().map_err(|error| {
1710 StoreError::Io(std::io::Error::other(error.to_string()))
1711 })?;
1712 }
1713 eprintln!("experimental replay rebuild exit before manifest");
1714 std::process::exit(182);
1715 }
1716 }
1717 drop(previous_versions);
1718 drop(indices);
1719 drop(collections);
1720 self.flush_paged_state()?;
1721 #[cfg(test)]
1722 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1723 eprintln!("experimental replay exit after manifest");
1724 std::process::exit(182);
1725 }
1726 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1729 let collections = self.resident.collections.read();
1730 let mut versions = self.checkpoint_versions.write();
1731 for (name, manager) in collections.iter() {
1732 versions.insert(
1733 name.clone(),
1734 CheckpointVersion {
1735 manager: Arc::downgrade(manager),
1736 segments: manager.checkpoint_revisions(),
1737 visibility_revision: 0,
1738 untracked_visibility_revision: 0,
1739 excluded_xids: Arc::default(),
1740 pending_vault: Arc::default(),
1741 format_version: self.format_version(),
1742 deferred_rows: Some(Arc::default()),
1743 has_unprojected_rows: false,
1744 },
1745 );
1746 }
1747 }
1748 Ok(())
1749 }
1750
1751 fn legacy_pending_collection_drops(&self) -> Result<Option<Vec<String>>, StoreError> {
1752 match self.try_get_config("storage.legacy_manifest_imported")? {
1753 Some(Value::Boolean(true)) => return Ok(None),
1754 None => {}
1755 Some(_) => {
1756 return Err(StoreError::Serialization(
1757 "invalid legacy manifest import marker".into(),
1758 ))
1759 }
1760 }
1761 let Some(path) = &self.db_path else {
1762 return Ok(None);
1763 };
1764 self.operational_manifest_for_path(path)
1765 .read_only_pending_drops_if_present()
1766 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))
1767 }
1768
1769 fn recover_operational_manifest(
1770 &self,
1771 collisions: &[SystemCollectionCollision],
1772 ) -> Result<(), StoreError> {
1773 let Some(pending) = self.legacy_pending_collection_drops()? else {
1774 return Ok(());
1775 };
1776 for name in pending {
1779 if !collisions.iter().any(|collision| collision.loser == name)
1780 && self.get_collection(&name).is_some()
1781 {
1782 self.drop_collection(&name)?;
1783 }
1784 }
1785 self.try_set_config_tree(
1786 "storage.legacy_manifest_imported",
1787 &crate::serde_json::Value::Bool(true),
1788 )?;
1789 Ok(())
1790 }
1791
1792 pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1794 let Some(pager) = &self.pager else {
1795 return Ok(0);
1796 };
1797 let Some(content) = read_meta_payload(pager)? else {
1798 return Ok(0);
1799 };
1800 reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1801 .map(|epoch| epoch.unwrap_or(0))
1802 .map_err(|error| StoreError::Serialization(error.to_string()))
1803 }
1804
1805 fn load_from_pages(
1807 &self,
1808 retain_canonical_system_config: bool,
1809 catalog_plan: Option<&crate::storage::compact::catalog_aliases::CanonicalCatalogPlan>,
1810 ) -> Result<CatalogLoadOutcome, StoreError> {
1811 let pager = match &self.pager {
1812 Some(p) => p,
1813 None => return Ok(CatalogLoadOutcome::default()), };
1815
1816 let page_count = pager.page_count().map_err(|e| {
1818 StoreError::Io(std::io::Error::other(format!(
1819 "failed to read page count: {}",
1820 e
1821 )))
1822 })?;
1823 if page_count <= 1 {
1824 return Ok(CatalogLoadOutcome::default());
1826 }
1827
1828 if let Some(content_vec) = read_meta_payload(pager)? {
1833 let content: &[u8] = &content_vec;
1834 let header = pager
1835 .header()
1836 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1837 let prepared = header.requires_disk_residency();
1838 let proofs = reddb_file::native_store::decode_native_paged_catalog_tree_proofs(content)
1839 .map_err(|error| StoreError::Serialization(error.to_string()))?;
1840 if proofs.is_some()
1841 && (header.required_capabilities
1842 & reddb_file::file_format::FILE_CAPABILITY_TREE_PROOFS
1843 == 0
1844 || pager
1845 .manifest_reference()
1846 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1847 .page_id
1848 == 0)
1849 {
1850 return Err(page_integrity(
1851 1,
1852 None,
1853 "tree proofs require an independently sealed catalog and reader capability",
1854 ));
1855 }
1856 if proofs.as_ref().is_some_and(|proofs| {
1857 proofs.iter().any(|(&root, proof)| {
1858 root >= header.page_count || proof.reference.page_id >= header.page_count
1859 })
1860 }) {
1861 return Err(page_integrity(
1862 1,
1863 None,
1864 "tree proof points outside allocation",
1865 ));
1866 }
1867 let resume = if prepared {
1868 reddb_file::native_resume::decode_catalog_resume(content)
1869 .map_err(|error| StoreError::Serialization(error.to_string()))?
1870 } else {
1871 None
1872 };
1873 let mut outcome = CatalogLoadOutcome::default();
1874 let mut ignored_collections = std::collections::HashSet::new();
1875 if retain_canonical_system_config
1876 && !resume.as_ref().is_some_and(|resume| {
1877 resume.collections.contains_key("red_config")
1878 && resume.collections.contains_key("red.config")
1879 })
1880 {
1881 return Err(page_integrity(
1882 1,
1883 None,
1884 "canonical config recovery requires both resume identities",
1885 ));
1886 }
1887 if let Some(resume) = &resume {
1888 for &(legacy, canonical) in
1889 reddb_engine::storage::system_collection_names::LEGACY_NAMES
1890 {
1891 let (Some(legacy_state), Some(canonical_state)) = (
1892 resume.collections.get(legacy),
1893 resume.collections.get(canonical),
1894 ) else {
1895 continue;
1896 };
1897 let merged_resume = merge_shared_alias_resume(legacy_state, canonical_state);
1898 let shared_physical_state = merged_resume.is_some();
1899 let resume_counter_drift = legacy_state != canonical_state;
1900 let (survivor, loser) = match (
1901 legacy_state.entities == 0,
1902 canonical_state.entities == 0,
1903 ) {
1904 _ if retain_canonical_system_config && legacy == "red_config" => {
1905 (canonical, legacy)
1906 }
1907 _ if catalog_plan.is_some_and(|plan| plan.retires(legacy, canonical)) => {
1908 (canonical, legacy)
1909 }
1910 (false, true) => (legacy, canonical),
1911 (true, false) | (true, true) => (canonical, legacy),
1912 (false, false) if shared_physical_state => (canonical, legacy),
1913 (false, false) => {
1914 let (
1915 data_root_match,
1916 key_index_root_match,
1917 secondary_index_state_match,
1918 ) = shared_alias_resume_matches(legacy_state, canonical_state);
1919 return Err(page_integrity(
1920 1,
1921 None,
1922 format!(
1923 "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})"
1924 ),
1925 ));
1926 }
1927 };
1928 ignored_collections.insert(loser.to_string());
1929 outcome.collisions.push(SystemCollectionCollision {
1930 survivor: survivor.to_string(),
1931 loser: loser.to_string(),
1932 canonical: canonical.to_string(),
1933 shared_physical_state,
1934 resume_counter_drift,
1935 merged_resume,
1936 });
1937 }
1938 let live_roots = resume
1939 .collections
1940 .iter()
1941 .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1942 .flat_map(|(_, state)| {
1943 std::iter::once(state.root_page)
1944 .chain(state.key_index_root)
1945 .chain(
1946 state
1947 .secondary_indices
1948 .values()
1949 .map(|index| index.root_page),
1950 )
1951 })
1952 .filter(|root| *root != 0)
1953 .collect::<std::collections::HashSet<_>>();
1954 for collision in &outcome.collisions {
1955 if collision.shared_physical_state {
1956 continue;
1957 }
1958 let loser_state = resume
1959 .collections
1960 .get(&collision.loser)
1961 .expect("collision loser is present");
1962 if std::iter::once(loser_state.root_page)
1963 .chain(loser_state.key_index_root)
1964 .chain(
1965 loser_state
1966 .secondary_indices
1967 .values()
1968 .map(|index| index.root_page),
1969 )
1970 .filter(|root| *root != 0)
1971 .any(|root| live_roots.contains(&root))
1972 {
1973 return Err(page_integrity(
1974 1,
1975 None,
1976 format!(
1977 "empty collection '{}' aliases a live collection root",
1978 collision.loser
1979 ),
1980 ));
1981 }
1982 }
1983 reddb_engine::storage::system_collection_names::migration_plan(
1984 resume
1985 .collections
1986 .keys()
1987 .map(String::as_str)
1988 .filter(|name| !ignored_collections.contains(*name)),
1989 )
1990 .map_err(|(legacy, canonical)| {
1991 page_integrity(
1992 1,
1993 None,
1994 format!(
1995 "catalog contains both legacy collection '{legacy}' and canonical collection '{canonical}'"
1996 ),
1997 )
1998 })?;
1999 for state in resume.collections.values() {
2000 if !state.secondary_indices.is_empty()
2001 && header.version
2002 < reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
2003 {
2004 return Err(page_integrity(
2005 1,
2006 None,
2007 "secondary roots require the file format fence",
2008 ));
2009 }
2010 for root in std::iter::once(state.root_page)
2011 .chain(state.key_index_root)
2012 .chain(
2013 state
2014 .secondary_indices
2015 .values()
2016 .map(|index| index.root_page),
2017 )
2018 {
2019 if root >= page_count {
2020 return Err(page_integrity(
2021 root,
2022 None,
2023 "published root exceeds the database page count",
2024 ));
2025 }
2026 }
2027 }
2028 self.resident
2029 .next_entity_id
2030 .fetch_max(resume.entity_id_next, Ordering::SeqCst);
2031 }
2032 *self.recovery_xid_summary.write() = resume.as_ref().and_then(|resume| {
2033 resume.xid_high_water_mark.map(|high_water_mark| {
2034 super::recovery_xid::RecoveryXidSummary {
2035 high_water_mark,
2036 roots: resume
2037 .collections
2038 .iter()
2039 .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
2040 .map(|(name, state)| {
2041 (
2042 reddb_engine::storage::system_collection_names::canonical_or_self(name)
2043 .to_owned(),
2044 state.root_page,
2045 )
2046 })
2047 .collect(),
2048 }
2049 })
2050 });
2051 if content.len() >= 4 {
2052 let mut pos = 0;
2053 let mut format_version = STORE_VERSION_V1;
2054
2055 let collection_count = if let Some(header) =
2056 reddb_file::decode_native_paged_metadata_header(content)
2057 .map_err(|err| StoreError::Serialization(err.to_string()))?
2058 {
2059 format_version = header.format_version;
2060 pos += reddb_file::METADATA_HEADER_BYTES;
2061 header.collection_count as usize
2062 } else {
2063 let count = u32::from_le_bytes([
2064 content[pos],
2065 content[pos + 1],
2066 content[pos + 2],
2067 content[pos + 3],
2068 ]) as usize;
2069 pos += 4;
2070 count
2071 };
2072
2073 self.set_format_version(format_version);
2074
2075 if pos > content.len() {
2076 return Ok(CatalogLoadOutcome::default());
2077 }
2078
2079 if resume.is_none() {
2080 let mut roots = std::collections::HashMap::new();
2084 let mut root_pos = pos;
2085 for _ in 0..collection_count {
2086 let root =
2087 reddb_file::decode_native_paged_collection_root(content, &mut root_pos)
2088 .map_err(|err| page_integrity(1, None, err.to_string()))?;
2089 if roots.insert(root.collection, root.root_page).is_some() {
2090 return Err(page_integrity(
2091 1,
2092 None,
2093 "duplicate raw collection identity",
2094 ));
2095 }
2096 }
2097 for &(legacy, canonical) in
2098 reddb_engine::storage::system_collection_names::LEGACY_NAMES
2099 {
2100 let (Some(&legacy_root), Some(&canonical_root)) =
2101 (roots.get(legacy), roots.get(canonical))
2102 else {
2103 continue;
2104 };
2105 let (survivor, loser) = match (legacy_root == 0, canonical_root == 0) {
2106 (false, true) => (legacy, canonical),
2107 (true, _) => (canonical, legacy),
2108 (false, false) => continue,
2109 };
2110 ignored_collections.insert(loser.to_owned());
2111 outcome.collisions.push(SystemCollectionCollision {
2112 survivor: survivor.to_owned(),
2113 loser: loser.to_owned(),
2114 canonical: canonical.to_owned(),
2115 shared_physical_state: false,
2116 resume_counter_drift: false,
2117 merged_resume: None,
2118 });
2119 }
2120 }
2121 let mut source_names = std::collections::HashMap::<String, String>::new();
2122 let mut migrated_catalog = !outcome.collisions.is_empty();
2123 let mut empty_duplicate_roots = std::collections::HashSet::new();
2124 let mut recovery_config_roots = [0u8; 2];
2125 let mut scanned_xid_high_water_mark = 0u64;
2129 let mut count_seed_bytes_remaining = self
2130 .config
2131 .resident_memory_bytes_max
2132 .and_then(|limit| usize::try_from(limit).ok())
2133 .unwrap_or(0)
2134 / 2;
2135
2136 for _ in 0..collection_count {
2138 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
2139 .map_err(|err| page_integrity(1, None, err.to_string()))?;
2140 let root_page = root.root_page;
2142 let stored_name = root.collection;
2143 if retain_canonical_system_config {
2144 let config_index = match stored_name.as_str() {
2145 "red_config" => Some(0),
2146 "red.config" => Some(1),
2147 _ => None,
2148 };
2149 if let Some(index) = config_index {
2150 recovery_config_roots[index] =
2151 recovery_config_roots[index].saturating_add(1);
2152 let state = resume
2153 .as_ref()
2154 .and_then(|resume| resume.collections.get(&stored_name))
2155 .expect("both recovery identities were checked");
2156 if state.root_page != root_page {
2157 return Err(page_integrity(
2158 1,
2159 None,
2160 "config collection root differs from resume catalog",
2161 ));
2162 }
2163 }
2164 }
2165 if ignored_collections.contains(&stored_name) {
2166 if resume.is_none() && root_page == 0 {
2167 continue;
2168 }
2169 let shared_physical_state = outcome
2170 .collisions
2171 .iter()
2172 .find(|collision| collision.loser == stored_name)
2173 .is_some_and(|collision| collision.shared_physical_state);
2174 if shared_physical_state {
2175 continue;
2176 }
2177 let state = resume
2178 .as_ref()
2179 .and_then(|resume| resume.collections.get(&stored_name))
2180 .expect("ignored collection was proven by catalog resume");
2181 for retired_root in std::iter::once(state.root_page)
2182 .chain(state.key_index_root)
2183 .chain(
2184 state
2185 .secondary_indices
2186 .values()
2187 .map(|index| index.root_page),
2188 )
2189 .filter(|root| *root != 0)
2190 {
2191 empty_duplicate_roots.insert(retired_root);
2192 }
2193 continue;
2194 }
2195 let name = reddb_engine::storage::system_collection_names::canonical_or_self(
2196 &stored_name,
2197 )
2198 .to_owned();
2199 if let Some(previous) = source_names.insert(name.clone(), stored_name.clone()) {
2200 if previous != stored_name {
2201 return Err(page_integrity(
2202 1,
2203 None,
2204 format!(
2205 "catalog contains both legacy collection '{previous}' and canonical collection '{name}'"
2206 ),
2207 ));
2208 }
2209 }
2210 migrated_catalog |= name != stored_name;
2211
2212 let _ = self.create_collection_in_memory(&name);
2216
2217 let shared_alias = outcome.collisions.iter().find(|collision| {
2218 collision.survivor == stored_name && collision.shared_physical_state
2219 });
2220 let counters = shared_alias
2221 .and_then(|collision| collision.merged_resume.as_ref())
2222 .or_else(|| {
2223 resume
2224 .as_ref()
2225 .and_then(|resume| resume.collections.get(&stored_name))
2226 });
2227 let manager = self.get_collection(&name);
2228 let mut count_seed =
2229 if counters.is_none() && self.commit.is_some() && !self.is_read_only() {
2230 let schema =
2231 manager.as_ref().and_then(|manager| manager.column_schema());
2232 super::count_index::CountProjectionSeedCollector::new(
2233 schema,
2234 count_seed_bytes_remaining,
2235 )
2236 } else {
2237 None
2238 };
2239 if let (Some(counters), Some(manager)) = (counters, self.get_collection(&name))
2240 {
2241 manager.restore_identity_counters(
2242 counters.entity_id_next,
2243 counters.row_id_next,
2244 );
2245 manager.restore_sequence_id_next(counters.sequence_id_next);
2246 let expiry_entries = counters
2247 .metadata_expiries
2248 .iter()
2249 .map(|entry| (entry.deadline_ms, EntityId::new(entry.entity_id)))
2250 .collect::<Vec<_>>();
2251 manager.restore_metadata_expiry_projection(
2252 &expiry_entries,
2253 resume
2254 .as_ref()
2255 .is_some_and(|resume| resume.metadata_expiry_prepared),
2256 );
2257 }
2258 if root_page > 0 || counters.is_some() {
2260 let btree = restore_published_tree(pager, root_page, proofs.as_ref())?;
2261
2262 let mut cursor = btree.cursor_first().map_err(|err| {
2264 page_integrity(root_page, Some(name.clone()), err.to_string())
2265 })?;
2266 let hydrate_read_only =
2267 self.is_read_only() && self.config.resident_memory_bytes_max.is_none();
2268 let mut persisted_entities = if hydrate_read_only {
2269 0
2270 } else {
2271 counters.map_or(0, |state| state.entities)
2272 };
2273 while counters.is_none() || hydrate_read_only {
2274 let next = cursor
2275 .next_with_budget(
2276 self.config
2277 .resident_memory_bytes_max
2278 .and_then(|limit| usize::try_from(limit).ok())
2279 .unwrap_or(usize::MAX),
2280 )
2281 .map_err(|err| {
2282 page_integrity(root_page, Some(name.clone()), err.to_string())
2283 })?;
2284 let Some((key, value)) = next else {
2285 break;
2286 };
2287 persisted_entities += 1;
2288 let key_bytes: [u8; 8] = key.as_slice().try_into().map_err(|_| {
2290 page_integrity(
2291 root_page,
2292 Some(name.clone()),
2293 "physical entity key must contain eight bytes",
2294 )
2295 })?;
2296 let record = reddb_engine::storage::collection_read::EncodedRecord {
2297 id: EntityId::new(u64::from_be_bytes(key_bytes)),
2298 bytes: value,
2299 };
2300 let decoded = if let Some(limit) = self.config.resident_memory_bytes_max
2301 {
2302 record.decode_with_budget(
2303 self.format_version(),
2304 usize::try_from(limit).unwrap_or(usize::MAX),
2305 )
2306 } else {
2307 record.decode(self.format_version())
2308 };
2309 let (entity, metadata) = decoded.map_err(|err| {
2310 page_integrity(root_page, Some(name.clone()), err.to_string())
2311 })?;
2312 scanned_xid_high_water_mark = scanned_xid_high_water_mark
2313 .max(entity.xmin)
2314 .max(entity.xmax);
2315 let discard_count_seed = if let Some(seed) = count_seed.as_mut() {
2316 match seed.push(&entity) {
2317 Ok(complete) => !complete,
2318 Err(error) => {
2319 return Err(page_integrity(
2320 root_page,
2321 Some(name.clone()),
2322 error.to_string(),
2323 ));
2324 }
2325 }
2326 } else {
2327 false
2328 };
2329 if discard_count_seed {
2330 count_seed = None;
2331 }
2332 if let Some(m) = &manager {
2333 let id = entity.id;
2334 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
2335 m.register_row_id(*row_id);
2336 }
2337 if self.config.resident_memory_bytes_max.is_some() {
2338 m.restore_metadata_expiry(&entity, metadata.as_ref());
2339 self.register_entity_id(id);
2340 m.restore_sequence_id_next(
2341 entity.sequence_id.saturating_add(1),
2342 );
2343 m.restore_identity_counters(
2344 id.raw().saturating_add(1),
2345 m.identity_counters().1,
2346 );
2347 continue;
2348 }
2349 self.resident.context_index.index_entity(&name, &entity);
2350 m.restore_snapshot_entity(entity.clone())?;
2351 if let Some(metadata) = metadata {
2352 let _ = m.set_metadata(id, metadata);
2353 }
2354 self.register_entity_id(id);
2355 if self.config.auto_index_refs {
2356 self.index_cross_refs(&entity, &name)?;
2357 }
2358 }
2359 }
2360
2361 if let Some(seed) = count_seed.take() {
2362 count_seed_bytes_remaining =
2363 count_seed_bytes_remaining.saturating_sub(seed.bytes());
2364 self.remember_count_projection_seed(&name, root_page, seed);
2365 }
2366
2367 let btree = Arc::new(btree);
2369 if shared_alias.is_some_and(|collision| collision.resume_counter_drift) {
2370 persisted_entities = u64::try_from(btree.count().map_err(|err| {
2371 page_integrity(root_page, Some(name.clone()), err.to_string())
2372 })?)
2373 .map_err(|_| {
2374 page_integrity(
2375 root_page,
2376 Some(name.clone()),
2377 "shared collection entity count exceeds u64",
2378 )
2379 })?;
2380 }
2381 let key_tree = counters
2382 .and_then(|state| state.key_index_root)
2383 .map(|root| {
2384 restore_published_tree(pager, root, proofs.as_ref()).map(Arc::new)
2385 })
2386 .transpose()?;
2387 if let Some(counters) = counters {
2388 self.key_indices.write().insert(
2389 name.clone(),
2390 PublishedKeyIndex {
2391 indexed: key_tree.is_some(),
2392 secondary: counters.secondary_indices.iter().map(|(name, index)| {
2393 Ok((name.clone(), super::secondary_index::PublishedSecondaryIndex {
2394 identity: super::secondary_index::SecondaryIndexIdentity {
2395 codec_version: index.codec_version,
2396 definition: index.definition,
2397 },
2398 tree: Arc::new(restore_published_tree(pager, index.root_page, proofs.as_ref())?),
2399 }))
2400 }).collect::<Result<_, StoreError>>()?,
2401 source: Arc::downgrade(&btree),
2402 tree: match &key_tree { Some(tree) => Arc::clone(tree), None => Arc::new(restore_published_tree(pager, 0, proofs.as_ref())?) },
2403 },
2404 );
2405 }
2406 if let (Some(manager), Some(limit)) =
2407 (&manager, self.config.resident_memory_bytes_max)
2408 {
2409 manager.install_backing(
2410 Arc::new(
2411 match key_tree {
2412 Some(keys) => reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(&btree), keys),
2413 None => reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(&btree)),
2414 },
2415 ),
2416 btree.root_page_id(),
2417 format_version,
2418 usize::try_from(limit).unwrap_or(usize::MAX),
2419 persisted_entities,
2420 );
2421 }
2422 self.btree_indices.write().insert(name.clone(), btree);
2423 if format_version == STORE_VERSION_V11 {
2424 if let Some(manager) = &manager {
2425 self.checkpoint_versions.write().insert(
2426 name,
2427 CheckpointVersion {
2428 manager: Arc::downgrade(manager),
2429 segments: manager.checkpoint_revisions(),
2430 visibility_revision: 0,
2431 untracked_visibility_revision: 0,
2432 excluded_xids: Arc::default(),
2433 pending_vault: Arc::default(),
2434 format_version,
2435 deferred_rows: Some(Arc::default()),
2436 has_unprojected_rows: false,
2437 },
2438 );
2439 }
2440 }
2441 }
2442 }
2443
2444 if retain_canonical_system_config && recovery_config_roots != [1, 1] {
2445 return Err(page_integrity(
2446 1,
2447 None,
2448 "canonical config recovery requires exactly one root for each identity",
2449 ));
2450 }
2451 if retain_canonical_system_config
2452 && (format_version < STORE_VERSION_V2 || pos + 4 > content.len())
2453 {
2454 return Err(page_integrity(
2455 1,
2456 None,
2457 "canonical config recovery requires a complete cross-reference catalog",
2458 ));
2459 }
2460 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
2461 let cross_ref_count = u32::from_le_bytes([
2462 content[pos],
2463 content[pos + 1],
2464 content[pos + 2],
2465 content[pos + 3],
2466 ]) as usize;
2467 pos += 4;
2468
2469 for _ in 0..cross_ref_count {
2470 let cross_ref =
2471 match reddb_file::decode_native_paged_cross_ref(content, &mut pos) {
2472 Ok(cross_ref) => cross_ref,
2473 Err(error) if retain_canonical_system_config => {
2474 return Err(page_integrity(1, None, error.to_string()));
2475 }
2476 Err(_) => break,
2477 };
2478 if retain_canonical_system_config
2479 && cross_ref.target_collection == "red_config"
2480 {
2481 return Err(page_integrity(
2482 1,
2483 None,
2484 "legacy red_config has cross-references and cannot be retired",
2485 ));
2486 }
2487 let source_id = EntityId::new(cross_ref.source_id);
2488 let target_id = EntityId::new(cross_ref.target_id);
2489 let ref_type = RefType::from_byte(cross_ref.ref_type);
2490 let target_collection =
2491 reddb_engine::storage::system_collection_names::canonical_owned(
2492 cross_ref.target_collection,
2493 );
2494
2495 self.resident
2496 .cross_refs
2497 .write()
2498 .entry(source_id)
2499 .or_default()
2500 .push((target_id, ref_type, target_collection.clone()));
2501
2502 if let Some((collection, mut entity)) = self.get_any(source_id) {
2503 let exists = entity.cross_refs().iter().any(|xref| {
2504 xref.target == target_id
2505 && xref.ref_type == ref_type
2506 && xref.target_collection == target_collection
2507 });
2508 if !exists {
2509 entity.cross_refs_mut().push(CrossRef::new(
2510 source_id,
2511 target_id,
2512 target_collection.clone(),
2513 ref_type,
2514 ));
2515 if let Some(manager) = self.get_collection(&collection) {
2516 let _ = manager.update(entity);
2517 }
2518 }
2519 }
2520 }
2521 }
2522 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
2523 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2524 let already_retired = retired
2525 .iter()
2526 .copied()
2527 .collect::<std::collections::HashSet<_>>();
2528 let mut live_roots: std::collections::HashSet<_> = self
2529 .btree_indices
2530 .read()
2531 .values()
2532 .map(|tree| tree.root_page_id())
2533 .collect();
2534 live_roots.extend(
2535 self.key_indices
2536 .read()
2537 .values()
2538 .flat_map(PublishedKeyIndex::trees)
2539 .map(|tree| tree.root_page_id()),
2540 );
2541 for root in retired {
2542 if live_roots.contains(&root)
2543 || root
2544 >= pager
2545 .page_count()
2546 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
2547 {
2548 return Err(page_integrity(
2549 root,
2550 None,
2551 "retired root aliases a live or missing page",
2552 ));
2553 }
2554 self.retired_btrees.lock().push(RetiredBTree {
2555 tree: Arc::new(restore_published_tree(pager, root, proofs.as_ref())?),
2556 published: true,
2557 });
2558 }
2559 for root in empty_duplicate_roots
2560 .iter()
2561 .copied()
2562 .filter(|root| !already_retired.contains(root))
2563 {
2564 self.retire_btree(Arc::new(restore_published_tree(
2565 pager,
2566 root,
2567 proofs.as_ref(),
2568 )?));
2569 }
2570 if proofs.as_ref().is_some_and(|proofs| {
2571 proofs.iter().any(|(root, proof)| {
2572 let owns = |id| {
2573 live_roots.contains(id)
2574 || already_retired.contains(id)
2575 || empty_duplicate_roots.contains(id)
2576 };
2577 !owns(root) || owns(&proof.reference.page_id)
2578 })
2579 }) {
2580 return Err(page_integrity(
2581 1,
2582 None,
2583 "unowned or aliased tree generation proof",
2584 ));
2585 }
2586 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
2587 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2588 if deferred
2589 .iter()
2590 .any(|id| live_roots.contains(id) || empty_duplicate_roots.contains(id))
2591 {
2592 return Err(page_integrity(1, None, "deferred page aliases a live root"));
2593 }
2594 pager
2595 .restore_deferred_reclamation(&deferred)
2596 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2597 let ownership_epoch =
2598 reddb_file::native_store::decode_native_paged_ownership_epoch(
2599 content, &mut pos,
2600 )
2601 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2602 self.ownership_catalog_epoch
2603 .store(ownership_epoch.unwrap_or(0), Ordering::Release);
2604 if migrated_catalog {
2605 self.mark_paged_registry_dirty();
2606 }
2607 if self.format_version() < STORE_VERSION_V11 {
2608 self.set_format_version(STORE_VERSION_V11);
2609 }
2610 if self.config.resident_memory_bytes_max.is_some()
2611 && resume.is_none()
2612 && !migrated_catalog
2613 && !retain_canonical_system_config
2614 {
2615 self.remember_startup_xid_high_water_mark(scanned_xid_high_water_mark);
2616 }
2617 outcome.migrated = migrated_catalog;
2618 return Ok(outcome);
2619 }
2620 }
2621
2622 if retain_canonical_system_config {
2623 return Err(page_integrity(
2624 1,
2625 None,
2626 "canonical config recovery requires a complete catalog",
2627 ));
2628 }
2629 if self.format_version() < STORE_VERSION_V11 {
2630 self.set_format_version(STORE_VERSION_V11);
2631 }
2632
2633 Ok(CatalogLoadOutcome::default())
2634 }
2635
2636 pub(crate) fn deserialize_entity(
2638 data: &[u8],
2639 format_version: u32,
2640 ) -> Result<UnifiedEntity, StoreError> {
2641 let mut pos = 0;
2642 Self::read_entity_binary(data, &mut pos, format_version)
2643 .map_err(|e| StoreError::Serialization(e.to_string()))
2644 }
2645
2646 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
2648 let mut buf = Vec::with_capacity(256);
2653 Self::write_entity_binary(&mut buf, entity, format_version);
2654 buf
2655 }
2656
2657 pub(crate) fn serialize_entity_record(
2658 entity: &UnifiedEntity,
2659 metadata: Option<&Metadata>,
2660 format_version: u32,
2661 ) -> Vec<u8> {
2662 reddb_engine::storage::metadata_codec::serialize_entity_record(
2663 entity,
2664 metadata,
2665 format_version,
2666 )
2667 }
2668
2669 pub(crate) fn deserialize_entity_record(
2670 data: &[u8],
2671 format_version: u32,
2672 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
2673 reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
2674 .map_err(StoreError::Serialization)
2675 }
2676
2677 pub fn persist(&self) -> Result<(), StoreError> {
2682 self.persist_inner(false)
2683 }
2684
2685 pub(crate) fn persist_after_runtime_snapshots(&self) -> Result<(), StoreError> {
2686 self.persist_inner(true)
2687 }
2688
2689 fn persist_inner(&self, runtime_snapshots_durable: bool) -> Result<(), StoreError> {
2690 if self.is_read_only() {
2691 return Ok(());
2692 }
2693 self.ensure_pager_writeback_available()?;
2694 let _vault_guard = self.vault_publication_lock.lock();
2695 let pending_vault_versions = self.pending_vault_versions.read();
2696 let pager = match &self.pager {
2697 Some(p) => p,
2698 None => {
2699 if let Some(path) = &self.db_path {
2701 return self
2702 .save_to_file(path)
2703 .map_err(|e| StoreError::Serialization(e.to_string()));
2704 }
2705 return Err(StoreError::Io(std::io::Error::other(
2706 "No pager or path configured for persistence",
2707 )));
2708 }
2709 };
2710 let _ownership_guard = pager.lock_ownership_mutations();
2713
2714 match pager.read_manifest_page() {
2715 Ok(_) => {}
2716 Err(PagerError::PageNotFound(_)) => {
2717 let meta_page = pager
2718 .allocate_page(crate::storage::engine::PageType::Header)
2719 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2720 pager
2721 .write_page(meta_page.page_id(), meta_page)
2722 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2723 }
2724 Err(e) => {
2725 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
2726 }
2727 }
2728
2729 if let Some(commit) = &self.commit {
2730 commit.force_sync().map_err(StoreError::Io)?;
2731 }
2732
2733 struct InterruptedOwnership<'a> {
2734 pager: &'a Pager,
2735 active: bool,
2736 }
2737 impl Drop for InterruptedOwnership<'_> {
2738 fn drop(&mut self) {
2739 if self.active {
2740 self.pager.fence_interrupted_ownership();
2741 }
2742 }
2743 }
2744 let mut ownership = InterruptedOwnership {
2745 pager,
2746 active: self.commit.is_some(),
2747 };
2748 let epoch = if let Some(commit) = &self.commit {
2749 let epoch = commit
2750 .next_page_ownership_epoch(self)
2751 .map_err(StoreError::Io)?;
2752 commit
2753 .begin_page_ownership_epoch(self, epoch)
2754 .map_err(StoreError::Io)?;
2755 Some(epoch)
2756 } else {
2757 None
2758 };
2759
2760 let collections = self.resident.collections.read();
2761 let mut btree_indices = self.btree_indices.write();
2762
2763 let mut collection_roots: Vec<(String, u32)> = Vec::new();
2765
2766 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
2769 let previous_versions = self.checkpoint_versions.read();
2770 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
2771 let pending_vault = Arc::new(pending_vault_versions.clone());
2772 let (visibility_revision, excluded, untracked_visibility_revision) =
2773 self.snapshot_manager.get().map_or_else(
2774 || (0, Default::default(), 0),
2775 |manager| manager.checkpoint_projection_state(),
2776 );
2777 let excluded = Arc::new(excluded);
2778 for (name, manager) in collections.iter() {
2779 let mut version = CheckpointVersion {
2780 manager: Arc::downgrade(manager),
2781 segments: manager.checkpoint_revisions(),
2782 visibility_revision,
2783 untracked_visibility_revision,
2784 excluded_xids: Arc::clone(&excluded),
2785 pending_vault: Arc::clone(&pending_vault),
2786 format_version: self.format_version(),
2787 deferred_rows: Some(Arc::default()),
2788 has_unprojected_rows: false,
2789 };
2790 if let (Some(previous), Some(tree)) = (
2791 previous_versions.get(name),
2792 btree_indices
2793 .get(name)
2794 .filter(|tree| tree.has_page_proofs()),
2795 ) {
2796 if previous.manager.ptr_eq(&version.manager)
2797 && previous.segments == version.segments
2798 && previous.format_version == version.format_version
2799 && !previous.has_unprojected_rows
2800 && checkpoint_visibility_matches(previous, &version)
2801 {
2802 version.deferred_rows = previous.deferred_rows.clone();
2803 collection_roots.push((name.clone(), tree.root_page_id()));
2804 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
2805 checkpoint_versions.insert(name.clone(), version);
2806 continue;
2807 }
2808 }
2809 if let (Some(previous), Some(tree)) = (
2810 previous_versions.get(name),
2811 btree_indices
2812 .get(name)
2813 .filter(|tree| tree.has_page_proofs()),
2814 ) {
2815 if previous.manager.ptr_eq(&version.manager)
2816 && previous.format_version == version.format_version
2817 {
2818 if let Some(tree) =
2819 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
2820 {
2821 collection_roots.push((name.clone(), tree.root_page_id()));
2822 rebuilt_indices.insert(name.clone(), tree);
2823 checkpoint_versions.insert(name.clone(), version);
2824 continue;
2825 }
2826 }
2827 }
2828 let btree = self.checkpoint_tree(
2829 pager,
2830 manager,
2831 btree_indices.get(name),
2832 |entity, metadata| {
2833 #[cfg(test)]
2834 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
2835 let hidden = pending_vault_versions.contains(&entity.id)
2836 || (matches!(
2837 entity.data,
2838 EntityData::Row(_)
2839 | EntityData::Vector(_)
2840 | EntityData::QueueMessage(_)
2841 ) && excluded.contains(&entity.xmin));
2842 let normalize_xmax = matches!(
2843 entity.data,
2844 EntityData::Row(_) | EntityData::QueueMessage(_)
2845 ) && excluded.contains(&entity.xmax);
2846 if hidden || normalize_xmax {
2847 version.remember_deferred_row(entity.id);
2848 }
2849 if hidden {
2850 return None;
2851 }
2852 let mut normalized = std::borrow::Cow::Borrowed(entity);
2853 if normalize_xmax {
2854 normalized.to_mut().xmax = 0;
2855 }
2856 Some(Self::serialize_entity_record(
2857 &normalized,
2858 metadata.as_ref(),
2859 self.format_version(),
2860 ))
2861 },
2862 )?;
2863 checkpoint_versions.insert(name.clone(), version);
2864
2865 collection_roots.push((name.clone(), btree.root_page_id()));
2866 rebuilt_indices.insert(name.clone(), btree);
2867 }
2868
2869 let mut meta_data = Vec::with_capacity(4096);
2871
2872 let format_version = STORE_VERSION_V11;
2873 self.set_format_version(format_version);
2874
2875 reddb_file::encode_native_paged_metadata_header(
2876 &mut meta_data,
2877 reddb_file::NativePagedMetadataHeader {
2878 format_version,
2879 collection_count: collection_roots.len() as u32,
2880 },
2881 );
2882
2883 for (name, root_page) in &collection_roots {
2885 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
2886 }
2887
2888 let cross_refs = self.resident.cross_refs.read();
2890 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
2891 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
2892 for (source_id, refs) in cross_refs.iter() {
2893 for (target_id, ref_type, collection) in refs {
2894 reddb_file::encode_native_paged_cross_ref(
2895 &mut meta_data,
2896 source_id.raw(),
2897 target_id.raw(),
2898 ref_type.to_byte(),
2899 collection,
2900 );
2901 }
2902 }
2903
2904 for (name, previous) in btree_indices.iter() {
2905 if !rebuilt_indices
2906 .get(name)
2907 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
2908 {
2909 self.retire_btree(Arc::clone(previous));
2910 }
2911 }
2912 #[cfg(test)]
2913 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
2914 pager
2915 .sync()
2916 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2917 eprintln!("experimental checkpoint exit before manifest");
2918 std::process::exit(183);
2919 }
2920 let rebuilt_keys = self.checkpoint_key_indices(pager, &rebuilt_indices)?;
2921 #[cfg(test)]
2922 if std::env::var_os("REDDB_TEST_EXIT_AFTER_SECONDARY_INDEX_BUILD").is_some() {
2923 pager
2924 .sync()
2925 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2926 std::process::exit(196);
2927 }
2928 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices, &rebuilt_keys)?;
2929
2930 #[cfg(test)]
2931 if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
2932 std::process::exit(195);
2933 }
2934
2935 *btree_indices = rebuilt_indices;
2938 *self.key_indices.write() = rebuilt_keys;
2939 drop(previous_versions);
2940 *self.checkpoint_versions.write() = checkpoint_versions;
2941
2942 if let Some(commit) = &self.commit {
2943 commit
2944 .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
2945 .map_err(StoreError::Io)?;
2946 ownership.active = false;
2947 if runtime_snapshots_durable {
2948 commit
2949 .truncate_after_runtime_checkpoint()
2950 .map_err(StoreError::Io)?;
2951 } else {
2952 commit.truncate().map_err(StoreError::Io)?;
2953 }
2954 }
2955 *self.checkpoint_durable_rows.write() = Default::default();
2956
2957 if let Some(limit) = self.config.resident_memory_bytes_max {
2958 let mut versions = self.checkpoint_versions.write();
2959 for (name, manager) in collections.iter() {
2960 if versions
2961 .get(name)
2962 .is_some_and(|version| !version.has_unprojected_rows)
2963 {
2964 if let Some(tree) = btree_indices.get(name) {
2965 manager.install_backing(
2966 Arc::new(
2967 {
2968 let keys = self.key_indices.read();
2969 let index = keys.get(name).expect("invariant: prepared checkpoint classifies its key index");
2970 if index.indexed {
2971 reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(tree), Arc::clone(&index.tree))
2972 } else {
2973 reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(tree))
2974 }
2975 },
2976 ),
2977 tree.root_page_id(),
2978 self.format_version(),
2979 usize::try_from(limit).unwrap_or(usize::MAX),
2980 manager.count() as u64,
2981 );
2982 if let Some(version) = versions.get_mut(name) {
2983 version.segments = manager.checkpoint_revisions();
2984 }
2985 }
2986 }
2987 }
2988 }
2989
2990 Ok(())
2991 }
2992
2993 pub fn is_paged(&self) -> bool {
2995 self.pager.is_some()
2996 }
2997
2998 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
3001 self.btree_indices
3002 .read()
3003 .get(Self::canonical_collection(collection))
3004 .map(|btree| btree.root_page_id())
3005 .filter(|root| *root != 0)
3006 }
3007
3008 pub(crate) fn operational_manifest_for_path(
3011 &self,
3012 path: &Path,
3013 ) -> reddb_file::OperationalManifest {
3014 let mut cached = self.operational_manifest.lock();
3015 if let Some((current, manifest)) = cached.as_ref() {
3016 if current == path {
3017 return manifest.clone();
3018 }
3019 }
3020 let manifest = reddb_file::OperationalManifest::for_db_path(path);
3021 *cached = Some((path.to_owned(), manifest.clone()));
3022 manifest
3023 }
3024
3025 pub fn db_path(&self) -> Option<&Path> {
3027 self.db_path.as_deref()
3028 }
3029}
3030
3031#[cfg(test)]
3032mod checkpoint_work_tests {
3033 use super::*;
3034 use reddb_file::PAGED_PAGE_SIZE as PAGE_SIZE;
3035
3036 struct SystemNameProjection;
3037
3038 impl super::super::secondary_index::SecondaryIndexProjector for SystemNameProjection {
3039 fn project(
3040 &self,
3041 entity: &UnifiedEntity,
3042 key: &mut [u8; super::super::secondary_index::SECONDARY_KEY_BYTES_MAX],
3043 ) -> Result<Option<usize>, StoreError> {
3044 key[..8].copy_from_slice(&entity.id.raw().to_be_bytes());
3045 Ok(Some(8))
3046 }
3047 }
3048
3049 #[cfg(target_os = "linux")]
3050 #[test]
3051 fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
3052 const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
3053 if let Some(path) = std::env::var_os(CHILD) {
3054 let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
3055 let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
3056 let store = UnifiedStore::open(path).expect("open store");
3057 let initial_count = if multi { 600 } else { 1 };
3058 for id in 1..=initial_count {
3059 store.insert_auto("rows", row(id, 1)).expect("seed");
3060 }
3061 store.persist().expect("baseline");
3062 let manager = store.get_collection("rows").expect("collection");
3063 let ids: Vec<u64> = if multi {
3064 (1..=initial_count).collect()
3065 } else {
3066 vec![if insert { 2 } else { 1 }]
3067 };
3068 let mut actions = Vec::new();
3069 for id in ids {
3070 if insert {
3071 manager.insert(row(id, 9)).expect("insert version");
3072 } else {
3073 manager.update(row(id, 9)).expect("update version");
3074 }
3075 let entity = manager.get(EntityId::new(id)).expect("changed entity");
3076 actions.push(StoreWalAction::upsert_entity(
3077 "rows",
3078 &entity,
3079 None,
3080 store.format_version(),
3081 ));
3082 }
3083 let references = store
3084 .commit
3085 .as_ref()
3086 .expect("coordinator")
3087 .append_checkpoint_actions(&actions)
3088 .expect("durable WAL");
3089 store.remember_checkpoint_rows(actions, references);
3090 store.persist().expect("exit after sparse write");
3091 panic!("inline sparse crash hook was not reached");
3092 }
3093 for (insert, multi) in [(false, false), (true, false), (false, true)] {
3094 let directory = tempfile::tempdir().expect("temporary store");
3095 let path = directory.path().join("data.rdb");
3096 let mut command =
3097 std::process::Command::new(std::env::current_exe().expect("test executable"));
3098 command
3099 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
3100 .env(CHILD, &path)
3101 .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
3102 if insert {
3103 command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
3104 }
3105 if multi {
3106 command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
3107 }
3108 let output = command.output().expect("crash child");
3109 assert_eq!(
3110 output.status.code(),
3111 Some(190),
3112 "insert={insert} multi={multi}: {}",
3113 String::from_utf8_lossy(&output.stderr)
3114 );
3115 let recovered = UnifiedStore::open(&path).expect("recover");
3116 let ids: Vec<u64> = if multi {
3117 (1..=600).collect()
3118 } else {
3119 vec![if insert { 2 } else { 1 }]
3120 };
3121 for id in ids {
3122 assert_eq!(
3123 recovered
3124 .get("rows", EntityId::new(id))
3125 .expect("recovered row")
3126 .data
3127 .as_row()
3128 .expect("row")
3129 .columns,
3130 vec![Value::Integer(9)]
3131 );
3132 }
3133 assert_eq!(
3134 recovered.get_collection("rows").expect("rows").count(),
3135 if multi {
3136 600
3137 } else if insert {
3138 2
3139 } else {
3140 1
3141 }
3142 );
3143 }
3144 }
3145
3146 #[test]
3147 fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
3148 let directory = tempfile::tempdir().expect("temporary store");
3149 let path = directory.path().join("data.rdb");
3150 {
3151 let store = UnifiedStore::open(&path).expect("open store");
3152 store.create_collection("rows").expect("create collection");
3153 store.persist().expect("publish ownership epoch");
3154 assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
3155 store.persist().expect("publish next ownership epoch");
3156 }
3157 let reopened = UnifiedStore::open(&path).expect("reopen store");
3158 assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
3159 }
3160
3161 #[test]
3162 fn metadata_overflow_with_missing_continuation_fails_closed() {
3163 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
3164
3165 let directory = tempfile::tempdir().expect("temporary store");
3166 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
3167 let pager = store.pager.as_ref().unwrap();
3168 let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
3169 let (valid_manifest, _) = build_meta_page1_with_overflow(pager, &valid_payload, 1).unwrap();
3170 pager.write_page(1, valid_manifest).unwrap();
3171 assert!(!existing_overflow_chain(pager).unwrap().is_empty());
3172 assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
3173
3174 let missing = pager.page_count().unwrap() + 100;
3175 let mut manifest = Page::new(PageType::Header, 1);
3176 reddb_file::encode_native_metadata_overflow_header(
3177 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
3178 reddb_file::NativeMetadataOverflowHeader {
3179 format_version: STORE_VERSION_V11,
3180 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
3181 next_overflow_page_id: missing,
3182 },
3183 )
3184 .unwrap();
3185 pager.write_page(1, manifest).unwrap();
3186
3187 assert!(
3188 existing_overflow_chain(pager).is_err(),
3189 "a missing continuation must not be treated as an empty chain"
3190 );
3191
3192 let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
3193 let mut manifest = Page::new(PageType::Header, 1);
3194 reddb_file::encode_native_metadata_overflow_header(
3195 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
3196 reddb_file::NativeMetadataOverflowHeader {
3197 format_version: STORE_VERSION_V11,
3198 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
3199 next_overflow_page_id: wrong_type.page_id(),
3200 },
3201 )
3202 .unwrap();
3203 pager.write_page(1, manifest).unwrap();
3204 assert!(
3205 existing_overflow_chain(pager).is_err(),
3206 "a live page must not be mistaken for metadata overflow"
3207 );
3208 assert!(
3209 read_meta_payload(pager).is_err(),
3210 "a live page must not be read as metadata overflow"
3211 );
3212
3213 let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
3214 let cycle_id = cycle.page_id();
3215 reddb_file::encode_native_metadata_overflow_continuation_header(
3216 &mut cycle.as_bytes_mut()[HEADER_SIZE..],
3217 reddb_file::NativeMetadataOverflowContinuationHeader {
3218 next_overflow_page_id: cycle_id,
3219 chunk_bytes: 1,
3220 },
3221 )
3222 .unwrap();
3223 pager.write_page(cycle_id, cycle).unwrap();
3224 let mut manifest = Page::new(PageType::Header, 1);
3225 reddb_file::encode_native_metadata_overflow_header(
3226 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
3227 reddb_file::NativeMetadataOverflowHeader {
3228 format_version: STORE_VERSION_V11,
3229 total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
3230 next_overflow_page_id: cycle_id,
3231 },
3232 )
3233 .unwrap();
3234 pager.write_page(1, manifest).unwrap();
3235 assert!(
3236 read_meta_payload(pager).is_err(),
3237 "cyclic overflow must not be accepted"
3238 );
3239
3240 let mut manifest = Page::new(PageType::Header, 1);
3241 reddb_file::encode_native_metadata_overflow_header(
3242 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
3243 reddb_file::NativeMetadataOverflowHeader {
3244 format_version: STORE_VERSION_V11,
3245 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
3246 next_overflow_page_id: 0,
3247 },
3248 )
3249 .unwrap();
3250 pager.write_page(1, manifest).unwrap();
3251 assert!(
3252 read_meta_payload(pager).is_err(),
3253 "a truncated metadata payload must not be accepted"
3254 );
3255 }
3256
3257 #[test]
3258 fn retired_tree_pages_remain_pinned_by_a_cursor() {
3259 let directory = tempfile::tempdir().expect("temporary store");
3260 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3261 for id in 1..=100 {
3262 store.insert_auto("rows", row(id, 1)).expect("seed");
3263 }
3264 store.persist().expect("initial checkpoint");
3265 let tree = store
3266 .btree_indices
3267 .read()
3268 .get("rows")
3269 .cloned()
3270 .expect("tree");
3271 let mut cursor = tree.cursor_first().expect("cursor");
3272 drop(tree);
3273 let manager = store.get_collection("rows").expect("rows");
3274 for value in 2..=6 {
3275 manager.update(row(1, value)).expect("change");
3276 store.persist().expect("checkpoint with old reader");
3277 }
3278 let mut count = 0;
3279 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
3280 let (entity, _) =
3281 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
3282 .expect("decode");
3283 assert_eq!(
3284 entity.data.as_row().expect("row").columns,
3285 [Value::Integer(1)]
3286 );
3287 count += 1;
3288 }
3289 assert_eq!(count, 100);
3290 drop(cursor);
3291 store.persist().expect("reclaim after reader release");
3292 assert!(store.retired_btrees.lock().is_empty());
3293 }
3294
3295 #[test]
3296 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
3297 let directory = tempfile::tempdir().expect("temporary store");
3298 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3299 store.create_collection("rows").expect("collection");
3300 let manager = store.get_collection("rows").expect("rows");
3301 for id in (1..=1200).rev() {
3302 manager
3303 .insert(UnifiedEntity::table_row(
3304 EntityId::new(id),
3305 "rows",
3306 id,
3307 vec![Value::text("x".repeat(4096))],
3308 ))
3309 .expect("seed");
3310 }
3311 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
3312 store.persist().expect("bounded checkpoint");
3313 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
3314 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
3315 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
3316 }
3317
3318 #[test]
3319 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
3320 let directory = tempfile::tempdir().expect("temporary store");
3321 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3322 store.create_collection("rows").expect("collection");
3323 for id in 1..=40 {
3324 store
3325 .insert(
3326 "rows",
3327 UnifiedEntity::table_row(
3328 EntityId::new(id),
3329 "rows",
3330 id,
3331 vec![Value::Integer(id as i64)],
3332 ),
3333 )
3334 .expect("seed row");
3335 }
3336 store.persist().expect("initial checkpoint");
3337 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3338 store.persist().expect("unchanged checkpoint");
3339 assert_eq!(
3340 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
3341 0,
3342 "unchanged collections must not hold the writer fence while serializing every row"
3343 );
3344 drop(store);
3345 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
3346 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3347 reopened.persist().expect("unchanged recovered generation");
3348 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3349 }
3350
3351 #[test]
3352 fn sparse_wal_replay_does_not_reencode_complete_collections() {
3353 let directory = tempfile::tempdir().expect("temporary store");
3354 let path = directory.path().join("data.rdb");
3355 let store = UnifiedStore::open(&path).expect("paged store");
3356 for (name, first) in [("rows", 1), ("cold", 129)] {
3357 store.create_collection(name).expect("collection");
3358 for id in first..first + 128 {
3359 store
3360 .insert(
3361 name,
3362 UnifiedEntity::table_row(
3363 EntityId::new(id),
3364 name,
3365 id,
3366 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
3367 ),
3368 )
3369 .expect("seed");
3370 }
3371 }
3372 store.persist().expect("durable baseline");
3373 let action = super::super::commit::StoreWalAction::upsert_entity(
3374 "rows",
3375 &row(1, 42),
3376 None,
3377 STORE_VERSION_V11,
3378 );
3379 store
3381 .commit
3382 .as_ref()
3383 .unwrap()
3384 .append_actions(&[action])
3385 .unwrap();
3386 drop(store);
3387
3388 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
3389 let recovered = UnifiedStore::open(&path).expect("replay committed change");
3390 assert_eq!(
3391 recovered
3392 .get("rows", EntityId::new(1))
3393 .unwrap()
3394 .data
3395 .as_row()
3396 .unwrap()
3397 .columns,
3398 vec![Value::Integer(42)]
3399 );
3400 for name in ["rows", "cold"] {
3401 assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
3402 }
3403 assert_eq!(
3404 REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
3405 0,
3406 "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
3407 );
3408 for (name, first) in [("rows", 2), ("cold", 129)] {
3409 let end = if name == "rows" { 129 } else { 257 };
3410 for id in first..end {
3411 assert_eq!(
3412 recovered
3413 .get(name, EntityId::new(id))
3414 .unwrap()
3415 .data
3416 .as_row()
3417 .unwrap()
3418 .columns,
3419 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
3420 );
3421 }
3422 }
3423
3424 drop(recovered);
3425 let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
3426 assert_eq!(
3427 reopened
3428 .get("rows", EntityId::new(1))
3429 .unwrap()
3430 .data
3431 .as_row()
3432 .unwrap()
3433 .columns,
3434 vec![Value::Integer(42)]
3435 );
3436 }
3437
3438 fn replay_test_payload(bytes: usize) -> String {
3439 let mut state = 0x1234_5678_u32;
3440 (0..bytes)
3441 .map(|_| {
3442 state ^= state << 13;
3443 state ^= state >> 17;
3444 state ^= state << 5;
3445 char::from(b'!' + (state % 90) as u8)
3446 })
3447 .collect()
3448 }
3449
3450 #[test]
3451 fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
3452 let directory = tempfile::tempdir().unwrap();
3453 let path = directory.path().join("data.rdb");
3454 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
3455 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3456 for id in 1..=16 {
3457 store.insert_auto("rows", row(id, 1)).unwrap();
3458 }
3459 store.persist().unwrap();
3460 let payload = replay_test_payload(64 * 1024);
3462 let changed =
3463 UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
3464 let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
3465 store
3466 .commit
3467 .as_ref()
3468 .unwrap()
3469 .append_actions(&[action])
3470 .unwrap();
3471 drop(store);
3472 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
3473 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3474 assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
3475 assert_eq!(
3476 recovered
3477 .get("rows", EntityId::new(1))
3478 .unwrap()
3479 .data
3480 .as_row()
3481 .unwrap()
3482 .columns,
3483 changed.data.as_row().unwrap().columns
3484 );
3485 drop(recovered);
3486 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3487 assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
3488 assert_eq!(
3489 reopened
3490 .get("rows", EntityId::new(1))
3491 .unwrap()
3492 .data
3493 .as_row()
3494 .unwrap()
3495 .columns,
3496 changed.data.as_row().unwrap().columns
3497 );
3498 }
3499
3500 #[cfg(target_os = "linux")]
3501 #[test]
3502 fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
3503 const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
3504 if let Some(path) = std::env::var_os(CHILD) {
3505 let _store = UnifiedStore::open(path).unwrap();
3506 panic!("replay crash hook was not reached");
3507 }
3508 for overflow in [false, true] {
3509 for (hook, code) in [
3510 ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
3511 ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
3512 ] {
3513 let directory = tempfile::tempdir().unwrap();
3514 let path = directory.path().join("data.rdb");
3515 let store = UnifiedStore::open(&path).unwrap();
3516 for id in 1..=128 {
3517 store.insert_auto("rows", row(id, 1)).unwrap();
3518 }
3519 store.persist().unwrap();
3520 let changed = if overflow {
3521 UnifiedEntity::table_row(
3522 EntityId::new(1),
3523 "rows",
3524 1,
3525 vec![Value::text(replay_test_payload(16 * 1024))],
3526 )
3527 } else {
3528 row(1, 42)
3529 };
3530 let action =
3531 StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
3532 store
3533 .commit
3534 .as_ref()
3535 .unwrap()
3536 .append_actions(&[action])
3537 .unwrap();
3538 drop(store);
3539 let mut first_size = 0;
3540 for cycle in 0..12 {
3541 let output = std::process::Command::new(std::env::current_exe().unwrap())
3542 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
3543 .env(CHILD, &path).env(hook, "1").output().unwrap();
3544 assert_eq!(
3545 output.status.code(),
3546 Some(code),
3547 "overflow={overflow} hook={hook} cycle={cycle}: {}",
3548 String::from_utf8_lossy(&output.stderr)
3549 );
3550 let size = std::fs::metadata(&path).unwrap().len();
3551 if cycle == 0 {
3552 first_size = size;
3553 }
3554 assert!(
3555 size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
3556 "repeated recovery must reuse reservations: {first_size} -> {size}"
3557 );
3558 }
3559 for _ in 0..2 {
3560 let recovered = UnifiedStore::open(&path).unwrap();
3561 assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
3562 for id in 1..=128 {
3563 let expected = if id == 1 { changed.clone() } else { row(id, 1) };
3564 assert_eq!(
3565 recovered
3566 .get("rows", EntityId::new(id))
3567 .unwrap()
3568 .data
3569 .as_row()
3570 .unwrap()
3571 .columns,
3572 expected.data.as_row().unwrap().columns
3573 );
3574 }
3575 }
3576 }
3577 }
3578 }
3579
3580 #[test]
3581 fn paged_reopen_preserves_stored_sequence_identity() {
3582 let directory = tempfile::tempdir().expect("temporary store");
3583 let path = directory.path().join("data.rdb");
3584 let store = UnifiedStore::open(&path).expect("paged store");
3585 store.create_collection("rows").expect("collection");
3586 for id in [30, 10, 20] {
3587 store.insert("rows", row(id, id as i64)).expect("insert");
3588 }
3589 store.persist().expect("checkpoint");
3590 let sequences = [10, 20, 30].map(|id| {
3591 store
3592 .get("rows", EntityId::new(id))
3593 .expect("row")
3594 .sequence_id
3595 });
3596 drop(store);
3597 let reopened = UnifiedStore::open(&path).expect("reopen");
3598 let recovered = [10, 20, 30].map(|id| {
3599 reopened
3600 .get("rows", EntityId::new(id))
3601 .expect("row")
3602 .sequence_id
3603 });
3604 assert_eq!(
3605 recovered, sequences,
3606 "opening a checkpoint must not rewrite stored row identities"
3607 );
3608 }
3609
3610 #[test]
3611 fn auto_insert_persists_the_assigned_sequence_identity() {
3612 let directory = tempfile::tempdir().expect("temporary store");
3613 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3614 for id in 1u64..=3 {
3615 store
3616 .insert_auto("rows", row(id, id as i64))
3617 .expect("insert");
3618 }
3619 let manager = store.get_collection("rows").expect("rows");
3620 let tree = store
3621 .btree_indices
3622 .read()
3623 .get("rows")
3624 .cloned()
3625 .expect("tree");
3626 for id in 1u64..=3 {
3627 let bytes = tree
3628 .get(&id.to_be_bytes())
3629 .expect("lookup")
3630 .expect("stored row");
3631 let (persisted, _) =
3632 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
3633 .expect("decode");
3634 assert_eq!(
3635 persisted.sequence_id,
3636 manager
3637 .get(EntityId::new(id))
3638 .expect("live row")
3639 .sequence_id
3640 );
3641 }
3642 }
3643
3644 #[test]
3645 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
3646 let directory = tempfile::tempdir().expect("temporary store");
3647 let path = directory.path().join("data.rdb");
3648 let store = UnifiedStore::open(&path).expect("paged store");
3649 store.insert_auto("rows", row(1, 1)).expect("seed");
3650 store.persist().expect("checkpoint");
3651 store.insert_auto("rows", row(2, 2)).expect("append");
3652 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3653 store.persist().expect("inline append checkpoint");
3654 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3655 store
3656 .get_collection("rows")
3657 .expect("rows")
3658 .update(row(1, 99))
3659 .expect("untracked edit");
3660 store.insert_auto("rows", row(3, 3)).expect("next append");
3661 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3662 store.persist().expect("untracked edit checkpoint");
3663 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
3664 drop(store);
3665 let reopened = UnifiedStore::open(&path).expect("reopen");
3666 assert_eq!(
3667 reopened
3668 .get("rows", EntityId::new(1))
3669 .expect("row")
3670 .data
3671 .as_row()
3672 .expect("table")
3673 .columns,
3674 vec![Value::Integer(99)]
3675 );
3676 }
3677
3678 #[test]
3679 fn replayed_trees_are_reusable_after_durable_reconciliation() {
3680 let directory = tempfile::tempdir().expect("temporary store");
3681 let path = directory.path().join("data.rdb");
3682 let store = UnifiedStore::open(&path).expect("paged store");
3683 store.insert_auto("rows", row(1, 1)).expect("seed");
3684 store.persist().expect("checkpoint");
3685 store
3686 .insert_auto("rows", row(2, 2))
3687 .expect("WAL-backed append");
3688 drop(store);
3689 let reopened = UnifiedStore::open(&path).expect("replay");
3690 assert!(reopened.get("rows", EntityId::new(2)).is_some());
3691 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3692 reopened.persist().expect("checkpoint reconciled tree");
3693 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3694 }
3695
3696 #[test]
3697 fn legacy_system_catalog_and_wal_migrate_without_cold_boot_rebuild() {
3698 use crate::storage::wal::{WalRecord, WalWriter};
3699
3700 let directory = tempfile::tempdir().expect("temporary store");
3701 let path = directory.path().join("data.rdb");
3702 let config = UnifiedStoreConfig {
3703 page_cache_slots: Some(32),
3704 resident_memory_bytes_max: Some(2 * 1024 * 1024),
3705 ..UnifiedStoreConfig::default()
3706 };
3707 let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3708 codec_version: 1,
3709 definition: [91; 32],
3710 };
3711 let baseline_index_root;
3712 {
3713 let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3714 store
3715 .create_collection("red_config")
3716 .expect("system collection");
3717 store
3718 .insert_auto(
3719 "red_config",
3720 UnifiedEntity::table_row(
3721 EntityId::new(1),
3722 "red_config",
3723 1,
3724 vec![Value::text("checkpoint")],
3725 ),
3726 )
3727 .expect("legacy-compatible insert");
3728 store
3729 .register_secondary_index(
3730 "red_config",
3731 "by_id",
3732 index_identity.clone(),
3733 Arc::new(SystemNameProjection),
3734 )
3735 .expect("secondary index");
3736 store.persist().expect("baseline checkpoint");
3737 baseline_index_root = store
3738 .secondary_index("red.config", "by_id", &index_identity)
3739 .expect("published secondary index")
3740 .root_page_id();
3741 }
3742
3743 {
3747 let pager = Pager::open(
3748 &path,
3749 PagerConfig {
3750 disk_residency: true,
3751 ..PagerConfig::default()
3752 },
3753 )
3754 .expect("open pager");
3755 let mut page = pager.read_page(1).expect("catalog page");
3756 let bytes = page.as_bytes_mut();
3757 let canonical = b"red.config";
3758 let legacy = b"red_config";
3759 let mut replacements = 0;
3760 for offset in 0..=bytes.len() - canonical.len() {
3761 if &bytes[offset..offset + canonical.len()] == canonical {
3762 bytes[offset..offset + legacy.len()].copy_from_slice(legacy);
3763 replacements += 1;
3764 }
3765 }
3766 assert!(replacements >= 1, "catalog must contain the canonical name");
3767 pager.write_page(1, page).expect("write legacy catalog");
3768 pager.sync().expect("sync legacy catalog");
3769 }
3770 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3771 .rename_collections(&[("red.config", "red_config")])
3772 .expect("restore legacy operational name");
3773
3774 let wal_upsert = reddb_file::encode_store_wal_action_frame(
3777 &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
3778 collection: "red_config".to_string(),
3779 record: UnifiedStore::serialize_entity_record(
3780 &UnifiedEntity::table_row(
3781 EntityId::new(2),
3782 "red_config",
3783 2,
3784 vec![Value::text("legacy WAL")],
3785 ),
3786 None,
3787 STORE_VERSION_CURRENT,
3788 ),
3789 },
3790 )
3791 .expect("legacy WAL action");
3792 let wal_delete = reddb_file::encode_store_wal_action_frame(
3793 &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
3794 collection: "red_config".to_string(),
3795 entity_id: EntityId::new(1).raw(),
3796 },
3797 )
3798 .expect("legacy WAL delete");
3799 let wal_path = reddb_file::layout::unified_wal_path(&path);
3800 {
3801 let mut writer = WalWriter::open(&wal_path).expect("open WAL");
3802 writer
3803 .append(&WalRecord::TxCommitBatch {
3804 tx_id: 77,
3805 actions: vec![wal_upsert, wal_delete],
3806 })
3807 .expect("append legacy transaction");
3808 writer.sync().expect("sync legacy transaction");
3809 }
3810
3811 let (root_after_recovery, pages_after_recovery) = {
3812 let recovered =
3813 UnifiedStore::open_with_config(&path, config.clone()).expect("migrate and recover");
3814 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3815 assert!(
3816 recovered.get("red.config", EntityId::new(1)).is_none(),
3817 "legacy WAL deletes must target the canonical collection"
3818 );
3819 assert_eq!(
3820 recovered
3821 .get("red.config", EntityId::new(2))
3822 .expect("WAL row")
3823 .kind
3824 .collection(),
3825 "red.config"
3826 );
3827 let root = recovered
3828 .collection_root_page("red.config")
3829 .expect("published canonical root");
3830 assert_eq!(
3831 recovered
3832 .secondary_index("red.config", "by_id", &index_identity)
3833 .expect("recovered secondary index")
3834 .root_page_id(),
3835 baseline_index_root,
3836 "name migration must reuse the secondary-index root"
3837 );
3838 let pages = recovered
3839 .pager()
3840 .expect("pager")
3841 .page_count()
3842 .expect("page count");
3843 (root, pages)
3844 };
3845
3846 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3849 .rename_collections(&[("red.config", "red_config")])
3850 .expect("restore stale manifest identity");
3851
3852 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3853 let cold = UnifiedStore::open_with_config(&path, config).expect("second cold boot");
3854 assert_eq!(
3855 cold.collection_root_page("red.config"),
3856 Some(root_after_recovery),
3857 "an idempotent cold boot must reuse the published data root"
3858 );
3859 assert_eq!(
3860 cold.pager()
3861 .expect("pager")
3862 .page_count()
3863 .expect("page count"),
3864 pages_after_recovery,
3865 "cold boot must not allocate replacement index trees"
3866 );
3867 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3868 assert_eq!(
3869 cold.secondary_index("red.config", "by_id", &index_identity)
3870 .expect("secondary index after cold boot")
3871 .root_page_id(),
3872 baseline_index_root
3873 );
3874 assert!(
3875 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3876 .recover_or_bootstrap(&["red.config".to_string()])
3877 .expect("canonical manifest")
3878 .is_empty()
3879 );
3880 }
3881
3882 struct DuplicateSystemCatalogFixture {
3883 _directory: tempfile::TempDir,
3884 path: std::path::PathBuf,
3885 config: UnifiedStoreConfig,
3886 pages_before: u32,
3887 legacy_root: Option<u32>,
3888 legacy_key_root: Option<u32>,
3889 legacy_secondary_root: Option<u32>,
3890 index_identity: super::super::secondary_index::SecondaryIndexIdentity,
3891 }
3892
3893 fn duplicate_system_catalog_fixture(
3894 legacy_populated: bool,
3895 canonical_populated: bool,
3896 index_legacy: bool,
3897 ) -> DuplicateSystemCatalogFixture {
3898 duplicate_system_catalog_fixture_with_ref(
3899 legacy_populated,
3900 canonical_populated,
3901 index_legacy,
3902 false,
3903 )
3904 }
3905
3906 fn duplicate_system_catalog_fixture_with_ref(
3907 legacy_populated: bool,
3908 canonical_populated: bool,
3909 index_legacy: bool,
3910 cross_reference_legacy: bool,
3911 ) -> DuplicateSystemCatalogFixture {
3912 duplicate_system_catalog_fixture_with_key(
3913 legacy_populated,
3914 canonical_populated,
3915 index_legacy,
3916 cross_reference_legacy,
3917 None,
3918 )
3919 }
3920
3921 fn config_recovery_row(id: u64, key: &str, value: i64) -> UnifiedEntity {
3922 UnifiedEntity::new(
3923 EntityId::new(id),
3924 EntityKind::TableRow {
3925 table: Arc::from("red.config"),
3926 row_id: id,
3927 },
3928 EntityData::Row(crate::storage::RowData::with_names(
3929 vec![Value::text(key), Value::Integer(value)],
3930 vec!["key".into(), "value".into()],
3931 )),
3932 )
3933 }
3934
3935 fn duplicate_system_catalog_fixture_with_key(
3936 legacy_populated: bool,
3937 canonical_populated: bool,
3938 index_legacy: bool,
3939 cross_reference_legacy: bool,
3940 config_key: Option<&str>,
3941 ) -> DuplicateSystemCatalogFixture {
3942 duplicate_system_catalog_fixture_with_aliases(
3943 legacy_populated,
3944 canonical_populated,
3945 index_legacy,
3946 cross_reference_legacy,
3947 config_key,
3948 None,
3949 )
3950 }
3951
3952 fn duplicate_system_catalog_fixture_with_aliases(
3953 legacy_populated: bool,
3954 canonical_populated: bool,
3955 index_legacy: bool,
3956 cross_reference_legacy: bool,
3957 config_key: Option<&str>,
3958 alias_case: Option<&str>,
3959 ) -> DuplicateSystemCatalogFixture {
3960 let directory = tempfile::tempdir().expect("temporary store");
3961 let path = directory.path().join("data.rdb");
3962 let config = UnifiedStoreConfig {
3963 page_cache_slots: Some(32),
3964 resident_memory_bytes_max: Some(2 * 1024 * 1024),
3965 ..UnifiedStoreConfig::default()
3966 };
3967 let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3968 codec_version: 1,
3969 definition: [73; 32],
3970 };
3971 let (legacy_root, legacy_key_root, legacy_secondary_root);
3972 {
3973 let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3974 store
3975 .create_collection("cfg_legacy")
3976 .expect("legacy source collection");
3977 if legacy_populated {
3978 store
3979 .insert_auto(
3980 "cfg_legacy",
3981 config_key.map_or_else(
3982 || {
3983 UnifiedEntity::table_row(
3984 EntityId::new(1),
3985 "red_config",
3986 1,
3987 vec![Value::text("legacy")],
3988 )
3989 },
3990 |key| config_recovery_row(1, key, 1),
3991 ),
3992 )
3993 .expect("legacy row");
3994 }
3995 store
3996 .create_collection("cfg.canon_")
3997 .expect("canonical source collection");
3998 if canonical_populated {
3999 store
4000 .insert_auto(
4001 "cfg.canon_",
4002 config_key.map_or_else(
4003 || {
4004 UnifiedEntity::table_row(
4005 EntityId::new(2),
4006 "red.config",
4007 2,
4008 vec![Value::text("canonical")],
4009 )
4010 },
4011 |key| config_recovery_row(2, key, 2),
4012 ),
4013 )
4014 .expect("canonical row");
4015 }
4016 if cross_reference_legacy {
4017 store
4018 .add_cross_ref(
4019 "cfg.canon_",
4020 EntityId::new(2),
4021 "cfg_legacy",
4022 EntityId::new(1),
4023 RefType::RowToNode,
4024 1.0,
4025 )
4026 .expect("reference to legacy config");
4027 }
4028 if index_legacy {
4029 store
4030 .register_secondary_index(
4031 "cfg_legacy",
4032 "by_id",
4033 index_identity.clone(),
4034 Arc::new(SystemNameProjection),
4035 )
4036 .expect("legacy secondary index");
4037 }
4038
4039 if let Some(case) = alias_case {
4040 use crate::storage::{EntityData, RowData};
4041 let pairs = [
4042 (
4043 "old_stats",
4044 "new.query.stats_catalog",
4045 "red_stats",
4046 "red.query.stats_catalog",
4047 ),
4048 (
4049 "old_index_registry",
4050 "new.index.registry",
4051 "red_index_registry",
4052 "red.index.registry",
4053 ),
4054 (
4055 "old_queue_meta",
4056 "new.queue.meta",
4057 "red_queue_meta",
4058 "red.queue.meta",
4059 ),
4060 (
4061 "old_materialized_view_defs",
4062 "new.materialized_view.definitions",
4063 "red_materialized_view_defs",
4064 "red.materialized_view.definitions",
4065 ),
4066 ];
4067 for (pair_index, (old, new, legacy, canonical)) in pairs.iter().enumerate() {
4068 for (is_old, name, table) in [(true, *old, *legacy), (false, *new, *canonical)]
4069 {
4070 store.create_collection(name).unwrap();
4071 let text = |s: &str| Value::text(s);
4072 let mut fields = match pair_index {
4073 0 => vec![
4074 ("kind", text("table")),
4075 ("table", text("users")),
4076 ("row_count", Value::Integer(if is_old { 1 } else { 2 })),
4077 ("avg_row_size", Value::Integer(20)),
4078 ("page_count", Value::Integer(1)),
4079 ],
4080 1 => vec![
4081 ("collection", text("users")),
4082 (
4083 "name",
4084 text(if is_old {
4085 "legacy_only"
4086 } else {
4087 "canonical_only"
4088 }),
4089 ),
4090 ("columns", text("id")),
4091 ("method", text("hash")),
4092 ("resolution", Value::Integer(0)),
4093 ("unique", Value::Boolean(false)),
4094 ("dropped", Value::Boolean(false)),
4095 ],
4096 2 => vec![
4097 ("kind", text("queue_group")),
4098 ("queue", text("jobs")),
4099 ("group", text("workers")),
4100 (
4101 "created_at_ns",
4102 Value::UnsignedInteger(if is_old { 1 } else { 2 }),
4103 ),
4104 ],
4105 _ => vec![
4106 ("name", text("view")),
4107 ("refresh_every_ms", Value::UnsignedInteger(1000)),
4108 ("retention_duration_ms", Value::UnsignedInteger(0)),
4109 ("source_collections", text("users")),
4110 (
4111 "source_sql",
4112 text(if case == "view-conflict" && !is_old {
4113 "SELECT 2"
4114 } else {
4115 "SELECT 1"
4116 }),
4117 ),
4118 ],
4119 };
4120 if case == "queue-delivery" && pair_index == 2 && is_old {
4121 fields[0].1 = text("queue_pending");
4122 }
4123 let id = if is_old {
4124 10000 + pair_index as u64
4125 } else {
4126 100 + pair_index as u64
4127 };
4128 let entity = UnifiedEntity::new(
4129 EntityId::new(id),
4130 EntityKind::TableRow {
4131 table: Arc::from(table),
4132 row_id: id,
4133 },
4134 EntityData::Row(RowData {
4135 columns: vec![],
4136 named: Some(
4137 fields.into_iter().map(|(k, v)| (k.to_owned(), v)).collect(),
4138 ),
4139 schema: None,
4140 }),
4141 );
4142 store.insert_auto(name, entity).unwrap();
4143 }
4144 }
4145 }
4146 store.persist().expect("checkpoint duplicate catalog");
4147 legacy_root = store.collection_root_page("cfg_legacy");
4148 legacy_key_root = store
4149 .key_indices
4150 .read()
4151 .get("cfg_legacy")
4152 .map(|index| index.tree.root_page_id());
4153 legacy_secondary_root = index_legacy.then(|| {
4154 store
4155 .secondary_index("cfg_legacy", "by_id", &index_identity)
4156 .expect("legacy secondary root")
4157 .root_page_id()
4158 });
4159 }
4160
4161 {
4166 let pager = Pager::open(
4167 &path,
4168 PagerConfig {
4169 disk_residency: true,
4170 ..PagerConfig::default()
4171 },
4172 )
4173 .expect("open pager");
4174 let mut page = pager.read_page(1).expect("catalog page");
4175 let bytes = page.as_bytes_mut();
4176 let mut names = vec![("cfg_legacy", "red_config"), ("cfg.canon_", "red.config")];
4177 if alias_case.is_some() {
4178 names.extend([
4179 ("old_stats", "red_stats"),
4180 ("new.query.stats_catalog", "red.query.stats_catalog"),
4181 ("old_index_registry", "red_index_registry"),
4182 ("new.index.registry", "red.index.registry"),
4183 ("old_queue_meta", "red_queue_meta"),
4184 ("new.queue.meta", "red.queue.meta"),
4185 ("old_materialized_view_defs", "red_materialized_view_defs"),
4186 (
4187 "new.materialized_view.definitions",
4188 "red.materialized_view.definitions",
4189 ),
4190 ]);
4191 }
4192 for (source, destination) in names {
4193 let (source, destination) = (source.as_bytes(), destination.as_bytes());
4194 let mut replacements = 0;
4195 for offset in 0..=bytes.len() - source.len() {
4196 if &bytes[offset..offset + source.len()] == source {
4197 bytes[offset..offset + destination.len()].copy_from_slice(destination);
4198 replacements += 1;
4199 }
4200 }
4201 assert!(replacements >= 1, "catalog must contain the source name");
4202 }
4203 pager.write_page(1, page).expect("write duplicate catalog");
4204 pager.sync().expect("sync duplicate catalog");
4205 if alias_case.is_some() {
4206 let mut metadata = read_meta_payload(&pager).unwrap().unwrap();
4207 let resume = reddb_file::native_resume::decode_catalog_resume(&metadata)
4208 .unwrap()
4209 .unwrap();
4210 let trailer = metadata
4211 .windows(4)
4212 .rposition(|bytes| bytes == b"RDRS")
4213 .unwrap();
4214 metadata.truncate(trailer);
4215 reddb_file::native_resume::encode_catalog_resume(&mut metadata, &resume).unwrap();
4216 publish_paged_metadata(&pager, &metadata).unwrap();
4217 pager.sync().unwrap();
4218 }
4219 }
4220 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
4221 .rename_collections(&[("cfg_legacy", "red_config"), ("cfg.canon_", "red.config")])
4222 .expect("publish duplicate operational names");
4223
4224 if alias_case.is_some() {
4225 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
4226 .rename_collections(&[
4227 ("old_stats", "red_stats"),
4228 ("new.query.stats_catalog", "red.query.stats_catalog"),
4229 ("old_index_registry", "red_index_registry"),
4230 ("new.index.registry", "red.index.registry"),
4231 ("old_queue_meta", "red_queue_meta"),
4232 ("new.queue.meta", "red.queue.meta"),
4233 ("old_materialized_view_defs", "red_materialized_view_defs"),
4234 (
4235 "new.materialized_view.definitions",
4236 "red.materialized_view.definitions",
4237 ),
4238 ])
4239 .unwrap();
4240 }
4241
4242 let pages_before = Pager::open(
4243 &path,
4244 PagerConfig {
4245 disk_residency: true,
4246 ..PagerConfig::default()
4247 },
4248 )
4249 .expect("inspect pager")
4250 .page_count()
4251 .expect("page count");
4252 DuplicateSystemCatalogFixture {
4253 _directory: directory,
4254 path,
4255 config,
4256 pages_before,
4257 legacy_root,
4258 legacy_key_root,
4259 legacy_secondary_root,
4260 index_identity,
4261 }
4262 }
4263
4264 fn remove_fixture_catalog_resume(fixture: &DuplicateSystemCatalogFixture) {
4265 let pager = Pager::open(
4266 &fixture.path,
4267 PagerConfig {
4268 disk_residency: true,
4269 ..PagerConfig::default()
4270 },
4271 )
4272 .expect("fixture pager");
4273 let mut metadata = read_meta_payload(&pager).unwrap().unwrap();
4274 assert!(reddb_file::native_resume::decode_catalog_resume(&metadata)
4275 .unwrap()
4276 .is_some());
4277 let trailer = metadata
4278 .windows(4)
4279 .rposition(|bytes| bytes == b"RDRS")
4280 .unwrap();
4281 metadata.truncate(trailer);
4282 publish_paged_metadata(&pager, &metadata).unwrap();
4283 pager.sync().unwrap();
4284 assert!(reddb_file::native_resume::decode_catalog_resume(
4285 &read_meta_payload(&pager).unwrap().unwrap()
4286 )
4287 .unwrap()
4288 .is_none());
4289 }
4290
4291 #[test]
4292 fn no_resume_catalog_keeps_populated_config_beside_zero_root_alias() {
4293 for (legacy_populated, canonical_populated) in
4294 [(true, false), (false, true), (false, false)]
4295 {
4296 let fixture =
4297 duplicate_system_catalog_fixture(legacy_populated, canonical_populated, false);
4298 remove_fixture_catalog_resume(&fixture);
4299 let store = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
4300 .expect("resolve physically empty alias without resume counters");
4301 assert_eq!(store.list_collections(), vec!["red.config".to_owned()]);
4302 assert_eq!(
4303 store.get("red.config", EntityId::new(1)).is_some(),
4304 legacy_populated
4305 );
4306 assert_eq!(
4307 store.get("red.config", EntityId::new(2)).is_some(),
4308 canonical_populated
4309 );
4310 if legacy_populated {
4311 assert_eq!(
4312 store.collection_root_page("red.config"),
4313 fixture.legacy_root
4314 );
4315 }
4316 store.persist().expect("checkpoint canonical catalog");
4317 drop(store);
4318 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4319 .expect("cold reopen canonical catalog");
4320 assert_eq!(cold.list_collections(), vec!["red.config".to_owned()]);
4321 assert_eq!(
4322 cold.get("red.config", EntityId::new(1)).is_some(),
4323 legacy_populated
4324 );
4325 assert_eq!(
4326 cold.get("red.config", EntityId::new(2)).is_some(),
4327 canonical_populated
4328 );
4329 }
4330 }
4331
4332 #[test]
4333 fn no_resume_catalog_refuses_two_populated_config_roots() {
4334 let fixture = duplicate_system_catalog_fixture(true, true, false);
4335 remove_fixture_catalog_resume(&fixture);
4336 let error = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4337 .err()
4338 .expect("two populated roots require reconciliation");
4339 assert!(error.to_string().contains("both legacy collection"));
4340 }
4341
4342 fn alias_duplicate_system_catalog_state_with(
4343 fixture: &DuplicateSystemCatalogFixture,
4344 mutate_canonical: impl FnOnce(&mut reddb_file::native_resume::CollectionResume),
4345 ) {
4346 let pager = Pager::open(
4347 &fixture.path,
4348 PagerConfig {
4349 disk_residency: true,
4350 ..PagerConfig::default()
4351 },
4352 )
4353 .expect("open duplicate catalog pager");
4354 let mut metadata = read_meta_payload(&pager)
4355 .expect("read duplicate catalog")
4356 .expect("published duplicate catalog");
4357 let mut resume = reddb_file::native_resume::decode_catalog_resume(&metadata)
4358 .expect("decode duplicate resume")
4359 .expect("prepared duplicate resume");
4360 let legacy_state = resume
4361 .collections
4362 .get("red_config")
4363 .expect("legacy resume state")
4364 .clone();
4365 let mut canonical_state = legacy_state.clone();
4366 mutate_canonical(&mut canonical_state);
4367 resume
4368 .collections
4369 .insert("red.config".to_string(), canonical_state);
4370
4371 let header = reddb_file::decode_native_paged_metadata_header(&metadata)
4372 .expect("decode metadata header")
4373 .expect("native metadata header");
4374 let mut position = reddb_file::METADATA_HEADER_BYTES;
4375 let mut replaced_root = false;
4376 for _ in 0..header.collection_count {
4377 let root = reddb_file::decode_native_paged_collection_root(&metadata, &mut position)
4378 .expect("decode collection root");
4379 if root.collection == "red.config" {
4380 metadata[position - 4..position]
4381 .copy_from_slice(&legacy_state.root_page.to_le_bytes());
4382 replaced_root = true;
4383 }
4384 }
4385 assert!(replaced_root, "canonical catalog root must be present");
4386
4387 let trailer = metadata
4388 .windows(4)
4389 .rposition(|bytes| bytes == b"RDRS")
4390 .expect("catalog resume trailer");
4391 metadata.truncate(trailer);
4392 reddb_file::native_resume::encode_catalog_resume(&mut metadata, &resume)
4393 .expect("encode aliased resume");
4394 publish_paged_metadata(&pager, &metadata).expect("publish aliased catalog");
4395 }
4396
4397 fn alias_duplicate_system_catalog_state(fixture: &DuplicateSystemCatalogFixture) {
4398 alias_duplicate_system_catalog_state_with(fixture, |_| {});
4399 }
4400
4401 #[test]
4402 fn populated_legacy_system_collection_replaces_empty_canonical_duplicate() {
4403 let fixture = duplicate_system_catalog_fixture(true, false, true);
4404 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
4405 .expect("resolve empty duplicate");
4406 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4407 assert_eq!(
4408 recovered
4409 .get("red.config", EntityId::new(1))
4410 .expect("preserved legacy row")
4411 .kind
4412 .collection(),
4413 "red.config"
4414 );
4415 assert_eq!(
4416 recovered.collection_root_page("red.config"),
4417 fixture.legacy_root,
4418 "the populated data root must be reused"
4419 );
4420 assert_eq!(
4421 recovered
4422 .key_indices
4423 .read()
4424 .get("red.config")
4425 .map(|index| index.tree.root_page_id()),
4426 fixture.legacy_key_root,
4427 "the populated key-index root must be reused"
4428 );
4429 assert_eq!(
4430 recovered
4431 .secondary_index("red.config", "by_id", &fixture.index_identity)
4432 .expect("surviving secondary index")
4433 .root_page_id(),
4434 fixture
4435 .legacy_secondary_root
4436 .expect("fixture secondary root")
4437 );
4438 drop(recovered);
4439
4440 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4441 .expect("idempotent cold boot");
4442 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
4443 assert_eq!(
4444 cold.pager()
4445 .expect("pager")
4446 .page_count()
4447 .expect("page count"),
4448 fixture.pages_before,
4449 "resolving an empty duplicate must not extend the file"
4450 );
4451 }
4452
4453 #[test]
4454 fn populated_canonical_system_collection_replaces_empty_legacy_duplicate() {
4455 let fixture = duplicate_system_catalog_fixture(false, true, false);
4456 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4457 .expect("retain populated canonical duplicate");
4458 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4459 assert_eq!(
4460 recovered
4461 .get("red.config", EntityId::new(2))
4462 .expect("preserved canonical row")
4463 .kind
4464 .collection(),
4465 "red.config"
4466 );
4467 assert!(recovered.get("red.config", EntityId::new(1)).is_none());
4468 }
4469
4470 #[test]
4471 fn empty_canonical_system_collection_replaces_empty_legacy_duplicate() {
4472 let fixture = duplicate_system_catalog_fixture(false, false, false);
4473 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4474 .expect("prefer canonical empty duplicate");
4475 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4476 assert_eq!(
4477 recovered
4478 .get_collection("red.config")
4479 .expect("canonical collection")
4480 .count(),
4481 0
4482 );
4483 }
4484
4485 #[test]
4486 fn populated_system_collection_duplicates_still_fail_closed() {
4487 let fixture = duplicate_system_catalog_fixture(true, true, false);
4488 let error = match UnifiedStore::open_with_config(&fixture.path, fixture.config) {
4489 Ok(_) => panic!("two populated identities are ambiguous"),
4490 Err(error) => error,
4491 };
4492 assert!(
4493 error
4494 .to_string()
4495 .contains("both populated legacy collection 'red_config' and canonical collection 'red.config'"),
4496 "unexpected error: {error}"
4497 );
4498 assert!(
4499 error.to_string().contains(
4500 "data_root_match=false, key_index_root_match=true, secondary_index_state_match=true"
4501 ),
4502 "the error must identify which physical state diverged without exposing page IDs: {error}"
4503 );
4504 }
4505
4506 #[test]
4507 fn explicit_compaction_retires_populated_legacy_system_config() {
4508 let fixture = duplicate_system_catalog_fixture(true, true, false);
4509 let destination = fixture._directory.path().join("canonical-system-config");
4510
4511 let report = crate::storage::compact::compact_paged_canonical_system_config(
4512 &fixture.path,
4513 &destination,
4514 )
4515 .expect("rewrite duplicate system config");
4516 assert!(report.records > 0);
4517
4518 let rewritten_path = destination.join("data.rdb");
4519 let rewritten = UnifiedStore::open_with_config(&rewritten_path, fixture.config.clone())
4520 .expect("open canonical rewrite");
4521 assert_eq!(rewritten.list_collections(), vec!["red.config".to_string()]);
4522 assert!(
4523 rewritten.get("red.config", EntityId::new(1)).is_none(),
4524 "the obsolete legacy tree must not leak into the canonical collection"
4525 );
4526 assert_eq!(
4527 rewritten
4528 .get("red.config", EntityId::new(2))
4529 .expect("canonical row survives")
4530 .kind
4531 .collection(),
4532 "red.config"
4533 );
4534 drop(rewritten);
4535
4536 let cold = UnifiedStore::open_with_config(&rewritten_path, fixture.config)
4537 .expect("cold reopen canonical rewrite");
4538 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
4539 assert_eq!(
4540 cold.get_collection("red.config")
4541 .expect("canonical collection")
4542 .count(),
4543 1
4544 );
4545 assert!(
4546 crate::storage::operational_manifest::OperationalManifest::for_db_path(&rewritten_path)
4547 .recover_or_bootstrap(&["red.config".to_string()])
4548 .expect("canonical operational manifest")
4549 .is_empty()
4550 );
4551 }
4552
4553 #[test]
4554 fn canonical_system_config_recovery_replays_pending_unrelated_wal() {
4555 use crate::storage::wal::{WalRecord, WalWriter};
4556
4557 let fixture = duplicate_system_catalog_fixture(true, true, true);
4558 let wal = reddb_file::unified_wal_path(&fixture.path);
4559 let action = reddb_file::encode_store_wal_action_frame(
4560 &reddb_file::StoreWalActionFrame::CreateCollection {
4561 name: "pending_collection".into(),
4562 },
4563 )
4564 .expect("encode pending action");
4565 let canonical_update = reddb_file::encode_store_wal_action_frame(
4566 &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
4567 collection: "red.config".into(),
4568 record: UnifiedStore::serialize_entity_record(
4569 &UnifiedEntity::table_row(
4570 EntityId::new(3),
4571 "red.config",
4572 3,
4573 vec![Value::text("canonical WAL")],
4574 ),
4575 None,
4576 STORE_VERSION_CURRENT,
4577 ),
4578 },
4579 )
4580 .expect("encode canonical update");
4581 {
4582 let mut writer = WalWriter::open(&wal).expect("open WAL");
4583 writer
4584 .append(&WalRecord::TxCommitBatch {
4585 tx_id: 999,
4586 actions: vec![action, canonical_update],
4587 })
4588 .expect("append committed WAL");
4589 writer.sync().expect("sync WAL");
4590 }
4591 let original = std::fs::read(&fixture.path).expect("source bytes");
4592 let original_wal = std::fs::read(&wal).expect("WAL bytes");
4593 let destination = fixture._directory.path().join("recovered-canonical-config");
4594
4595 let default_destination = fixture._directory.path().join("default-recovery");
4596 let error = crate::storage::compact::recover_paged_wal(&fixture.path, &default_destination)
4597 .expect_err("ordinary recovery must reject divergent identities");
4598 assert!(error
4599 .to_string()
4600 .contains("both populated legacy collection"));
4601
4602 crate::storage::compact::recover_paged_wal_canonical_system_config(
4603 &fixture.path,
4604 &destination,
4605 )
4606 .expect("recover pending WAL and retain canonical config");
4607
4608 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4609 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4610 for _ in 0..2 {
4611 let recovered = UnifiedStore::open_with_config(
4612 destination.join("data.rdb"),
4613 fixture.config.clone(),
4614 )
4615 .expect("open recovered store");
4616 assert!(recovered.get_collection("pending_collection").is_some());
4617 assert!(recovered.get("red.config", EntityId::new(1)).is_none());
4618 assert!(recovered.get("red.config", EntityId::new(2)).is_some());
4619 assert!(recovered.get("red.config", EntityId::new(3)).is_some());
4620 }
4621 }
4622
4623 #[test]
4624 fn canonical_system_catalog_recovery_reconciles_all_metadata_and_keeps_missing_index() {
4625 use crate::storage::wal::{WalRecord, WalWriter};
4626 for case in [
4627 "valid",
4628 "view-conflict",
4629 "queue-delivery",
4630 "legacy-wal",
4631 "canonical-wal",
4632 ] {
4633 let fixture = duplicate_system_catalog_fixture_with_aliases(
4634 true,
4635 true,
4636 true,
4637 false,
4638 Some("system.pid"),
4639 Some(case),
4640 );
4641 let wal = reddb_file::unified_wal_path(&fixture.path);
4642 {
4643 let mut writer = WalWriter::open(&wal).unwrap();
4644 let name = match case {
4645 "legacy-wal" => "red_index_registry",
4646 "canonical-wal" => "red.index.registry",
4647 _ => "durable_rows",
4648 };
4649 writer
4650 .append(&WalRecord::TxCommitBatch {
4651 tx_id: 1001,
4652 actions: vec![reddb_file::encode_store_wal_action_frame(
4653 &reddb_file::StoreWalActionFrame::CreateCollection {
4654 name: name.into(),
4655 },
4656 )
4657 .unwrap()],
4658 })
4659 .unwrap();
4660 writer.sync().unwrap();
4661 }
4662 let original = std::fs::read(&fixture.path).unwrap();
4663 let original_wal = std::fs::read(&wal).unwrap();
4664 let baseline = UnifiedStore::open_with_config_for_recovery(
4665 &fixture.path,
4666 fixture.config.clone(),
4667 true,
4668 None,
4669 None,
4670 )
4671 .err()
4672 .expect("unproved aliases must still fail");
4673 assert!(baseline.to_string().contains("red_stats"), "{baseline}");
4674 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4675 let destination = fixture._directory.path().join("catalog-recovered");
4676 let result = crate::storage::compact::recover_paged_wal_canonical_system_config(
4677 &fixture.path,
4678 &destination,
4679 );
4680 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4681 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4682 if case != "valid" {
4683 let error = result.expect_err(case).to_string();
4684 let expected = match case {
4685 "view-conflict" => "conflicting canonical definition",
4686 "queue-delivery" => "unsupported legacy metadata",
4687 _ => "WAL changes metadata",
4688 };
4689 assert!(error.contains(expected), "{case}: {error}");
4690 assert!(!destination.join("data.rdb").exists());
4691 continue;
4692 }
4693 let report = result.expect("recover all supported aliases together");
4694 assert_eq!(report.reconciled_system_collections, 4);
4695 assert_eq!(report.imported_system_records, 1);
4696 for _ in 0..2 {
4697 let store = UnifiedStore::open_with_config(
4698 destination.join("data.rdb"),
4699 fixture.config.clone(),
4700 )
4701 .unwrap();
4702 assert!(store.get_collection("durable_rows").is_some());
4703 let mut imported = Vec::new();
4704 store
4705 .get_collection("red.index.registry")
4706 .unwrap()
4707 .try_for_each_entity(4 * 1024 * 1024, |entity| {
4708 if entity.data.as_row().and_then(|r| r.get_field("name"))
4709 == Some(&Value::text("legacy_only"))
4710 {
4711 imported.push(entity.clone());
4712 }
4713 true
4714 })
4715 .unwrap();
4716 assert_eq!(imported.len(), 1, "legacy-only index must survive");
4717 assert!(
4718 imported[0].id.raw() > 10003,
4719 "all retired identities must be reserved first"
4720 );
4721 assert!(
4722 matches!(imported[0].kind, EntityKind::TableRow{row_id,..} if row_id>10001)
4723 );
4724 assert_eq!(imported[0].logical_id(), imported[0].id);
4725 assert!(store
4726 .get("red.index.registry", EntityId::new(10001))
4727 .is_none());
4728 assert!(store
4729 .get("red.index.registry", EntityId::new(101))
4730 .is_some());
4731 assert!(store
4732 .get("red.query.stats_catalog", EntityId::new(100))
4733 .is_some());
4734 assert!(store
4735 .get("red.query.stats_catalog", EntityId::new(10000))
4736 .is_none());
4737 assert_eq!(store.get_collection("red.queue.meta").unwrap().count(), 1);
4738 assert_eq!(
4739 store
4740 .get_collection("red.materialized_view.definitions")
4741 .unwrap()
4742 .count(),
4743 1
4744 );
4745 assert!(
4746 store
4747 .get_collection("red.index.registry")
4748 .unwrap()
4749 .identity_counters()
4750 .0
4751 > 10001
4752 );
4753 }
4754 }
4755 }
4756
4757 #[test]
4758 fn canonical_system_config_recovery_retires_boot_wal_without_losing_other_actions() {
4759 use crate::storage::wal::{WalRecord, WalWriter};
4760 use reddb_file::StoreWalActionFrame as Action;
4761 for pending_dwb in [false, true] {
4762 let fixture = duplicate_system_catalog_fixture_with_key(
4763 true,
4764 true,
4765 true,
4766 false,
4767 Some("system.pid"),
4768 );
4769 let encode = |action| reddb_file::encode_store_wal_action_frame(&action).unwrap();
4770 let upsert = |id, value| {
4771 encode(Action::UpsertEntityRecord {
4772 collection: "red_config".into(),
4773 record: UnifiedStore::serialize_entity_record(
4774 &config_recovery_row(id, "system.pid", value),
4775 None,
4776 STORE_VERSION_CURRENT,
4777 ),
4778 })
4779 };
4780 let delete = |id| {
4781 encode(Action::DeleteEntityRecord {
4782 collection: "red_config".into(),
4783 entity_id: id,
4784 })
4785 };
4786 let wal = reddb_file::unified_wal_path(&fixture.path);
4787 {
4788 let mut writer = WalWriter::open(&wal).unwrap();
4789 writer
4790 .append(&WalRecord::TxCommitBatch {
4791 tx_id: 1001,
4792 actions: vec![
4793 upsert(10000, 3),
4794 delete(1),
4795 encode(Action::CreateCollection {
4796 name: "durable_rows".into(),
4797 }),
4798 ],
4799 })
4800 .unwrap();
4801 writer
4802 .append(&WalRecord::TxCommitBatch {
4803 tx_id: 1002,
4804 actions: vec![
4805 upsert(10001, 4),
4806 delete(10000),
4807 encode(Action::UpsertEntityRecord {
4808 collection: "durable_rows".into(),
4809 record: UnifiedStore::serialize_entity_record(
4810 &UnifiedEntity::table_row(
4811 EntityId::new(3),
4812 "durable_rows",
4813 1,
4814 vec![Value::text("must survive")],
4815 ),
4816 None,
4817 STORE_VERSION_CURRENT,
4818 ),
4819 }),
4820 ],
4821 })
4822 .unwrap();
4823 writer.sync().unwrap();
4824 }
4825 if pending_dwb {
4826 use std::io::{Seek, SeekFrom, Write};
4827 let bytes = std::fs::read(&fixture.path).unwrap();
4828 let root = fixture.legacy_root.unwrap();
4829 let offset = root as usize * PAGE_SIZE;
4830 let page: &[u8; PAGE_SIZE] = bytes[offset..offset + PAGE_SIZE].try_into().unwrap();
4831 let frame = reddb_file::encode_paged_dwb_frame([(root, page)]);
4832 let mut file = std::fs::OpenOptions::new()
4833 .write(true)
4834 .open(&fixture.path)
4835 .unwrap();
4836 file.seek(SeekFrom::Start(3 * PAGE_SIZE as u64)).unwrap();
4837 file.write_all(&frame).unwrap();
4838 file.seek(SeekFrom::Start(offset as u64)).unwrap();
4841 file.write_all(&[0u8; PAGE_SIZE]).unwrap();
4842 file.sync_all().unwrap();
4843 }
4844 let original = std::fs::read(&fixture.path).unwrap();
4845 let original_wal = std::fs::read(&wal).unwrap();
4846 let destination = fixture._directory.path().join("boot-config-recovered");
4847 let report = crate::storage::compact::recover_paged_wal_canonical_system_config(
4848 &fixture.path,
4849 &destination,
4850 )
4851 .expect("retire only proved boot metadata actions on the isolated copy");
4852 assert_eq!(report.retired_boot_system_wal_actions, 4);
4853 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4854 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4855 for _ in 0..2 {
4856 let recovered = UnifiedStore::open_with_config(
4857 destination.join("data.rdb"),
4858 fixture.config.clone(),
4859 )
4860 .unwrap();
4861 let manager = recovered.get_collection("red.config").unwrap();
4862 assert!(manager.identity_counters().0 > 10001);
4863 assert!(manager.identity_counters().1 > 10001);
4864 assert_eq!(recovered.get_config("system.pid"), Some(Value::Integer(2)));
4865 assert!(recovered.get("red.config", EntityId::new(1)).is_none());
4866 assert!(recovered.get("red.config", EntityId::new(10000)).is_none());
4867 assert!(recovered.get("red.config", EntityId::new(10001)).is_none());
4868 assert_eq!(
4869 recovered
4870 .get("durable_rows", EntityId::new(3))
4871 .unwrap()
4872 .data
4873 .as_row()
4874 .unwrap()
4875 .columns,
4876 vec![Value::text("must survive")]
4877 );
4878 }
4879 }
4880 }
4881
4882 #[test]
4883 fn canonical_system_config_recovery_requires_stable_boot_identity_evidence() {
4884 use crate::storage::wal::{WalRecord, WalWriter};
4885 let fixture =
4886 duplicate_system_catalog_fixture_with_key(true, true, false, false, Some("system.pid"));
4887 let original = std::fs::read(&fixture.path).unwrap();
4888 let wal = reddb_file::unified_wal_path(&fixture.path);
4889 {
4890 let mut writer = WalWriter::open(&wal).unwrap();
4891 writer
4892 .append(&WalRecord::TxCommitBatch {
4893 tx_id: 1004,
4894 actions: vec![reddb_file::encode_store_wal_action_frame(
4895 &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
4896 collection: "red_config".into(),
4897 entity_id: 1,
4898 },
4899 )
4900 .unwrap()],
4901 })
4902 .unwrap();
4903 writer
4904 .append(&WalRecord::FullPageImage {
4905 tx_id: 1004,
4906 page_id: 1,
4907 ckpt_epoch: 1,
4908 data: original[PAGE_SIZE..2 * PAGE_SIZE].to_vec(),
4909 })
4910 .unwrap();
4911 writer.sync().unwrap();
4912 }
4913 let original_wal = std::fs::read(&wal).unwrap();
4914 let destination = fixture._directory.path().join("unproved-checkpoint");
4915 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
4916 &fixture.path,
4917 &destination,
4918 )
4919 .expect_err("undo images must be recovered before proving legacy identities");
4920 assert!(error
4921 .to_string()
4922 .contains("requires a stable checkpoint image"));
4923 assert!(!destination.exists());
4924 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4925 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4926 }
4927
4928 #[test]
4929 fn canonical_system_config_recovery_rejects_unproved_boot_wal() {
4930 use crate::storage::wal::{WalRecord, WalWriter};
4931 use reddb_file::StoreWalActionFrame as Action;
4932 for (case, checkpoint_key, wal_key, delete_id) in [
4933 ("unknown-delete", "system.pid", "system.pid", Some(90000)),
4934 (
4935 "user-key",
4936 "system.customer_setting",
4937 "system.customer_setting",
4938 None,
4939 ),
4940 (
4941 "identity-reuse",
4942 "red.config.customer_setting",
4943 "system.pid",
4944 None,
4945 ),
4946 ] {
4947 let fixture = duplicate_system_catalog_fixture_with_key(
4948 true,
4949 true,
4950 false,
4951 false,
4952 Some(checkpoint_key),
4953 );
4954 let action = if let Some(entity_id) = delete_id {
4955 Action::DeleteEntityRecord {
4956 collection: "red_config".into(),
4957 entity_id,
4958 }
4959 } else {
4960 Action::UpsertEntityRecord {
4961 collection: "red_config".into(),
4962 record: UnifiedStore::serialize_entity_record(
4963 &config_recovery_row(1, wal_key, 99),
4964 None,
4965 STORE_VERSION_CURRENT,
4966 ),
4967 }
4968 };
4969 let wal = reddb_file::unified_wal_path(&fixture.path);
4970 {
4971 let mut writer = WalWriter::open(&wal).unwrap();
4972 writer
4973 .append(&WalRecord::TxCommitBatch {
4974 tx_id: 1003,
4975 actions: vec![reddb_file::encode_store_wal_action_frame(&action).unwrap()],
4976 })
4977 .unwrap();
4978 writer.sync().unwrap();
4979 }
4980 let original = std::fs::read(&fixture.path).unwrap();
4981 let original_wal = std::fs::read(&wal).unwrap();
4982 let destination = fixture._directory.path().join(case);
4983 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
4984 &fixture.path,
4985 &destination,
4986 )
4987 .expect_err(case);
4988 assert!(
4989 error
4990 .to_string()
4991 .contains("WAL references divergent legacy red_config"),
4992 "{case}: {error}"
4993 );
4994 assert!(!destination.exists());
4995 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4996 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4997 }
4998 }
4999
5000 #[test]
5001 fn canonical_system_config_recovery_rejects_legacy_wal_actions() {
5002 use crate::storage::wal::{WalRecord, WalWriter};
5003
5004 let fixture = duplicate_system_catalog_fixture(true, true, false);
5005 let wal = reddb_file::unified_wal_path(&fixture.path);
5006 let action = reddb_file::encode_store_wal_action_frame(
5007 &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
5008 collection: "red_config".into(),
5009 entity_id: 1,
5010 },
5011 )
5012 .unwrap();
5013 {
5014 let mut writer = WalWriter::open(&wal).unwrap();
5015 writer
5016 .append(&WalRecord::TxCommitBatch {
5017 tx_id: 999,
5018 actions: vec![action],
5019 })
5020 .unwrap();
5021 writer.sync().unwrap();
5022 }
5023 let original = std::fs::read(&fixture.path).unwrap();
5024 let original_wal = std::fs::read(&wal).unwrap();
5025 let destination = fixture._directory.path().join("rejected-legacy-wal");
5026 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
5027 &fixture.path,
5028 &destination,
5029 )
5030 .expect_err("legacy WAL must not be replayed against the canonical tree");
5031 assert!(error
5032 .to_string()
5033 .contains("WAL references divergent legacy red_config"));
5034 assert!(!destination.exists());
5035 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
5036 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
5037 }
5038
5039 #[test]
5040 fn canonical_system_config_recovery_rejects_legacy_cross_references() {
5041 use crate::storage::wal::{WalRecord, WalWriter};
5042
5043 let fixture = duplicate_system_catalog_fixture_with_ref(true, true, false, true);
5044 let wal = reddb_file::unified_wal_path(&fixture.path);
5045 {
5046 let mut writer = WalWriter::open(&wal).unwrap();
5047 writer.append(&WalRecord::Begin { tx_id: 999 }).unwrap();
5048 writer.sync().unwrap();
5049 }
5050 let original = std::fs::read(&fixture.path).unwrap();
5051 let original_wal = std::fs::read(&wal).unwrap();
5052 let destination = fixture._directory.path().join("rejected-legacy-reference");
5053 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
5054 &fixture.path,
5055 &destination,
5056 )
5057 .expect_err("legacy references must not be retargeted silently");
5058 assert!(error
5059 .to_string()
5060 .contains("legacy red_config has cross-references"));
5061 assert!(!destination.join("data.rdb").exists());
5062 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
5063 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
5064 }
5065
5066 #[test]
5067 fn populated_system_collection_aliases_reconcile_without_rebuild() {
5068 let fixture = duplicate_system_catalog_fixture(true, false, true);
5069 alias_duplicate_system_catalog_state(&fixture);
5070
5071 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
5072 .expect("reconcile identical populated aliases");
5073 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
5074 assert_eq!(
5075 recovered
5076 .get("red.config", EntityId::new(1))
5077 .expect("preserved aliased row")
5078 .kind
5079 .collection(),
5080 "red.config"
5081 );
5082 assert_eq!(
5083 recovered.collection_root_page("red.config"),
5084 fixture.legacy_root,
5085 "the shared data root must be reused"
5086 );
5087 assert_eq!(
5088 recovered
5089 .key_indices
5090 .read()
5091 .get("red.config")
5092 .map(|index| index.tree.root_page_id()),
5093 fixture.legacy_key_root,
5094 "the shared key-index root must be reused"
5095 );
5096 assert_eq!(
5097 recovered
5098 .secondary_index("red.config", "by_id", &fixture.index_identity)
5099 .expect("shared secondary index")
5100 .root_page_id(),
5101 fixture
5102 .legacy_secondary_root
5103 .expect("fixture secondary root")
5104 );
5105 drop(recovered);
5106
5107 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5108 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
5109 .expect("idempotent cold boot");
5110 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
5111 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5112 assert_eq!(
5113 cold.pager()
5114 .expect("pager")
5115 .page_count()
5116 .expect("page count"),
5117 fixture.pages_before,
5118 "reconciling a physical alias must not extend the file"
5119 );
5120 }
5121
5122 #[test]
5123 fn populated_system_collection_aliases_merge_resume_counter_drift() {
5124 let fixture = duplicate_system_catalog_fixture(true, false, false);
5125 alias_duplicate_system_catalog_state_with(&fixture, |canonical| {
5126 canonical.entities += 7;
5127 canonical.entity_id_next += 100;
5128 canonical.row_id_next += 200;
5129 canonical.sequence_id_next += 300;
5130 });
5131
5132 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
5133 .expect("reconcile shared roots with conservative counters");
5134 let manager = recovered
5135 .get_collection("red.config")
5136 .expect("canonical collection");
5137 assert_eq!(manager.count(), 1, "the shared tree owns the exact count");
5138 assert_eq!(manager.identity_counters(), (102, 202));
5139 assert_eq!(manager.sequence_id_next(), 301);
5140 assert_eq!(
5141 recovered.collection_root_page("red.config"),
5142 fixture.legacy_root,
5143 "counter reconciliation must keep the shared data root"
5144 );
5145 drop(manager);
5146 drop(recovered);
5147
5148 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5149 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
5150 .expect("idempotent cold boot after counter reconciliation");
5151 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
5152 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5153 assert_eq!(
5154 cold.pager()
5155 .expect("pager")
5156 .page_count()
5157 .expect("page count"),
5158 fixture.pages_before,
5159 "counter reconciliation must not extend the file"
5160 );
5161 }
5162
5163 fn row(id: u64, value: i64) -> UnifiedEntity {
5164 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
5165 }
5166
5167 #[test]
5168 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
5169 let directory = tempfile::tempdir().expect("temporary store");
5170 let path = directory.path().join("data.rdb");
5171 let store = UnifiedStore::open(&path).expect("paged store");
5172 store.create_collection("rows").expect("collection");
5173 store.insert("rows", row(1, 1)).expect("seed");
5174 store.persist().expect("initial checkpoint");
5175 let manager = store.get_collection("rows").expect("rows");
5176 manager.update(row(1, 2)).expect("direct update");
5177 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5178 store.persist().expect("updated row checkpoint");
5179 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
5180 let mut metadata = Metadata::new();
5181 metadata.set("verified", MetadataValue::Bool(true));
5182 manager
5183 .set_metadata(EntityId::new(1), metadata)
5184 .expect("metadata");
5185 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5186 store.persist().expect("metadata-only checkpoint");
5187 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
5188 manager
5189 .bulk_insert(vec![row(2, 2), row(3, 3)])
5190 .expect("bulk insert");
5191 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5192 store.persist().expect("bulk insert checkpoint");
5193 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
5194 manager.delete(EntityId::new(2)).expect("delete");
5195 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5196 store.persist().expect("changed checkpoint");
5197 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
5198 drop(store);
5199 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
5200 let manager = recovered.get_collection("rows").expect("rows");
5201 assert_eq!(manager.count(), 2);
5202 assert_eq!(
5203 manager
5204 .get(EntityId::new(1))
5205 .expect("row")
5206 .data
5207 .as_row()
5208 .expect("table")
5209 .columns,
5210 vec![Value::Integer(2)]
5211 );
5212 assert!(manager.get(EntityId::new(2)).is_none());
5213 assert!(manager
5214 .get_metadata(EntityId::new(1))
5215 .expect("metadata")
5216 .get("verified")
5217 .is_some());
5218 }
5219
5220 #[test]
5221 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
5222 let directory = tempfile::tempdir().expect("temporary store");
5223 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
5224 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5225 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5226 store.create_collection("rows").expect("collection");
5227 for id in 1..=40 {
5228 store.insert("rows", row(id, id as i64)).expect("seed");
5229 }
5230 store.persist().expect("initial checkpoint");
5231 let unrelated = snapshots.begin();
5232 snapshots.rollback(unrelated);
5233 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5234 store.persist().expect("unrelated rollback checkpoint");
5235 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5236
5237 store
5238 .pending_vault_versions
5239 .write()
5240 .insert(EntityId::new(100));
5241 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5242 store.persist().expect("unrelated vault checkpoint");
5243 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5244 store.pending_vault_versions.write().clear();
5245 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5246 store
5247 .persist()
5248 .expect("unrelated vault publication checkpoint");
5249 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5250 }
5251
5252 #[test]
5253 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
5254 let directory = tempfile::tempdir().expect("temporary store");
5255 let path = directory.path().join("data.rdb");
5256 let store = UnifiedStore::open(&path).expect("paged store");
5257 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5258 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5259 store.create_collection("rows").expect("collection");
5260 let xid = snapshots.begin();
5261 let mut entity = row(1, 1);
5262 entity.xmin = xid;
5263 store.insert("rows", entity).expect("committed row");
5264 snapshots.commit(xid);
5265 store.persist().expect("committed baseline");
5266 snapshots.rollback(xid);
5267 store
5268 .persist()
5269 .expect("untracked outcome requires validation");
5270 drop(store);
5271 let recovered = UnifiedStore::open(&path).expect("recover");
5272 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
5273 }
5274
5275 #[test]
5276 fn exhausted_deferred_row_cache_keeps_full_validation() {
5277 let directory = tempfile::tempdir().expect("temporary store");
5278 let path = directory.path().join("data.rdb");
5279 let store = UnifiedStore::open(&path).expect("paged store");
5280 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5281 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5282 store.create_collection("rows").expect("collection");
5283 let xid = snapshots.begin();
5284 for id in 1..=1025 {
5285 let mut entity = row(id, id as i64);
5286 entity.xmin = xid;
5287 store.insert("rows", entity).expect("pending row");
5288 }
5289 store.persist().expect("exclude pending rows");
5290 assert!(store.checkpoint_versions.read()["rows"]
5291 .deferred_rows
5292 .is_none());
5293 snapshots.commit(xid);
5294 store
5295 .persist()
5296 .expect("publish committed rows conservatively");
5297 drop(store);
5298 let recovered = UnifiedStore::open(&path).expect("recover");
5299 assert_eq!(
5300 recovered.get_collection("rows").expect("rows").count(),
5301 1025
5302 );
5303 }
5304
5305 #[test]
5306 fn rollback_invalidates_reuse_without_a_segment_mutation() {
5307 let directory = tempfile::tempdir().expect("temporary store");
5308 let path = directory.path().join("data.rdb");
5309 let store = UnifiedStore::open(&path).expect("paged store");
5310 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5311 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5312 store.create_collection("rows").expect("collection");
5313 let xid = snapshots.begin();
5314 let mut pending = row(1, 1);
5315 pending.xmin = xid;
5316 store.insert("rows", pending).expect("version");
5317 store.persist().expect("before rollback");
5318 snapshots.rollback(xid);
5319 store.persist().expect("after rollback");
5320 drop(store);
5321 let recovered = UnifiedStore::open(&path).expect("recover");
5322 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
5323 }
5324
5325 #[test]
5326 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
5327 let directory = tempfile::tempdir().expect("temporary store");
5328 let path = directory.path().join("data.rdb");
5329 let store = UnifiedStore::open(&path).expect("paged store");
5330 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5331 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5332 store.create_collection("rows").expect("collection");
5333 let xid = snapshots.begin();
5334 let mut deleted = row(1, 1);
5335 deleted.xmax = xid;
5336 store.insert("rows", deleted).expect("version");
5337 store.persist().expect("before rollback");
5338 snapshots.rollback(xid);
5339 store.persist().expect("after rollback");
5340 drop(store);
5341 let recovered = UnifiedStore::open(&path).expect("recover");
5342 let manager = recovered.get_collection("rows").expect("rows");
5343 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
5344 }
5345
5346 #[test]
5347 fn publishing_a_pending_vault_version_invalidates_reuse() {
5348 let directory = tempfile::tempdir().expect("temporary store");
5349 let path = directory.path().join("data.rdb");
5350 let store = UnifiedStore::open(&path).expect("paged store");
5351 store.create_collection("rows").expect("collection");
5352 store.insert("rows", row(1, 1)).expect("row");
5353 store
5354 .pending_vault_versions
5355 .write()
5356 .insert(EntityId::new(1));
5357 store.persist().expect("exclude pending vault version");
5358 store
5359 .pending_vault_versions
5360 .write()
5361 .remove(&EntityId::new(1));
5362 store.persist().expect("include published vault version");
5363 drop(store);
5364 let recovered = UnifiedStore::open(&path).expect("recover");
5365 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
5366 }
5367}