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