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