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