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 let mut cursor = previous
373 .map(|tree| tree.cursor_first())
374 .transpose()
375 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
376 let mut matched = 0;
377 let mut builder = None;
378 let mut failure = None;
379 manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
380 let Some(value) = encode(entity, metadata) else {
381 return true;
382 };
383 let record = (entity.id.raw().to_be_bytes().to_vec(), value);
384 let result = (|| {
385 if builder.is_none() {
386 if let Some(cursor) = cursor.as_mut() {
387 if cursor
388 .next()
389 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
390 .as_ref()
391 == Some(&record)
392 {
393 matched += 1;
394 return Ok(());
395 }
396 }
397 let mut replacement = CheckpointTreeBuilder::new(pager);
398 if let Some(previous) = previous {
399 replacement.copy_prefix(previous, matched)?;
400 }
401 builder = Some(replacement);
402 }
403 builder
404 .as_mut()
405 .expect("replacement initialized")
406 .push(record)
407 })();
408 if let Err(error) = result {
409 failure = Some(error);
410 return false;
411 }
412 true
413 });
414 if let Some(error) = failure {
415 return Err(error);
416 }
417 if builder.is_none() {
418 if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
419 if cursor
420 .next()
421 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
422 .is_none()
423 {
424 return Ok(Arc::clone(previous));
425 }
426 }
427 let mut replacement = CheckpointTreeBuilder::new(pager);
428 if let Some(previous) = previous {
429 replacement.copy_prefix(previous, matched)?;
430 }
431 builder = Some(replacement);
432 }
433 let mut builder = builder.expect("replacement or reused tree");
434 builder.flush()?;
435 Ok(builder.tree)
436 }
437
438 fn publish_reclaimable_trees(
442 &self,
443 pager: &Pager,
444 metadata: &mut Vec<u8>,
445 live: &HashMap<String, Arc<BTree>>,
446 ) -> Result<(), StoreError> {
447 let mut retired = self.retired_btrees.lock();
448 let mut roots = Vec::new();
449 let mut reclaim = Vec::new();
450 let mut remove = std::collections::HashSet::new();
451 for (index, entry) in retired.iter().enumerate() {
452 if live.values().any(|tree| Arc::ptr_eq(tree, &entry.tree)) {
453 remove.insert(index);
454 continue;
455 }
456 if entry.published
457 && Arc::strong_count(&entry.tree) == 1
458 && !entry.tree.has_retirement_readers()
459 {
460 #[cfg(test)]
464 if std::env::var_os("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT").is_some() {
465 roots.push(entry.tree.root_page_id());
466 }
467 reclaim.extend(
468 entry
469 .tree
470 .retired_pages()
471 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
472 );
473 remove.insert(index);
474 } else if entry.tree.root_page_id() != 0 {
475 roots.push(entry.tree.root_page_id());
476 }
477 }
478 roots.sort_unstable();
479 roots.dedup();
480 reddb_file::encode_native_paged_retired_trees(metadata, &roots)
481 .map_err(|e| StoreError::Serialization(e.to_string()))?;
482 let (pending_pages, published_pages) = pager
483 .deferred_reclamation_pages()
484 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
485 let readers = live.values().any(|tree| tree.has_retirement_readers())
486 || retired
487 .iter()
488 .any(|entry| entry.tree.has_retirement_readers());
489 let released_pages = if readers { Vec::new() } else { published_pages };
490 let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
491 let retained_pages: Vec<_> = pending_pages
492 .into_iter()
493 .filter(|id| !released.contains(id))
494 .collect();
495 reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
496 .map_err(|e| StoreError::Serialization(e.to_string()))?;
497 let active_epoch = self.ownership_active_epoch.load(Ordering::Acquire);
498 let catalog_epoch = if active_epoch == 0 {
499 self.ownership_catalog_epoch.load(Ordering::Acquire)
500 } else {
501 active_epoch
502 };
503 if catalog_epoch != 0 {
504 reddb_file::native_store::encode_native_paged_ownership_epoch(metadata, catalog_epoch)
505 .map_err(|e| StoreError::Serialization(e.to_string()))?;
506 }
507 let mut retirement_intent = reclaim.clone();
510 retirement_intent.extend_from_slice(&released_pages);
511 if !retirement_intent.is_empty() {
512 pager
513 .record_retirement_intent(&retirement_intent)
514 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
515 }
516 publish_paged_metadata(pager, metadata)
519 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
520 self.ownership_catalog_epoch
521 .store(catalog_epoch, Ordering::Release);
522 #[cfg(test)]
523 if !reclaim.is_empty()
524 && std::env::var_os("REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE").is_some()
525 {
526 eprintln!(
527 "experimental exit before retired free pages={}",
528 reclaim.len()
529 );
530 std::process::exit(184);
531 }
532 let mut index = 0;
533 retired.retain_mut(|entry| {
534 let keep = !remove.contains(&index);
535 entry.published = true;
536 index += 1;
537 keep
538 });
539 if !reclaim.is_empty() {
540 pager
541 .free_retired_pages(&reclaim)
542 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
543 #[cfg(test)]
544 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE").is_some() {
545 eprintln!(
546 "experimental exit after retired enqueue pages={}",
547 reclaim.len()
548 );
549 std::process::exit(186);
550 }
551 pager
552 .sync()
553 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
554 #[cfg(test)]
555 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE").is_some() {
556 eprintln!(
557 "experimental exit after retired free pages={}",
558 reclaim.len()
559 );
560 std::process::exit(185);
561 }
562 }
563 pager
564 .complete_deferred_publication(&retained_pages, &released_pages)
565 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
566 if !released_pages.is_empty() {
567 pager
568 .sync()
569 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
570 }
571 Ok(())
572 }
573
574 pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
575 let mut retired = self.retired_btrees.lock();
576 if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
577 {
578 retired.push(RetiredBTree {
579 tree,
580 published: false,
581 });
582 }
583 }
584
585 pub(crate) fn note_paged_insert(
589 &self,
590 collection: &str,
591 manager: &Arc<SegmentManager>,
592 before: &[(u64, u64)],
593 id: EntityId,
594 ) {
595 let after = manager.checkpoint_revisions();
596 if before.len() != after.len() {
597 return;
598 }
599 let mut changed = 0;
600 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
601 if old_id != new_id {
602 return;
603 }
604 if old_revision != new_revision {
605 if old_revision.checked_add(1) != Some(*new_revision) {
606 return;
607 }
608 changed += 1;
609 }
610 }
611 if changed != 1 {
612 return;
613 }
614 let mut versions = self.checkpoint_versions.write();
615 let Some(previous) = versions.get_mut(collection) else {
616 return;
617 };
618 if previous.manager.ptr_eq(&Arc::downgrade(manager))
619 && previous.segments == before
620 && previous.format_version == self.format_version()
621 && !previous.pending_vault.contains(&id)
622 && !previous.excluded_xids.contains(&0)
623 {
624 previous.segments = after;
625 }
626 }
627
628 pub(crate) fn note_paged_delete(
632 &self,
633 collection: &str,
634 manager: &Arc<SegmentManager>,
635 before: &[(u64, u64)],
636 deleted: &[EntityId],
637 ) {
638 let after = manager.checkpoint_revisions();
639 if before.len() != after.len() {
640 return;
641 }
642 let mut changes = 0_u64;
643 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
644 if old_id != new_id {
645 return;
646 }
647 let Some(delta) = new_revision.checked_sub(*old_revision) else {
648 return;
649 };
650 let Some(total) = changes.checked_add(delta) else {
651 return;
652 };
653 changes = total;
654 }
655 let Ok(expected) = u64::try_from(deleted.len()) else {
656 return;
657 };
658 if changes != expected {
659 return;
660 }
661 let mut versions = self.checkpoint_versions.write();
662 let Some(previous) = versions.get_mut(collection) else {
663 return;
664 };
665 if !previous.manager.ptr_eq(&Arc::downgrade(manager))
666 || previous.format_version != self.format_version()
667 || deleted.iter().any(|id| previous.pending_vault.contains(id))
668 || previous.excluded_xids.contains(&0)
669 {
670 return;
671 }
672 if previous.segments != before {
673 let Some(deferred) = previous.deferred_rows.as_ref() else {
674 return;
675 };
676 let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
677 return;
678 };
679 let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
680 let pending: std::collections::BTreeSet<_> = deferred
681 .iter()
682 .copied()
683 .chain(changed)
684 .filter(|id| !removed.contains(id))
685 .collect();
686 if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
687 return;
688 }
689 previous.has_unprojected_rows = !pending.is_empty();
694 previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
695 }
696 previous.segments = after;
697 }
698
699 pub(crate) fn mark_paged_registry_dirty(&self) {
700 self.resident.mark_paged_registry_dirty()
701 }
702
703 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
709 let pager = self.pager.as_ref()?;
710 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
711 return Some(btree);
712 }
713 let mut write = self.btree_indices.write();
714 let btree = write
715 .entry(collection.to_string())
716 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
717 .clone();
718 Some(btree)
719 }
720
721 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
722 let Some(pager) = &self.pager else {
723 return Ok(());
724 };
725
726 if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
727 self.flush_paged_registry()?;
728 self.resident
729 .paged_registry_dirty
730 .store(false, Ordering::Release);
731 return Ok(());
732 }
733
734 pager
735 .flush()
736 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
737 }
738
739 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
740 let _vault_guard = self.vault_publication_lock.lock();
741 let Some(pager) = &self.pager else {
742 return Ok(());
743 };
744
745 match pager.read_page(1) {
746 Ok(_) => {}
747 Err(PagerError::PageNotFound(_)) => {
748 let meta_page = pager
749 .allocate_page(crate::storage::engine::PageType::Header)
750 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
751 pager
752 .write_page(meta_page.page_id(), meta_page)
753 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
754 }
755 Err(e) => {
756 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
757 }
758 }
759
760 let format_version = STORE_VERSION_V11;
761 self.set_format_version(format_version);
762
763 let collections = self.resident.collections.read();
764 let btree_indices = self.btree_indices.read();
765 let mut collection_roots = Vec::with_capacity(collections.len());
766 for (name, _) in collections.iter() {
767 let root_page = btree_indices
768 .get(name)
769 .map_or(0, |btree| btree.root_page_id());
770 collection_roots.push((name.clone(), root_page));
771 }
772 drop(btree_indices);
773 drop(collections);
774
775 let mut meta_data = Vec::with_capacity(4096);
776 reddb_file::encode_native_paged_metadata_header(
777 &mut meta_data,
778 reddb_file::NativePagedMetadataHeader {
779 format_version,
780 collection_count: collection_roots.len() as u32,
781 },
782 );
783 for (name, root_page) in &collection_roots {
784 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
785 }
786
787 let cross_refs = self.resident.cross_refs.read();
788 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
789 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
790 for (source_id, refs) in cross_refs.iter() {
791 for (target_id, ref_type, collection) in refs {
792 reddb_file::encode_native_paged_cross_ref(
793 &mut meta_data,
794 source_id.raw(),
795 target_id.raw(),
796 ref_type.to_byte(),
797 collection,
798 );
799 }
800 }
801
802 self.publish_reclaimable_trees(pager, &mut meta_data, &self.btree_indices.read())?;
803
804 Ok(())
805 }
806
807 pub fn pager(&self) -> Option<&Arc<Pager>> {
809 self.pager.as_ref()
810 }
811
812 pub fn page_cache_bytes_in_use(&self) -> u64 {
816 self.pager.as_ref().map_or(0, |pager| {
817 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
818 })
819 }
820
821 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
824 self.commit
825 .as_ref()
826 .map_or(0, |commit| commit.buffered_bytes())
827 }
828
829 pub fn segment_memory_bytes(&self) -> u64 {
833 let managers: Vec<Arc<SegmentManager>> = self
834 .resident
835 .collections
836 .read()
837 .values()
838 .map(Arc::clone)
839 .collect();
840
841 managers
842 .iter()
843 .map(|manager| manager.resident_bytes())
844 .fold(0, u64::saturating_add)
845 }
846
847 pub fn config(&self) -> &UnifiedStoreConfig {
851 &self.config
852 }
853
854 pub fn with_config(config: UnifiedStoreConfig) -> Self {
855 Self {
856 config,
857 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
858 format_version: AtomicU32::new(STORE_VERSION_V11),
859 pager: None,
860 db_path: None,
861 btree_indices: RwLock::new(HashMap::new()),
862 checkpoint_versions: RwLock::new(HashMap::new()),
863 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
864 retired_btrees: parking_lot::Mutex::new(Vec::new()),
865 ownership_catalog_epoch: AtomicU64::new(0),
866 ownership_active_epoch: AtomicU64::new(0),
867 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
868 commit: None,
869 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
870 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
871 snapshot_manager: std::sync::OnceLock::new(),
872 embedded_commit_session: std::sync::OnceLock::new(),
873 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
874 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
875 }
876 }
877
878 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
896 Self::open_with_config(path, UnifiedStoreConfig::default())
897 }
898
899 pub fn open_with_config(
900 path: impl AsRef<Path>,
901 config: UnifiedStoreConfig,
902 ) -> Result<Self, StoreError> {
903 let path = path.as_ref();
904 let mut pager_config = PagerConfig::default();
905 if let Some(slots) = config.page_cache_slots {
909 pager_config.cache_size = slots;
910 }
911 if matches!(
919 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
920 Some("0") | Some("false") | Some("off")
921 ) {
922 pager_config.double_write = false;
923 }
924 let pager = Pager::open(path, pager_config)
925 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
926 pager
927 .enable_deferred_reclamation()
928 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
929
930 let wal_path = reddb_file::layout::unified_wal_path(path);
931 let commit = Some(Arc::new(
934 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
935 .map_err(StoreError::Io)?,
936 ));
937
938 let store = Self {
939 config,
940 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
941 format_version: AtomicU32::new(STORE_VERSION_V11),
942 pager: Some(Arc::new(pager)),
943 db_path: Some(path.to_path_buf()),
944 btree_indices: RwLock::new(HashMap::new()),
945 checkpoint_versions: RwLock::new(HashMap::new()),
946 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
947 retired_btrees: parking_lot::Mutex::new(Vec::new()),
948 ownership_catalog_epoch: AtomicU64::new(0),
949 ownership_active_epoch: AtomicU64::new(0),
950 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
951 commit,
952 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
953 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
954 snapshot_manager: std::sync::OnceLock::new(),
955 embedded_commit_session: std::sync::OnceLock::new(),
956 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
957 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
958 };
959
960 if let Some(commit) = &store.commit {
963 commit
964 .restore_checkpoint_page_images(&store)
965 .map_err(StoreError::Io)?;
966 }
967 store.load_from_pages()?;
969 if let Some(commit) = &store.commit {
970 commit.replay_into(&store).map_err(StoreError::Io)?;
971 }
972 store.recover_operational_manifest()?;
973
974 Ok(store)
975 }
976
977 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
981 let Some(pager) = &self.pager else {
982 return Ok(());
983 };
984 let collections = self.resident.collections.read();
985 let mut indices = self.btree_indices.write();
986 let previous_versions = self.checkpoint_versions.read();
987 let raw_replay =
988 self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
989 for (name, manager) in collections.iter() {
990 let mut reusable = None;
991 if raw_replay {
992 if let (Some(previous), Some(tree)) =
993 (previous_versions.get(name), indices.get(name))
994 {
995 let mut current = CheckpointVersion {
996 manager: Arc::downgrade(manager),
997 segments: manager.checkpoint_revisions(),
998 visibility_revision: 0,
999 untracked_visibility_revision: 0,
1000 excluded_xids: Arc::default(),
1001 pending_vault: Arc::default(),
1002 format_version: self.format_version(),
1003 deferred_rows: Some(Arc::default()),
1004 has_unprojected_rows: false,
1005 };
1006 if previous.manager.ptr_eq(¤t.manager)
1007 && previous.format_version == current.format_version
1008 {
1009 if previous.segments == current.segments
1010 && !previous.has_unprojected_rows
1011 && checkpoint_visibility_matches(previous, ¤t)
1012 {
1013 reusable = Some(Arc::clone(tree));
1014 } else {
1015 reusable = self.sparse_checkpoint_tree(
1021 name,
1022 manager,
1023 tree,
1024 previous,
1025 &mut current,
1026 )?;
1027 }
1028 }
1029 }
1030 }
1031 let tree = match reusable {
1032 Some(tree) => tree,
1033 None => {
1034 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1035 #[cfg(test)]
1036 REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1037 Some(Self::serialize_entity_record(
1038 entity,
1039 metadata.as_ref(),
1040 self.format_version(),
1041 ))
1042 })?
1043 }
1044 };
1045 if indices
1046 .get(name)
1047 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1048 {
1049 continue;
1050 }
1051 if let Some(previous) = indices.insert(name.clone(), tree) {
1052 self.retire_btree(previous);
1053 }
1054 self.mark_paged_registry_dirty();
1055 #[cfg(test)]
1056 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1057 if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1058 pager.sync().map_err(|error| {
1059 StoreError::Io(std::io::Error::other(error.to_string()))
1060 })?;
1061 }
1062 eprintln!("experimental replay rebuild exit before manifest");
1063 std::process::exit(182);
1064 }
1065 }
1066 drop(previous_versions);
1067 drop(indices);
1068 drop(collections);
1069 self.flush_paged_state()?;
1070 #[cfg(test)]
1071 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1072 eprintln!("experimental replay exit after manifest");
1073 std::process::exit(182);
1074 }
1075 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1078 let collections = self.resident.collections.read();
1079 let mut versions = self.checkpoint_versions.write();
1080 for (name, manager) in collections.iter() {
1081 versions.insert(
1082 name.clone(),
1083 CheckpointVersion {
1084 manager: Arc::downgrade(manager),
1085 segments: manager.checkpoint_revisions(),
1086 visibility_revision: 0,
1087 untracked_visibility_revision: 0,
1088 excluded_xids: Arc::default(),
1089 pending_vault: Arc::default(),
1090 format_version: self.format_version(),
1091 deferred_rows: Some(Arc::default()),
1092 has_unprojected_rows: false,
1093 },
1094 );
1095 }
1096 }
1097 Ok(())
1098 }
1099
1100 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
1101 let Some(path) = &self.db_path else {
1102 return Ok(());
1103 };
1104 let mut collections = self.list_collections();
1105 collections.sort();
1106 let pending_drops =
1107 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1108 .recover_or_bootstrap(&collections)
1109 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1110 for name in pending_drops {
1111 if self.get_collection(&name).is_some() {
1112 self.drop_collection(&name)?;
1113 }
1114 }
1115 Ok(())
1116 }
1117
1118 pub(crate) fn prepare_operational_collection_create(
1119 &self,
1120 name: &str,
1121 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
1122 let Some(path) = &self.db_path else {
1123 return Ok(None);
1124 };
1125 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1126 .prepare_collection_create(name)
1127 .map(Some)
1128 .map_err(StoreError::Io)
1129 }
1130
1131 pub(crate) fn prepare_operational_collection_drop(
1132 &self,
1133 name: &str,
1134 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
1135 let Some(path) = &self.db_path else {
1136 return Ok(None);
1137 };
1138 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1139 .prepare_collection_drop(name)
1140 .map(Some)
1141 .map_err(StoreError::Io)
1142 }
1143
1144 pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1146 let Some(pager) = &self.pager else {
1147 return Ok(0);
1148 };
1149 let Some(content) = read_meta_payload(pager)? else {
1150 return Ok(0);
1151 };
1152 reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1153 .map(|epoch| epoch.unwrap_or(0))
1154 .map_err(|error| StoreError::Serialization(error.to_string()))
1155 }
1156
1157 fn load_from_pages(&self) -> Result<(), StoreError> {
1159 let pager = match &self.pager {
1160 Some(p) => p,
1161 None => return Ok(()), };
1163
1164 let page_count = pager.page_count().map_err(|e| {
1166 StoreError::Io(std::io::Error::other(format!(
1167 "failed to read page count: {}",
1168 e
1169 )))
1170 })?;
1171 if page_count <= 1 {
1172 return Ok(());
1174 }
1175
1176 if let Some(content_vec) = read_meta_payload(pager)? {
1181 let content: &[u8] = &content_vec;
1182 if content.len() >= 4 {
1183 let mut pos = 0;
1184 let mut format_version = STORE_VERSION_V1;
1185
1186 let collection_count = if let Some(header) =
1187 reddb_file::decode_native_paged_metadata_header(content)
1188 .map_err(|err| StoreError::Serialization(err.to_string()))?
1189 {
1190 format_version = header.format_version;
1191 pos += reddb_file::METADATA_HEADER_BYTES;
1192 header.collection_count as usize
1193 } else {
1194 let count = u32::from_le_bytes([
1195 content[pos],
1196 content[pos + 1],
1197 content[pos + 2],
1198 content[pos + 3],
1199 ]) as usize;
1200 pos += 4;
1201 count
1202 };
1203
1204 self.set_format_version(format_version);
1205
1206 if pos > content.len() {
1207 return Ok(());
1208 }
1209
1210 for _ in 0..collection_count {
1212 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1213 .map_err(|err| page_integrity(1, None, err.to_string()))?;
1214 let root_page = root.root_page;
1216 let name = root.collection;
1217
1218 let _ = self.create_collection_in_memory(&name);
1222
1223 if root_page > 0 {
1225 let btree = BTree::with_root(Arc::clone(pager), root_page);
1226
1227 let mut cursor = btree.cursor_first().map_err(|err| {
1229 page_integrity(root_page, Some(name.clone()), err.to_string())
1230 })?;
1231 let manager = self.get_collection(&name);
1232 loop {
1233 let next = cursor.next().map_err(|err| {
1234 page_integrity(root_page, Some(name.clone()), err.to_string())
1235 })?;
1236 let Some((key, value)) = next else {
1237 break;
1238 };
1239 let (entity, metadata) =
1241 Self::deserialize_entity_record(&value, self.format_version())
1242 .map_err(|err| {
1243 page_integrity(
1244 root_page,
1245 Some(name.clone()),
1246 err.to_string(),
1247 )
1248 })?;
1249 if let Some(m) = &manager {
1250 let id = entity.id;
1251 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1252 m.register_row_id(*row_id);
1253 }
1254 self.resident.context_index.index_entity(&name, &entity);
1255 m.restore_snapshot_entity(entity.clone())?;
1256 if let Some(metadata) = metadata {
1257 let _ = m.set_metadata(id, metadata);
1258 }
1259 self.register_entity_id(id);
1260 if self.config.auto_index_refs {
1261 self.index_cross_refs(&entity, &name)?;
1262 }
1263 }
1264 }
1265
1266 self.btree_indices
1268 .write()
1269 .insert(name.clone(), Arc::new(btree));
1270 if format_version == STORE_VERSION_V11 {
1271 if let Some(manager) = &manager {
1272 self.checkpoint_versions.write().insert(
1273 name,
1274 CheckpointVersion {
1275 manager: Arc::downgrade(manager),
1276 segments: manager.checkpoint_revisions(),
1277 visibility_revision: 0,
1278 untracked_visibility_revision: 0,
1279 excluded_xids: Arc::default(),
1280 pending_vault: Arc::default(),
1281 format_version,
1282 deferred_rows: Some(Arc::default()),
1283 has_unprojected_rows: false,
1284 },
1285 );
1286 }
1287 }
1288 }
1289 }
1290
1291 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1292 let cross_ref_count = u32::from_le_bytes([
1293 content[pos],
1294 content[pos + 1],
1295 content[pos + 2],
1296 content[pos + 3],
1297 ]) as usize;
1298 pos += 4;
1299
1300 for _ in 0..cross_ref_count {
1301 let Ok(cross_ref) =
1302 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1303 else {
1304 break;
1305 };
1306 let source_id = EntityId::new(cross_ref.source_id);
1307 let target_id = EntityId::new(cross_ref.target_id);
1308 let ref_type = RefType::from_byte(cross_ref.ref_type);
1309 let target_collection = cross_ref.target_collection;
1310
1311 self.resident
1312 .cross_refs
1313 .write()
1314 .entry(source_id)
1315 .or_default()
1316 .push((target_id, ref_type, target_collection.clone()));
1317
1318 if let Some((collection, mut entity)) = self.get_any(source_id) {
1319 let exists = entity.cross_refs().iter().any(|xref| {
1320 xref.target == target_id
1321 && xref.ref_type == ref_type
1322 && xref.target_collection == target_collection
1323 });
1324 if !exists {
1325 entity.cross_refs_mut().push(CrossRef::new(
1326 source_id,
1327 target_id,
1328 target_collection.clone(),
1329 ref_type,
1330 ));
1331 if let Some(manager) = self.get_collection(&collection) {
1332 let _ = manager.update(entity);
1333 }
1334 }
1335 }
1336 }
1337 }
1338 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1339 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1340 let live_roots: std::collections::HashSet<_> = self
1341 .btree_indices
1342 .read()
1343 .values()
1344 .map(|tree| tree.root_page_id())
1345 .collect();
1346 for root in retired {
1347 if live_roots.contains(&root)
1348 || root
1349 >= pager
1350 .page_count()
1351 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1352 {
1353 return Err(page_integrity(
1354 root,
1355 None,
1356 "retired root aliases a live or missing page",
1357 ));
1358 }
1359 self.retired_btrees.lock().push(RetiredBTree {
1360 tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1361 published: true,
1362 });
1363 }
1364 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1365 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1366 if deferred.iter().any(|id| live_roots.contains(id)) {
1367 return Err(page_integrity(1, None, "deferred page aliases a live root"));
1368 }
1369 pager
1370 .restore_deferred_reclamation(&deferred)
1371 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1372 let ownership_epoch =
1373 reddb_file::native_store::decode_native_paged_ownership_epoch(
1374 content, &mut pos,
1375 )
1376 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1377 self.ownership_catalog_epoch
1378 .store(ownership_epoch.unwrap_or(0), Ordering::Release);
1379 }
1380 }
1381
1382 if self.format_version() < STORE_VERSION_V11 {
1383 self.set_format_version(STORE_VERSION_V11);
1384 }
1385
1386 Ok(())
1387 }
1388
1389 pub(crate) fn deserialize_entity(
1391 data: &[u8],
1392 format_version: u32,
1393 ) -> Result<UnifiedEntity, StoreError> {
1394 let mut pos = 0;
1395 Self::read_entity_binary(data, &mut pos, format_version)
1396 .map_err(|e| StoreError::Serialization(e.to_string()))
1397 }
1398
1399 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1401 let mut buf = Vec::with_capacity(256);
1406 Self::write_entity_binary(&mut buf, entity, format_version);
1407 buf
1408 }
1409
1410 pub(crate) fn serialize_entity_record(
1411 entity: &UnifiedEntity,
1412 metadata: Option<&Metadata>,
1413 format_version: u32,
1414 ) -> Vec<u8> {
1415 reddb_engine::storage::metadata_codec::serialize_entity_record(
1416 entity,
1417 metadata,
1418 format_version,
1419 )
1420 }
1421
1422 pub(crate) fn deserialize_entity_record(
1423 data: &[u8],
1424 format_version: u32,
1425 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1426 reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
1427 .map_err(StoreError::Serialization)
1428 }
1429
1430 pub fn persist(&self) -> Result<(), StoreError> {
1435 let _vault_guard = self.vault_publication_lock.lock();
1436 let pending_vault_versions = self.pending_vault_versions.read();
1437 let pager = match &self.pager {
1438 Some(p) => p,
1439 None => {
1440 if let Some(path) = &self.db_path {
1442 return self
1443 .save_to_file(path)
1444 .map_err(|e| StoreError::Serialization(e.to_string()));
1445 }
1446 return Err(StoreError::Io(std::io::Error::other(
1447 "No pager or path configured for persistence",
1448 )));
1449 }
1450 };
1451 let _ownership_guard = pager.lock_ownership_mutations();
1454
1455 match pager.read_page(1) {
1456 Ok(_) => {}
1457 Err(PagerError::PageNotFound(_)) => {
1458 let meta_page = pager
1459 .allocate_page(crate::storage::engine::PageType::Header)
1460 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1461 pager
1462 .write_page(meta_page.page_id(), meta_page)
1463 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1464 }
1465 Err(e) => {
1466 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1467 }
1468 }
1469
1470 if let Some(commit) = &self.commit {
1471 commit.force_sync().map_err(StoreError::Io)?;
1472 }
1473
1474 struct InterruptedOwnership<'a> {
1475 pager: &'a Pager,
1476 active: bool,
1477 }
1478 impl Drop for InterruptedOwnership<'_> {
1479 fn drop(&mut self) {
1480 if self.active {
1481 self.pager.fence_interrupted_ownership();
1482 }
1483 }
1484 }
1485 let mut ownership = InterruptedOwnership {
1486 pager,
1487 active: self.commit.is_some(),
1488 };
1489 let epoch = if let Some(commit) = &self.commit {
1490 let epoch = commit
1491 .next_page_ownership_epoch(self)
1492 .map_err(StoreError::Io)?;
1493 commit
1494 .begin_page_ownership_epoch(self, epoch)
1495 .map_err(StoreError::Io)?;
1496 Some(epoch)
1497 } else {
1498 None
1499 };
1500
1501 let collections = self.resident.collections.read();
1502 let mut btree_indices = self.btree_indices.write();
1503
1504 let mut collection_roots: Vec<(String, u32)> = Vec::new();
1506
1507 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1510 let previous_versions = self.checkpoint_versions.read();
1511 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1512 let pending_vault = Arc::new(pending_vault_versions.clone());
1513 let (visibility_revision, excluded, untracked_visibility_revision) =
1514 self.snapshot_manager.get().map_or_else(
1515 || (0, Default::default(), 0),
1516 |manager| manager.checkpoint_projection_state(),
1517 );
1518 let excluded = Arc::new(excluded);
1519 for (name, manager) in collections.iter() {
1520 let mut version = CheckpointVersion {
1521 manager: Arc::downgrade(manager),
1522 segments: manager.checkpoint_revisions(),
1523 visibility_revision,
1524 untracked_visibility_revision,
1525 excluded_xids: Arc::clone(&excluded),
1526 pending_vault: Arc::clone(&pending_vault),
1527 format_version: self.format_version(),
1528 deferred_rows: Some(Arc::default()),
1529 has_unprojected_rows: false,
1530 };
1531 if let (Some(previous), Some(tree)) =
1532 (previous_versions.get(name), btree_indices.get(name))
1533 {
1534 if previous.manager.ptr_eq(&version.manager)
1535 && previous.segments == version.segments
1536 && previous.format_version == version.format_version
1537 && !previous.has_unprojected_rows
1538 && checkpoint_visibility_matches(previous, &version)
1539 {
1540 version.deferred_rows = previous.deferred_rows.clone();
1541 collection_roots.push((name.clone(), tree.root_page_id()));
1542 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1543 checkpoint_versions.insert(name.clone(), version);
1544 continue;
1545 }
1546 }
1547 if let (Some(previous), Some(tree)) =
1548 (previous_versions.get(name), btree_indices.get(name))
1549 {
1550 if previous.manager.ptr_eq(&version.manager)
1551 && previous.format_version == version.format_version
1552 {
1553 if let Some(tree) =
1554 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
1555 {
1556 collection_roots.push((name.clone(), tree.root_page_id()));
1557 rebuilt_indices.insert(name.clone(), tree);
1558 checkpoint_versions.insert(name.clone(), version);
1559 continue;
1560 }
1561 }
1562 }
1563 let btree = self.checkpoint_tree(
1564 pager,
1565 manager,
1566 btree_indices.get(name),
1567 |entity, metadata| {
1568 #[cfg(test)]
1569 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1570 let hidden = pending_vault_versions.contains(&entity.id)
1571 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1572 && excluded.contains(&entity.xmin));
1573 let normalize_xmax = matches!(entity.data, EntityData::Row(_))
1574 && excluded.contains(&entity.xmax);
1575 if hidden || normalize_xmax {
1576 version.remember_deferred_row(entity.id);
1577 }
1578 if hidden {
1579 return None;
1580 }
1581 let mut normalized = std::borrow::Cow::Borrowed(entity);
1582 if normalize_xmax {
1583 normalized.to_mut().xmax = 0;
1584 }
1585 Some(Self::serialize_entity_record(
1586 &normalized,
1587 metadata.as_ref(),
1588 self.format_version(),
1589 ))
1590 },
1591 )?;
1592 checkpoint_versions.insert(name.clone(), version);
1593
1594 collection_roots.push((name.clone(), btree.root_page_id()));
1595 rebuilt_indices.insert(name.clone(), btree);
1596 }
1597
1598 let mut meta_data = Vec::with_capacity(4096);
1600
1601 let format_version = STORE_VERSION_V11;
1602 self.set_format_version(format_version);
1603
1604 reddb_file::encode_native_paged_metadata_header(
1605 &mut meta_data,
1606 reddb_file::NativePagedMetadataHeader {
1607 format_version,
1608 collection_count: collection_roots.len() as u32,
1609 },
1610 );
1611
1612 for (name, root_page) in &collection_roots {
1614 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1615 }
1616
1617 let cross_refs = self.resident.cross_refs.read();
1619 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1620 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1621 for (source_id, refs) in cross_refs.iter() {
1622 for (target_id, ref_type, collection) in refs {
1623 reddb_file::encode_native_paged_cross_ref(
1624 &mut meta_data,
1625 source_id.raw(),
1626 target_id.raw(),
1627 ref_type.to_byte(),
1628 collection,
1629 );
1630 }
1631 }
1632
1633 for (name, previous) in btree_indices.iter() {
1634 if !rebuilt_indices
1635 .get(name)
1636 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1637 {
1638 self.retire_btree(Arc::clone(previous));
1639 }
1640 }
1641 #[cfg(test)]
1642 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
1643 pager
1644 .sync()
1645 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1646 eprintln!("experimental checkpoint exit before manifest");
1647 std::process::exit(183);
1648 }
1649 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices)?;
1650
1651 #[cfg(test)]
1652 if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
1653 std::process::exit(195);
1654 }
1655
1656 *btree_indices = rebuilt_indices;
1659 drop(previous_versions);
1660 *self.checkpoint_versions.write() = checkpoint_versions;
1661
1662 if let Some(commit) = &self.commit {
1663 commit
1664 .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
1665 .map_err(StoreError::Io)?;
1666 ownership.active = false;
1667 commit.truncate().map_err(StoreError::Io)?;
1668 }
1669 *self.checkpoint_durable_rows.write() = Default::default();
1670
1671 Ok(())
1672 }
1673
1674 pub fn is_paged(&self) -> bool {
1676 self.pager.is_some()
1677 }
1678
1679 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1682 self.btree_indices
1683 .read()
1684 .get(collection)
1685 .map(|btree| btree.root_page_id())
1686 .filter(|root| *root != 0)
1687 }
1688
1689 pub fn db_path(&self) -> Option<&Path> {
1691 self.db_path.as_deref()
1692 }
1693}
1694
1695#[cfg(test)]
1696mod checkpoint_work_tests {
1697 use super::*;
1698
1699 #[cfg(target_os = "linux")]
1700 #[test]
1701 fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
1702 const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
1703 if let Some(path) = std::env::var_os(CHILD) {
1704 let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
1705 let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
1706 let store = UnifiedStore::open(path).expect("open store");
1707 let initial_count = if multi { 600 } else { 1 };
1708 for id in 1..=initial_count {
1709 store.insert_auto("rows", row(id, 1)).expect("seed");
1710 }
1711 store.persist().expect("baseline");
1712 let manager = store.get_collection("rows").expect("collection");
1713 let ids: Vec<u64> = if multi {
1714 (1..=initial_count).collect()
1715 } else {
1716 vec![if insert { 2 } else { 1 }]
1717 };
1718 let mut actions = Vec::new();
1719 for id in ids {
1720 if insert {
1721 manager.insert(row(id, 9)).expect("insert version");
1722 } else {
1723 manager.update(row(id, 9)).expect("update version");
1724 }
1725 let entity = manager.get(EntityId::new(id)).expect("changed entity");
1726 actions.push(StoreWalAction::upsert_entity(
1727 "rows",
1728 &entity,
1729 None,
1730 store.format_version(),
1731 ));
1732 }
1733 let references = store
1734 .commit
1735 .as_ref()
1736 .expect("coordinator")
1737 .append_checkpoint_actions(&actions)
1738 .expect("durable WAL");
1739 store.remember_checkpoint_rows(actions, references);
1740 store.persist().expect("exit after sparse write");
1741 panic!("inline sparse crash hook was not reached");
1742 }
1743 for (insert, multi) in [(false, false), (true, false), (false, true)] {
1744 let directory = tempfile::tempdir().expect("temporary store");
1745 let path = directory.path().join("data.rdb");
1746 let mut command =
1747 std::process::Command::new(std::env::current_exe().expect("test executable"));
1748 command
1749 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
1750 .env(CHILD, &path)
1751 .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
1752 if insert {
1753 command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
1754 }
1755 if multi {
1756 command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
1757 }
1758 let output = command.output().expect("crash child");
1759 assert_eq!(
1760 output.status.code(),
1761 Some(190),
1762 "insert={insert} multi={multi}: {}",
1763 String::from_utf8_lossy(&output.stderr)
1764 );
1765 let recovered = UnifiedStore::open(&path).expect("recover");
1766 let ids: Vec<u64> = if multi {
1767 (1..=600).collect()
1768 } else {
1769 vec![if insert { 2 } else { 1 }]
1770 };
1771 for id in ids {
1772 assert_eq!(
1773 recovered
1774 .get("rows", EntityId::new(id))
1775 .expect("recovered row")
1776 .data
1777 .as_row()
1778 .expect("row")
1779 .columns,
1780 vec![Value::Integer(9)]
1781 );
1782 }
1783 assert_eq!(
1784 recovered.get_collection("rows").expect("rows").count(),
1785 if multi {
1786 600
1787 } else if insert {
1788 2
1789 } else {
1790 1
1791 }
1792 );
1793 }
1794 }
1795
1796 #[test]
1797 fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
1798 let directory = tempfile::tempdir().expect("temporary store");
1799 let path = directory.path().join("data.rdb");
1800 {
1801 let store = UnifiedStore::open(&path).expect("open store");
1802 store.create_collection("rows").expect("create collection");
1803 store.persist().expect("publish ownership epoch");
1804 assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
1805 store.persist().expect("publish next ownership epoch");
1806 }
1807 let reopened = UnifiedStore::open(&path).expect("reopen store");
1808 assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
1809 }
1810
1811 #[test]
1812 fn metadata_overflow_with_missing_continuation_fails_closed() {
1813 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
1814
1815 let directory = tempfile::tempdir().expect("temporary store");
1816 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
1817 let pager = store.pager.as_ref().unwrap();
1818 let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
1819 let valid_manifest = build_meta_page1_with_overflow(pager, &valid_payload).unwrap();
1820 pager.write_page(1, valid_manifest).unwrap();
1821 assert!(!existing_overflow_chain(pager).unwrap().is_empty());
1822 assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
1823
1824 let missing = pager.page_count().unwrap() + 100;
1825 let mut manifest = Page::new(PageType::Header, 1);
1826 reddb_file::encode_native_metadata_overflow_header(
1827 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1828 reddb_file::NativeMetadataOverflowHeader {
1829 format_version: STORE_VERSION_V11,
1830 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
1831 next_overflow_page_id: missing,
1832 },
1833 )
1834 .unwrap();
1835 pager.write_page(1, manifest).unwrap();
1836
1837 assert!(
1838 existing_overflow_chain(pager).is_err(),
1839 "a missing continuation must not be treated as an empty chain"
1840 );
1841
1842 let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
1843 let mut manifest = Page::new(PageType::Header, 1);
1844 reddb_file::encode_native_metadata_overflow_header(
1845 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1846 reddb_file::NativeMetadataOverflowHeader {
1847 format_version: STORE_VERSION_V11,
1848 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
1849 next_overflow_page_id: wrong_type.page_id(),
1850 },
1851 )
1852 .unwrap();
1853 pager.write_page(1, manifest).unwrap();
1854 assert!(
1855 existing_overflow_chain(pager).is_err(),
1856 "a live page must not be mistaken for metadata overflow"
1857 );
1858 assert!(
1859 read_meta_payload(pager).is_err(),
1860 "a live page must not be read as metadata overflow"
1861 );
1862
1863 let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
1864 let cycle_id = cycle.page_id();
1865 reddb_file::encode_native_metadata_overflow_continuation_header(
1866 &mut cycle.as_bytes_mut()[HEADER_SIZE..],
1867 reddb_file::NativeMetadataOverflowContinuationHeader {
1868 next_overflow_page_id: cycle_id,
1869 chunk_bytes: 1,
1870 },
1871 )
1872 .unwrap();
1873 pager.write_page(cycle_id, cycle).unwrap();
1874 let mut manifest = Page::new(PageType::Header, 1);
1875 reddb_file::encode_native_metadata_overflow_header(
1876 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1877 reddb_file::NativeMetadataOverflowHeader {
1878 format_version: STORE_VERSION_V11,
1879 total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
1880 next_overflow_page_id: cycle_id,
1881 },
1882 )
1883 .unwrap();
1884 pager.write_page(1, manifest).unwrap();
1885 assert!(
1886 read_meta_payload(pager).is_err(),
1887 "cyclic overflow must not be accepted"
1888 );
1889
1890 let mut manifest = Page::new(PageType::Header, 1);
1891 reddb_file::encode_native_metadata_overflow_header(
1892 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
1893 reddb_file::NativeMetadataOverflowHeader {
1894 format_version: STORE_VERSION_V11,
1895 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
1896 next_overflow_page_id: 0,
1897 },
1898 )
1899 .unwrap();
1900 pager.write_page(1, manifest).unwrap();
1901 assert!(
1902 read_meta_payload(pager).is_err(),
1903 "a truncated metadata payload must not be accepted"
1904 );
1905 }
1906
1907 #[test]
1908 fn retired_tree_pages_remain_pinned_by_a_cursor() {
1909 let directory = tempfile::tempdir().expect("temporary store");
1910 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1911 for id in 1..=100 {
1912 store.insert_auto("rows", row(id, 1)).expect("seed");
1913 }
1914 store.persist().expect("initial checkpoint");
1915 let tree = store
1916 .btree_indices
1917 .read()
1918 .get("rows")
1919 .cloned()
1920 .expect("tree");
1921 let mut cursor = tree.cursor_first().expect("cursor");
1922 drop(tree);
1923 let manager = store.get_collection("rows").expect("rows");
1924 for value in 2..=6 {
1925 manager.update(row(1, value)).expect("change");
1926 store.persist().expect("checkpoint with old reader");
1927 }
1928 let mut count = 0;
1929 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
1930 let (entity, _) =
1931 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1932 .expect("decode");
1933 assert_eq!(
1934 entity.data.as_row().expect("row").columns,
1935 [Value::Integer(1)]
1936 );
1937 count += 1;
1938 }
1939 assert_eq!(count, 100);
1940 drop(cursor);
1941 store.persist().expect("reclaim after reader release");
1942 assert!(store.retired_btrees.lock().is_empty());
1943 }
1944
1945 #[test]
1946 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
1947 let directory = tempfile::tempdir().expect("temporary store");
1948 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1949 store.create_collection("rows").expect("collection");
1950 let manager = store.get_collection("rows").expect("rows");
1951 for id in (1..=1200).rev() {
1952 manager
1953 .insert(UnifiedEntity::table_row(
1954 EntityId::new(id),
1955 "rows",
1956 id,
1957 vec![Value::text("x".repeat(4096))],
1958 ))
1959 .expect("seed");
1960 }
1961 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
1962 store.persist().expect("bounded checkpoint");
1963 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
1964 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
1965 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
1966 }
1967
1968 #[test]
1969 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1970 let directory = tempfile::tempdir().expect("temporary store");
1971 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1972 store.create_collection("rows").expect("collection");
1973 for id in 1..=40 {
1974 store
1975 .insert(
1976 "rows",
1977 UnifiedEntity::table_row(
1978 EntityId::new(id),
1979 "rows",
1980 id,
1981 vec![Value::Integer(id as i64)],
1982 ),
1983 )
1984 .expect("seed row");
1985 }
1986 store.persist().expect("initial checkpoint");
1987 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1988 store.persist().expect("unchanged checkpoint");
1989 assert_eq!(
1990 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1991 0,
1992 "unchanged collections must not hold the writer fence while serializing every row"
1993 );
1994 drop(store);
1995 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1996 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1997 reopened.persist().expect("unchanged recovered generation");
1998 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1999 }
2000
2001 #[test]
2002 fn sparse_wal_replay_does_not_reencode_complete_collections() {
2003 let directory = tempfile::tempdir().expect("temporary store");
2004 let path = directory.path().join("data.rdb");
2005 let store = UnifiedStore::open(&path).expect("paged store");
2006 for (name, first) in [("rows", 1), ("cold", 129)] {
2007 store.create_collection(name).expect("collection");
2008 for id in first..first + 128 {
2009 store
2010 .insert(
2011 name,
2012 UnifiedEntity::table_row(
2013 EntityId::new(id),
2014 name,
2015 id,
2016 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2017 ),
2018 )
2019 .expect("seed");
2020 }
2021 }
2022 store.persist().expect("durable baseline");
2023 let action = super::super::commit::StoreWalAction::upsert_entity(
2024 "rows",
2025 &row(1, 42),
2026 None,
2027 STORE_VERSION_V11,
2028 );
2029 store
2031 .commit
2032 .as_ref()
2033 .unwrap()
2034 .append_actions(&[action])
2035 .unwrap();
2036 drop(store);
2037
2038 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2039 let recovered = UnifiedStore::open(&path).expect("replay committed change");
2040 assert_eq!(
2041 recovered
2042 .get("rows", EntityId::new(1))
2043 .unwrap()
2044 .data
2045 .as_row()
2046 .unwrap()
2047 .columns,
2048 vec![Value::Integer(42)]
2049 );
2050 for name in ["rows", "cold"] {
2051 assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
2052 }
2053 assert_eq!(
2054 REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
2055 0,
2056 "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
2057 );
2058 for (name, first) in [("rows", 2), ("cold", 129)] {
2059 let end = if name == "rows" { 129 } else { 257 };
2060 for id in first..end {
2061 assert_eq!(
2062 recovered
2063 .get(name, EntityId::new(id))
2064 .unwrap()
2065 .data
2066 .as_row()
2067 .unwrap()
2068 .columns,
2069 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2070 );
2071 }
2072 }
2073
2074 drop(recovered);
2075 let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
2076 assert_eq!(
2077 reopened
2078 .get("rows", EntityId::new(1))
2079 .unwrap()
2080 .data
2081 .as_row()
2082 .unwrap()
2083 .columns,
2084 vec![Value::Integer(42)]
2085 );
2086 }
2087
2088 fn replay_test_payload(bytes: usize) -> String {
2089 let mut state = 0x1234_5678_u32;
2090 (0..bytes)
2091 .map(|_| {
2092 state ^= state << 13;
2093 state ^= state >> 17;
2094 state ^= state << 5;
2095 char::from(b'!' + (state % 90) as u8)
2096 })
2097 .collect()
2098 }
2099
2100 #[test]
2101 fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
2102 let directory = tempfile::tempdir().unwrap();
2103 let path = directory.path().join("data.rdb");
2104 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
2105 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2106 for id in 1..=16 {
2107 store.insert_auto("rows", row(id, 1)).unwrap();
2108 }
2109 store.persist().unwrap();
2110 let payload = replay_test_payload(64 * 1024);
2112 let changed =
2113 UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
2114 let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2115 store
2116 .commit
2117 .as_ref()
2118 .unwrap()
2119 .append_actions(&[action])
2120 .unwrap();
2121 drop(store);
2122 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2123 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
2124 assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
2125 assert_eq!(
2126 recovered
2127 .get("rows", EntityId::new(1))
2128 .unwrap()
2129 .data
2130 .as_row()
2131 .unwrap()
2132 .columns,
2133 changed.data.as_row().unwrap().columns
2134 );
2135 drop(recovered);
2136 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
2137 assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
2138 assert_eq!(
2139 reopened
2140 .get("rows", EntityId::new(1))
2141 .unwrap()
2142 .data
2143 .as_row()
2144 .unwrap()
2145 .columns,
2146 changed.data.as_row().unwrap().columns
2147 );
2148 }
2149
2150 #[cfg(target_os = "linux")]
2151 #[test]
2152 fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
2153 const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
2154 if let Some(path) = std::env::var_os(CHILD) {
2155 let _store = UnifiedStore::open(path).unwrap();
2156 panic!("replay crash hook was not reached");
2157 }
2158 for overflow in [false, true] {
2159 for (hook, code) in [
2160 ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
2161 ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
2162 ] {
2163 let directory = tempfile::tempdir().unwrap();
2164 let path = directory.path().join("data.rdb");
2165 let store = UnifiedStore::open(&path).unwrap();
2166 for id in 1..=128 {
2167 store.insert_auto("rows", row(id, 1)).unwrap();
2168 }
2169 store.persist().unwrap();
2170 let changed = if overflow {
2171 UnifiedEntity::table_row(
2172 EntityId::new(1),
2173 "rows",
2174 1,
2175 vec![Value::text(replay_test_payload(16 * 1024))],
2176 )
2177 } else {
2178 row(1, 42)
2179 };
2180 let action =
2181 StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2182 store
2183 .commit
2184 .as_ref()
2185 .unwrap()
2186 .append_actions(&[action])
2187 .unwrap();
2188 drop(store);
2189 let mut first_size = 0;
2190 for cycle in 0..12 {
2191 let output = std::process::Command::new(std::env::current_exe().unwrap())
2192 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
2193 .env(CHILD, &path).env(hook, "1").output().unwrap();
2194 assert_eq!(
2195 output.status.code(),
2196 Some(code),
2197 "overflow={overflow} hook={hook} cycle={cycle}: {}",
2198 String::from_utf8_lossy(&output.stderr)
2199 );
2200 let size = std::fs::metadata(&path).unwrap().len();
2201 if cycle == 0 {
2202 first_size = size;
2203 }
2204 assert!(
2205 size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
2206 "repeated recovery must reuse reservations: {first_size} -> {size}"
2207 );
2208 }
2209 for _ in 0..2 {
2210 let recovered = UnifiedStore::open(&path).unwrap();
2211 assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
2212 for id in 1..=128 {
2213 let expected = if id == 1 { changed.clone() } else { row(id, 1) };
2214 assert_eq!(
2215 recovered
2216 .get("rows", EntityId::new(id))
2217 .unwrap()
2218 .data
2219 .as_row()
2220 .unwrap()
2221 .columns,
2222 expected.data.as_row().unwrap().columns
2223 );
2224 }
2225 }
2226 }
2227 }
2228 }
2229
2230 #[test]
2231 fn paged_reopen_preserves_stored_sequence_identity() {
2232 let directory = tempfile::tempdir().expect("temporary store");
2233 let path = directory.path().join("data.rdb");
2234 let store = UnifiedStore::open(&path).expect("paged store");
2235 store.create_collection("rows").expect("collection");
2236 for id in [30, 10, 20] {
2237 store.insert("rows", row(id, id as i64)).expect("insert");
2238 }
2239 store.persist().expect("checkpoint");
2240 let sequences = [10, 20, 30].map(|id| {
2241 store
2242 .get("rows", EntityId::new(id))
2243 .expect("row")
2244 .sequence_id
2245 });
2246 drop(store);
2247 let reopened = UnifiedStore::open(&path).expect("reopen");
2248 let recovered = [10, 20, 30].map(|id| {
2249 reopened
2250 .get("rows", EntityId::new(id))
2251 .expect("row")
2252 .sequence_id
2253 });
2254 assert_eq!(
2255 recovered, sequences,
2256 "opening a checkpoint must not rewrite stored row identities"
2257 );
2258 }
2259
2260 #[test]
2261 fn auto_insert_persists_the_assigned_sequence_identity() {
2262 let directory = tempfile::tempdir().expect("temporary store");
2263 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2264 for id in 1u64..=3 {
2265 store
2266 .insert_auto("rows", row(id, id as i64))
2267 .expect("insert");
2268 }
2269 let manager = store.get_collection("rows").expect("rows");
2270 let tree = store
2271 .btree_indices
2272 .read()
2273 .get("rows")
2274 .cloned()
2275 .expect("tree");
2276 for id in 1u64..=3 {
2277 let bytes = tree
2278 .get(&id.to_be_bytes())
2279 .expect("lookup")
2280 .expect("stored row");
2281 let (persisted, _) =
2282 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2283 .expect("decode");
2284 assert_eq!(
2285 persisted.sequence_id,
2286 manager
2287 .get(EntityId::new(id))
2288 .expect("live row")
2289 .sequence_id
2290 );
2291 }
2292 }
2293
2294 #[test]
2295 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
2296 let directory = tempfile::tempdir().expect("temporary store");
2297 let path = directory.path().join("data.rdb");
2298 let store = UnifiedStore::open(&path).expect("paged store");
2299 store.insert_auto("rows", row(1, 1)).expect("seed");
2300 store.persist().expect("checkpoint");
2301 store.insert_auto("rows", row(2, 2)).expect("append");
2302 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2303 store.persist().expect("inline append checkpoint");
2304 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2305 store
2306 .get_collection("rows")
2307 .expect("rows")
2308 .update(row(1, 99))
2309 .expect("untracked edit");
2310 store.insert_auto("rows", row(3, 3)).expect("next append");
2311 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2312 store.persist().expect("untracked edit checkpoint");
2313 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
2314 drop(store);
2315 let reopened = UnifiedStore::open(&path).expect("reopen");
2316 assert_eq!(
2317 reopened
2318 .get("rows", EntityId::new(1))
2319 .expect("row")
2320 .data
2321 .as_row()
2322 .expect("table")
2323 .columns,
2324 vec![Value::Integer(99)]
2325 );
2326 }
2327
2328 #[test]
2329 fn replayed_trees_are_reusable_after_durable_reconciliation() {
2330 let directory = tempfile::tempdir().expect("temporary store");
2331 let path = directory.path().join("data.rdb");
2332 let store = UnifiedStore::open(&path).expect("paged store");
2333 store.insert_auto("rows", row(1, 1)).expect("seed");
2334 store.persist().expect("checkpoint");
2335 store
2336 .insert_auto("rows", row(2, 2))
2337 .expect("WAL-backed append");
2338 drop(store);
2339 let reopened = UnifiedStore::open(&path).expect("replay");
2340 assert!(reopened.get("rows", EntityId::new(2)).is_some());
2341 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2342 reopened.persist().expect("checkpoint reconciled tree");
2343 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2344 }
2345
2346 fn row(id: u64, value: i64) -> UnifiedEntity {
2347 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
2348 }
2349
2350 #[test]
2351 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
2352 let directory = tempfile::tempdir().expect("temporary store");
2353 let path = directory.path().join("data.rdb");
2354 let store = UnifiedStore::open(&path).expect("paged store");
2355 store.create_collection("rows").expect("collection");
2356 store.insert("rows", row(1, 1)).expect("seed");
2357 store.persist().expect("initial checkpoint");
2358 let manager = store.get_collection("rows").expect("rows");
2359 manager.update(row(1, 2)).expect("direct update");
2360 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2361 store.persist().expect("updated row checkpoint");
2362 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2363 let mut metadata = Metadata::new();
2364 metadata.set("verified", MetadataValue::Bool(true));
2365 manager
2366 .set_metadata(EntityId::new(1), metadata)
2367 .expect("metadata");
2368 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2369 store.persist().expect("metadata-only checkpoint");
2370 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2371 manager
2372 .bulk_insert(vec![row(2, 2), row(3, 3)])
2373 .expect("bulk insert");
2374 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2375 store.persist().expect("bulk insert checkpoint");
2376 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
2377 manager.delete(EntityId::new(2)).expect("delete");
2378 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2379 store.persist().expect("changed checkpoint");
2380 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
2381 drop(store);
2382 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
2383 let manager = recovered.get_collection("rows").expect("rows");
2384 assert_eq!(manager.count(), 2);
2385 assert_eq!(
2386 manager
2387 .get(EntityId::new(1))
2388 .expect("row")
2389 .data
2390 .as_row()
2391 .expect("table")
2392 .columns,
2393 vec![Value::Integer(2)]
2394 );
2395 assert!(manager.get(EntityId::new(2)).is_none());
2396 assert!(manager
2397 .get_metadata(EntityId::new(1))
2398 .expect("metadata")
2399 .get("verified")
2400 .is_some());
2401 }
2402
2403 #[test]
2404 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
2405 let directory = tempfile::tempdir().expect("temporary store");
2406 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2407 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2408 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2409 store.create_collection("rows").expect("collection");
2410 for id in 1..=40 {
2411 store.insert("rows", row(id, id as i64)).expect("seed");
2412 }
2413 store.persist().expect("initial checkpoint");
2414 let unrelated = snapshots.begin();
2415 snapshots.rollback(unrelated);
2416 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2417 store.persist().expect("unrelated rollback checkpoint");
2418 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2419
2420 store
2421 .pending_vault_versions
2422 .write()
2423 .insert(EntityId::new(100));
2424 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2425 store.persist().expect("unrelated vault checkpoint");
2426 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2427 store.pending_vault_versions.write().clear();
2428 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2429 store
2430 .persist()
2431 .expect("unrelated vault publication checkpoint");
2432 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2433 }
2434
2435 #[test]
2436 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
2437 let directory = tempfile::tempdir().expect("temporary store");
2438 let path = directory.path().join("data.rdb");
2439 let store = UnifiedStore::open(&path).expect("paged store");
2440 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2441 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2442 store.create_collection("rows").expect("collection");
2443 let xid = snapshots.begin();
2444 let mut entity = row(1, 1);
2445 entity.xmin = xid;
2446 store.insert("rows", entity).expect("committed row");
2447 snapshots.commit(xid);
2448 store.persist().expect("committed baseline");
2449 snapshots.rollback(xid);
2450 store
2451 .persist()
2452 .expect("untracked outcome requires validation");
2453 drop(store);
2454 let recovered = UnifiedStore::open(&path).expect("recover");
2455 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2456 }
2457
2458 #[test]
2459 fn exhausted_deferred_row_cache_keeps_full_validation() {
2460 let directory = tempfile::tempdir().expect("temporary store");
2461 let path = directory.path().join("data.rdb");
2462 let store = UnifiedStore::open(&path).expect("paged store");
2463 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2464 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2465 store.create_collection("rows").expect("collection");
2466 let xid = snapshots.begin();
2467 for id in 1..=1025 {
2468 let mut entity = row(id, id as i64);
2469 entity.xmin = xid;
2470 store.insert("rows", entity).expect("pending row");
2471 }
2472 store.persist().expect("exclude pending rows");
2473 assert!(store.checkpoint_versions.read()["rows"]
2474 .deferred_rows
2475 .is_none());
2476 snapshots.commit(xid);
2477 store
2478 .persist()
2479 .expect("publish committed rows conservatively");
2480 drop(store);
2481 let recovered = UnifiedStore::open(&path).expect("recover");
2482 assert_eq!(
2483 recovered.get_collection("rows").expect("rows").count(),
2484 1025
2485 );
2486 }
2487
2488 #[test]
2489 fn rollback_invalidates_reuse_without_a_segment_mutation() {
2490 let directory = tempfile::tempdir().expect("temporary store");
2491 let path = directory.path().join("data.rdb");
2492 let store = UnifiedStore::open(&path).expect("paged store");
2493 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2494 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2495 store.create_collection("rows").expect("collection");
2496 let xid = snapshots.begin();
2497 let mut pending = row(1, 1);
2498 pending.xmin = xid;
2499 store.insert("rows", pending).expect("version");
2500 store.persist().expect("before rollback");
2501 snapshots.rollback(xid);
2502 store.persist().expect("after rollback");
2503 drop(store);
2504 let recovered = UnifiedStore::open(&path).expect("recover");
2505 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2506 }
2507
2508 #[test]
2509 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
2510 let directory = tempfile::tempdir().expect("temporary store");
2511 let path = directory.path().join("data.rdb");
2512 let store = UnifiedStore::open(&path).expect("paged store");
2513 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2514 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2515 store.create_collection("rows").expect("collection");
2516 let xid = snapshots.begin();
2517 let mut deleted = row(1, 1);
2518 deleted.xmax = xid;
2519 store.insert("rows", deleted).expect("version");
2520 store.persist().expect("before rollback");
2521 snapshots.rollback(xid);
2522 store.persist().expect("after rollback");
2523 drop(store);
2524 let recovered = UnifiedStore::open(&path).expect("recover");
2525 let manager = recovered.get_collection("rows").expect("rows");
2526 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
2527 }
2528
2529 #[test]
2530 fn publishing_a_pending_vault_version_invalidates_reuse() {
2531 let directory = tempfile::tempdir().expect("temporary store");
2532 let path = directory.path().join("data.rdb");
2533 let store = UnifiedStore::open(&path).expect("paged store");
2534 store.create_collection("rows").expect("collection");
2535 store.insert("rows", row(1, 1)).expect("row");
2536 store
2537 .pending_vault_versions
2538 .write()
2539 .insert(EntityId::new(1));
2540 store.persist().expect("exclude pending vault version");
2541 store
2542 .pending_vault_versions
2543 .write()
2544 .remove(&EntityId::new(1));
2545 store.persist().expect("include published vault version");
2546 drop(store);
2547 let recovered = UnifiedStore::open(&path).expect("recover");
2548 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
2549 }
2550}