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.max_transaction_id());
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 = self.format_version().max(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 excluded = self
1259 .snapshot_manager
1260 .get()
1261 .map_or_else(Default::default, |manager| {
1262 manager.checkpoint_excluded_state().1
1263 });
1264 let hidden_graph_ids = self
1265 .resident
1266 .snapshot_excluded_graph_ids(&Default::default(), &excluded);
1267 let cross_refs = self.resident.cross_refs.read();
1268 let total_refs: usize = cross_refs
1269 .iter()
1270 .map(|(source, refs)| {
1271 if hidden_graph_ids.contains(source) {
1272 0
1273 } else {
1274 refs.iter()
1275 .filter(|(target, _, _)| !hidden_graph_ids.contains(target))
1276 .count()
1277 }
1278 })
1279 .sum();
1280 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1281 for (source_id, refs) in cross_refs.iter() {
1282 if hidden_graph_ids.contains(source_id) {
1283 continue;
1284 }
1285 for (target_id, ref_type, collection) in refs {
1286 if hidden_graph_ids.contains(target_id) {
1287 continue;
1288 }
1289 reddb_file::encode_native_paged_cross_ref(
1290 &mut meta_data,
1291 source_id.raw(),
1292 target_id.raw(),
1293 ref_type.to_byte(),
1294 reddb_engine::storage::system_collection_names::canonical_or_self(collection),
1295 );
1296 }
1297 }
1298
1299 self.publish_reclaimable_trees(
1300 pager,
1301 &mut meta_data,
1302 &self.btree_indices.read(),
1303 &self.key_indices.read(),
1304 )?;
1305 if let Some(epoch) = epoch {
1306 self.commit
1307 .as_ref()
1308 .expect("registry epoch has a coordinator")
1309 .finish_count_batch_ownership_epoch(self, epoch)
1310 .map_err(StoreError::Io)?;
1311 publication.active = false;
1312 }
1313 Ok(())
1314 }
1315
1316 pub fn pager(&self) -> Option<&Arc<Pager>> {
1318 self.pager.as_ref()
1319 }
1320
1321 pub(crate) fn ensure_pager_writeback_available(&self) -> Result<(), StoreError> {
1322 self.pager.as_ref().map_or(Ok(()), |pager| {
1323 pager
1324 .check_writeback_available()
1325 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))
1326 })
1327 }
1328
1329 pub(crate) fn is_read_only(&self) -> bool {
1330 self.pager
1331 .as_ref()
1332 .is_some_and(|pager| pager.is_read_only())
1333 }
1334
1335 pub fn page_cache_bytes_in_use(&self) -> u64 {
1339 self.pager.as_ref().map_or(0, |pager| {
1340 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
1341 })
1342 }
1343
1344 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
1347 self.commit
1348 .as_ref()
1349 .map_or(0, |commit| commit.buffered_bytes())
1350 }
1351
1352 pub fn segment_memory_bytes(&self) -> u64 {
1356 let managers: Vec<Arc<SegmentManager>> = self
1357 .resident
1358 .collections
1359 .read()
1360 .values()
1361 .map(Arc::clone)
1362 .collect();
1363
1364 managers
1365 .iter()
1366 .map(|manager| manager.resident_bytes())
1367 .fold(0, u64::saturating_add)
1368 }
1369
1370 pub fn config(&self) -> &UnifiedStoreConfig {
1374 &self.config
1375 }
1376
1377 pub fn with_config(config: UnifiedStoreConfig) -> Self {
1378 Self {
1379 config,
1380 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1381 format_version: AtomicU32::new(STORE_VERSION_V11),
1382 pager: None,
1383 db_path: None,
1384 operational_manifest: parking_lot::Mutex::new(None),
1385 btree_indices: RwLock::new(HashMap::new()),
1386 key_indices: RwLock::new(HashMap::new()),
1387 secondary_index_specs: RwLock::new(HashMap::new()),
1388 count_builds: RwLock::new(HashMap::new()),
1389 count_projection_seeds: parking_lot::Mutex::new(HashMap::new()),
1390 queue_dedup_cursors: parking_lot::Mutex::new(HashMap::new()),
1391 recovery_xid_summary: RwLock::new(None),
1392 checkpoint_versions: RwLock::new(HashMap::new()),
1393 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1394 retired_btrees: parking_lot::Mutex::new(Vec::new()),
1395 ownership_catalog_epoch: AtomicU64::new(0),
1396 ownership_active_epoch: AtomicU64::new(0),
1397 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1398 commit: None,
1399 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1400 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1401 result_cache_epoch: Arc::new(result_cache_epoch::ResultCacheEpochState::default()),
1402 snapshot_manager: std::sync::OnceLock::new(),
1403 embedded_commit_session: std::sync::OnceLock::new(),
1404 #[cfg(test)]
1405 test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1406 #[cfg(test)]
1407 test_versioned_update_after_old: parking_lot::Mutex::new(None),
1408 #[cfg(test)]
1409 test_fail_versioned_update_phase: std::sync::atomic::AtomicU8::new(0),
1410 #[cfg(test)]
1411 test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1412 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1413 row_publications: Default::default(),
1414 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1415 }
1416 }
1417
1418 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
1436 Self::open_with_config(path, UnifiedStoreConfig::default())
1437 }
1438
1439 pub fn open_with_config(
1440 path: impl AsRef<Path>,
1441 config: UnifiedStoreConfig,
1442 ) -> Result<Self, StoreError> {
1443 Self::open_with_config_for_recovery(path, config, false, None, None)
1444 }
1445
1446 pub fn open_read_only_with_config(
1449 path: impl AsRef<Path>,
1450 mut config: UnifiedStoreConfig,
1451 ) -> Result<Self, StoreError> {
1452 let path = path.as_ref();
1453 config.embedded_wal_path = None;
1454 let pager = Pager::open(
1455 path,
1456 PagerConfig {
1457 read_only: true,
1458 create: false,
1459 disk_residency: config.resident_memory_bytes_max.is_some(),
1460 cache_size: config
1461 .page_cache_slots
1462 .unwrap_or_else(|| PagerConfig::default().cache_size),
1463 ..PagerConfig::default()
1464 },
1465 )
1466 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1467 pager
1468 .enable_deferred_reclamation()
1469 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1470 let mut store = Self::with_config(config);
1471 store.pager = Some(Arc::new(pager));
1472 store.db_path = Some(path.to_path_buf());
1473 let wal_path = reddb_file::layout::unified_wal_path(path);
1474 StoreCommitCoordinator::restore_checkpoint_page_images_from_path(&store, &wal_path)
1475 .map_err(StoreError::Io)?;
1476 let catalog = store.load_from_pages(false, None)?;
1477 StoreCommitCoordinator::replay_read_only(&store, &wal_path).map_err(StoreError::Io)?;
1478 store.load_result_cache_epoch()?;
1479 store.require_normalized_red_config_keys()?;
1480 let pending_drops = store.legacy_pending_collection_drops()?.unwrap_or_default();
1481 for name in pending_drops {
1482 if !catalog
1483 .collisions
1484 .iter()
1485 .any(|collision| collision.loser == name)
1486 && store.get_collection(&name).is_some()
1487 {
1488 store.drop_collection_in_memory(&name)?;
1489 }
1490 }
1491 Ok(store)
1492 }
1493
1494 pub(crate) fn open_with_config_for_recovery(
1497 path: impl AsRef<Path>,
1498 config: UnifiedStoreConfig,
1499 retain_canonical_system_config: bool,
1500 boot_wal: Option<&crate::storage::compact::boot_system_wal::BootSystemWalPlan>,
1501 catalog_plan: Option<&crate::storage::compact::catalog_aliases::CanonicalCatalogPlan>,
1502 ) -> Result<Self, StoreError> {
1503 let path = path.as_ref();
1504 let mut pager_config = PagerConfig {
1505 disk_residency: config.resident_memory_bytes_max.is_some(),
1506 ..PagerConfig::default()
1507 };
1508 if let Some(slots) = config.page_cache_slots {
1512 pager_config.cache_size = slots;
1513 }
1514 if matches!(
1522 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
1523 Some("0") | Some("false") | Some("off")
1524 ) {
1525 pager_config.double_write = false;
1526 }
1527 let pager_open_started = std::time::Instant::now();
1528 let pager = Pager::open(path, pager_config)
1529 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1530 pager
1531 .enable_deferred_reclamation()
1532 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1533 trace_startup_phase(path, "paged_pager_open", pager_open_started);
1534
1535 let header = pager
1536 .header()
1537 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1538 if config.resident_memory_bytes_max.is_none() && header.requires_disk_residency() {
1539 return Err(StoreError::Io(std::io::Error::new(std::io::ErrorKind::Unsupported,
1540 "this database was prepared for disk residency; reopen with the disk-resident storage profile")));
1541 }
1542
1543 let wal_path = reddb_file::layout::unified_wal_path(path);
1544 let commit = Some(Arc::new(
1547 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
1548 .map_err(StoreError::Io)?,
1549 ));
1550
1551 let store = Self {
1552 config,
1553 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1554 format_version: AtomicU32::new(STORE_VERSION_V11),
1555 pager: Some(Arc::new(pager)),
1556 db_path: Some(path.to_path_buf()),
1557 operational_manifest: parking_lot::Mutex::new(None),
1558 btree_indices: RwLock::new(HashMap::new()),
1559 key_indices: RwLock::new(HashMap::new()),
1560 secondary_index_specs: RwLock::new(HashMap::new()),
1561 count_builds: RwLock::new(HashMap::new()),
1562 count_projection_seeds: parking_lot::Mutex::new(HashMap::new()),
1563 queue_dedup_cursors: parking_lot::Mutex::new(HashMap::new()),
1564 recovery_xid_summary: RwLock::new(None),
1565 checkpoint_versions: RwLock::new(HashMap::new()),
1566 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1567 retired_btrees: parking_lot::Mutex::new(Vec::new()),
1568 ownership_catalog_epoch: AtomicU64::new(0),
1569 ownership_active_epoch: AtomicU64::new(0),
1570 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1571 commit,
1572 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1573 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1574 result_cache_epoch: Arc::new(result_cache_epoch::ResultCacheEpochState::default()),
1575 snapshot_manager: std::sync::OnceLock::new(),
1576 embedded_commit_session: std::sync::OnceLock::new(),
1577 #[cfg(test)]
1578 test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1579 #[cfg(test)]
1580 test_versioned_update_after_old: parking_lot::Mutex::new(None),
1581 #[cfg(test)]
1582 test_fail_versioned_update_phase: std::sync::atomic::AtomicU8::new(0),
1583 #[cfg(test)]
1584 test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1585 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1586 row_publications: Default::default(),
1587 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1588 };
1589
1590 let checkpoint_recovery_started = std::time::Instant::now();
1593 if let Some(commit) = &store.commit {
1594 commit
1595 .restore_checkpoint_page_images(&store)
1596 .map_err(StoreError::Io)?;
1597 }
1598 trace_startup_phase(
1599 path,
1600 "paged_checkpoint_recovery",
1601 checkpoint_recovery_started,
1602 );
1603 let page_load_started = std::time::Instant::now();
1605 let catalog_load = store.load_from_pages(retain_canonical_system_config, catalog_plan)?;
1606 trace_startup_phase(path, "paged_catalog_and_roots", page_load_started);
1607 let wal_replay_started = std::time::Instant::now();
1608 if let Some(commit) = &store.commit {
1609 commit
1610 .replay_into(&store, boot_wal)
1611 .map_err(StoreError::Io)?;
1612 }
1613 store.load_result_cache_epoch()?;
1614 let normalized_config_keys = store.normalize_red_config_keys()?;
1615 if normalized_config_keys > 0 {
1616 tracing::warn!(
1617 migrated = normalized_config_keys,
1618 "normalized legacy red.config keys before boot config consumers"
1619 );
1620 }
1621 trace_startup_phase(path, "paged_wal_replay", wal_replay_started);
1622 if catalog_load.migrated {
1623 store.mark_paged_registry_dirty();
1626 store.flush_paged_state()?;
1627 }
1628 let manifest_recovery_started = std::time::Instant::now();
1629 store.recover_operational_manifest(&catalog_load.collisions)?;
1630 if store.config.resident_memory_bytes_max.is_some() {
1631 let missing_key_index = {
1632 let trees = store.btree_indices.read();
1633 let keys = store.key_indices.read();
1634 trees.iter().any(|(name, tree)| {
1635 !keys
1636 .get(name)
1637 .is_some_and(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
1638 })
1639 };
1640 if missing_key_index {
1641 store.persist()?;
1642 }
1643 }
1644 trace_startup_phase(
1645 path,
1646 "paged_manifest_and_derived_roots",
1647 manifest_recovery_started,
1648 );
1649
1650 Ok(store)
1651 }
1652
1653 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
1657 let Some(pager) = &self.pager else {
1658 return Ok(());
1659 };
1660 let collections = self.resident.collections.read();
1661 let mut indices = self.btree_indices.write();
1662 let previous_versions = self.checkpoint_versions.read();
1663 let raw_replay =
1664 self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
1665 for (name, manager) in collections.iter() {
1666 let mut reusable = None;
1667 if raw_replay {
1668 if let (Some(previous), Some(tree)) = (
1669 previous_versions.get(name),
1670 indices.get(name).filter(|tree| tree.has_page_proofs()),
1671 ) {
1672 let mut current = CheckpointVersion {
1673 manager: Arc::downgrade(manager),
1674 segments: manager.checkpoint_revisions(),
1675 visibility_revision: 0,
1676 untracked_visibility_revision: 0,
1677 excluded_xids: Arc::default(),
1678 pending_vault: Arc::default(),
1679 format_version: self.format_version(),
1680 deferred_rows: Some(Arc::default()),
1681 has_unprojected_rows: false,
1682 };
1683 if previous.manager.ptr_eq(¤t.manager)
1684 && previous.format_version == current.format_version
1685 {
1686 if previous.segments == current.segments
1687 && !previous.has_unprojected_rows
1688 && checkpoint_visibility_matches(previous, ¤t)
1689 {
1690 reusable = Some(Arc::clone(tree));
1691 } else {
1692 reusable = self.sparse_checkpoint_tree(
1698 name,
1699 manager,
1700 tree,
1701 previous,
1702 &mut current,
1703 )?;
1704 }
1705 }
1706 }
1707 }
1708 let tree = match reusable {
1709 Some(tree) => tree,
1710 None => {
1711 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1712 #[cfg(test)]
1713 REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1714 Some(Self::serialize_entity_record(
1715 entity,
1716 metadata.as_ref(),
1717 self.format_version(),
1718 ))
1719 })?
1720 }
1721 };
1722 if indices
1723 .get(name)
1724 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1725 {
1726 continue;
1727 }
1728 if let Some(previous) = indices.insert(name.clone(), tree) {
1729 self.retire_btree(previous);
1730 }
1731 self.mark_paged_registry_dirty();
1732 #[cfg(test)]
1733 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1734 if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1735 pager.sync().map_err(|error| {
1736 StoreError::Io(std::io::Error::other(error.to_string()))
1737 })?;
1738 }
1739 eprintln!("experimental replay rebuild exit before manifest");
1740 std::process::exit(182);
1741 }
1742 }
1743 drop(previous_versions);
1744 drop(indices);
1745 drop(collections);
1746 self.flush_paged_state()?;
1747 #[cfg(test)]
1748 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1749 eprintln!("experimental replay exit after manifest");
1750 std::process::exit(182);
1751 }
1752 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1755 let collections = self.resident.collections.read();
1756 let mut versions = self.checkpoint_versions.write();
1757 for (name, manager) in collections.iter() {
1758 versions.insert(
1759 name.clone(),
1760 CheckpointVersion {
1761 manager: Arc::downgrade(manager),
1762 segments: manager.checkpoint_revisions(),
1763 visibility_revision: 0,
1764 untracked_visibility_revision: 0,
1765 excluded_xids: Arc::default(),
1766 pending_vault: Arc::default(),
1767 format_version: self.format_version(),
1768 deferred_rows: Some(Arc::default()),
1769 has_unprojected_rows: false,
1770 },
1771 );
1772 }
1773 }
1774 Ok(())
1775 }
1776
1777 fn legacy_pending_collection_drops(&self) -> Result<Option<Vec<String>>, StoreError> {
1778 match self.try_get_config("storage.legacy_manifest_imported")? {
1779 Some(Value::Boolean(true)) => return Ok(None),
1780 None => {}
1781 Some(_) => {
1782 return Err(StoreError::Serialization(
1783 "invalid legacy manifest import marker".into(),
1784 ))
1785 }
1786 }
1787 let Some(path) = &self.db_path else {
1788 return Ok(None);
1789 };
1790 self.operational_manifest_for_path(path)
1791 .read_only_pending_drops_if_present()
1792 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))
1793 }
1794
1795 fn recover_operational_manifest(
1796 &self,
1797 collisions: &[SystemCollectionCollision],
1798 ) -> Result<(), StoreError> {
1799 let Some(pending) = self.legacy_pending_collection_drops()? else {
1800 return Ok(());
1801 };
1802 for name in pending {
1805 if !collisions.iter().any(|collision| collision.loser == name)
1806 && self.get_collection(&name).is_some()
1807 {
1808 self.drop_collection(&name)?;
1809 }
1810 }
1811 self.try_set_config_tree(
1812 "storage.legacy_manifest_imported",
1813 &crate::serde_json::Value::Bool(true),
1814 )?;
1815 Ok(())
1816 }
1817
1818 pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1820 let Some(pager) = &self.pager else {
1821 return Ok(0);
1822 };
1823 let Some(content) = read_meta_payload(pager)? else {
1824 return Ok(0);
1825 };
1826 reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1827 .map(|epoch| epoch.unwrap_or(0))
1828 .map_err(|error| StoreError::Serialization(error.to_string()))
1829 }
1830
1831 fn load_from_pages(
1833 &self,
1834 retain_canonical_system_config: bool,
1835 catalog_plan: Option<&crate::storage::compact::catalog_aliases::CanonicalCatalogPlan>,
1836 ) -> Result<CatalogLoadOutcome, StoreError> {
1837 let pager = match &self.pager {
1838 Some(p) => p,
1839 None => return Ok(CatalogLoadOutcome::default()), };
1841
1842 let page_count = pager.page_count().map_err(|e| {
1844 StoreError::Io(std::io::Error::other(format!(
1845 "failed to read page count: {}",
1846 e
1847 )))
1848 })?;
1849 if page_count <= 1 {
1850 return Ok(CatalogLoadOutcome::default());
1852 }
1853
1854 if let Some(content_vec) = read_meta_payload(pager)? {
1859 let content: &[u8] = &content_vec;
1860 let header = pager
1861 .header()
1862 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1863 let prepared = header.requires_disk_residency();
1864 let proofs = reddb_file::native_store::decode_native_paged_catalog_tree_proofs(content)
1865 .map_err(|error| StoreError::Serialization(error.to_string()))?;
1866 if proofs.is_some()
1867 && (header.required_capabilities
1868 & reddb_file::file_format::FILE_CAPABILITY_TREE_PROOFS
1869 == 0
1870 || pager
1871 .manifest_reference()
1872 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1873 .page_id
1874 == 0)
1875 {
1876 return Err(page_integrity(
1877 1,
1878 None,
1879 "tree proofs require an independently sealed catalog and reader capability",
1880 ));
1881 }
1882 if proofs.as_ref().is_some_and(|proofs| {
1883 proofs.iter().any(|(&root, proof)| {
1884 root >= header.page_count || proof.reference.page_id >= header.page_count
1885 })
1886 }) {
1887 return Err(page_integrity(
1888 1,
1889 None,
1890 "tree proof points outside allocation",
1891 ));
1892 }
1893 let resume = if prepared {
1894 reddb_file::native_resume::decode_catalog_resume(content)
1895 .map_err(|error| StoreError::Serialization(error.to_string()))?
1896 } else {
1897 None
1898 };
1899 let mut outcome = CatalogLoadOutcome::default();
1900 let mut ignored_collections = std::collections::HashSet::new();
1901 if retain_canonical_system_config
1902 && !resume.as_ref().is_some_and(|resume| {
1903 resume.collections.contains_key("red_config")
1904 && resume.collections.contains_key("red.config")
1905 })
1906 {
1907 return Err(page_integrity(
1908 1,
1909 None,
1910 "canonical config recovery requires both resume identities",
1911 ));
1912 }
1913 if let Some(resume) = &resume {
1914 for &(legacy, canonical) in
1915 reddb_engine::storage::system_collection_names::LEGACY_NAMES
1916 {
1917 let (Some(legacy_state), Some(canonical_state)) = (
1918 resume.collections.get(legacy),
1919 resume.collections.get(canonical),
1920 ) else {
1921 continue;
1922 };
1923 let merged_resume = merge_shared_alias_resume(legacy_state, canonical_state);
1924 let shared_physical_state = merged_resume.is_some();
1925 let resume_counter_drift = legacy_state != canonical_state;
1926 let (survivor, loser) = match (
1927 legacy_state.entities == 0,
1928 canonical_state.entities == 0,
1929 ) {
1930 _ if retain_canonical_system_config && legacy == "red_config" => {
1931 (canonical, legacy)
1932 }
1933 _ if catalog_plan.is_some_and(|plan| plan.retires(legacy, canonical)) => {
1934 (canonical, legacy)
1935 }
1936 (false, true) => (legacy, canonical),
1937 (true, false) | (true, true) => (canonical, legacy),
1938 (false, false) if shared_physical_state => (canonical, legacy),
1939 (false, false) => {
1940 let (
1941 data_root_match,
1942 key_index_root_match,
1943 secondary_index_state_match,
1944 ) = shared_alias_resume_matches(legacy_state, canonical_state);
1945 return Err(page_integrity(
1946 1,
1947 None,
1948 format!(
1949 "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})"
1950 ),
1951 ));
1952 }
1953 };
1954 ignored_collections.insert(loser.to_string());
1955 outcome.collisions.push(SystemCollectionCollision {
1956 survivor: survivor.to_string(),
1957 loser: loser.to_string(),
1958 canonical: canonical.to_string(),
1959 shared_physical_state,
1960 resume_counter_drift,
1961 merged_resume,
1962 });
1963 }
1964 let live_roots = resume
1965 .collections
1966 .iter()
1967 .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
1968 .flat_map(|(_, state)| {
1969 std::iter::once(state.root_page)
1970 .chain(state.key_index_root)
1971 .chain(
1972 state
1973 .secondary_indices
1974 .values()
1975 .map(|index| index.root_page),
1976 )
1977 })
1978 .filter(|root| *root != 0)
1979 .collect::<std::collections::HashSet<_>>();
1980 for collision in &outcome.collisions {
1981 if collision.shared_physical_state {
1982 continue;
1983 }
1984 let loser_state = resume
1985 .collections
1986 .get(&collision.loser)
1987 .expect("collision loser is present");
1988 if std::iter::once(loser_state.root_page)
1989 .chain(loser_state.key_index_root)
1990 .chain(
1991 loser_state
1992 .secondary_indices
1993 .values()
1994 .map(|index| index.root_page),
1995 )
1996 .filter(|root| *root != 0)
1997 .any(|root| live_roots.contains(&root))
1998 {
1999 return Err(page_integrity(
2000 1,
2001 None,
2002 format!(
2003 "empty collection '{}' aliases a live collection root",
2004 collision.loser
2005 ),
2006 ));
2007 }
2008 }
2009 reddb_engine::storage::system_collection_names::migration_plan(
2010 resume
2011 .collections
2012 .keys()
2013 .map(String::as_str)
2014 .filter(|name| !ignored_collections.contains(*name)),
2015 )
2016 .map_err(|(legacy, canonical)| {
2017 page_integrity(
2018 1,
2019 None,
2020 format!(
2021 "catalog contains both legacy collection '{legacy}' and canonical collection '{canonical}'"
2022 ),
2023 )
2024 })?;
2025 for state in resume.collections.values() {
2026 if !state.secondary_indices.is_empty()
2027 && header.version
2028 < reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
2029 {
2030 return Err(page_integrity(
2031 1,
2032 None,
2033 "secondary roots require the file format fence",
2034 ));
2035 }
2036 for root in std::iter::once(state.root_page)
2037 .chain(state.key_index_root)
2038 .chain(
2039 state
2040 .secondary_indices
2041 .values()
2042 .map(|index| index.root_page),
2043 )
2044 {
2045 if root >= page_count {
2046 return Err(page_integrity(
2047 root,
2048 None,
2049 "published root exceeds the database page count",
2050 ));
2051 }
2052 }
2053 }
2054 self.resident
2055 .next_entity_id
2056 .fetch_max(resume.entity_id_next, Ordering::SeqCst);
2057 }
2058 *self.recovery_xid_summary.write() = resume.as_ref().and_then(|resume| {
2059 resume.xid_high_water_mark.map(|high_water_mark| {
2060 super::recovery_xid::RecoveryXidSummary {
2061 high_water_mark,
2062 roots: resume
2063 .collections
2064 .iter()
2065 .filter(|(name, _)| !ignored_collections.contains(name.as_str()))
2066 .map(|(name, state)| {
2067 (
2068 reddb_engine::storage::system_collection_names::canonical_or_self(name)
2069 .to_owned(),
2070 state.root_page,
2071 )
2072 })
2073 .collect(),
2074 }
2075 })
2076 });
2077 if content.len() >= 4 {
2078 let mut pos = 0;
2079 let mut format_version = STORE_VERSION_V1;
2080
2081 let collection_count = if let Some(header) =
2082 reddb_file::decode_native_paged_metadata_header(content)
2083 .map_err(|err| StoreError::Serialization(err.to_string()))?
2084 {
2085 format_version = header.format_version;
2086 pos += reddb_file::METADATA_HEADER_BYTES;
2087 header.collection_count as usize
2088 } else {
2089 let count = u32::from_le_bytes([
2090 content[pos],
2091 content[pos + 1],
2092 content[pos + 2],
2093 content[pos + 3],
2094 ]) as usize;
2095 pos += 4;
2096 count
2097 };
2098
2099 self.set_format_version(format_version);
2100
2101 if pos > content.len() {
2102 return Ok(CatalogLoadOutcome::default());
2103 }
2104
2105 if resume.is_none() {
2106 let mut roots = std::collections::HashMap::new();
2110 let mut root_pos = pos;
2111 for _ in 0..collection_count {
2112 let root =
2113 reddb_file::decode_native_paged_collection_root(content, &mut root_pos)
2114 .map_err(|err| page_integrity(1, None, err.to_string()))?;
2115 if roots.insert(root.collection, root.root_page).is_some() {
2116 return Err(page_integrity(
2117 1,
2118 None,
2119 "duplicate raw collection identity",
2120 ));
2121 }
2122 }
2123 for &(legacy, canonical) in
2124 reddb_engine::storage::system_collection_names::LEGACY_NAMES
2125 {
2126 let (Some(&legacy_root), Some(&canonical_root)) =
2127 (roots.get(legacy), roots.get(canonical))
2128 else {
2129 continue;
2130 };
2131 let (survivor, loser) = match (legacy_root == 0, canonical_root == 0) {
2132 (false, true) => (legacy, canonical),
2133 (true, _) => (canonical, legacy),
2134 (false, false) => continue,
2135 };
2136 ignored_collections.insert(loser.to_owned());
2137 outcome.collisions.push(SystemCollectionCollision {
2138 survivor: survivor.to_owned(),
2139 loser: loser.to_owned(),
2140 canonical: canonical.to_owned(),
2141 shared_physical_state: false,
2142 resume_counter_drift: false,
2143 merged_resume: None,
2144 });
2145 }
2146 }
2147 let mut source_names = std::collections::HashMap::<String, String>::new();
2148 let mut migrated_catalog = !outcome.collisions.is_empty();
2149 let mut empty_duplicate_roots = std::collections::HashSet::new();
2150 let mut recovery_config_roots = [0u8; 2];
2151 let mut scanned_xid_high_water_mark = 0u64;
2155 let mut count_seed_bytes_remaining = self
2156 .config
2157 .resident_memory_bytes_max
2158 .and_then(|limit| usize::try_from(limit).ok())
2159 .unwrap_or(0)
2160 / 2;
2161
2162 for _ in 0..collection_count {
2164 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
2165 .map_err(|err| page_integrity(1, None, err.to_string()))?;
2166 let root_page = root.root_page;
2168 let stored_name = root.collection;
2169 if retain_canonical_system_config {
2170 let config_index = match stored_name.as_str() {
2171 "red_config" => Some(0),
2172 "red.config" => Some(1),
2173 _ => None,
2174 };
2175 if let Some(index) = config_index {
2176 recovery_config_roots[index] =
2177 recovery_config_roots[index].saturating_add(1);
2178 let state = resume
2179 .as_ref()
2180 .and_then(|resume| resume.collections.get(&stored_name))
2181 .expect("both recovery identities were checked");
2182 if state.root_page != root_page {
2183 return Err(page_integrity(
2184 1,
2185 None,
2186 "config collection root differs from resume catalog",
2187 ));
2188 }
2189 }
2190 }
2191 if ignored_collections.contains(&stored_name) {
2192 if resume.is_none() && root_page == 0 {
2193 continue;
2194 }
2195 let shared_physical_state = outcome
2196 .collisions
2197 .iter()
2198 .find(|collision| collision.loser == stored_name)
2199 .is_some_and(|collision| collision.shared_physical_state);
2200 if shared_physical_state {
2201 continue;
2202 }
2203 let state = resume
2204 .as_ref()
2205 .and_then(|resume| resume.collections.get(&stored_name))
2206 .expect("ignored collection was proven by catalog resume");
2207 for retired_root in std::iter::once(state.root_page)
2208 .chain(state.key_index_root)
2209 .chain(
2210 state
2211 .secondary_indices
2212 .values()
2213 .map(|index| index.root_page),
2214 )
2215 .filter(|root| *root != 0)
2216 {
2217 empty_duplicate_roots.insert(retired_root);
2218 }
2219 continue;
2220 }
2221 let name = reddb_engine::storage::system_collection_names::canonical_or_self(
2222 &stored_name,
2223 )
2224 .to_owned();
2225 if let Some(previous) = source_names.insert(name.clone(), stored_name.clone()) {
2226 if previous != stored_name {
2227 return Err(page_integrity(
2228 1,
2229 None,
2230 format!(
2231 "catalog contains both legacy collection '{previous}' and canonical collection '{name}'"
2232 ),
2233 ));
2234 }
2235 }
2236 migrated_catalog |= name != stored_name;
2237
2238 let _ = self.create_collection_in_memory(&name);
2242
2243 let shared_alias = outcome.collisions.iter().find(|collision| {
2244 collision.survivor == stored_name && collision.shared_physical_state
2245 });
2246 let counters = shared_alias
2247 .and_then(|collision| collision.merged_resume.as_ref())
2248 .or_else(|| {
2249 resume
2250 .as_ref()
2251 .and_then(|resume| resume.collections.get(&stored_name))
2252 });
2253 let manager = self.get_collection(&name);
2254 let mut count_seed =
2255 if counters.is_none() && self.commit.is_some() && !self.is_read_only() {
2256 let schema =
2257 manager.as_ref().and_then(|manager| manager.column_schema());
2258 super::count_index::CountProjectionSeedCollector::new(
2259 schema,
2260 count_seed_bytes_remaining,
2261 )
2262 } else {
2263 None
2264 };
2265 if let (Some(counters), Some(manager)) = (counters, self.get_collection(&name))
2266 {
2267 manager.restore_identity_counters(
2268 counters.entity_id_next,
2269 counters.row_id_next,
2270 );
2271 manager.restore_sequence_id_next(counters.sequence_id_next);
2272 let expiry_entries = counters
2273 .metadata_expiries
2274 .iter()
2275 .map(|entry| (entry.deadline_ms, EntityId::new(entry.entity_id)))
2276 .collect::<Vec<_>>();
2277 manager.restore_metadata_expiry_projection(
2278 &expiry_entries,
2279 resume
2280 .as_ref()
2281 .is_some_and(|resume| resume.metadata_expiry_prepared),
2282 );
2283 }
2284 if root_page > 0 || counters.is_some() {
2286 let btree = restore_published_tree(pager, root_page, proofs.as_ref())?;
2287
2288 let mut cursor = btree.cursor_first().map_err(|err| {
2290 page_integrity(root_page, Some(name.clone()), err.to_string())
2291 })?;
2292 let hydrate_read_only =
2293 self.is_read_only() && self.config.resident_memory_bytes_max.is_none();
2294 let mut persisted_entities = if hydrate_read_only {
2295 0
2296 } else {
2297 counters.map_or(0, |state| state.entities)
2298 };
2299 while counters.is_none() || hydrate_read_only {
2300 let next = cursor
2301 .next_with_budget(
2302 self.config
2303 .resident_memory_bytes_max
2304 .and_then(|limit| usize::try_from(limit).ok())
2305 .unwrap_or(usize::MAX),
2306 )
2307 .map_err(|err| {
2308 page_integrity(root_page, Some(name.clone()), err.to_string())
2309 })?;
2310 let Some((key, value)) = next else {
2311 break;
2312 };
2313 persisted_entities += 1;
2314 let key_bytes: [u8; 8] = key.as_slice().try_into().map_err(|_| {
2316 page_integrity(
2317 root_page,
2318 Some(name.clone()),
2319 "physical entity key must contain eight bytes",
2320 )
2321 })?;
2322 let record = reddb_engine::storage::collection_read::EncodedRecord {
2323 id: EntityId::new(u64::from_be_bytes(key_bytes)),
2324 bytes: value,
2325 };
2326 let decoded = if let Some(limit) = self.config.resident_memory_bytes_max
2327 {
2328 record.decode_with_budget(
2329 self.format_version(),
2330 usize::try_from(limit).unwrap_or(usize::MAX),
2331 )
2332 } else {
2333 record.decode(self.format_version())
2334 };
2335 let (entity, metadata) = decoded.map_err(|err| {
2336 page_integrity(root_page, Some(name.clone()), err.to_string())
2337 })?;
2338 scanned_xid_high_water_mark =
2339 scanned_xid_high_water_mark.max(entity.max_transaction_id());
2340 let discard_count_seed = if let Some(seed) = count_seed.as_mut() {
2341 match seed.push(&entity) {
2342 Ok(complete) => !complete,
2343 Err(error) => {
2344 return Err(page_integrity(
2345 root_page,
2346 Some(name.clone()),
2347 error.to_string(),
2348 ));
2349 }
2350 }
2351 } else {
2352 false
2353 };
2354 if discard_count_seed {
2355 count_seed = None;
2356 }
2357 if let Some(m) = &manager {
2358 let id = entity.id;
2359 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
2360 m.register_row_id(*row_id);
2361 }
2362 if self.config.resident_memory_bytes_max.is_some() {
2363 m.restore_metadata_expiry(&entity, metadata.as_ref());
2364 self.register_entity_id(id);
2365 m.restore_sequence_id_next(
2366 entity.sequence_id.saturating_add(1),
2367 );
2368 m.restore_identity_counters(
2369 id.raw().saturating_add(1),
2370 m.identity_counters().1,
2371 );
2372 continue;
2373 }
2374 self.resident.context_index.index_entity(&name, &entity);
2375 m.restore_snapshot_entity(entity.clone())?;
2376 if let Some(metadata) = metadata {
2377 let _ = m.set_metadata(id, metadata);
2378 }
2379 self.register_entity_id(id);
2380 if self.config.auto_index_refs {
2381 self.index_cross_refs(&entity, &name)?;
2382 }
2383 }
2384 }
2385
2386 if let Some(seed) = count_seed.take() {
2387 count_seed_bytes_remaining =
2388 count_seed_bytes_remaining.saturating_sub(seed.bytes());
2389 self.remember_count_projection_seed(&name, root_page, seed);
2390 }
2391
2392 let btree = Arc::new(btree);
2394 if shared_alias.is_some_and(|collision| collision.resume_counter_drift) {
2395 persisted_entities = u64::try_from(btree.count().map_err(|err| {
2396 page_integrity(root_page, Some(name.clone()), err.to_string())
2397 })?)
2398 .map_err(|_| {
2399 page_integrity(
2400 root_page,
2401 Some(name.clone()),
2402 "shared collection entity count exceeds u64",
2403 )
2404 })?;
2405 }
2406 let key_tree = counters
2407 .and_then(|state| state.key_index_root)
2408 .map(|root| {
2409 restore_published_tree(pager, root, proofs.as_ref()).map(Arc::new)
2410 })
2411 .transpose()?;
2412 if let Some(counters) = counters {
2413 self.key_indices.write().insert(
2414 name.clone(),
2415 PublishedKeyIndex {
2416 indexed: key_tree.is_some(),
2417 secondary: counters.secondary_indices.iter().map(|(name, index)| {
2418 Ok((name.clone(), super::secondary_index::PublishedSecondaryIndex {
2419 identity: super::secondary_index::SecondaryIndexIdentity {
2420 codec_version: index.codec_version,
2421 definition: index.definition,
2422 },
2423 tree: Arc::new(restore_published_tree(pager, index.root_page, proofs.as_ref())?),
2424 }))
2425 }).collect::<Result<_, StoreError>>()?,
2426 source: Arc::downgrade(&btree),
2427 tree: match &key_tree { Some(tree) => Arc::clone(tree), None => Arc::new(restore_published_tree(pager, 0, proofs.as_ref())?) },
2428 },
2429 );
2430 }
2431 if let (Some(manager), Some(limit)) =
2432 (&manager, self.config.resident_memory_bytes_max)
2433 {
2434 manager.install_backing(
2435 Arc::new(
2436 match key_tree {
2437 Some(keys) => reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(&btree), keys),
2438 None => reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(&btree)),
2439 },
2440 ),
2441 btree.root_page_id(),
2442 format_version,
2443 usize::try_from(limit).unwrap_or(usize::MAX),
2444 persisted_entities,
2445 );
2446 }
2447 self.btree_indices.write().insert(name.clone(), btree);
2448 if format_version == STORE_VERSION_V11 {
2449 if let Some(manager) = &manager {
2450 self.checkpoint_versions.write().insert(
2451 name,
2452 CheckpointVersion {
2453 manager: Arc::downgrade(manager),
2454 segments: manager.checkpoint_revisions(),
2455 visibility_revision: 0,
2456 untracked_visibility_revision: 0,
2457 excluded_xids: Arc::default(),
2458 pending_vault: Arc::default(),
2459 format_version,
2460 deferred_rows: Some(Arc::default()),
2461 has_unprojected_rows: false,
2462 },
2463 );
2464 }
2465 }
2466 }
2467 }
2468
2469 if retain_canonical_system_config && recovery_config_roots != [1, 1] {
2470 return Err(page_integrity(
2471 1,
2472 None,
2473 "canonical config recovery requires exactly one root for each identity",
2474 ));
2475 }
2476 if retain_canonical_system_config
2477 && (format_version < STORE_VERSION_V2 || pos + 4 > content.len())
2478 {
2479 return Err(page_integrity(
2480 1,
2481 None,
2482 "canonical config recovery requires a complete cross-reference catalog",
2483 ));
2484 }
2485 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
2486 let cross_ref_count = u32::from_le_bytes([
2487 content[pos],
2488 content[pos + 1],
2489 content[pos + 2],
2490 content[pos + 3],
2491 ]) as usize;
2492 pos += 4;
2493
2494 for _ in 0..cross_ref_count {
2495 let cross_ref =
2496 match reddb_file::decode_native_paged_cross_ref(content, &mut pos) {
2497 Ok(cross_ref) => cross_ref,
2498 Err(error) if retain_canonical_system_config => {
2499 return Err(page_integrity(1, None, error.to_string()));
2500 }
2501 Err(_) => break,
2502 };
2503 if retain_canonical_system_config
2504 && cross_ref.target_collection == "red_config"
2505 {
2506 return Err(page_integrity(
2507 1,
2508 None,
2509 "legacy red_config has cross-references and cannot be retired",
2510 ));
2511 }
2512 let source_id = EntityId::new(cross_ref.source_id);
2513 let target_id = EntityId::new(cross_ref.target_id);
2514 let ref_type = RefType::from_byte(cross_ref.ref_type);
2515 let target_collection =
2516 reddb_engine::storage::system_collection_names::canonical_owned(
2517 cross_ref.target_collection,
2518 );
2519
2520 self.resident
2521 .cross_refs
2522 .write()
2523 .entry(source_id)
2524 .or_default()
2525 .push((target_id, ref_type, target_collection.clone()));
2526
2527 if let Some((collection, mut entity)) = self.get_any(source_id) {
2528 let exists = entity.cross_refs().iter().any(|xref| {
2529 xref.target == target_id
2530 && xref.ref_type == ref_type
2531 && xref.target_collection == target_collection
2532 });
2533 if !exists {
2534 entity.cross_refs_mut().push(CrossRef::new(
2535 source_id,
2536 target_id,
2537 target_collection.clone(),
2538 ref_type,
2539 ));
2540 if let Some(manager) = self.get_collection(&collection) {
2541 let _ = manager.update(entity);
2542 }
2543 }
2544 }
2545 }
2546 }
2547 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
2548 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2549 let already_retired = retired
2550 .iter()
2551 .copied()
2552 .collect::<std::collections::HashSet<_>>();
2553 let mut live_roots: std::collections::HashSet<_> = self
2554 .btree_indices
2555 .read()
2556 .values()
2557 .map(|tree| tree.root_page_id())
2558 .collect();
2559 live_roots.extend(
2560 self.key_indices
2561 .read()
2562 .values()
2563 .flat_map(PublishedKeyIndex::trees)
2564 .map(|tree| tree.root_page_id()),
2565 );
2566 for root in retired {
2567 if live_roots.contains(&root)
2568 || root
2569 >= pager
2570 .page_count()
2571 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
2572 {
2573 return Err(page_integrity(
2574 root,
2575 None,
2576 "retired root aliases a live or missing page",
2577 ));
2578 }
2579 self.retired_btrees.lock().push(RetiredBTree {
2580 tree: Arc::new(restore_published_tree(pager, root, proofs.as_ref())?),
2581 published: true,
2582 });
2583 }
2584 for root in empty_duplicate_roots
2585 .iter()
2586 .copied()
2587 .filter(|root| !already_retired.contains(root))
2588 {
2589 self.retire_btree(Arc::new(restore_published_tree(
2590 pager,
2591 root,
2592 proofs.as_ref(),
2593 )?));
2594 }
2595 if proofs.as_ref().is_some_and(|proofs| {
2596 proofs.iter().any(|(root, proof)| {
2597 let owns = |id| {
2598 live_roots.contains(id)
2599 || already_retired.contains(id)
2600 || empty_duplicate_roots.contains(id)
2601 };
2602 !owns(root) || owns(&proof.reference.page_id)
2603 })
2604 }) {
2605 return Err(page_integrity(
2606 1,
2607 None,
2608 "unowned or aliased tree generation proof",
2609 ));
2610 }
2611 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
2612 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2613 if deferred
2614 .iter()
2615 .any(|id| live_roots.contains(id) || empty_duplicate_roots.contains(id))
2616 {
2617 return Err(page_integrity(1, None, "deferred page aliases a live root"));
2618 }
2619 pager
2620 .restore_deferred_reclamation(&deferred)
2621 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2622 let ownership_epoch =
2623 reddb_file::native_store::decode_native_paged_ownership_epoch(
2624 content, &mut pos,
2625 )
2626 .map_err(|e| StoreError::Serialization(e.to_string()))?;
2627 self.ownership_catalog_epoch
2628 .store(ownership_epoch.unwrap_or(0), Ordering::Release);
2629 if migrated_catalog {
2630 self.mark_paged_registry_dirty();
2631 }
2632 if self.format_version() < STORE_VERSION_V11 {
2633 self.set_format_version(STORE_VERSION_V11);
2634 }
2635 if self.config.resident_memory_bytes_max.is_some()
2636 && resume.is_none()
2637 && !migrated_catalog
2638 && !retain_canonical_system_config
2639 {
2640 self.remember_startup_xid_high_water_mark(scanned_xid_high_water_mark);
2641 }
2642 outcome.migrated = migrated_catalog;
2643 return Ok(outcome);
2644 }
2645 }
2646
2647 if retain_canonical_system_config {
2648 return Err(page_integrity(
2649 1,
2650 None,
2651 "canonical config recovery requires a complete catalog",
2652 ));
2653 }
2654 if self.format_version() < STORE_VERSION_V11 {
2655 self.set_format_version(STORE_VERSION_V11);
2656 }
2657
2658 Ok(CatalogLoadOutcome::default())
2659 }
2660
2661 pub(crate) fn deserialize_entity(
2663 data: &[u8],
2664 format_version: u32,
2665 ) -> Result<UnifiedEntity, StoreError> {
2666 let mut pos = 0;
2667 Self::read_entity_binary(data, &mut pos, format_version)
2668 .map_err(|e| StoreError::Serialization(e.to_string()))
2669 }
2670
2671 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
2673 let mut buf = Vec::with_capacity(256);
2678 Self::write_entity_binary(&mut buf, entity, format_version);
2679 buf
2680 }
2681
2682 pub(crate) fn serialize_entity_record(
2683 entity: &UnifiedEntity,
2684 metadata: Option<&Metadata>,
2685 format_version: u32,
2686 ) -> Vec<u8> {
2687 reddb_engine::storage::metadata_codec::serialize_entity_record(
2688 entity,
2689 metadata,
2690 format_version,
2691 )
2692 }
2693
2694 pub(crate) fn deserialize_entity_record(
2695 data: &[u8],
2696 format_version: u32,
2697 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
2698 reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
2699 .map_err(StoreError::Serialization)
2700 }
2701
2702 pub fn persist(&self) -> Result<(), StoreError> {
2707 self.persist_inner(false)
2708 }
2709
2710 pub(crate) fn persist_after_runtime_snapshots(&self) -> Result<(), StoreError> {
2711 self.persist_inner(true)
2712 }
2713
2714 fn persist_inner(&self, runtime_snapshots_durable: bool) -> Result<(), StoreError> {
2715 if self.is_read_only() {
2716 return Ok(());
2717 }
2718 self.ensure_pager_writeback_available()?;
2719 let _vault_guard = self.vault_publication_lock.lock();
2720 let pending_vault_versions = self.pending_vault_versions.read();
2721 let pager = match &self.pager {
2722 Some(p) => p,
2723 None => {
2724 if let Some(path) = &self.db_path {
2726 return self
2727 .save_to_file(path)
2728 .map_err(|e| StoreError::Serialization(e.to_string()));
2729 }
2730 return Err(StoreError::Io(std::io::Error::other(
2731 "No pager or path configured for persistence",
2732 )));
2733 }
2734 };
2735 let _ownership_guard = pager.lock_ownership_mutations();
2738
2739 match pager.read_manifest_page() {
2740 Ok(_) => {}
2741 Err(PagerError::PageNotFound(_)) => {
2742 let meta_page = pager
2743 .allocate_page(crate::storage::engine::PageType::Header)
2744 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2745 pager
2746 .write_page(meta_page.page_id(), meta_page)
2747 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
2748 }
2749 Err(e) => {
2750 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
2751 }
2752 }
2753
2754 if let Some(commit) = &self.commit {
2755 commit.force_sync().map_err(StoreError::Io)?;
2756 }
2757
2758 struct InterruptedOwnership<'a> {
2759 pager: &'a Pager,
2760 active: bool,
2761 }
2762 impl Drop for InterruptedOwnership<'_> {
2763 fn drop(&mut self) {
2764 if self.active {
2765 self.pager.fence_interrupted_ownership();
2766 }
2767 }
2768 }
2769 let mut ownership = InterruptedOwnership {
2770 pager,
2771 active: self.commit.is_some(),
2772 };
2773 let epoch = if let Some(commit) = &self.commit {
2774 let epoch = commit
2775 .next_page_ownership_epoch(self)
2776 .map_err(StoreError::Io)?;
2777 commit
2778 .begin_page_ownership_epoch(self, epoch)
2779 .map_err(StoreError::Io)?;
2780 Some(epoch)
2781 } else {
2782 None
2783 };
2784
2785 let (visibility_revision, excluded, untracked_visibility_revision) =
2786 self.snapshot_manager.get().map_or_else(
2787 || (0, Default::default(), 0),
2788 |manager| manager.checkpoint_projection_state(),
2789 );
2790 let hidden_graph_ids = self
2791 .resident
2792 .snapshot_excluded_graph_ids(&pending_vault_versions, &excluded);
2793 let collections = self.resident.collections.read();
2794 let mut btree_indices = self.btree_indices.write();
2795
2796 let mut collection_roots: Vec<(String, u32)> = Vec::new();
2798
2799 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
2802 let previous_versions = self.checkpoint_versions.read();
2803 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
2804 let pending_vault = Arc::new(pending_vault_versions.clone());
2805 let excluded = Arc::new(excluded);
2806 for (name, manager) in collections.iter() {
2807 let mut version = CheckpointVersion {
2808 manager: Arc::downgrade(manager),
2809 segments: manager.checkpoint_revisions(),
2810 visibility_revision,
2811 untracked_visibility_revision,
2812 excluded_xids: Arc::clone(&excluded),
2813 pending_vault: Arc::clone(&pending_vault),
2814 format_version: self.format_version(),
2815 deferred_rows: Some(Arc::default()),
2816 has_unprojected_rows: false,
2817 };
2818 if let (Some(previous), Some(tree)) = (
2819 previous_versions.get(name),
2820 btree_indices
2821 .get(name)
2822 .filter(|tree| tree.has_page_proofs()),
2823 ) {
2824 if hidden_graph_ids.is_empty()
2825 && previous.manager.ptr_eq(&version.manager)
2826 && previous.segments == version.segments
2827 && previous.format_version == version.format_version
2828 && !previous.has_unprojected_rows
2829 && checkpoint_visibility_matches(previous, &version)
2830 {
2831 version.deferred_rows = previous.deferred_rows.clone();
2832 collection_roots.push((name.clone(), tree.root_page_id()));
2833 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
2834 checkpoint_versions.insert(name.clone(), version);
2835 continue;
2836 }
2837 }
2838 if let (Some(previous), Some(tree)) = (
2839 previous_versions.get(name),
2840 btree_indices
2841 .get(name)
2842 .filter(|tree| tree.has_page_proofs()),
2843 ) {
2844 if hidden_graph_ids.is_empty()
2845 && previous.manager.ptr_eq(&version.manager)
2846 && previous.format_version == version.format_version
2847 {
2848 if let Some(tree) =
2849 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
2850 {
2851 collection_roots.push((name.clone(), tree.root_page_id()));
2852 rebuilt_indices.insert(name.clone(), tree);
2853 checkpoint_versions.insert(name.clone(), version);
2854 continue;
2855 }
2856 }
2857 }
2858 let btree = self.checkpoint_tree(
2859 pager,
2860 manager,
2861 btree_indices.get(name),
2862 |entity, metadata| {
2863 #[cfg(test)]
2864 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
2865 let hidden = pending_vault_versions.contains(&entity.id)
2866 || excluded.contains(&entity.xmin);
2867 let normalize_xmax = excluded.contains(&entity.xmax);
2868 let normalize_branch = entity.branch_version().is_some_and(|branch| {
2869 branch
2870 .deletions
2871 .iter()
2872 .any(|(_, xid)| excluded.contains(xid))
2873 });
2874 let hide_refs = entity.cross_refs().iter().any(|reference| {
2875 hidden_graph_ids.contains(&reference.source)
2876 || hidden_graph_ids.contains(&reference.target)
2877 });
2878 if hidden || normalize_xmax || normalize_branch || hide_refs {
2879 version.remember_deferred_row(entity.id);
2880 }
2881 if hidden {
2882 return None;
2883 }
2884 let mut normalized = std::borrow::Cow::Borrowed(entity);
2885 if normalize_xmax {
2886 normalized.to_mut().xmax = 0;
2887 }
2888 if normalize_branch {
2889 normalized
2890 .to_mut()
2891 .retain_branch_deletions(|xid| !excluded.contains(&xid));
2892 }
2893 if hide_refs {
2894 normalized.to_mut().cross_refs_mut().retain(|reference| {
2895 !hidden_graph_ids.contains(&reference.source)
2896 && !hidden_graph_ids.contains(&reference.target)
2897 });
2898 }
2899 Some(Self::serialize_entity_record(
2900 &normalized,
2901 metadata.as_ref(),
2902 self.format_version(),
2903 ))
2904 },
2905 )?;
2906 checkpoint_versions.insert(name.clone(), version);
2907
2908 collection_roots.push((name.clone(), btree.root_page_id()));
2909 rebuilt_indices.insert(name.clone(), btree);
2910 }
2911
2912 let mut meta_data = Vec::with_capacity(4096);
2914
2915 let format_version = self.format_version().max(STORE_VERSION_V11);
2916 self.set_format_version(format_version);
2917
2918 reddb_file::encode_native_paged_metadata_header(
2919 &mut meta_data,
2920 reddb_file::NativePagedMetadataHeader {
2921 format_version,
2922 collection_count: collection_roots.len() as u32,
2923 },
2924 );
2925
2926 for (name, root_page) in &collection_roots {
2928 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
2929 }
2930
2931 let cross_refs = self.resident.cross_refs.read();
2933 let total_refs: usize = cross_refs
2934 .iter()
2935 .map(|(source, refs)| {
2936 if hidden_graph_ids.contains(source) {
2937 0
2938 } else {
2939 refs.iter()
2940 .filter(|(target, _, _)| !hidden_graph_ids.contains(target))
2941 .count()
2942 }
2943 })
2944 .sum();
2945 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
2946 for (source_id, refs) in cross_refs.iter() {
2947 if hidden_graph_ids.contains(source_id) {
2948 continue;
2949 }
2950 for (target_id, ref_type, collection) in refs {
2951 if hidden_graph_ids.contains(target_id) {
2952 continue;
2953 }
2954 reddb_file::encode_native_paged_cross_ref(
2955 &mut meta_data,
2956 source_id.raw(),
2957 target_id.raw(),
2958 ref_type.to_byte(),
2959 collection,
2960 );
2961 }
2962 }
2963
2964 for (name, previous) in btree_indices.iter() {
2965 if !rebuilt_indices
2966 .get(name)
2967 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
2968 {
2969 self.retire_btree(Arc::clone(previous));
2970 }
2971 }
2972 #[cfg(test)]
2973 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
2974 pager
2975 .sync()
2976 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2977 eprintln!("experimental checkpoint exit before manifest");
2978 std::process::exit(183);
2979 }
2980 let rebuilt_keys = self.checkpoint_key_indices(pager, &rebuilt_indices)?;
2981 #[cfg(test)]
2982 if std::env::var_os("REDDB_TEST_EXIT_AFTER_SECONDARY_INDEX_BUILD").is_some() {
2983 pager
2984 .sync()
2985 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2986 std::process::exit(196);
2987 }
2988 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices, &rebuilt_keys)?;
2989
2990 #[cfg(test)]
2991 if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
2992 std::process::exit(195);
2993 }
2994
2995 *btree_indices = rebuilt_indices;
2998 *self.key_indices.write() = rebuilt_keys;
2999 drop(previous_versions);
3000 *self.checkpoint_versions.write() = checkpoint_versions;
3001
3002 if let Some(commit) = &self.commit {
3003 commit
3004 .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
3005 .map_err(StoreError::Io)?;
3006 ownership.active = false;
3007 if runtime_snapshots_durable {
3008 commit
3009 .truncate_after_runtime_checkpoint()
3010 .map_err(StoreError::Io)?;
3011 } else {
3012 commit.truncate().map_err(StoreError::Io)?;
3013 }
3014 }
3015 *self.checkpoint_durable_rows.write() = Default::default();
3016
3017 if let Some(limit) = self.config.resident_memory_bytes_max {
3018 let mut versions = self.checkpoint_versions.write();
3019 for (name, manager) in collections.iter() {
3020 if versions
3021 .get(name)
3022 .is_some_and(|version| !version.has_unprojected_rows)
3023 {
3024 if let Some(tree) = btree_indices.get(name) {
3025 manager.install_backing(
3026 Arc::new(
3027 {
3028 let keys = self.key_indices.read();
3029 let index = keys.get(name).expect("invariant: prepared checkpoint classifies its key index");
3030 if index.indexed {
3031 reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(tree), Arc::clone(&index.tree))
3032 } else {
3033 reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(tree))
3034 }
3035 },
3036 ),
3037 tree.root_page_id(),
3038 self.format_version(),
3039 usize::try_from(limit).unwrap_or(usize::MAX),
3040 manager.count() as u64,
3041 );
3042 if let Some(version) = versions.get_mut(name) {
3043 version.segments = manager.checkpoint_revisions();
3044 }
3045 }
3046 }
3047 }
3048 }
3049
3050 Ok(())
3051 }
3052
3053 pub fn is_paged(&self) -> bool {
3055 self.pager.is_some()
3056 }
3057
3058 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
3061 self.btree_indices
3062 .read()
3063 .get(Self::canonical_collection(collection))
3064 .map(|btree| btree.root_page_id())
3065 .filter(|root| *root != 0)
3066 }
3067
3068 pub(crate) fn operational_manifest_for_path(
3071 &self,
3072 path: &Path,
3073 ) -> reddb_file::OperationalManifest {
3074 let mut cached = self.operational_manifest.lock();
3075 if let Some((current, manifest)) = cached.as_ref() {
3076 if current == path {
3077 return manifest.clone();
3078 }
3079 }
3080 let manifest = reddb_file::OperationalManifest::for_db_path(path);
3081 *cached = Some((path.to_owned(), manifest.clone()));
3082 manifest
3083 }
3084
3085 pub fn db_path(&self) -> Option<&Path> {
3087 self.db_path.as_deref()
3088 }
3089}
3090
3091#[cfg(test)]
3092mod checkpoint_work_tests {
3093 use super::*;
3094 use reddb_file::PAGED_PAGE_SIZE as PAGE_SIZE;
3095
3096 struct SystemNameProjection;
3097
3098 impl super::super::secondary_index::SecondaryIndexProjector for SystemNameProjection {
3099 fn project(
3100 &self,
3101 entity: &UnifiedEntity,
3102 key: &mut [u8; super::super::secondary_index::SECONDARY_KEY_BYTES_MAX],
3103 ) -> Result<Option<usize>, StoreError> {
3104 key[..8].copy_from_slice(&entity.id.raw().to_be_bytes());
3105 Ok(Some(8))
3106 }
3107 }
3108
3109 #[cfg(target_os = "linux")]
3110 #[test]
3111 fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
3112 const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
3113 if let Some(path) = std::env::var_os(CHILD) {
3114 let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
3115 let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
3116 let store = UnifiedStore::open(path).expect("open store");
3117 let initial_count = if multi { 600 } else { 1 };
3118 for id in 1..=initial_count {
3119 store.insert_auto("rows", row(id, 1)).expect("seed");
3120 }
3121 store.persist().expect("baseline");
3122 let manager = store.get_collection("rows").expect("collection");
3123 let ids: Vec<u64> = if multi {
3124 (1..=initial_count).collect()
3125 } else {
3126 vec![if insert { 2 } else { 1 }]
3127 };
3128 let mut actions = Vec::new();
3129 for id in ids {
3130 if insert {
3131 manager.insert(row(id, 9)).expect("insert version");
3132 } else {
3133 manager.update(row(id, 9)).expect("update version");
3134 }
3135 let entity = manager.get(EntityId::new(id)).expect("changed entity");
3136 actions.push(StoreWalAction::upsert_entity(
3137 "rows",
3138 &entity,
3139 None,
3140 store.format_version(),
3141 ));
3142 }
3143 let references = store
3144 .commit
3145 .as_ref()
3146 .expect("coordinator")
3147 .append_checkpoint_actions(&actions)
3148 .expect("durable WAL");
3149 store.remember_checkpoint_rows(actions, references);
3150 store.persist().expect("exit after sparse write");
3151 panic!("inline sparse crash hook was not reached");
3152 }
3153 for (insert, multi) in [(false, false), (true, false), (false, true)] {
3154 let directory = tempfile::tempdir().expect("temporary store");
3155 let path = directory.path().join("data.rdb");
3156 let mut command =
3157 std::process::Command::new(std::env::current_exe().expect("test executable"));
3158 command
3159 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
3160 .env(CHILD, &path)
3161 .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
3162 if insert {
3163 command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
3164 }
3165 if multi {
3166 command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
3167 }
3168 let output = command.output().expect("crash child");
3169 assert_eq!(
3170 output.status.code(),
3171 Some(190),
3172 "insert={insert} multi={multi}: {}",
3173 String::from_utf8_lossy(&output.stderr)
3174 );
3175 let recovered = UnifiedStore::open(&path).expect("recover");
3176 let ids: Vec<u64> = if multi {
3177 (1..=600).collect()
3178 } else {
3179 vec![if insert { 2 } else { 1 }]
3180 };
3181 for id in ids {
3182 assert_eq!(
3183 recovered
3184 .get("rows", EntityId::new(id))
3185 .expect("recovered row")
3186 .data
3187 .as_row()
3188 .expect("row")
3189 .columns,
3190 vec![Value::Integer(9)]
3191 );
3192 }
3193 assert_eq!(
3194 recovered.get_collection("rows").expect("rows").count(),
3195 if multi {
3196 600
3197 } else if insert {
3198 2
3199 } else {
3200 1
3201 }
3202 );
3203 }
3204 }
3205
3206 #[test]
3207 fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
3208 let directory = tempfile::tempdir().expect("temporary store");
3209 let path = directory.path().join("data.rdb");
3210 {
3211 let store = UnifiedStore::open(&path).expect("open store");
3212 store.create_collection("rows").expect("create collection");
3213 store.persist().expect("publish ownership epoch");
3214 assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
3215 store.persist().expect("publish next ownership epoch");
3216 }
3217 let reopened = UnifiedStore::open(&path).expect("reopen store");
3218 assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
3219 }
3220
3221 #[test]
3222 fn metadata_overflow_with_missing_continuation_fails_closed() {
3223 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
3224
3225 let directory = tempfile::tempdir().expect("temporary store");
3226 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
3227 let pager = store.pager.as_ref().unwrap();
3228 let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
3229 let (valid_manifest, _) = build_meta_page1_with_overflow(pager, &valid_payload, 1).unwrap();
3230 pager.write_page(1, valid_manifest).unwrap();
3231 assert!(!existing_overflow_chain(pager).unwrap().is_empty());
3232 assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
3233
3234 let missing = pager.page_count().unwrap() + 100;
3235 let mut manifest = Page::new(PageType::Header, 1);
3236 reddb_file::encode_native_metadata_overflow_header(
3237 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
3238 reddb_file::NativeMetadataOverflowHeader {
3239 format_version: STORE_VERSION_V11,
3240 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
3241 next_overflow_page_id: missing,
3242 },
3243 )
3244 .unwrap();
3245 pager.write_page(1, manifest).unwrap();
3246
3247 assert!(
3248 existing_overflow_chain(pager).is_err(),
3249 "a missing continuation must not be treated as an empty chain"
3250 );
3251
3252 let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
3253 let mut manifest = Page::new(PageType::Header, 1);
3254 reddb_file::encode_native_metadata_overflow_header(
3255 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
3256 reddb_file::NativeMetadataOverflowHeader {
3257 format_version: STORE_VERSION_V11,
3258 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
3259 next_overflow_page_id: wrong_type.page_id(),
3260 },
3261 )
3262 .unwrap();
3263 pager.write_page(1, manifest).unwrap();
3264 assert!(
3265 existing_overflow_chain(pager).is_err(),
3266 "a live page must not be mistaken for metadata overflow"
3267 );
3268 assert!(
3269 read_meta_payload(pager).is_err(),
3270 "a live page must not be read as metadata overflow"
3271 );
3272
3273 let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
3274 let cycle_id = cycle.page_id();
3275 reddb_file::encode_native_metadata_overflow_continuation_header(
3276 &mut cycle.as_bytes_mut()[HEADER_SIZE..],
3277 reddb_file::NativeMetadataOverflowContinuationHeader {
3278 next_overflow_page_id: cycle_id,
3279 chunk_bytes: 1,
3280 },
3281 )
3282 .unwrap();
3283 pager.write_page(cycle_id, cycle).unwrap();
3284 let mut manifest = Page::new(PageType::Header, 1);
3285 reddb_file::encode_native_metadata_overflow_header(
3286 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
3287 reddb_file::NativeMetadataOverflowHeader {
3288 format_version: STORE_VERSION_V11,
3289 total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
3290 next_overflow_page_id: cycle_id,
3291 },
3292 )
3293 .unwrap();
3294 pager.write_page(1, manifest).unwrap();
3295 assert!(
3296 read_meta_payload(pager).is_err(),
3297 "cyclic overflow must not be accepted"
3298 );
3299
3300 let mut manifest = Page::new(PageType::Header, 1);
3301 reddb_file::encode_native_metadata_overflow_header(
3302 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
3303 reddb_file::NativeMetadataOverflowHeader {
3304 format_version: STORE_VERSION_V11,
3305 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
3306 next_overflow_page_id: 0,
3307 },
3308 )
3309 .unwrap();
3310 pager.write_page(1, manifest).unwrap();
3311 assert!(
3312 read_meta_payload(pager).is_err(),
3313 "a truncated metadata payload must not be accepted"
3314 );
3315 }
3316
3317 #[test]
3318 fn retired_tree_pages_remain_pinned_by_a_cursor() {
3319 let directory = tempfile::tempdir().expect("temporary store");
3320 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3321 for id in 1..=100 {
3322 store.insert_auto("rows", row(id, 1)).expect("seed");
3323 }
3324 store.persist().expect("initial checkpoint");
3325 let tree = store
3326 .btree_indices
3327 .read()
3328 .get("rows")
3329 .cloned()
3330 .expect("tree");
3331 let mut cursor = tree.cursor_first().expect("cursor");
3332 drop(tree);
3333 let manager = store.get_collection("rows").expect("rows");
3334 for value in 2..=6 {
3335 manager.update(row(1, value)).expect("change");
3336 store.persist().expect("checkpoint with old reader");
3337 }
3338 let mut count = 0;
3339 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
3340 let (entity, _) =
3341 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
3342 .expect("decode");
3343 assert_eq!(
3344 entity.data.as_row().expect("row").columns,
3345 [Value::Integer(1)]
3346 );
3347 count += 1;
3348 }
3349 assert_eq!(count, 100);
3350 drop(cursor);
3351 store.persist().expect("reclaim after reader release");
3352 assert!(store.retired_btrees.lock().is_empty());
3353 }
3354
3355 #[test]
3356 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
3357 let directory = tempfile::tempdir().expect("temporary store");
3358 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3359 store.create_collection("rows").expect("collection");
3360 let manager = store.get_collection("rows").expect("rows");
3361 for id in (1..=1200).rev() {
3362 manager
3363 .insert(UnifiedEntity::table_row(
3364 EntityId::new(id),
3365 "rows",
3366 id,
3367 vec![Value::text("x".repeat(4096))],
3368 ))
3369 .expect("seed");
3370 }
3371 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
3372 store.persist().expect("bounded checkpoint");
3373 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
3374 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
3375 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
3376 }
3377
3378 #[test]
3379 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
3380 let directory = tempfile::tempdir().expect("temporary store");
3381 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3382 store.create_collection("rows").expect("collection");
3383 for id in 1..=40 {
3384 store
3385 .insert(
3386 "rows",
3387 UnifiedEntity::table_row(
3388 EntityId::new(id),
3389 "rows",
3390 id,
3391 vec![Value::Integer(id as i64)],
3392 ),
3393 )
3394 .expect("seed row");
3395 }
3396 store.persist().expect("initial checkpoint");
3397 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3398 store.persist().expect("unchanged checkpoint");
3399 assert_eq!(
3400 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
3401 0,
3402 "unchanged collections must not hold the writer fence while serializing every row"
3403 );
3404 drop(store);
3405 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
3406 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3407 reopened.persist().expect("unchanged recovered generation");
3408 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3409 }
3410
3411 #[test]
3412 fn sparse_wal_replay_does_not_reencode_complete_collections() {
3413 let directory = tempfile::tempdir().expect("temporary store");
3414 let path = directory.path().join("data.rdb");
3415 let store = UnifiedStore::open(&path).expect("paged store");
3416 for (name, first) in [("rows", 1), ("cold", 129)] {
3417 store.create_collection(name).expect("collection");
3418 for id in first..first + 128 {
3419 store
3420 .insert(
3421 name,
3422 UnifiedEntity::table_row(
3423 EntityId::new(id),
3424 name,
3425 id,
3426 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
3427 ),
3428 )
3429 .expect("seed");
3430 }
3431 }
3432 store.persist().expect("durable baseline");
3433 let action = super::super::commit::StoreWalAction::upsert_entity(
3434 "rows",
3435 &row(1, 42),
3436 None,
3437 STORE_VERSION_V11,
3438 );
3439 store
3441 .commit
3442 .as_ref()
3443 .unwrap()
3444 .append_actions(&[action])
3445 .unwrap();
3446 drop(store);
3447
3448 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
3449 let recovered = UnifiedStore::open(&path).expect("replay committed change");
3450 assert_eq!(
3451 recovered
3452 .get("rows", EntityId::new(1))
3453 .unwrap()
3454 .data
3455 .as_row()
3456 .unwrap()
3457 .columns,
3458 vec![Value::Integer(42)]
3459 );
3460 for name in ["rows", "cold"] {
3461 assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
3462 }
3463 assert_eq!(
3464 REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
3465 0,
3466 "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
3467 );
3468 for (name, first) in [("rows", 2), ("cold", 129)] {
3469 let end = if name == "rows" { 129 } else { 257 };
3470 for id in first..end {
3471 assert_eq!(
3472 recovered
3473 .get(name, EntityId::new(id))
3474 .unwrap()
3475 .data
3476 .as_row()
3477 .unwrap()
3478 .columns,
3479 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
3480 );
3481 }
3482 }
3483
3484 drop(recovered);
3485 let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
3486 assert_eq!(
3487 reopened
3488 .get("rows", EntityId::new(1))
3489 .unwrap()
3490 .data
3491 .as_row()
3492 .unwrap()
3493 .columns,
3494 vec![Value::Integer(42)]
3495 );
3496 }
3497
3498 fn replay_test_payload(bytes: usize) -> String {
3499 let mut state = 0x1234_5678_u32;
3500 (0..bytes)
3501 .map(|_| {
3502 state ^= state << 13;
3503 state ^= state >> 17;
3504 state ^= state << 5;
3505 char::from(b'!' + (state % 90) as u8)
3506 })
3507 .collect()
3508 }
3509
3510 #[test]
3511 fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
3512 let directory = tempfile::tempdir().unwrap();
3513 let path = directory.path().join("data.rdb");
3514 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
3515 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3516 for id in 1..=16 {
3517 store.insert_auto("rows", row(id, 1)).unwrap();
3518 }
3519 store.persist().unwrap();
3520 let payload = replay_test_payload(64 * 1024);
3522 let changed =
3523 UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
3524 let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
3525 store
3526 .commit
3527 .as_ref()
3528 .unwrap()
3529 .append_actions(&[action])
3530 .unwrap();
3531 drop(store);
3532 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
3533 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3534 assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
3535 assert_eq!(
3536 recovered
3537 .get("rows", EntityId::new(1))
3538 .unwrap()
3539 .data
3540 .as_row()
3541 .unwrap()
3542 .columns,
3543 changed.data.as_row().unwrap().columns
3544 );
3545 drop(recovered);
3546 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3547 assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
3548 assert_eq!(
3549 reopened
3550 .get("rows", EntityId::new(1))
3551 .unwrap()
3552 .data
3553 .as_row()
3554 .unwrap()
3555 .columns,
3556 changed.data.as_row().unwrap().columns
3557 );
3558 }
3559
3560 #[cfg(target_os = "linux")]
3561 #[test]
3562 fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
3563 const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
3564 if let Some(path) = std::env::var_os(CHILD) {
3565 let _store = UnifiedStore::open(path).unwrap();
3566 panic!("replay crash hook was not reached");
3567 }
3568 for overflow in [false, true] {
3569 for (hook, code) in [
3570 ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
3571 ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
3572 ] {
3573 let directory = tempfile::tempdir().unwrap();
3574 let path = directory.path().join("data.rdb");
3575 let store = UnifiedStore::open(&path).unwrap();
3576 for id in 1..=128 {
3577 store.insert_auto("rows", row(id, 1)).unwrap();
3578 }
3579 store.persist().unwrap();
3580 let changed = if overflow {
3581 UnifiedEntity::table_row(
3582 EntityId::new(1),
3583 "rows",
3584 1,
3585 vec![Value::text(replay_test_payload(16 * 1024))],
3586 )
3587 } else {
3588 row(1, 42)
3589 };
3590 let action =
3591 StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
3592 store
3593 .commit
3594 .as_ref()
3595 .unwrap()
3596 .append_actions(&[action])
3597 .unwrap();
3598 drop(store);
3599 let mut first_size = 0;
3600 for cycle in 0..12 {
3601 let output = std::process::Command::new(std::env::current_exe().unwrap())
3602 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
3603 .env(CHILD, &path).env(hook, "1").output().unwrap();
3604 assert_eq!(
3605 output.status.code(),
3606 Some(code),
3607 "overflow={overflow} hook={hook} cycle={cycle}: {}",
3608 String::from_utf8_lossy(&output.stderr)
3609 );
3610 let size = std::fs::metadata(&path).unwrap().len();
3611 if cycle == 0 {
3612 first_size = size;
3613 }
3614 assert!(
3615 size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
3616 "repeated recovery must reuse reservations: {first_size} -> {size}"
3617 );
3618 }
3619 for _ in 0..2 {
3620 let recovered = UnifiedStore::open(&path).unwrap();
3621 assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
3622 for id in 1..=128 {
3623 let expected = if id == 1 { changed.clone() } else { row(id, 1) };
3624 assert_eq!(
3625 recovered
3626 .get("rows", EntityId::new(id))
3627 .unwrap()
3628 .data
3629 .as_row()
3630 .unwrap()
3631 .columns,
3632 expected.data.as_row().unwrap().columns
3633 );
3634 }
3635 }
3636 }
3637 }
3638 }
3639
3640 #[test]
3641 fn paged_reopen_preserves_stored_sequence_identity() {
3642 let directory = tempfile::tempdir().expect("temporary store");
3643 let path = directory.path().join("data.rdb");
3644 let store = UnifiedStore::open(&path).expect("paged store");
3645 store.create_collection("rows").expect("collection");
3646 for id in [30, 10, 20] {
3647 store.insert("rows", row(id, id as i64)).expect("insert");
3648 }
3649 store.persist().expect("checkpoint");
3650 let sequences = [10, 20, 30].map(|id| {
3651 store
3652 .get("rows", EntityId::new(id))
3653 .expect("row")
3654 .sequence_id
3655 });
3656 drop(store);
3657 let reopened = UnifiedStore::open(&path).expect("reopen");
3658 let recovered = [10, 20, 30].map(|id| {
3659 reopened
3660 .get("rows", EntityId::new(id))
3661 .expect("row")
3662 .sequence_id
3663 });
3664 assert_eq!(
3665 recovered, sequences,
3666 "opening a checkpoint must not rewrite stored row identities"
3667 );
3668 }
3669
3670 #[test]
3671 fn auto_insert_persists_the_assigned_sequence_identity() {
3672 let directory = tempfile::tempdir().expect("temporary store");
3673 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
3674 for id in 1u64..=3 {
3675 store
3676 .insert_auto("rows", row(id, id as i64))
3677 .expect("insert");
3678 }
3679 let manager = store.get_collection("rows").expect("rows");
3680 let tree = store
3681 .btree_indices
3682 .read()
3683 .get("rows")
3684 .cloned()
3685 .expect("tree");
3686 for id in 1u64..=3 {
3687 let bytes = tree
3688 .get(&id.to_be_bytes())
3689 .expect("lookup")
3690 .expect("stored row");
3691 let (persisted, _) =
3692 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
3693 .expect("decode");
3694 assert_eq!(
3695 persisted.sequence_id,
3696 manager
3697 .get(EntityId::new(id))
3698 .expect("live row")
3699 .sequence_id
3700 );
3701 }
3702 }
3703
3704 #[test]
3705 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
3706 let directory = tempfile::tempdir().expect("temporary store");
3707 let path = directory.path().join("data.rdb");
3708 let store = UnifiedStore::open(&path).expect("paged store");
3709 store.insert_auto("rows", row(1, 1)).expect("seed");
3710 store.persist().expect("checkpoint");
3711 store.insert_auto("rows", row(2, 2)).expect("append");
3712 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3713 store.persist().expect("inline append checkpoint");
3714 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3715 store
3716 .get_collection("rows")
3717 .expect("rows")
3718 .update(row(1, 99))
3719 .expect("untracked edit");
3720 store.insert_auto("rows", row(3, 3)).expect("next append");
3721 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3722 store.persist().expect("untracked edit checkpoint");
3723 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
3724 drop(store);
3725 let reopened = UnifiedStore::open(&path).expect("reopen");
3726 assert_eq!(
3727 reopened
3728 .get("rows", EntityId::new(1))
3729 .expect("row")
3730 .data
3731 .as_row()
3732 .expect("table")
3733 .columns,
3734 vec![Value::Integer(99)]
3735 );
3736 }
3737
3738 #[test]
3739 fn replayed_trees_are_reusable_after_durable_reconciliation() {
3740 let directory = tempfile::tempdir().expect("temporary store");
3741 let path = directory.path().join("data.rdb");
3742 let store = UnifiedStore::open(&path).expect("paged store");
3743 store.insert_auto("rows", row(1, 1)).expect("seed");
3744 store.persist().expect("checkpoint");
3745 store
3746 .insert_auto("rows", row(2, 2))
3747 .expect("WAL-backed append");
3748 drop(store);
3749 let reopened = UnifiedStore::open(&path).expect("replay");
3750 assert!(reopened.get("rows", EntityId::new(2)).is_some());
3751 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3752 reopened.persist().expect("checkpoint reconciled tree");
3753 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3754 }
3755
3756 #[test]
3757 fn legacy_system_catalog_and_wal_migrate_without_cold_boot_rebuild() {
3758 use crate::storage::wal::{WalRecord, WalWriter};
3759
3760 let directory = tempfile::tempdir().expect("temporary store");
3761 let path = directory.path().join("data.rdb");
3762 let config = UnifiedStoreConfig {
3763 page_cache_slots: Some(32),
3764 resident_memory_bytes_max: Some(2 * 1024 * 1024),
3765 ..UnifiedStoreConfig::default()
3766 };
3767 let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
3768 codec_version: 1,
3769 definition: [91; 32],
3770 };
3771 let baseline_index_root;
3772 {
3773 let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
3774 store
3775 .create_collection("red_config")
3776 .expect("system collection");
3777 store
3778 .insert_auto(
3779 "red_config",
3780 UnifiedEntity::table_row(
3781 EntityId::new(1),
3782 "red_config",
3783 1,
3784 vec![Value::text("checkpoint")],
3785 ),
3786 )
3787 .expect("legacy-compatible insert");
3788 store
3789 .register_secondary_index(
3790 "red_config",
3791 "by_id",
3792 index_identity.clone(),
3793 Arc::new(SystemNameProjection),
3794 )
3795 .expect("secondary index");
3796 store.persist().expect("baseline checkpoint");
3797 baseline_index_root = store
3798 .secondary_index("red.config", "by_id", &index_identity)
3799 .expect("published secondary index")
3800 .root_page_id();
3801 }
3802
3803 {
3807 let pager = Pager::open(
3808 &path,
3809 PagerConfig {
3810 disk_residency: true,
3811 ..PagerConfig::default()
3812 },
3813 )
3814 .expect("open pager");
3815 let mut page = pager.read_page(1).expect("catalog page");
3816 let bytes = page.as_bytes_mut();
3817 let canonical = b"red.config";
3818 let legacy = b"red_config";
3819 let mut replacements = 0;
3820 for offset in 0..=bytes.len() - canonical.len() {
3821 if &bytes[offset..offset + canonical.len()] == canonical {
3822 bytes[offset..offset + legacy.len()].copy_from_slice(legacy);
3823 replacements += 1;
3824 }
3825 }
3826 assert!(replacements >= 1, "catalog must contain the canonical name");
3827 pager.write_page(1, page).expect("write legacy catalog");
3828 pager.sync().expect("sync legacy catalog");
3829 }
3830 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3831 .rename_collections(&[("red.config", "red_config")])
3832 .expect("restore legacy operational name");
3833
3834 let wal_upsert = reddb_file::encode_store_wal_action_frame(
3837 &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
3838 collection: "red_config".to_string(),
3839 record: UnifiedStore::serialize_entity_record(
3840 &UnifiedEntity::table_row(
3841 EntityId::new(2),
3842 "red_config",
3843 2,
3844 vec![Value::text("legacy WAL")],
3845 ),
3846 None,
3847 STORE_VERSION_CURRENT,
3848 ),
3849 },
3850 )
3851 .expect("legacy WAL action");
3852 let wal_delete = reddb_file::encode_store_wal_action_frame(
3853 &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
3854 collection: "red_config".to_string(),
3855 entity_id: EntityId::new(1).raw(),
3856 },
3857 )
3858 .expect("legacy WAL delete");
3859 let wal_path = reddb_file::layout::unified_wal_path(&path);
3860 {
3861 let mut writer = WalWriter::open(&wal_path).expect("open WAL");
3862 writer
3863 .append(&WalRecord::TxCommitBatch {
3864 tx_id: 77,
3865 actions: vec![wal_upsert, wal_delete],
3866 })
3867 .expect("append legacy transaction");
3868 writer.sync().expect("sync legacy transaction");
3869 }
3870
3871 let (root_after_recovery, pages_after_recovery) = {
3872 let recovered =
3873 UnifiedStore::open_with_config(&path, config.clone()).expect("migrate and recover");
3874 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
3875 assert!(
3876 recovered.get("red.config", EntityId::new(1)).is_none(),
3877 "legacy WAL deletes must target the canonical collection"
3878 );
3879 assert_eq!(
3880 recovered
3881 .get("red.config", EntityId::new(2))
3882 .expect("WAL row")
3883 .kind
3884 .collection(),
3885 "red.config"
3886 );
3887 let root = recovered
3888 .collection_root_page("red.config")
3889 .expect("published canonical root");
3890 assert_eq!(
3891 recovered
3892 .secondary_index("red.config", "by_id", &index_identity)
3893 .expect("recovered secondary index")
3894 .root_page_id(),
3895 baseline_index_root,
3896 "name migration must reuse the secondary-index root"
3897 );
3898 let pages = recovered
3899 .pager()
3900 .expect("pager")
3901 .page_count()
3902 .expect("page count");
3903 (root, pages)
3904 };
3905
3906 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3909 .rename_collections(&[("red.config", "red_config")])
3910 .expect("restore stale manifest identity");
3911
3912 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
3913 let cold = UnifiedStore::open_with_config(&path, config).expect("second cold boot");
3914 assert_eq!(
3915 cold.collection_root_page("red.config"),
3916 Some(root_after_recovery),
3917 "an idempotent cold boot must reuse the published data root"
3918 );
3919 assert_eq!(
3920 cold.pager()
3921 .expect("pager")
3922 .page_count()
3923 .expect("page count"),
3924 pages_after_recovery,
3925 "cold boot must not allocate replacement index trees"
3926 );
3927 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
3928 assert_eq!(
3929 cold.secondary_index("red.config", "by_id", &index_identity)
3930 .expect("secondary index after cold boot")
3931 .root_page_id(),
3932 baseline_index_root
3933 );
3934 assert!(
3935 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
3936 .recover_or_bootstrap(&["red.config".to_string()])
3937 .expect("canonical manifest")
3938 .is_empty()
3939 );
3940 }
3941
3942 struct DuplicateSystemCatalogFixture {
3943 _directory: tempfile::TempDir,
3944 path: std::path::PathBuf,
3945 config: UnifiedStoreConfig,
3946 pages_before: u32,
3947 legacy_root: Option<u32>,
3948 legacy_key_root: Option<u32>,
3949 legacy_secondary_root: Option<u32>,
3950 index_identity: super::super::secondary_index::SecondaryIndexIdentity,
3951 }
3952
3953 fn duplicate_system_catalog_fixture(
3954 legacy_populated: bool,
3955 canonical_populated: bool,
3956 index_legacy: bool,
3957 ) -> DuplicateSystemCatalogFixture {
3958 duplicate_system_catalog_fixture_with_ref(
3959 legacy_populated,
3960 canonical_populated,
3961 index_legacy,
3962 false,
3963 )
3964 }
3965
3966 fn duplicate_system_catalog_fixture_with_ref(
3967 legacy_populated: bool,
3968 canonical_populated: bool,
3969 index_legacy: bool,
3970 cross_reference_legacy: bool,
3971 ) -> DuplicateSystemCatalogFixture {
3972 duplicate_system_catalog_fixture_with_key(
3973 legacy_populated,
3974 canonical_populated,
3975 index_legacy,
3976 cross_reference_legacy,
3977 None,
3978 )
3979 }
3980
3981 fn config_recovery_row(id: u64, key: &str, value: i64) -> UnifiedEntity {
3982 UnifiedEntity::new(
3983 EntityId::new(id),
3984 EntityKind::TableRow {
3985 table: Arc::from("red.config"),
3986 row_id: id,
3987 },
3988 EntityData::Row(crate::storage::RowData::with_names(
3989 vec![Value::text(key), Value::Integer(value)],
3990 vec!["key".into(), "value".into()],
3991 )),
3992 )
3993 }
3994
3995 fn duplicate_system_catalog_fixture_with_key(
3996 legacy_populated: bool,
3997 canonical_populated: bool,
3998 index_legacy: bool,
3999 cross_reference_legacy: bool,
4000 config_key: Option<&str>,
4001 ) -> DuplicateSystemCatalogFixture {
4002 duplicate_system_catalog_fixture_with_aliases(
4003 legacy_populated,
4004 canonical_populated,
4005 index_legacy,
4006 cross_reference_legacy,
4007 config_key,
4008 None,
4009 )
4010 }
4011
4012 fn duplicate_system_catalog_fixture_with_aliases(
4013 legacy_populated: bool,
4014 canonical_populated: bool,
4015 index_legacy: bool,
4016 cross_reference_legacy: bool,
4017 config_key: Option<&str>,
4018 alias_case: Option<&str>,
4019 ) -> DuplicateSystemCatalogFixture {
4020 let directory = tempfile::tempdir().expect("temporary store");
4021 let path = directory.path().join("data.rdb");
4022 let config = UnifiedStoreConfig {
4023 page_cache_slots: Some(32),
4024 resident_memory_bytes_max: Some(2 * 1024 * 1024),
4025 ..UnifiedStoreConfig::default()
4026 };
4027 let index_identity = super::super::secondary_index::SecondaryIndexIdentity {
4028 codec_version: 1,
4029 definition: [73; 32],
4030 };
4031 let (legacy_root, legacy_key_root, legacy_secondary_root);
4032 {
4033 let store = UnifiedStore::open_with_config(&path, config.clone()).expect("paged store");
4034 store
4035 .create_collection("cfg_legacy")
4036 .expect("legacy source collection");
4037 if legacy_populated {
4038 store
4039 .insert_auto(
4040 "cfg_legacy",
4041 config_key.map_or_else(
4042 || {
4043 UnifiedEntity::table_row(
4044 EntityId::new(1),
4045 "red_config",
4046 1,
4047 vec![Value::text("legacy")],
4048 )
4049 },
4050 |key| config_recovery_row(1, key, 1),
4051 ),
4052 )
4053 .expect("legacy row");
4054 }
4055 store
4056 .create_collection("cfg.canon_")
4057 .expect("canonical source collection");
4058 if canonical_populated {
4059 store
4060 .insert_auto(
4061 "cfg.canon_",
4062 config_key.map_or_else(
4063 || {
4064 UnifiedEntity::table_row(
4065 EntityId::new(2),
4066 "red.config",
4067 2,
4068 vec![Value::text("canonical")],
4069 )
4070 },
4071 |key| config_recovery_row(2, key, 2),
4072 ),
4073 )
4074 .expect("canonical row");
4075 }
4076 if cross_reference_legacy {
4077 store
4078 .add_cross_ref(
4079 "cfg.canon_",
4080 EntityId::new(2),
4081 "cfg_legacy",
4082 EntityId::new(1),
4083 RefType::RowToNode,
4084 1.0,
4085 )
4086 .expect("reference to legacy config");
4087 }
4088 if index_legacy {
4089 store
4090 .register_secondary_index(
4091 "cfg_legacy",
4092 "by_id",
4093 index_identity.clone(),
4094 Arc::new(SystemNameProjection),
4095 )
4096 .expect("legacy secondary index");
4097 }
4098
4099 if let Some(case) = alias_case {
4100 use crate::storage::{EntityData, RowData};
4101 let pairs = [
4102 (
4103 "old_stats",
4104 "new.query.stats_catalog",
4105 "red_stats",
4106 "red.query.stats_catalog",
4107 ),
4108 (
4109 "old_index_registry",
4110 "new.index.registry",
4111 "red_index_registry",
4112 "red.index.registry",
4113 ),
4114 (
4115 "old_queue_meta",
4116 "new.queue.meta",
4117 "red_queue_meta",
4118 "red.queue.meta",
4119 ),
4120 (
4121 "old_materialized_view_defs",
4122 "new.materialized_view.definitions",
4123 "red_materialized_view_defs",
4124 "red.materialized_view.definitions",
4125 ),
4126 ];
4127 for (pair_index, (old, new, legacy, canonical)) in pairs.iter().enumerate() {
4128 for (is_old, name, table) in [(true, *old, *legacy), (false, *new, *canonical)]
4129 {
4130 store.create_collection(name).unwrap();
4131 let text = |s: &str| Value::text(s);
4132 let mut fields = match pair_index {
4133 0 => vec![
4134 ("kind", text("table")),
4135 ("table", text("users")),
4136 ("row_count", Value::Integer(if is_old { 1 } else { 2 })),
4137 ("avg_row_size", Value::Integer(20)),
4138 ("page_count", Value::Integer(1)),
4139 ],
4140 1 => vec![
4141 ("collection", text("users")),
4142 (
4143 "name",
4144 text(if is_old {
4145 "legacy_only"
4146 } else {
4147 "canonical_only"
4148 }),
4149 ),
4150 ("columns", text("id")),
4151 ("method", text("hash")),
4152 ("resolution", Value::Integer(0)),
4153 ("unique", Value::Boolean(false)),
4154 ("dropped", Value::Boolean(false)),
4155 ],
4156 2 => vec![
4157 ("kind", text("queue_group")),
4158 ("queue", text("jobs")),
4159 ("group", text("workers")),
4160 (
4161 "created_at_ns",
4162 Value::UnsignedInteger(if is_old { 1 } else { 2 }),
4163 ),
4164 ],
4165 _ => vec![
4166 ("name", text("view")),
4167 ("refresh_every_ms", Value::UnsignedInteger(1000)),
4168 ("retention_duration_ms", Value::UnsignedInteger(0)),
4169 ("source_collections", text("users")),
4170 (
4171 "source_sql",
4172 text(if case == "view-conflict" && !is_old {
4173 "SELECT 2"
4174 } else {
4175 "SELECT 1"
4176 }),
4177 ),
4178 ],
4179 };
4180 if case == "queue-delivery" && pair_index == 2 && is_old {
4181 fields[0].1 = text("queue_pending");
4182 }
4183 let id = if is_old {
4184 10000 + pair_index as u64
4185 } else {
4186 100 + pair_index as u64
4187 };
4188 let entity = UnifiedEntity::new(
4189 EntityId::new(id),
4190 EntityKind::TableRow {
4191 table: Arc::from(table),
4192 row_id: id,
4193 },
4194 EntityData::Row(RowData {
4195 columns: vec![],
4196 named: Some(
4197 fields.into_iter().map(|(k, v)| (k.to_owned(), v)).collect(),
4198 ),
4199 schema: None,
4200 }),
4201 );
4202 store.insert_auto(name, entity).unwrap();
4203 }
4204 }
4205 }
4206 store.persist().expect("checkpoint duplicate catalog");
4207 legacy_root = store.collection_root_page("cfg_legacy");
4208 legacy_key_root = store
4209 .key_indices
4210 .read()
4211 .get("cfg_legacy")
4212 .map(|index| index.tree.root_page_id());
4213 legacy_secondary_root = index_legacy.then(|| {
4214 store
4215 .secondary_index("cfg_legacy", "by_id", &index_identity)
4216 .expect("legacy secondary root")
4217 .root_page_id()
4218 });
4219 }
4220
4221 {
4226 let pager = Pager::open(
4227 &path,
4228 PagerConfig {
4229 disk_residency: true,
4230 ..PagerConfig::default()
4231 },
4232 )
4233 .expect("open pager");
4234 let mut page = pager.read_page(1).expect("catalog page");
4235 let bytes = page.as_bytes_mut();
4236 let mut names = vec![("cfg_legacy", "red_config"), ("cfg.canon_", "red.config")];
4237 if alias_case.is_some() {
4238 names.extend([
4239 ("old_stats", "red_stats"),
4240 ("new.query.stats_catalog", "red.query.stats_catalog"),
4241 ("old_index_registry", "red_index_registry"),
4242 ("new.index.registry", "red.index.registry"),
4243 ("old_queue_meta", "red_queue_meta"),
4244 ("new.queue.meta", "red.queue.meta"),
4245 ("old_materialized_view_defs", "red_materialized_view_defs"),
4246 (
4247 "new.materialized_view.definitions",
4248 "red.materialized_view.definitions",
4249 ),
4250 ]);
4251 }
4252 for (source, destination) in names {
4253 let (source, destination) = (source.as_bytes(), destination.as_bytes());
4254 let mut replacements = 0;
4255 for offset in 0..=bytes.len() - source.len() {
4256 if &bytes[offset..offset + source.len()] == source {
4257 bytes[offset..offset + destination.len()].copy_from_slice(destination);
4258 replacements += 1;
4259 }
4260 }
4261 assert!(replacements >= 1, "catalog must contain the source name");
4262 }
4263 pager.write_page(1, page).expect("write duplicate catalog");
4264 pager.sync().expect("sync duplicate catalog");
4265 if alias_case.is_some() {
4266 let mut metadata = read_meta_payload(&pager).unwrap().unwrap();
4267 let resume = reddb_file::native_resume::decode_catalog_resume(&metadata)
4268 .unwrap()
4269 .unwrap();
4270 let trailer = metadata
4271 .windows(4)
4272 .rposition(|bytes| bytes == b"RDRS")
4273 .unwrap();
4274 metadata.truncate(trailer);
4275 reddb_file::native_resume::encode_catalog_resume(&mut metadata, &resume).unwrap();
4276 publish_paged_metadata(&pager, &metadata).unwrap();
4277 pager.sync().unwrap();
4278 }
4279 }
4280 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
4281 .rename_collections(&[("cfg_legacy", "red_config"), ("cfg.canon_", "red.config")])
4282 .expect("publish duplicate operational names");
4283
4284 if alias_case.is_some() {
4285 crate::storage::operational_manifest::OperationalManifest::for_db_path(&path)
4286 .rename_collections(&[
4287 ("old_stats", "red_stats"),
4288 ("new.query.stats_catalog", "red.query.stats_catalog"),
4289 ("old_index_registry", "red_index_registry"),
4290 ("new.index.registry", "red.index.registry"),
4291 ("old_queue_meta", "red_queue_meta"),
4292 ("new.queue.meta", "red.queue.meta"),
4293 ("old_materialized_view_defs", "red_materialized_view_defs"),
4294 (
4295 "new.materialized_view.definitions",
4296 "red.materialized_view.definitions",
4297 ),
4298 ])
4299 .unwrap();
4300 }
4301
4302 let pages_before = Pager::open(
4303 &path,
4304 PagerConfig {
4305 disk_residency: true,
4306 ..PagerConfig::default()
4307 },
4308 )
4309 .expect("inspect pager")
4310 .page_count()
4311 .expect("page count");
4312 DuplicateSystemCatalogFixture {
4313 _directory: directory,
4314 path,
4315 config,
4316 pages_before,
4317 legacy_root,
4318 legacy_key_root,
4319 legacy_secondary_root,
4320 index_identity,
4321 }
4322 }
4323
4324 fn remove_fixture_catalog_resume(fixture: &DuplicateSystemCatalogFixture) {
4325 let pager = Pager::open(
4326 &fixture.path,
4327 PagerConfig {
4328 disk_residency: true,
4329 ..PagerConfig::default()
4330 },
4331 )
4332 .expect("fixture pager");
4333 let mut metadata = read_meta_payload(&pager).unwrap().unwrap();
4334 assert!(reddb_file::native_resume::decode_catalog_resume(&metadata)
4335 .unwrap()
4336 .is_some());
4337 let trailer = metadata
4338 .windows(4)
4339 .rposition(|bytes| bytes == b"RDRS")
4340 .unwrap();
4341 metadata.truncate(trailer);
4342 publish_paged_metadata(&pager, &metadata).unwrap();
4343 pager.sync().unwrap();
4344 assert!(reddb_file::native_resume::decode_catalog_resume(
4345 &read_meta_payload(&pager).unwrap().unwrap()
4346 )
4347 .unwrap()
4348 .is_none());
4349 }
4350
4351 #[test]
4352 fn no_resume_catalog_keeps_populated_config_beside_zero_root_alias() {
4353 for (legacy_populated, canonical_populated) in
4354 [(true, false), (false, true), (false, false)]
4355 {
4356 let fixture =
4357 duplicate_system_catalog_fixture(legacy_populated, canonical_populated, false);
4358 remove_fixture_catalog_resume(&fixture);
4359 let store = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
4360 .expect("resolve physically empty alias without resume counters");
4361 assert_eq!(store.list_collections(), vec!["red.config".to_owned()]);
4362 assert_eq!(
4363 store.get("red.config", EntityId::new(1)).is_some(),
4364 legacy_populated
4365 );
4366 assert_eq!(
4367 store.get("red.config", EntityId::new(2)).is_some(),
4368 canonical_populated
4369 );
4370 if legacy_populated {
4371 assert_eq!(
4372 store.collection_root_page("red.config"),
4373 fixture.legacy_root
4374 );
4375 }
4376 store.persist().expect("checkpoint canonical catalog");
4377 drop(store);
4378 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4379 .expect("cold reopen canonical catalog");
4380 assert_eq!(cold.list_collections(), vec!["red.config".to_owned()]);
4381 assert_eq!(
4382 cold.get("red.config", EntityId::new(1)).is_some(),
4383 legacy_populated
4384 );
4385 assert_eq!(
4386 cold.get("red.config", EntityId::new(2)).is_some(),
4387 canonical_populated
4388 );
4389 }
4390 }
4391
4392 #[test]
4393 fn no_resume_catalog_refuses_two_populated_config_roots() {
4394 let fixture = duplicate_system_catalog_fixture(true, true, false);
4395 remove_fixture_catalog_resume(&fixture);
4396 let error = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4397 .err()
4398 .expect("two populated roots require reconciliation");
4399 assert!(error.to_string().contains("both legacy collection"));
4400 }
4401
4402 fn alias_duplicate_system_catalog_state_with(
4403 fixture: &DuplicateSystemCatalogFixture,
4404 mutate_canonical: impl FnOnce(&mut reddb_file::native_resume::CollectionResume),
4405 ) {
4406 let pager = Pager::open(
4407 &fixture.path,
4408 PagerConfig {
4409 disk_residency: true,
4410 ..PagerConfig::default()
4411 },
4412 )
4413 .expect("open duplicate catalog pager");
4414 let mut metadata = read_meta_payload(&pager)
4415 .expect("read duplicate catalog")
4416 .expect("published duplicate catalog");
4417 let mut resume = reddb_file::native_resume::decode_catalog_resume(&metadata)
4418 .expect("decode duplicate resume")
4419 .expect("prepared duplicate resume");
4420 let legacy_state = resume
4421 .collections
4422 .get("red_config")
4423 .expect("legacy resume state")
4424 .clone();
4425 let mut canonical_state = legacy_state.clone();
4426 mutate_canonical(&mut canonical_state);
4427 resume
4428 .collections
4429 .insert("red.config".to_string(), canonical_state);
4430
4431 let header = reddb_file::decode_native_paged_metadata_header(&metadata)
4432 .expect("decode metadata header")
4433 .expect("native metadata header");
4434 let mut position = reddb_file::METADATA_HEADER_BYTES;
4435 let mut replaced_root = false;
4436 for _ in 0..header.collection_count {
4437 let root = reddb_file::decode_native_paged_collection_root(&metadata, &mut position)
4438 .expect("decode collection root");
4439 if root.collection == "red.config" {
4440 metadata[position - 4..position]
4441 .copy_from_slice(&legacy_state.root_page.to_le_bytes());
4442 replaced_root = true;
4443 }
4444 }
4445 assert!(replaced_root, "canonical catalog root must be present");
4446
4447 let trailer = metadata
4448 .windows(4)
4449 .rposition(|bytes| bytes == b"RDRS")
4450 .expect("catalog resume trailer");
4451 metadata.truncate(trailer);
4452 reddb_file::native_resume::encode_catalog_resume(&mut metadata, &resume)
4453 .expect("encode aliased resume");
4454 publish_paged_metadata(&pager, &metadata).expect("publish aliased catalog");
4455 }
4456
4457 fn alias_duplicate_system_catalog_state(fixture: &DuplicateSystemCatalogFixture) {
4458 alias_duplicate_system_catalog_state_with(fixture, |_| {});
4459 }
4460
4461 #[test]
4462 fn populated_legacy_system_collection_replaces_empty_canonical_duplicate() {
4463 let fixture = duplicate_system_catalog_fixture(true, false, true);
4464 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
4465 .expect("resolve empty duplicate");
4466 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4467 assert_eq!(
4468 recovered
4469 .get("red.config", EntityId::new(1))
4470 .expect("preserved legacy row")
4471 .kind
4472 .collection(),
4473 "red.config"
4474 );
4475 assert_eq!(
4476 recovered.collection_root_page("red.config"),
4477 fixture.legacy_root,
4478 "the populated data root must be reused"
4479 );
4480 assert_eq!(
4481 recovered
4482 .key_indices
4483 .read()
4484 .get("red.config")
4485 .map(|index| index.tree.root_page_id()),
4486 fixture.legacy_key_root,
4487 "the populated key-index root must be reused"
4488 );
4489 assert_eq!(
4490 recovered
4491 .secondary_index("red.config", "by_id", &fixture.index_identity)
4492 .expect("surviving secondary index")
4493 .root_page_id(),
4494 fixture
4495 .legacy_secondary_root
4496 .expect("fixture secondary root")
4497 );
4498 drop(recovered);
4499
4500 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4501 .expect("idempotent cold boot");
4502 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
4503 assert_eq!(
4504 cold.pager()
4505 .expect("pager")
4506 .page_count()
4507 .expect("page count"),
4508 fixture.pages_before,
4509 "resolving an empty duplicate must not extend the file"
4510 );
4511 }
4512
4513 #[test]
4514 fn populated_canonical_system_collection_replaces_empty_legacy_duplicate() {
4515 let fixture = duplicate_system_catalog_fixture(false, true, false);
4516 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4517 .expect("retain populated canonical duplicate");
4518 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4519 assert_eq!(
4520 recovered
4521 .get("red.config", EntityId::new(2))
4522 .expect("preserved canonical row")
4523 .kind
4524 .collection(),
4525 "red.config"
4526 );
4527 assert!(recovered.get("red.config", EntityId::new(1)).is_none());
4528 }
4529
4530 #[test]
4531 fn empty_canonical_system_collection_replaces_empty_legacy_duplicate() {
4532 let fixture = duplicate_system_catalog_fixture(false, false, false);
4533 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config)
4534 .expect("prefer canonical empty duplicate");
4535 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
4536 assert_eq!(
4537 recovered
4538 .get_collection("red.config")
4539 .expect("canonical collection")
4540 .count(),
4541 0
4542 );
4543 }
4544
4545 #[test]
4546 fn populated_system_collection_duplicates_still_fail_closed() {
4547 let fixture = duplicate_system_catalog_fixture(true, true, false);
4548 let error = match UnifiedStore::open_with_config(&fixture.path, fixture.config) {
4549 Ok(_) => panic!("two populated identities are ambiguous"),
4550 Err(error) => error,
4551 };
4552 assert!(
4553 error
4554 .to_string()
4555 .contains("both populated legacy collection 'red_config' and canonical collection 'red.config'"),
4556 "unexpected error: {error}"
4557 );
4558 assert!(
4559 error.to_string().contains(
4560 "data_root_match=false, key_index_root_match=true, secondary_index_state_match=true"
4561 ),
4562 "the error must identify which physical state diverged without exposing page IDs: {error}"
4563 );
4564 }
4565
4566 #[test]
4567 fn explicit_compaction_retires_populated_legacy_system_config() {
4568 let fixture = duplicate_system_catalog_fixture(true, true, false);
4569 let destination = fixture._directory.path().join("canonical-system-config");
4570
4571 let report = crate::storage::compact::compact_paged_canonical_system_config(
4572 &fixture.path,
4573 &destination,
4574 )
4575 .expect("rewrite duplicate system config");
4576 assert!(report.records > 0);
4577
4578 let rewritten_path = destination.join("data.rdb");
4579 let rewritten = UnifiedStore::open_with_config(&rewritten_path, fixture.config.clone())
4580 .expect("open canonical rewrite");
4581 assert_eq!(rewritten.list_collections(), vec!["red.config".to_string()]);
4582 assert!(
4583 rewritten.get("red.config", EntityId::new(1)).is_none(),
4584 "the obsolete legacy tree must not leak into the canonical collection"
4585 );
4586 assert_eq!(
4587 rewritten
4588 .get("red.config", EntityId::new(2))
4589 .expect("canonical row survives")
4590 .kind
4591 .collection(),
4592 "red.config"
4593 );
4594 drop(rewritten);
4595
4596 let cold = UnifiedStore::open_with_config(&rewritten_path, fixture.config)
4597 .expect("cold reopen canonical rewrite");
4598 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
4599 assert_eq!(
4600 cold.get_collection("red.config")
4601 .expect("canonical collection")
4602 .count(),
4603 1
4604 );
4605 assert!(
4606 crate::storage::operational_manifest::OperationalManifest::for_db_path(&rewritten_path)
4607 .recover_or_bootstrap(&["red.config".to_string()])
4608 .expect("canonical operational manifest")
4609 .is_empty()
4610 );
4611 }
4612
4613 #[test]
4614 fn canonical_system_config_recovery_replays_pending_unrelated_wal() {
4615 use crate::storage::wal::{WalRecord, WalWriter};
4616
4617 let fixture = duplicate_system_catalog_fixture(true, true, true);
4618 let wal = reddb_file::unified_wal_path(&fixture.path);
4619 let action = reddb_file::encode_store_wal_action_frame(
4620 &reddb_file::StoreWalActionFrame::CreateCollection {
4621 name: "pending_collection".into(),
4622 },
4623 )
4624 .expect("encode pending action");
4625 let canonical_update = reddb_file::encode_store_wal_action_frame(
4626 &reddb_file::StoreWalActionFrame::UpsertEntityRecord {
4627 collection: "red.config".into(),
4628 record: UnifiedStore::serialize_entity_record(
4629 &UnifiedEntity::table_row(
4630 EntityId::new(3),
4631 "red.config",
4632 3,
4633 vec![Value::text("canonical WAL")],
4634 ),
4635 None,
4636 STORE_VERSION_CURRENT,
4637 ),
4638 },
4639 )
4640 .expect("encode canonical update");
4641 {
4642 let mut writer = WalWriter::open(&wal).expect("open WAL");
4643 writer
4644 .append(&WalRecord::TxCommitBatch {
4645 tx_id: 999,
4646 actions: vec![action, canonical_update],
4647 })
4648 .expect("append committed WAL");
4649 writer.sync().expect("sync WAL");
4650 }
4651 let original = std::fs::read(&fixture.path).expect("source bytes");
4652 let original_wal = std::fs::read(&wal).expect("WAL bytes");
4653 let destination = fixture._directory.path().join("recovered-canonical-config");
4654
4655 let default_destination = fixture._directory.path().join("default-recovery");
4656 let error = crate::storage::compact::recover_paged_wal(&fixture.path, &default_destination)
4657 .expect_err("ordinary recovery must reject divergent identities");
4658 assert!(error
4659 .to_string()
4660 .contains("both populated legacy collection"));
4661
4662 crate::storage::compact::recover_paged_wal_canonical_system_config(
4663 &fixture.path,
4664 &destination,
4665 )
4666 .expect("recover pending WAL and retain canonical config");
4667
4668 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4669 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4670 for _ in 0..2 {
4671 let recovered = UnifiedStore::open_with_config(
4672 destination.join("data.rdb"),
4673 fixture.config.clone(),
4674 )
4675 .expect("open recovered store");
4676 assert!(recovered.get_collection("pending_collection").is_some());
4677 assert!(recovered.get("red.config", EntityId::new(1)).is_none());
4678 assert!(recovered.get("red.config", EntityId::new(2)).is_some());
4679 assert!(recovered.get("red.config", EntityId::new(3)).is_some());
4680 }
4681 }
4682
4683 #[test]
4684 fn canonical_system_catalog_recovery_reconciles_all_metadata_and_keeps_missing_index() {
4685 use crate::storage::wal::{WalRecord, WalWriter};
4686 for case in [
4687 "valid",
4688 "view-conflict",
4689 "queue-delivery",
4690 "legacy-wal",
4691 "canonical-wal",
4692 ] {
4693 let fixture = duplicate_system_catalog_fixture_with_aliases(
4694 true,
4695 true,
4696 true,
4697 false,
4698 Some("system.pid"),
4699 Some(case),
4700 );
4701 let wal = reddb_file::unified_wal_path(&fixture.path);
4702 {
4703 let mut writer = WalWriter::open(&wal).unwrap();
4704 let name = match case {
4705 "legacy-wal" => "red_index_registry",
4706 "canonical-wal" => "red.index.registry",
4707 _ => "durable_rows",
4708 };
4709 writer
4710 .append(&WalRecord::TxCommitBatch {
4711 tx_id: 1001,
4712 actions: vec![reddb_file::encode_store_wal_action_frame(
4713 &reddb_file::StoreWalActionFrame::CreateCollection {
4714 name: name.into(),
4715 },
4716 )
4717 .unwrap()],
4718 })
4719 .unwrap();
4720 writer.sync().unwrap();
4721 }
4722 let original = std::fs::read(&fixture.path).unwrap();
4723 let original_wal = std::fs::read(&wal).unwrap();
4724 let baseline = UnifiedStore::open_with_config_for_recovery(
4725 &fixture.path,
4726 fixture.config.clone(),
4727 true,
4728 None,
4729 None,
4730 )
4731 .err()
4732 .expect("unproved aliases must still fail");
4733 assert!(baseline.to_string().contains("red_stats"), "{baseline}");
4734 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4735 let destination = fixture._directory.path().join("catalog-recovered");
4736 let result = crate::storage::compact::recover_paged_wal_canonical_system_config(
4737 &fixture.path,
4738 &destination,
4739 );
4740 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4741 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4742 if case != "valid" {
4743 let error = result.expect_err(case).to_string();
4744 let expected = match case {
4745 "view-conflict" => "conflicting canonical definition",
4746 "queue-delivery" => "unsupported legacy metadata",
4747 _ => "WAL changes metadata",
4748 };
4749 assert!(error.contains(expected), "{case}: {error}");
4750 assert!(!destination.join("data.rdb").exists());
4751 continue;
4752 }
4753 let report = result.expect("recover all supported aliases together");
4754 assert_eq!(report.reconciled_system_collections, 4);
4755 assert_eq!(report.imported_system_records, 1);
4756 for _ in 0..2 {
4757 let store = UnifiedStore::open_with_config(
4758 destination.join("data.rdb"),
4759 fixture.config.clone(),
4760 )
4761 .unwrap();
4762 assert!(store.get_collection("durable_rows").is_some());
4763 let mut imported = Vec::new();
4764 store
4765 .get_collection("red.index.registry")
4766 .unwrap()
4767 .try_for_each_entity(4 * 1024 * 1024, |entity| {
4768 if entity.data.as_row().and_then(|r| r.get_field("name"))
4769 == Some(&Value::text("legacy_only"))
4770 {
4771 imported.push(entity.clone());
4772 }
4773 true
4774 })
4775 .unwrap();
4776 assert_eq!(imported.len(), 1, "legacy-only index must survive");
4777 assert!(
4778 imported[0].id.raw() > 10003,
4779 "all retired identities must be reserved first"
4780 );
4781 assert!(
4782 matches!(imported[0].kind, EntityKind::TableRow{row_id,..} if row_id>10001)
4783 );
4784 assert_eq!(imported[0].logical_id(), imported[0].id);
4785 assert!(store
4786 .get("red.index.registry", EntityId::new(10001))
4787 .is_none());
4788 assert!(store
4789 .get("red.index.registry", EntityId::new(101))
4790 .is_some());
4791 assert!(store
4792 .get("red.query.stats_catalog", EntityId::new(100))
4793 .is_some());
4794 assert!(store
4795 .get("red.query.stats_catalog", EntityId::new(10000))
4796 .is_none());
4797 assert_eq!(store.get_collection("red.queue.meta").unwrap().count(), 1);
4798 assert_eq!(
4799 store
4800 .get_collection("red.materialized_view.definitions")
4801 .unwrap()
4802 .count(),
4803 1
4804 );
4805 assert!(
4806 store
4807 .get_collection("red.index.registry")
4808 .unwrap()
4809 .identity_counters()
4810 .0
4811 > 10001
4812 );
4813 }
4814 }
4815 }
4816
4817 #[test]
4818 fn canonical_system_config_recovery_retires_boot_wal_without_losing_other_actions() {
4819 use crate::storage::wal::{WalRecord, WalWriter};
4820 use reddb_file::StoreWalActionFrame as Action;
4821 for pending_dwb in [false, true] {
4822 let fixture = duplicate_system_catalog_fixture_with_key(
4823 true,
4824 true,
4825 true,
4826 false,
4827 Some("system.pid"),
4828 );
4829 let encode = |action| reddb_file::encode_store_wal_action_frame(&action).unwrap();
4830 let upsert = |id, value| {
4831 encode(Action::UpsertEntityRecord {
4832 collection: "red_config".into(),
4833 record: UnifiedStore::serialize_entity_record(
4834 &config_recovery_row(id, "system.pid", value),
4835 None,
4836 STORE_VERSION_CURRENT,
4837 ),
4838 })
4839 };
4840 let delete = |id| {
4841 encode(Action::DeleteEntityRecord {
4842 collection: "red_config".into(),
4843 entity_id: id,
4844 })
4845 };
4846 let wal = reddb_file::unified_wal_path(&fixture.path);
4847 {
4848 let mut writer = WalWriter::open(&wal).unwrap();
4849 writer
4850 .append(&WalRecord::TxCommitBatch {
4851 tx_id: 1001,
4852 actions: vec![
4853 upsert(10000, 3),
4854 delete(1),
4855 encode(Action::CreateCollection {
4856 name: "durable_rows".into(),
4857 }),
4858 ],
4859 })
4860 .unwrap();
4861 writer
4862 .append(&WalRecord::TxCommitBatch {
4863 tx_id: 1002,
4864 actions: vec![
4865 upsert(10001, 4),
4866 delete(10000),
4867 encode(Action::UpsertEntityRecord {
4868 collection: "durable_rows".into(),
4869 record: UnifiedStore::serialize_entity_record(
4870 &UnifiedEntity::table_row(
4871 EntityId::new(3),
4872 "durable_rows",
4873 1,
4874 vec![Value::text("must survive")],
4875 ),
4876 None,
4877 STORE_VERSION_CURRENT,
4878 ),
4879 }),
4880 ],
4881 })
4882 .unwrap();
4883 writer.sync().unwrap();
4884 }
4885 if pending_dwb {
4886 use std::io::{Seek, SeekFrom, Write};
4887 let bytes = std::fs::read(&fixture.path).unwrap();
4888 let root = fixture.legacy_root.unwrap();
4889 let offset = root as usize * PAGE_SIZE;
4890 let page: &[u8; PAGE_SIZE] = bytes[offset..offset + PAGE_SIZE].try_into().unwrap();
4891 let frame = reddb_file::encode_paged_dwb_frame([(root, page)]);
4892 let mut file = std::fs::OpenOptions::new()
4893 .write(true)
4894 .open(&fixture.path)
4895 .unwrap();
4896 file.seek(SeekFrom::Start(3 * PAGE_SIZE as u64)).unwrap();
4897 file.write_all(&frame).unwrap();
4898 file.seek(SeekFrom::Start(offset as u64)).unwrap();
4901 file.write_all(&[0u8; PAGE_SIZE]).unwrap();
4902 file.sync_all().unwrap();
4903 }
4904 let original = std::fs::read(&fixture.path).unwrap();
4905 let original_wal = std::fs::read(&wal).unwrap();
4906 let destination = fixture._directory.path().join("boot-config-recovered");
4907 let report = crate::storage::compact::recover_paged_wal_canonical_system_config(
4908 &fixture.path,
4909 &destination,
4910 )
4911 .expect("retire only proved boot metadata actions on the isolated copy");
4912 assert_eq!(report.retired_boot_system_wal_actions, 4);
4913 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4914 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4915 for _ in 0..2 {
4916 let recovered = UnifiedStore::open_with_config(
4917 destination.join("data.rdb"),
4918 fixture.config.clone(),
4919 )
4920 .unwrap();
4921 let manager = recovered.get_collection("red.config").unwrap();
4922 assert!(manager.identity_counters().0 > 10001);
4923 assert!(manager.identity_counters().1 > 10001);
4924 assert_eq!(recovered.get_config("system.pid"), Some(Value::Integer(2)));
4925 assert!(recovered.get("red.config", EntityId::new(1)).is_none());
4926 assert!(recovered.get("red.config", EntityId::new(10000)).is_none());
4927 assert!(recovered.get("red.config", EntityId::new(10001)).is_none());
4928 assert_eq!(
4929 recovered
4930 .get("durable_rows", EntityId::new(3))
4931 .unwrap()
4932 .data
4933 .as_row()
4934 .unwrap()
4935 .columns,
4936 vec![Value::text("must survive")]
4937 );
4938 }
4939 }
4940 }
4941
4942 #[test]
4943 fn canonical_system_config_recovery_requires_stable_boot_identity_evidence() {
4944 use crate::storage::wal::{WalRecord, WalWriter};
4945 let fixture =
4946 duplicate_system_catalog_fixture_with_key(true, true, false, false, Some("system.pid"));
4947 let original = std::fs::read(&fixture.path).unwrap();
4948 let wal = reddb_file::unified_wal_path(&fixture.path);
4949 {
4950 let mut writer = WalWriter::open(&wal).unwrap();
4951 writer
4952 .append(&WalRecord::TxCommitBatch {
4953 tx_id: 1004,
4954 actions: vec![reddb_file::encode_store_wal_action_frame(
4955 &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
4956 collection: "red_config".into(),
4957 entity_id: 1,
4958 },
4959 )
4960 .unwrap()],
4961 })
4962 .unwrap();
4963 writer
4964 .append(&WalRecord::FullPageImage {
4965 tx_id: 1004,
4966 page_id: 1,
4967 ckpt_epoch: 1,
4968 data: original[PAGE_SIZE..2 * PAGE_SIZE].to_vec(),
4969 })
4970 .unwrap();
4971 writer.sync().unwrap();
4972 }
4973 let original_wal = std::fs::read(&wal).unwrap();
4974 let destination = fixture._directory.path().join("unproved-checkpoint");
4975 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
4976 &fixture.path,
4977 &destination,
4978 )
4979 .expect_err("undo images must be recovered before proving legacy identities");
4980 assert!(error
4981 .to_string()
4982 .contains("requires a stable checkpoint image"));
4983 assert!(!destination.exists());
4984 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
4985 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
4986 }
4987
4988 #[test]
4989 fn canonical_system_config_recovery_rejects_unproved_boot_wal() {
4990 use crate::storage::wal::{WalRecord, WalWriter};
4991 use reddb_file::StoreWalActionFrame as Action;
4992 for (case, checkpoint_key, wal_key, delete_id) in [
4993 ("unknown-delete", "system.pid", "system.pid", Some(90000)),
4994 (
4995 "user-key",
4996 "system.customer_setting",
4997 "system.customer_setting",
4998 None,
4999 ),
5000 (
5001 "identity-reuse",
5002 "red.config.customer_setting",
5003 "system.pid",
5004 None,
5005 ),
5006 ] {
5007 let fixture = duplicate_system_catalog_fixture_with_key(
5008 true,
5009 true,
5010 false,
5011 false,
5012 Some(checkpoint_key),
5013 );
5014 let action = if let Some(entity_id) = delete_id {
5015 Action::DeleteEntityRecord {
5016 collection: "red_config".into(),
5017 entity_id,
5018 }
5019 } else {
5020 Action::UpsertEntityRecord {
5021 collection: "red_config".into(),
5022 record: UnifiedStore::serialize_entity_record(
5023 &config_recovery_row(1, wal_key, 99),
5024 None,
5025 STORE_VERSION_CURRENT,
5026 ),
5027 }
5028 };
5029 let wal = reddb_file::unified_wal_path(&fixture.path);
5030 {
5031 let mut writer = WalWriter::open(&wal).unwrap();
5032 writer
5033 .append(&WalRecord::TxCommitBatch {
5034 tx_id: 1003,
5035 actions: vec![reddb_file::encode_store_wal_action_frame(&action).unwrap()],
5036 })
5037 .unwrap();
5038 writer.sync().unwrap();
5039 }
5040 let original = std::fs::read(&fixture.path).unwrap();
5041 let original_wal = std::fs::read(&wal).unwrap();
5042 let destination = fixture._directory.path().join(case);
5043 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
5044 &fixture.path,
5045 &destination,
5046 )
5047 .expect_err(case);
5048 assert!(
5049 error
5050 .to_string()
5051 .contains("WAL references divergent legacy red_config"),
5052 "{case}: {error}"
5053 );
5054 assert!(!destination.exists());
5055 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
5056 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
5057 }
5058 }
5059
5060 #[test]
5061 fn canonical_system_config_recovery_rejects_legacy_wal_actions() {
5062 use crate::storage::wal::{WalRecord, WalWriter};
5063
5064 let fixture = duplicate_system_catalog_fixture(true, true, false);
5065 let wal = reddb_file::unified_wal_path(&fixture.path);
5066 let action = reddb_file::encode_store_wal_action_frame(
5067 &reddb_file::StoreWalActionFrame::DeleteEntityRecord {
5068 collection: "red_config".into(),
5069 entity_id: 1,
5070 },
5071 )
5072 .unwrap();
5073 {
5074 let mut writer = WalWriter::open(&wal).unwrap();
5075 writer
5076 .append(&WalRecord::TxCommitBatch {
5077 tx_id: 999,
5078 actions: vec![action],
5079 })
5080 .unwrap();
5081 writer.sync().unwrap();
5082 }
5083 let original = std::fs::read(&fixture.path).unwrap();
5084 let original_wal = std::fs::read(&wal).unwrap();
5085 let destination = fixture._directory.path().join("rejected-legacy-wal");
5086 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
5087 &fixture.path,
5088 &destination,
5089 )
5090 .expect_err("legacy WAL must not be replayed against the canonical tree");
5091 assert!(error
5092 .to_string()
5093 .contains("WAL references divergent legacy red_config"));
5094 assert!(!destination.exists());
5095 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
5096 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
5097 }
5098
5099 #[test]
5100 fn canonical_system_config_recovery_rejects_legacy_cross_references() {
5101 use crate::storage::wal::{WalRecord, WalWriter};
5102
5103 let fixture = duplicate_system_catalog_fixture_with_ref(true, true, false, true);
5104 let wal = reddb_file::unified_wal_path(&fixture.path);
5105 {
5106 let mut writer = WalWriter::open(&wal).unwrap();
5107 writer.append(&WalRecord::Begin { tx_id: 999 }).unwrap();
5108 writer.sync().unwrap();
5109 }
5110 let original = std::fs::read(&fixture.path).unwrap();
5111 let original_wal = std::fs::read(&wal).unwrap();
5112 let destination = fixture._directory.path().join("rejected-legacy-reference");
5113 let error = crate::storage::compact::recover_paged_wal_canonical_system_config(
5114 &fixture.path,
5115 &destination,
5116 )
5117 .expect_err("legacy references must not be retargeted silently");
5118 assert!(error
5119 .to_string()
5120 .contains("legacy red_config has cross-references"));
5121 assert!(!destination.join("data.rdb").exists());
5122 assert_eq!(std::fs::read(&fixture.path).unwrap(), original);
5123 assert_eq!(std::fs::read(&wal).unwrap(), original_wal);
5124 }
5125
5126 #[test]
5127 fn populated_system_collection_aliases_reconcile_without_rebuild() {
5128 let fixture = duplicate_system_catalog_fixture(true, false, true);
5129 alias_duplicate_system_catalog_state(&fixture);
5130
5131 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
5132 .expect("reconcile identical populated aliases");
5133 assert_eq!(recovered.list_collections(), vec!["red.config".to_string()]);
5134 assert_eq!(
5135 recovered
5136 .get("red.config", EntityId::new(1))
5137 .expect("preserved aliased row")
5138 .kind
5139 .collection(),
5140 "red.config"
5141 );
5142 assert_eq!(
5143 recovered.collection_root_page("red.config"),
5144 fixture.legacy_root,
5145 "the shared data root must be reused"
5146 );
5147 assert_eq!(
5148 recovered
5149 .key_indices
5150 .read()
5151 .get("red.config")
5152 .map(|index| index.tree.root_page_id()),
5153 fixture.legacy_key_root,
5154 "the shared key-index root must be reused"
5155 );
5156 assert_eq!(
5157 recovered
5158 .secondary_index("red.config", "by_id", &fixture.index_identity)
5159 .expect("shared secondary index")
5160 .root_page_id(),
5161 fixture
5162 .legacy_secondary_root
5163 .expect("fixture secondary root")
5164 );
5165 drop(recovered);
5166
5167 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5168 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
5169 .expect("idempotent cold boot");
5170 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
5171 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5172 assert_eq!(
5173 cold.pager()
5174 .expect("pager")
5175 .page_count()
5176 .expect("page count"),
5177 fixture.pages_before,
5178 "reconciling a physical alias must not extend the file"
5179 );
5180 }
5181
5182 #[test]
5183 fn populated_system_collection_aliases_merge_resume_counter_drift() {
5184 let fixture = duplicate_system_catalog_fixture(true, false, false);
5185 alias_duplicate_system_catalog_state_with(&fixture, |canonical| {
5186 canonical.entities += 7;
5187 canonical.entity_id_next += 100;
5188 canonical.row_id_next += 200;
5189 canonical.sequence_id_next += 300;
5190 });
5191
5192 let recovered = UnifiedStore::open_with_config(&fixture.path, fixture.config.clone())
5193 .expect("reconcile shared roots with conservative counters");
5194 let manager = recovered
5195 .get_collection("red.config")
5196 .expect("canonical collection");
5197 assert_eq!(manager.count(), 1, "the shared tree owns the exact count");
5198 assert_eq!(manager.identity_counters(), (102, 202));
5199 assert_eq!(manager.sequence_id_next(), 301);
5200 assert_eq!(
5201 recovered.collection_root_page("red.config"),
5202 fixture.legacy_root,
5203 "counter reconciliation must keep the shared data root"
5204 );
5205 drop(manager);
5206 drop(recovered);
5207
5208 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5209 let cold = UnifiedStore::open_with_config(&fixture.path, fixture.config)
5210 .expect("idempotent cold boot after counter reconciliation");
5211 assert_eq!(cold.list_collections(), vec!["red.config".to_string()]);
5212 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5213 assert_eq!(
5214 cold.pager()
5215 .expect("pager")
5216 .page_count()
5217 .expect("page count"),
5218 fixture.pages_before,
5219 "counter reconciliation must not extend the file"
5220 );
5221 }
5222
5223 fn row(id: u64, value: i64) -> UnifiedEntity {
5224 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
5225 }
5226
5227 #[test]
5228 fn paged_checkpoint_excludes_uncommitted_graph_versions() {
5229 let directory = tempfile::tempdir().unwrap();
5230 let path = directory.path().join("graph.rdb");
5231 let store = UnifiedStore::open(&path).unwrap();
5232 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5233 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5234 store.create_collection("graph").unwrap();
5235 let node = UnifiedEntity::graph_node(EntityId::new(1), "a", "person", HashMap::new());
5236 let edge =
5237 UnifiedEntity::graph_edge(EntityId::new(2), "knows", "1", "1", 1.0, HashMap::new());
5238 store.insert("graph", node).unwrap();
5239 store.insert("graph", edge).unwrap();
5240 store.persist().unwrap();
5241
5242 let xid = snapshots.begin();
5243 let manager = store.get_collection("graph").unwrap();
5244 for id in [1, 2] {
5245 let mut entity = manager.get(EntityId::new(id)).unwrap();
5246 entity.xmax = xid;
5247 manager.update(entity).unwrap();
5248 }
5249 let mut node = UnifiedEntity::graph_node(EntityId::new(3), "b", "person", HashMap::new());
5250 let mut edge =
5251 UnifiedEntity::graph_edge(EntityId::new(4), "knows", "1", "3", 1.0, HashMap::new());
5252 node.xmin = xid;
5253 edge.xmin = xid;
5254 store.insert("graph", node).unwrap();
5255 store.insert("graph", edge).unwrap();
5256 store
5257 .add_cross_ref(
5258 "graph",
5259 EntityId::new(1),
5260 "graph",
5261 EntityId::new(4),
5262 RefType::RelatedTo,
5263 1.0,
5264 )
5265 .unwrap();
5266 store.persist().unwrap();
5267 snapshots.rollback(xid);
5268 store.persist().unwrap();
5269 drop(manager);
5270 drop(store);
5271
5272 let recovered = UnifiedStore::open(&path).unwrap();
5273 let manager = recovered.get_collection("graph").unwrap();
5274 assert_eq!(manager.get(EntityId::new(1)).unwrap().xmax, 0);
5275 assert_eq!(manager.get(EntityId::new(2)).unwrap().xmax, 0);
5276 assert!(manager.get(EntityId::new(3)).is_none());
5277 assert!(manager.get(EntityId::new(4)).is_none());
5278 assert!(!manager
5279 .get(EntityId::new(1))
5280 .unwrap()
5281 .cross_refs()
5282 .iter()
5283 .any(|reference| reference.target == EntityId::new(4)));
5284 assert!(!recovered
5285 .get_refs_from(EntityId::new(1))
5286 .iter()
5287 .any(|(target, _, _)| *target == EntityId::new(4)));
5288 }
5289
5290 #[test]
5291 fn paged_checkpoint_restores_graph_references_after_commit() {
5292 let directory = tempfile::tempdir().unwrap();
5293 let path = directory.path().join("graph.rdb");
5294 let store = UnifiedStore::open(&path).unwrap();
5295 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5296 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5297 store.create_collection("graph").unwrap();
5298 store
5299 .insert(
5300 "graph",
5301 UnifiedEntity::graph_node(EntityId::new(1), "a", "person", HashMap::new()),
5302 )
5303 .unwrap();
5304 store.persist().unwrap();
5305 let xid = snapshots.begin();
5306 let mut edge =
5307 UnifiedEntity::graph_edge(EntityId::new(2), "knows", "1", "1", 1.0, HashMap::new());
5308 edge.xmin = xid;
5309 store.insert("graph", edge).unwrap();
5310 store
5311 .add_cross_ref(
5312 "graph",
5313 EntityId::new(1),
5314 "graph",
5315 EntityId::new(2),
5316 RefType::RelatedTo,
5317 1.0,
5318 )
5319 .unwrap();
5320 store.persist().unwrap();
5321 snapshots.commit(xid);
5322 store.persist().unwrap();
5323 drop(store);
5324
5325 let recovered = UnifiedStore::open(&path).unwrap();
5326 let node = recovered.get("graph", EntityId::new(1)).unwrap();
5327 assert!(recovered.get("graph", EntityId::new(2)).is_some());
5328 assert!(node
5329 .cross_refs()
5330 .iter()
5331 .any(|reference| reference.target == EntityId::new(2)));
5332 assert!(recovered
5333 .get_refs_from(EntityId::new(1))
5334 .iter()
5335 .any(|(target, _, _)| *target == EntityId::new(2)));
5336 }
5337
5338 #[test]
5339 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
5340 let directory = tempfile::tempdir().expect("temporary store");
5341 let path = directory.path().join("data.rdb");
5342 let store = UnifiedStore::open(&path).expect("paged store");
5343 store.create_collection("rows").expect("collection");
5344 store.insert("rows", row(1, 1)).expect("seed");
5345 store.persist().expect("initial checkpoint");
5346 let manager = store.get_collection("rows").expect("rows");
5347 manager.update(row(1, 2)).expect("direct update");
5348 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5349 store.persist().expect("updated row checkpoint");
5350 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
5351 let mut metadata = Metadata::new();
5352 metadata.set("verified", MetadataValue::Bool(true));
5353 manager
5354 .set_metadata(EntityId::new(1), metadata)
5355 .expect("metadata");
5356 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5357 store.persist().expect("metadata-only checkpoint");
5358 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
5359 manager
5360 .bulk_insert(vec![row(2, 2), row(3, 3)])
5361 .expect("bulk insert");
5362 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5363 store.persist().expect("bulk insert checkpoint");
5364 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
5365 manager.delete(EntityId::new(2)).expect("delete");
5366 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5367 store.persist().expect("changed checkpoint");
5368 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
5369 drop(store);
5370 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
5371 let manager = recovered.get_collection("rows").expect("rows");
5372 assert_eq!(manager.count(), 2);
5373 assert_eq!(
5374 manager
5375 .get(EntityId::new(1))
5376 .expect("row")
5377 .data
5378 .as_row()
5379 .expect("table")
5380 .columns,
5381 vec![Value::Integer(2)]
5382 );
5383 assert!(manager.get(EntityId::new(2)).is_none());
5384 assert!(manager
5385 .get_metadata(EntityId::new(1))
5386 .expect("metadata")
5387 .get("verified")
5388 .is_some());
5389 }
5390
5391 #[test]
5392 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
5393 let directory = tempfile::tempdir().expect("temporary store");
5394 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
5395 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5396 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5397 store.create_collection("rows").expect("collection");
5398 for id in 1..=40 {
5399 store.insert("rows", row(id, id as i64)).expect("seed");
5400 }
5401 store.persist().expect("initial checkpoint");
5402 let unrelated = snapshots.begin();
5403 snapshots.rollback(unrelated);
5404 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5405 store.persist().expect("unrelated rollback checkpoint");
5406 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5407
5408 store
5409 .pending_vault_versions
5410 .write()
5411 .insert(EntityId::new(100));
5412 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5413 store.persist().expect("unrelated vault checkpoint");
5414 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5415 store.pending_vault_versions.write().clear();
5416 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
5417 store
5418 .persist()
5419 .expect("unrelated vault publication checkpoint");
5420 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
5421 }
5422
5423 #[test]
5424 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
5425 let directory = tempfile::tempdir().expect("temporary store");
5426 let path = directory.path().join("data.rdb");
5427 let store = UnifiedStore::open(&path).expect("paged store");
5428 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5429 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5430 store.create_collection("rows").expect("collection");
5431 let xid = snapshots.begin();
5432 let mut entity = row(1, 1);
5433 entity.xmin = xid;
5434 store.insert("rows", entity).expect("committed row");
5435 snapshots.commit(xid);
5436 store.persist().expect("committed baseline");
5437 snapshots.rollback(xid);
5438 store
5439 .persist()
5440 .expect("untracked outcome requires validation");
5441 drop(store);
5442 let recovered = UnifiedStore::open(&path).expect("recover");
5443 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
5444 }
5445
5446 #[test]
5447 fn exhausted_deferred_row_cache_keeps_full_validation() {
5448 let directory = tempfile::tempdir().expect("temporary store");
5449 let path = directory.path().join("data.rdb");
5450 let store = UnifiedStore::open(&path).expect("paged store");
5451 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5452 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5453 store.create_collection("rows").expect("collection");
5454 let xid = snapshots.begin();
5455 for id in 1..=1025 {
5456 let mut entity = row(id, id as i64);
5457 entity.xmin = xid;
5458 store.insert("rows", entity).expect("pending row");
5459 }
5460 store.persist().expect("exclude pending rows");
5461 assert!(store.checkpoint_versions.read()["rows"]
5462 .deferred_rows
5463 .is_none());
5464 snapshots.commit(xid);
5465 store
5466 .persist()
5467 .expect("publish committed rows conservatively");
5468 drop(store);
5469 let recovered = UnifiedStore::open(&path).expect("recover");
5470 assert_eq!(
5471 recovered.get_collection("rows").expect("rows").count(),
5472 1025
5473 );
5474 }
5475
5476 #[test]
5477 fn rollback_invalidates_reuse_without_a_segment_mutation() {
5478 let directory = tempfile::tempdir().expect("temporary store");
5479 let path = directory.path().join("data.rdb");
5480 let store = UnifiedStore::open(&path).expect("paged store");
5481 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5482 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5483 store.create_collection("rows").expect("collection");
5484 let xid = snapshots.begin();
5485 let mut pending = row(1, 1);
5486 pending.xmin = xid;
5487 store.insert("rows", pending).expect("version");
5488 store.persist().expect("before rollback");
5489 snapshots.rollback(xid);
5490 store.persist().expect("after rollback");
5491 drop(store);
5492 let recovered = UnifiedStore::open(&path).expect("recover");
5493 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
5494 }
5495
5496 #[test]
5497 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
5498 let directory = tempfile::tempdir().expect("temporary store");
5499 let path = directory.path().join("data.rdb");
5500 let store = UnifiedStore::open(&path).expect("paged store");
5501 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
5502 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
5503 store.create_collection("rows").expect("collection");
5504 let xid = snapshots.begin();
5505 let mut deleted = row(1, 1);
5506 deleted.xmax = xid;
5507 store.insert("rows", deleted).expect("version");
5508 store.persist().expect("before rollback");
5509 snapshots.rollback(xid);
5510 store.persist().expect("after rollback");
5511 drop(store);
5512 let recovered = UnifiedStore::open(&path).expect("recover");
5513 let manager = recovered.get_collection("rows").expect("rows");
5514 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
5515 }
5516
5517 #[test]
5518 fn publishing_a_pending_vault_version_invalidates_reuse() {
5519 let directory = tempfile::tempdir().expect("temporary store");
5520 let path = directory.path().join("data.rdb");
5521 let store = UnifiedStore::open(&path).expect("paged store");
5522 store.create_collection("rows").expect("collection");
5523 store.insert("rows", row(1, 1)).expect("row");
5524 store
5525 .pending_vault_versions
5526 .write()
5527 .insert(EntityId::new(1));
5528 store.persist().expect("exclude pending vault version");
5529 store
5530 .pending_vault_versions
5531 .write()
5532 .remove(&EntityId::new(1));
5533 store.persist().expect("include published vault version");
5534 drop(store);
5535 let recovered = UnifiedStore::open(&path).expect("recover");
5536 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
5537 }
5538}