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 pager = self.pager.as_ref()?;
844 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
845 return Some(btree);
846 }
847 let mut write = self.btree_indices.write();
848 let btree = write
849 .entry(collection.to_string())
850 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
851 .clone();
852 Some(btree)
853 }
854
855 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
856 let Some(pager) = &self.pager else {
857 return Ok(());
858 };
859
860 if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
861 self.flush_paged_registry()?;
862 self.resident
863 .paged_registry_dirty
864 .store(false, Ordering::Release);
865 return Ok(());
866 }
867
868 pager
869 .flush()
870 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
871 }
872
873 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
874 let _vault_guard = self.vault_publication_lock.lock();
875 let Some(pager) = &self.pager else {
876 return Ok(());
877 };
878
879 match pager.read_page(1) {
880 Ok(_) => {}
881 Err(PagerError::PageNotFound(_)) => {
882 let meta_page = pager
883 .allocate_page(crate::storage::engine::PageType::Header)
884 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
885 pager
886 .write_page(meta_page.page_id(), meta_page)
887 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
888 }
889 Err(e) => {
890 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
891 }
892 }
893
894 let format_version = STORE_VERSION_V11;
895 self.set_format_version(format_version);
896
897 let collections = self.resident.collections.read();
898 let btree_indices = self.btree_indices.read();
899 let mut collection_roots = Vec::with_capacity(collections.len());
900 for (name, _) in collections.iter() {
901 let root_page = btree_indices
902 .get(name)
903 .map_or(0, |btree| btree.root_page_id());
904 collection_roots.push((name.clone(), root_page));
905 }
906 drop(btree_indices);
907 drop(collections);
908
909 let mut meta_data = Vec::with_capacity(4096);
910 reddb_file::encode_native_paged_metadata_header(
911 &mut meta_data,
912 reddb_file::NativePagedMetadataHeader {
913 format_version,
914 collection_count: collection_roots.len() as u32,
915 },
916 );
917 for (name, root_page) in &collection_roots {
918 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
919 }
920
921 let cross_refs = self.resident.cross_refs.read();
922 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
923 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
924 for (source_id, refs) in cross_refs.iter() {
925 for (target_id, ref_type, collection) in refs {
926 reddb_file::encode_native_paged_cross_ref(
927 &mut meta_data,
928 source_id.raw(),
929 target_id.raw(),
930 ref_type.to_byte(),
931 collection,
932 );
933 }
934 }
935
936 self.publish_reclaimable_trees(
937 pager,
938 &mut meta_data,
939 &self.btree_indices.read(),
940 &self.key_indices.read(),
941 )?;
942
943 Ok(())
944 }
945
946 pub fn pager(&self) -> Option<&Arc<Pager>> {
948 self.pager.as_ref()
949 }
950
951 pub fn page_cache_bytes_in_use(&self) -> u64 {
955 self.pager.as_ref().map_or(0, |pager| {
956 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
957 })
958 }
959
960 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
963 self.commit
964 .as_ref()
965 .map_or(0, |commit| commit.buffered_bytes())
966 }
967
968 pub fn segment_memory_bytes(&self) -> u64 {
972 let managers: Vec<Arc<SegmentManager>> = self
973 .resident
974 .collections
975 .read()
976 .values()
977 .map(Arc::clone)
978 .collect();
979
980 managers
981 .iter()
982 .map(|manager| manager.resident_bytes())
983 .fold(0, u64::saturating_add)
984 }
985
986 pub fn config(&self) -> &UnifiedStoreConfig {
990 &self.config
991 }
992
993 pub fn with_config(config: UnifiedStoreConfig) -> Self {
994 Self {
995 config,
996 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
997 format_version: AtomicU32::new(STORE_VERSION_V11),
998 pager: None,
999 db_path: None,
1000 btree_indices: RwLock::new(HashMap::new()),
1001 key_indices: RwLock::new(HashMap::new()),
1002 secondary_index_specs: RwLock::new(HashMap::new()),
1003 recovery_xid_summary: RwLock::new(None),
1004 checkpoint_versions: RwLock::new(HashMap::new()),
1005 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1006 retired_btrees: parking_lot::Mutex::new(Vec::new()),
1007 ownership_catalog_epoch: AtomicU64::new(0),
1008 ownership_active_epoch: AtomicU64::new(0),
1009 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1010 commit: None,
1011 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1012 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1013 snapshot_manager: std::sync::OnceLock::new(),
1014 embedded_commit_session: std::sync::OnceLock::new(),
1015 #[cfg(test)]
1016 test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1017 #[cfg(test)]
1018 test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1019 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1020 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1021 }
1022 }
1023
1024 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
1042 Self::open_with_config(path, UnifiedStoreConfig::default())
1043 }
1044
1045 pub fn open_with_config(
1046 path: impl AsRef<Path>,
1047 config: UnifiedStoreConfig,
1048 ) -> Result<Self, StoreError> {
1049 let path = path.as_ref();
1050 let mut pager_config = PagerConfig {
1051 disk_residency: config.resident_memory_bytes_max.is_some(),
1052 ..PagerConfig::default()
1053 };
1054 if let Some(slots) = config.page_cache_slots {
1058 pager_config.cache_size = slots;
1059 }
1060 if matches!(
1068 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
1069 Some("0") | Some("false") | Some("off")
1070 ) {
1071 pager_config.double_write = false;
1072 }
1073 let pager_open_started = std::time::Instant::now();
1074 let pager = Pager::open(path, pager_config)
1075 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1076 pager
1077 .enable_deferred_reclamation()
1078 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1079 trace_startup_phase(path, "paged_pager_open", pager_open_started);
1080
1081 if config.resident_memory_bytes_max.is_none()
1082 && pager
1083 .header()
1084 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1085 .version
1086 >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
1087 {
1088 return Err(StoreError::Io(std::io::Error::new(std::io::ErrorKind::Unsupported,
1089 "this database was prepared for disk residency; reopen with the disk-resident storage profile")));
1090 }
1091
1092 let wal_path = reddb_file::layout::unified_wal_path(path);
1093 let commit = Some(Arc::new(
1096 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
1097 .map_err(StoreError::Io)?,
1098 ));
1099
1100 let store = Self {
1101 config,
1102 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
1103 format_version: AtomicU32::new(STORE_VERSION_V11),
1104 pager: Some(Arc::new(pager)),
1105 db_path: Some(path.to_path_buf()),
1106 btree_indices: RwLock::new(HashMap::new()),
1107 key_indices: RwLock::new(HashMap::new()),
1108 secondary_index_specs: RwLock::new(HashMap::new()),
1109 recovery_xid_summary: RwLock::new(None),
1110 checkpoint_versions: RwLock::new(HashMap::new()),
1111 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
1112 retired_btrees: parking_lot::Mutex::new(Vec::new()),
1113 ownership_catalog_epoch: AtomicU64::new(0),
1114 ownership_active_epoch: AtomicU64::new(0),
1115 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
1116 commit,
1117 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
1118 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
1119 snapshot_manager: std::sync::OnceLock::new(),
1120 embedded_commit_session: std::sync::OnceLock::new(),
1121 #[cfg(test)]
1122 test_fail_deferred_wal_append: std::sync::atomic::AtomicBool::new(false),
1123 #[cfg(test)]
1124 test_require_deferred_publication_fence: std::sync::atomic::AtomicBool::new(false),
1125 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
1126 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
1127 };
1128
1129 let checkpoint_recovery_started = std::time::Instant::now();
1132 if let Some(commit) = &store.commit {
1133 commit
1134 .restore_checkpoint_page_images(&store)
1135 .map_err(StoreError::Io)?;
1136 }
1137 trace_startup_phase(
1138 path,
1139 "paged_checkpoint_recovery",
1140 checkpoint_recovery_started,
1141 );
1142 let page_load_started = std::time::Instant::now();
1144 store.load_from_pages()?;
1145 trace_startup_phase(path, "paged_catalog_and_roots", page_load_started);
1146 let wal_replay_started = std::time::Instant::now();
1147 if let Some(commit) = &store.commit {
1148 commit.replay_into(&store).map_err(StoreError::Io)?;
1149 }
1150 trace_startup_phase(path, "paged_wal_replay", wal_replay_started);
1151 let manifest_recovery_started = std::time::Instant::now();
1152 store.recover_operational_manifest()?;
1153 if store.config.resident_memory_bytes_max.is_some() {
1154 let missing_key_index = {
1155 let trees = store.btree_indices.read();
1156 let keys = store.key_indices.read();
1157 trees.iter().any(|(name, tree)| {
1158 !keys
1159 .get(name)
1160 .is_some_and(|index| index.source.ptr_eq(&Arc::downgrade(tree)))
1161 })
1162 };
1163 if missing_key_index {
1164 store.persist()?;
1165 }
1166 }
1167 trace_startup_phase(
1168 path,
1169 "paged_manifest_and_derived_roots",
1170 manifest_recovery_started,
1171 );
1172
1173 Ok(store)
1174 }
1175
1176 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
1180 let Some(pager) = &self.pager else {
1181 return Ok(());
1182 };
1183 let collections = self.resident.collections.read();
1184 let mut indices = self.btree_indices.write();
1185 let previous_versions = self.checkpoint_versions.read();
1186 let raw_replay =
1187 self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty();
1188 for (name, manager) in collections.iter() {
1189 let mut reusable = None;
1190 if raw_replay {
1191 if let (Some(previous), Some(tree)) =
1192 (previous_versions.get(name), indices.get(name))
1193 {
1194 let mut current = CheckpointVersion {
1195 manager: Arc::downgrade(manager),
1196 segments: manager.checkpoint_revisions(),
1197 visibility_revision: 0,
1198 untracked_visibility_revision: 0,
1199 excluded_xids: Arc::default(),
1200 pending_vault: Arc::default(),
1201 format_version: self.format_version(),
1202 deferred_rows: Some(Arc::default()),
1203 has_unprojected_rows: false,
1204 };
1205 if previous.manager.ptr_eq(¤t.manager)
1206 && previous.format_version == current.format_version
1207 {
1208 if previous.segments == current.segments
1209 && !previous.has_unprojected_rows
1210 && checkpoint_visibility_matches(previous, ¤t)
1211 {
1212 reusable = Some(Arc::clone(tree));
1213 } else {
1214 reusable = self.sparse_checkpoint_tree(
1220 name,
1221 manager,
1222 tree,
1223 previous,
1224 &mut current,
1225 )?;
1226 }
1227 }
1228 }
1229 }
1230 let tree = match reusable {
1231 Some(tree) => tree,
1232 None => {
1233 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
1234 #[cfg(test)]
1235 REPLAY_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1236 Some(Self::serialize_entity_record(
1237 entity,
1238 metadata.as_ref(),
1239 self.format_version(),
1240 ))
1241 })?
1242 }
1243 };
1244 if indices
1245 .get(name)
1246 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
1247 {
1248 continue;
1249 }
1250 if let Some(previous) = indices.insert(name.clone(), tree) {
1251 self.retire_btree(previous);
1252 }
1253 self.mark_paged_registry_dirty();
1254 #[cfg(test)]
1255 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD").is_some() {
1256 if std::env::var_os("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC").is_none() {
1257 pager.sync().map_err(|error| {
1258 StoreError::Io(std::io::Error::other(error.to_string()))
1259 })?;
1260 }
1261 eprintln!("experimental replay rebuild exit before manifest");
1262 std::process::exit(182);
1263 }
1264 }
1265 drop(previous_versions);
1266 drop(indices);
1267 drop(collections);
1268 self.flush_paged_state()?;
1269 #[cfg(test)]
1270 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION").is_some() {
1271 eprintln!("experimental replay exit after manifest");
1272 std::process::exit(182);
1273 }
1274 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
1277 let collections = self.resident.collections.read();
1278 let mut versions = self.checkpoint_versions.write();
1279 for (name, manager) in collections.iter() {
1280 versions.insert(
1281 name.clone(),
1282 CheckpointVersion {
1283 manager: Arc::downgrade(manager),
1284 segments: manager.checkpoint_revisions(),
1285 visibility_revision: 0,
1286 untracked_visibility_revision: 0,
1287 excluded_xids: Arc::default(),
1288 pending_vault: Arc::default(),
1289 format_version: self.format_version(),
1290 deferred_rows: Some(Arc::default()),
1291 has_unprojected_rows: false,
1292 },
1293 );
1294 }
1295 }
1296 Ok(())
1297 }
1298
1299 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
1300 let Some(path) = &self.db_path else {
1301 return Ok(());
1302 };
1303 let mut collections = self.list_collections();
1304 collections.sort();
1305 let pending_drops =
1306 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1307 .recover_or_bootstrap(&collections)
1308 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
1309 for name in pending_drops {
1310 if self.get_collection(&name).is_some() {
1311 self.drop_collection(&name)?;
1312 }
1313 }
1314 Ok(())
1315 }
1316
1317 pub(crate) fn prepare_operational_collection_create(
1318 &self,
1319 name: &str,
1320 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
1321 let Some(path) = &self.db_path else {
1322 return Ok(None);
1323 };
1324 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1325 .prepare_collection_create(name)
1326 .map(Some)
1327 .map_err(StoreError::Io)
1328 }
1329
1330 pub(crate) fn prepare_operational_collection_drop(
1331 &self,
1332 name: &str,
1333 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
1334 let Some(path) = &self.db_path else {
1335 return Ok(None);
1336 };
1337 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
1338 .prepare_collection_drop(name)
1339 .map(Some)
1340 .map_err(StoreError::Io)
1341 }
1342
1343 pub(super) fn published_ownership_epoch(&self) -> Result<u64, StoreError> {
1345 let Some(pager) = &self.pager else {
1346 return Ok(0);
1347 };
1348 let Some(content) = read_meta_payload(pager)? else {
1349 return Ok(0);
1350 };
1351 reddb_file::native_store::decode_native_paged_catalog_ownership_epoch(&content)
1352 .map(|epoch| epoch.unwrap_or(0))
1353 .map_err(|error| StoreError::Serialization(error.to_string()))
1354 }
1355
1356 fn load_from_pages(&self) -> Result<(), StoreError> {
1358 let pager = match &self.pager {
1359 Some(p) => p,
1360 None => return Ok(()), };
1362
1363 let page_count = pager.page_count().map_err(|e| {
1365 StoreError::Io(std::io::Error::other(format!(
1366 "failed to read page count: {}",
1367 e
1368 )))
1369 })?;
1370 if page_count <= 1 {
1371 return Ok(());
1373 }
1374
1375 if let Some(content_vec) = read_meta_payload(pager)? {
1380 let content: &[u8] = &content_vec;
1381 let file_version = pager
1382 .header()
1383 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?
1384 .version;
1385 let prepared =
1386 file_version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION;
1387 let resume = if prepared {
1388 reddb_file::native_resume::decode_catalog_resume(content)
1389 .map_err(|error| StoreError::Serialization(error.to_string()))?
1390 } else {
1391 None
1392 };
1393 if let Some(resume) = &resume {
1394 for state in resume.collections.values() {
1395 if !state.secondary_indices.is_empty()
1396 && file_version < reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
1397 {
1398 return Err(page_integrity(
1399 1,
1400 None,
1401 "secondary roots require the file format fence",
1402 ));
1403 }
1404 for root in std::iter::once(state.root_page)
1405 .chain(state.key_index_root)
1406 .chain(
1407 state
1408 .secondary_indices
1409 .values()
1410 .map(|index| index.root_page),
1411 )
1412 {
1413 if root >= page_count {
1414 return Err(page_integrity(
1415 root,
1416 None,
1417 "published root exceeds the database page count",
1418 ));
1419 }
1420 }
1421 }
1422 self.resident
1423 .next_entity_id
1424 .fetch_max(resume.entity_id_next, Ordering::SeqCst);
1425 }
1426 *self.recovery_xid_summary.write() = resume.as_ref().and_then(|resume| {
1427 resume.xid_high_water_mark.map(|high_water_mark| {
1428 super::recovery_xid::RecoveryXidSummary {
1429 high_water_mark,
1430 roots: resume
1431 .collections
1432 .iter()
1433 .map(|(name, state)| (name.clone(), state.root_page))
1434 .collect(),
1435 }
1436 })
1437 });
1438 if content.len() >= 4 {
1439 let mut pos = 0;
1440 let mut format_version = STORE_VERSION_V1;
1441
1442 let collection_count = if let Some(header) =
1443 reddb_file::decode_native_paged_metadata_header(content)
1444 .map_err(|err| StoreError::Serialization(err.to_string()))?
1445 {
1446 format_version = header.format_version;
1447 pos += reddb_file::METADATA_HEADER_BYTES;
1448 header.collection_count as usize
1449 } else {
1450 let count = u32::from_le_bytes([
1451 content[pos],
1452 content[pos + 1],
1453 content[pos + 2],
1454 content[pos + 3],
1455 ]) as usize;
1456 pos += 4;
1457 count
1458 };
1459
1460 self.set_format_version(format_version);
1461
1462 if pos > content.len() {
1463 return Ok(());
1464 }
1465
1466 for _ in 0..collection_count {
1468 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1469 .map_err(|err| page_integrity(1, None, err.to_string()))?;
1470 let root_page = root.root_page;
1472 let name = root.collection;
1473
1474 let _ = self.create_collection_in_memory(&name);
1478
1479 let counters = resume
1480 .as_ref()
1481 .and_then(|resume| resume.collections.get(&name));
1482 if let (Some(counters), Some(manager)) = (counters, self.get_collection(&name))
1483 {
1484 manager.restore_identity_counters(
1485 counters.entity_id_next,
1486 counters.row_id_next,
1487 );
1488 manager.restore_sequence_id_next(counters.sequence_id_next);
1489 }
1490 if root_page > 0 || counters.is_some() {
1492 let btree = BTree::with_root(Arc::clone(pager), root_page);
1493
1494 let mut cursor = btree.cursor_first().map_err(|err| {
1496 page_integrity(root_page, Some(name.clone()), err.to_string())
1497 })?;
1498 let manager = self.get_collection(&name);
1499 let mut persisted_entities = counters.map_or(0, |state| state.entities);
1500 while counters.is_none() {
1501 let next = cursor
1502 .next_with_budget(
1503 self.config
1504 .resident_memory_bytes_max
1505 .and_then(|limit| usize::try_from(limit).ok())
1506 .unwrap_or(usize::MAX),
1507 )
1508 .map_err(|err| {
1509 page_integrity(root_page, Some(name.clone()), err.to_string())
1510 })?;
1511 let Some((key, value)) = next else {
1512 break;
1513 };
1514 persisted_entities += 1;
1515 let key_bytes: [u8; 8] = key.as_slice().try_into().map_err(|_| {
1517 page_integrity(
1518 root_page,
1519 Some(name.clone()),
1520 "physical entity key must contain eight bytes",
1521 )
1522 })?;
1523 let record = reddb_engine::storage::collection_read::EncodedRecord {
1524 id: EntityId::new(u64::from_be_bytes(key_bytes)),
1525 bytes: value,
1526 };
1527 let decoded = if let Some(limit) = self.config.resident_memory_bytes_max
1528 {
1529 record.decode_with_budget(
1530 self.format_version(),
1531 usize::try_from(limit).unwrap_or(usize::MAX),
1532 )
1533 } else {
1534 record.decode(self.format_version())
1535 };
1536 let (entity, metadata) = decoded.map_err(|err| {
1537 page_integrity(root_page, Some(name.clone()), err.to_string())
1538 })?;
1539 if let Some(m) = &manager {
1540 let id = entity.id;
1541 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1542 m.register_row_id(*row_id);
1543 }
1544 if self.config.resident_memory_bytes_max.is_some() {
1545 self.register_entity_id(id);
1546 m.restore_sequence_id_next(
1547 entity.sequence_id.saturating_add(1),
1548 );
1549 m.restore_identity_counters(
1550 id.raw().saturating_add(1),
1551 m.identity_counters().1,
1552 );
1553 continue;
1554 }
1555 self.resident.context_index.index_entity(&name, &entity);
1556 m.restore_snapshot_entity(entity.clone())?;
1557 if let Some(metadata) = metadata {
1558 let _ = m.set_metadata(id, metadata);
1559 }
1560 self.register_entity_id(id);
1561 if self.config.auto_index_refs {
1562 self.index_cross_refs(&entity, &name)?;
1563 }
1564 }
1565 }
1566
1567 let btree = Arc::new(btree);
1569 let key_tree = counters
1570 .and_then(|state| state.key_index_root)
1571 .map(|root| Arc::new(BTree::with_root(Arc::clone(pager), root)));
1572 if let Some(counters) = counters {
1573 self.key_indices.write().insert(
1574 name.clone(),
1575 PublishedKeyIndex {
1576 indexed: key_tree.is_some(),
1577 secondary: counters.secondary_indices.iter().map(|(name, index)| {
1578 (name.clone(), super::secondary_index::PublishedSecondaryIndex {
1579 identity: super::secondary_index::SecondaryIndexIdentity {
1580 codec_version: index.codec_version,
1581 definition: index.definition,
1582 },
1583 tree: Arc::new(BTree::with_root(Arc::clone(pager), index.root_page)),
1584 })
1585 }).collect(),
1586 source: Arc::downgrade(&btree),
1587 tree: key_tree.clone().unwrap_or_else(|| {
1588 Arc::new(BTree::with_root(Arc::clone(pager), 0))
1589 }),
1590 },
1591 );
1592 }
1593 if let (Some(manager), Some(limit)) =
1594 (&manager, self.config.resident_memory_bytes_max)
1595 {
1596 manager.install_backing(
1597 Arc::new(
1598 match key_tree {
1599 Some(keys) => reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(&btree), keys),
1600 None => reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(&btree)),
1601 },
1602 ),
1603 btree.root_page_id(),
1604 format_version,
1605 usize::try_from(limit).unwrap_or(usize::MAX),
1606 persisted_entities,
1607 );
1608 }
1609 self.btree_indices.write().insert(name.clone(), btree);
1610 if format_version == STORE_VERSION_V11 {
1611 if let Some(manager) = &manager {
1612 self.checkpoint_versions.write().insert(
1613 name,
1614 CheckpointVersion {
1615 manager: Arc::downgrade(manager),
1616 segments: manager.checkpoint_revisions(),
1617 visibility_revision: 0,
1618 untracked_visibility_revision: 0,
1619 excluded_xids: Arc::default(),
1620 pending_vault: Arc::default(),
1621 format_version,
1622 deferred_rows: Some(Arc::default()),
1623 has_unprojected_rows: false,
1624 },
1625 );
1626 }
1627 }
1628 }
1629 }
1630
1631 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1632 let cross_ref_count = u32::from_le_bytes([
1633 content[pos],
1634 content[pos + 1],
1635 content[pos + 2],
1636 content[pos + 3],
1637 ]) as usize;
1638 pos += 4;
1639
1640 for _ in 0..cross_ref_count {
1641 let Ok(cross_ref) =
1642 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1643 else {
1644 break;
1645 };
1646 let source_id = EntityId::new(cross_ref.source_id);
1647 let target_id = EntityId::new(cross_ref.target_id);
1648 let ref_type = RefType::from_byte(cross_ref.ref_type);
1649 let target_collection = cross_ref.target_collection;
1650
1651 self.resident
1652 .cross_refs
1653 .write()
1654 .entry(source_id)
1655 .or_default()
1656 .push((target_id, ref_type, target_collection.clone()));
1657
1658 if let Some((collection, mut entity)) = self.get_any(source_id) {
1659 let exists = entity.cross_refs().iter().any(|xref| {
1660 xref.target == target_id
1661 && xref.ref_type == ref_type
1662 && xref.target_collection == target_collection
1663 });
1664 if !exists {
1665 entity.cross_refs_mut().push(CrossRef::new(
1666 source_id,
1667 target_id,
1668 target_collection.clone(),
1669 ref_type,
1670 ));
1671 if let Some(manager) = self.get_collection(&collection) {
1672 let _ = manager.update(entity);
1673 }
1674 }
1675 }
1676 }
1677 }
1678 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1679 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1680 let mut live_roots: std::collections::HashSet<_> = self
1681 .btree_indices
1682 .read()
1683 .values()
1684 .map(|tree| tree.root_page_id())
1685 .collect();
1686 live_roots.extend(
1687 self.key_indices
1688 .read()
1689 .values()
1690 .flat_map(PublishedKeyIndex::trees)
1691 .map(|tree| tree.root_page_id()),
1692 );
1693 for root in retired {
1694 if live_roots.contains(&root)
1695 || root
1696 >= pager
1697 .page_count()
1698 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1699 {
1700 return Err(page_integrity(
1701 root,
1702 None,
1703 "retired root aliases a live or missing page",
1704 ));
1705 }
1706 self.retired_btrees.lock().push(RetiredBTree {
1707 tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1708 published: true,
1709 });
1710 }
1711 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1712 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1713 if deferred.iter().any(|id| live_roots.contains(id)) {
1714 return Err(page_integrity(1, None, "deferred page aliases a live root"));
1715 }
1716 pager
1717 .restore_deferred_reclamation(&deferred)
1718 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1719 let ownership_epoch =
1720 reddb_file::native_store::decode_native_paged_ownership_epoch(
1721 content, &mut pos,
1722 )
1723 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1724 self.ownership_catalog_epoch
1725 .store(ownership_epoch.unwrap_or(0), Ordering::Release);
1726 }
1727 }
1728
1729 if self.format_version() < STORE_VERSION_V11 {
1730 self.set_format_version(STORE_VERSION_V11);
1731 }
1732
1733 Ok(())
1734 }
1735
1736 pub(crate) fn deserialize_entity(
1738 data: &[u8],
1739 format_version: u32,
1740 ) -> Result<UnifiedEntity, StoreError> {
1741 let mut pos = 0;
1742 Self::read_entity_binary(data, &mut pos, format_version)
1743 .map_err(|e| StoreError::Serialization(e.to_string()))
1744 }
1745
1746 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1748 let mut buf = Vec::with_capacity(256);
1753 Self::write_entity_binary(&mut buf, entity, format_version);
1754 buf
1755 }
1756
1757 pub(crate) fn serialize_entity_record(
1758 entity: &UnifiedEntity,
1759 metadata: Option<&Metadata>,
1760 format_version: u32,
1761 ) -> Vec<u8> {
1762 reddb_engine::storage::metadata_codec::serialize_entity_record(
1763 entity,
1764 metadata,
1765 format_version,
1766 )
1767 }
1768
1769 pub(crate) fn deserialize_entity_record(
1770 data: &[u8],
1771 format_version: u32,
1772 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1773 reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
1774 .map_err(StoreError::Serialization)
1775 }
1776
1777 pub fn persist(&self) -> Result<(), StoreError> {
1782 let _vault_guard = self.vault_publication_lock.lock();
1783 let pending_vault_versions = self.pending_vault_versions.read();
1784 let pager = match &self.pager {
1785 Some(p) => p,
1786 None => {
1787 if let Some(path) = &self.db_path {
1789 return self
1790 .save_to_file(path)
1791 .map_err(|e| StoreError::Serialization(e.to_string()));
1792 }
1793 return Err(StoreError::Io(std::io::Error::other(
1794 "No pager or path configured for persistence",
1795 )));
1796 }
1797 };
1798 let _ownership_guard = pager.lock_ownership_mutations();
1801
1802 match pager.read_page(1) {
1803 Ok(_) => {}
1804 Err(PagerError::PageNotFound(_)) => {
1805 let meta_page = pager
1806 .allocate_page(crate::storage::engine::PageType::Header)
1807 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1808 pager
1809 .write_page(meta_page.page_id(), meta_page)
1810 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1811 }
1812 Err(e) => {
1813 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1814 }
1815 }
1816
1817 if let Some(commit) = &self.commit {
1818 commit.force_sync().map_err(StoreError::Io)?;
1819 }
1820
1821 struct InterruptedOwnership<'a> {
1822 pager: &'a Pager,
1823 active: bool,
1824 }
1825 impl Drop for InterruptedOwnership<'_> {
1826 fn drop(&mut self) {
1827 if self.active {
1828 self.pager.fence_interrupted_ownership();
1829 }
1830 }
1831 }
1832 let mut ownership = InterruptedOwnership {
1833 pager,
1834 active: self.commit.is_some(),
1835 };
1836 let epoch = if let Some(commit) = &self.commit {
1837 let epoch = commit
1838 .next_page_ownership_epoch(self)
1839 .map_err(StoreError::Io)?;
1840 commit
1841 .begin_page_ownership_epoch(self, epoch)
1842 .map_err(StoreError::Io)?;
1843 Some(epoch)
1844 } else {
1845 None
1846 };
1847
1848 let collections = self.resident.collections.read();
1849 let mut btree_indices = self.btree_indices.write();
1850
1851 let mut collection_roots: Vec<(String, u32)> = Vec::new();
1853
1854 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1857 let previous_versions = self.checkpoint_versions.read();
1858 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1859 let pending_vault = Arc::new(pending_vault_versions.clone());
1860 let (visibility_revision, excluded, untracked_visibility_revision) =
1861 self.snapshot_manager.get().map_or_else(
1862 || (0, Default::default(), 0),
1863 |manager| manager.checkpoint_projection_state(),
1864 );
1865 let excluded = Arc::new(excluded);
1866 for (name, manager) in collections.iter() {
1867 let mut version = CheckpointVersion {
1868 manager: Arc::downgrade(manager),
1869 segments: manager.checkpoint_revisions(),
1870 visibility_revision,
1871 untracked_visibility_revision,
1872 excluded_xids: Arc::clone(&excluded),
1873 pending_vault: Arc::clone(&pending_vault),
1874 format_version: self.format_version(),
1875 deferred_rows: Some(Arc::default()),
1876 has_unprojected_rows: false,
1877 };
1878 if let (Some(previous), Some(tree)) =
1879 (previous_versions.get(name), btree_indices.get(name))
1880 {
1881 if previous.manager.ptr_eq(&version.manager)
1882 && previous.segments == version.segments
1883 && previous.format_version == version.format_version
1884 && !previous.has_unprojected_rows
1885 && checkpoint_visibility_matches(previous, &version)
1886 {
1887 version.deferred_rows = previous.deferred_rows.clone();
1888 collection_roots.push((name.clone(), tree.root_page_id()));
1889 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1890 checkpoint_versions.insert(name.clone(), version);
1891 continue;
1892 }
1893 }
1894 if let (Some(previous), Some(tree)) =
1895 (previous_versions.get(name), btree_indices.get(name))
1896 {
1897 if previous.manager.ptr_eq(&version.manager)
1898 && previous.format_version == version.format_version
1899 {
1900 if let Some(tree) =
1901 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
1902 {
1903 collection_roots.push((name.clone(), tree.root_page_id()));
1904 rebuilt_indices.insert(name.clone(), tree);
1905 checkpoint_versions.insert(name.clone(), version);
1906 continue;
1907 }
1908 }
1909 }
1910 let btree = self.checkpoint_tree(
1911 pager,
1912 manager,
1913 btree_indices.get(name),
1914 |entity, metadata| {
1915 #[cfg(test)]
1916 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1917 let hidden = pending_vault_versions.contains(&entity.id)
1918 || (matches!(
1919 entity.data,
1920 EntityData::Row(_)
1921 | EntityData::Vector(_)
1922 | EntityData::QueueMessage(_)
1923 ) && excluded.contains(&entity.xmin));
1924 let normalize_xmax = matches!(
1925 entity.data,
1926 EntityData::Row(_) | EntityData::QueueMessage(_)
1927 ) && excluded.contains(&entity.xmax);
1928 if hidden || normalize_xmax {
1929 version.remember_deferred_row(entity.id);
1930 }
1931 if hidden {
1932 return None;
1933 }
1934 let mut normalized = std::borrow::Cow::Borrowed(entity);
1935 if normalize_xmax {
1936 normalized.to_mut().xmax = 0;
1937 }
1938 Some(Self::serialize_entity_record(
1939 &normalized,
1940 metadata.as_ref(),
1941 self.format_version(),
1942 ))
1943 },
1944 )?;
1945 checkpoint_versions.insert(name.clone(), version);
1946
1947 collection_roots.push((name.clone(), btree.root_page_id()));
1948 rebuilt_indices.insert(name.clone(), btree);
1949 }
1950
1951 let mut meta_data = Vec::with_capacity(4096);
1953
1954 let format_version = STORE_VERSION_V11;
1955 self.set_format_version(format_version);
1956
1957 reddb_file::encode_native_paged_metadata_header(
1958 &mut meta_data,
1959 reddb_file::NativePagedMetadataHeader {
1960 format_version,
1961 collection_count: collection_roots.len() as u32,
1962 },
1963 );
1964
1965 for (name, root_page) in &collection_roots {
1967 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1968 }
1969
1970 let cross_refs = self.resident.cross_refs.read();
1972 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1973 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1974 for (source_id, refs) in cross_refs.iter() {
1975 for (target_id, ref_type, collection) in refs {
1976 reddb_file::encode_native_paged_cross_ref(
1977 &mut meta_data,
1978 source_id.raw(),
1979 target_id.raw(),
1980 ref_type.to_byte(),
1981 collection,
1982 );
1983 }
1984 }
1985
1986 for (name, previous) in btree_indices.iter() {
1987 if !rebuilt_indices
1988 .get(name)
1989 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1990 {
1991 self.retire_btree(Arc::clone(previous));
1992 }
1993 }
1994 #[cfg(test)]
1995 if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD").is_some() {
1996 pager
1997 .sync()
1998 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
1999 eprintln!("experimental checkpoint exit before manifest");
2000 std::process::exit(183);
2001 }
2002 let rebuilt_keys = self.checkpoint_key_indices(pager, &rebuilt_indices)?;
2003 #[cfg(test)]
2004 if std::env::var_os("REDDB_TEST_EXIT_AFTER_SECONDARY_INDEX_BUILD").is_some() {
2005 pager
2006 .sync()
2007 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
2008 std::process::exit(196);
2009 }
2010 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices, &rebuilt_keys)?;
2011
2012 #[cfg(test)]
2013 if std::env::var_os("REDDB_TEST_EXIT_AFTER_UNDO_CATALOG_PUBLICATION").is_some() {
2014 std::process::exit(195);
2015 }
2016
2017 *btree_indices = rebuilt_indices;
2020 *self.key_indices.write() = rebuilt_keys;
2021 drop(previous_versions);
2022 *self.checkpoint_versions.write() = checkpoint_versions;
2023
2024 if let Some(commit) = &self.commit {
2025 commit
2026 .finish_page_ownership_epoch(self, epoch.expect("WAL ownership epoch"))
2027 .map_err(StoreError::Io)?;
2028 ownership.active = false;
2029 commit.truncate().map_err(StoreError::Io)?;
2030 }
2031 *self.checkpoint_durable_rows.write() = Default::default();
2032
2033 if let Some(limit) = self.config.resident_memory_bytes_max {
2034 let mut versions = self.checkpoint_versions.write();
2035 for (name, manager) in collections.iter() {
2036 if versions
2037 .get(name)
2038 .is_some_and(|version| !version.has_unprojected_rows)
2039 {
2040 if let Some(tree) = btree_indices.get(name) {
2041 manager.install_backing(
2042 Arc::new(
2043 {
2044 let keys = self.key_indices.read();
2045 let index = keys.get(name).expect("invariant: prepared checkpoint classifies its key index");
2046 if index.indexed {
2047 reddb_engine::storage::collection_read::PagedCollection::with_key_index(Arc::clone(tree), Arc::clone(&index.tree))
2048 } else {
2049 reddb_engine::storage::collection_read::PagedCollection::new(Arc::clone(tree))
2050 }
2051 },
2052 ),
2053 tree.root_page_id(),
2054 self.format_version(),
2055 usize::try_from(limit).unwrap_or(usize::MAX),
2056 manager.count() as u64,
2057 );
2058 if let Some(version) = versions.get_mut(name) {
2059 version.segments = manager.checkpoint_revisions();
2060 }
2061 }
2062 }
2063 }
2064 }
2065
2066 Ok(())
2067 }
2068
2069 pub fn is_paged(&self) -> bool {
2071 self.pager.is_some()
2072 }
2073
2074 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
2077 self.btree_indices
2078 .read()
2079 .get(collection)
2080 .map(|btree| btree.root_page_id())
2081 .filter(|root| *root != 0)
2082 }
2083
2084 pub fn db_path(&self) -> Option<&Path> {
2086 self.db_path.as_deref()
2087 }
2088}
2089
2090#[cfg(test)]
2091mod checkpoint_work_tests {
2092 use super::*;
2093
2094 #[cfg(target_os = "linux")]
2095 #[test]
2096 fn interrupted_inline_sparse_checkpoint_replays_durable_rows() {
2097 const CHILD: &str = "REDDB_TEST_INLINE_SPARSE_PATH";
2098 if let Some(path) = std::env::var_os(CHILD) {
2099 let insert = std::env::var_os("REDDB_TEST_INLINE_SPARSE_INSERT").is_some();
2100 let multi = std::env::var_os("REDDB_TEST_INLINE_SPARSE_MULTI").is_some();
2101 let store = UnifiedStore::open(path).expect("open store");
2102 let initial_count = if multi { 600 } else { 1 };
2103 for id in 1..=initial_count {
2104 store.insert_auto("rows", row(id, 1)).expect("seed");
2105 }
2106 store.persist().expect("baseline");
2107 let manager = store.get_collection("rows").expect("collection");
2108 let ids: Vec<u64> = if multi {
2109 (1..=initial_count).collect()
2110 } else {
2111 vec![if insert { 2 } else { 1 }]
2112 };
2113 let mut actions = Vec::new();
2114 for id in ids {
2115 if insert {
2116 manager.insert(row(id, 9)).expect("insert version");
2117 } else {
2118 manager.update(row(id, 9)).expect("update version");
2119 }
2120 let entity = manager.get(EntityId::new(id)).expect("changed entity");
2121 actions.push(StoreWalAction::upsert_entity(
2122 "rows",
2123 &entity,
2124 None,
2125 store.format_version(),
2126 ));
2127 }
2128 let references = store
2129 .commit
2130 .as_ref()
2131 .expect("coordinator")
2132 .append_checkpoint_actions(&actions)
2133 .expect("durable WAL");
2134 store.remember_checkpoint_rows(actions, references);
2135 store.persist().expect("exit after sparse write");
2136 panic!("inline sparse crash hook was not reached");
2137 }
2138 for (insert, multi) in [(false, false), (true, false), (false, true)] {
2139 let directory = tempfile::tempdir().expect("temporary store");
2140 let path = directory.path().join("data.rdb");
2141 let mut command =
2142 std::process::Command::new(std::env::current_exe().expect("test executable"));
2143 command
2144 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_inline_sparse_checkpoint_replays_durable_rows", "--nocapture"])
2145 .env(CHILD, &path)
2146 .env("REDDB_TEST_EXIT_AFTER_INLINE_SPARSE_WRITE", "1");
2147 if insert {
2148 command.env("REDDB_TEST_INLINE_SPARSE_INSERT", "1");
2149 }
2150 if multi {
2151 command.env("REDDB_TEST_INLINE_SPARSE_MULTI", "1");
2152 }
2153 let output = command.output().expect("crash child");
2154 assert_eq!(
2155 output.status.code(),
2156 Some(190),
2157 "insert={insert} multi={multi}: {}",
2158 String::from_utf8_lossy(&output.stderr)
2159 );
2160 let recovered = UnifiedStore::open(&path).expect("recover");
2161 let ids: Vec<u64> = if multi {
2162 (1..=600).collect()
2163 } else {
2164 vec![if insert { 2 } else { 1 }]
2165 };
2166 for id in ids {
2167 assert_eq!(
2168 recovered
2169 .get("rows", EntityId::new(id))
2170 .expect("recovered row")
2171 .data
2172 .as_row()
2173 .expect("row")
2174 .columns,
2175 vec![Value::Integer(9)]
2176 );
2177 }
2178 assert_eq!(
2179 recovered.get_collection("rows").expect("rows").count(),
2180 if multi {
2181 600
2182 } else if insert {
2183 2
2184 } else {
2185 1
2186 }
2187 );
2188 }
2189 }
2190
2191 #[test]
2192 fn catalog_ownership_epoch_survives_reopen_and_later_publication() {
2193 let directory = tempfile::tempdir().expect("temporary store");
2194 let path = directory.path().join("data.rdb");
2195 {
2196 let store = UnifiedStore::open(&path).expect("open store");
2197 store.create_collection("rows").expect("create collection");
2198 store.persist().expect("publish ownership epoch");
2199 assert_eq!(store.ownership_catalog_epoch.load(Ordering::Acquire), 1);
2200 store.persist().expect("publish next ownership epoch");
2201 }
2202 let reopened = UnifiedStore::open(&path).expect("reopen store");
2203 assert_eq!(reopened.ownership_catalog_epoch.load(Ordering::Acquire), 2);
2204 }
2205
2206 #[test]
2207 fn metadata_overflow_with_missing_continuation_fails_closed() {
2208 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
2209
2210 let directory = tempfile::tempdir().expect("temporary store");
2211 let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("paged store");
2212 let pager = store.pager.as_ref().unwrap();
2213 let valid_payload = vec![42; META_PAGE_CONTENT_CAP * 3];
2214 let valid_manifest = build_meta_page1_with_overflow(pager, &valid_payload).unwrap();
2215 pager.write_page(1, valid_manifest).unwrap();
2216 assert!(!existing_overflow_chain(pager).unwrap().is_empty());
2217 assert_eq!(read_meta_payload(pager).unwrap().unwrap(), valid_payload);
2218
2219 let missing = pager.page_count().unwrap() + 100;
2220 let mut manifest = Page::new(PageType::Header, 1);
2221 reddb_file::encode_native_metadata_overflow_header(
2222 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2223 reddb_file::NativeMetadataOverflowHeader {
2224 format_version: STORE_VERSION_V11,
2225 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2226 next_overflow_page_id: missing,
2227 },
2228 )
2229 .unwrap();
2230 pager.write_page(1, manifest).unwrap();
2231
2232 assert!(
2233 existing_overflow_chain(pager).is_err(),
2234 "a missing continuation must not be treated as an empty chain"
2235 );
2236
2237 let wrong_type = pager.allocate_page(PageType::BTreeLeaf).unwrap();
2238 let mut manifest = Page::new(PageType::Header, 1);
2239 reddb_file::encode_native_metadata_overflow_header(
2240 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2241 reddb_file::NativeMetadataOverflowHeader {
2242 format_version: STORE_VERSION_V11,
2243 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2244 next_overflow_page_id: wrong_type.page_id(),
2245 },
2246 )
2247 .unwrap();
2248 pager.write_page(1, manifest).unwrap();
2249 assert!(
2250 existing_overflow_chain(pager).is_err(),
2251 "a live page must not be mistaken for metadata overflow"
2252 );
2253 assert!(
2254 read_meta_payload(pager).is_err(),
2255 "a live page must not be read as metadata overflow"
2256 );
2257
2258 let mut cycle = pager.allocate_page(PageType::Overflow).unwrap();
2259 let cycle_id = cycle.page_id();
2260 reddb_file::encode_native_metadata_overflow_continuation_header(
2261 &mut cycle.as_bytes_mut()[HEADER_SIZE..],
2262 reddb_file::NativeMetadataOverflowContinuationHeader {
2263 next_overflow_page_id: cycle_id,
2264 chunk_bytes: 1,
2265 },
2266 )
2267 .unwrap();
2268 pager.write_page(cycle_id, cycle).unwrap();
2269 let mut manifest = Page::new(PageType::Header, 1);
2270 reddb_file::encode_native_metadata_overflow_header(
2271 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2272 reddb_file::NativeMetadataOverflowHeader {
2273 format_version: STORE_VERSION_V11,
2274 total_payload_bytes: META_V3_FIRST_PAYLOAD_CAP as u32 + 3,
2275 next_overflow_page_id: cycle_id,
2276 },
2277 )
2278 .unwrap();
2279 pager.write_page(1, manifest).unwrap();
2280 assert!(
2281 read_meta_payload(pager).is_err(),
2282 "cyclic overflow must not be accepted"
2283 );
2284
2285 let mut manifest = Page::new(PageType::Header, 1);
2286 reddb_file::encode_native_metadata_overflow_header(
2287 &mut manifest.as_bytes_mut()[HEADER_SIZE..],
2288 reddb_file::NativeMetadataOverflowHeader {
2289 format_version: STORE_VERSION_V11,
2290 total_payload_bytes: META_PAGE_CONTENT_CAP as u32 + 1,
2291 next_overflow_page_id: 0,
2292 },
2293 )
2294 .unwrap();
2295 pager.write_page(1, manifest).unwrap();
2296 assert!(
2297 read_meta_payload(pager).is_err(),
2298 "a truncated metadata payload must not be accepted"
2299 );
2300 }
2301
2302 #[test]
2303 fn retired_tree_pages_remain_pinned_by_a_cursor() {
2304 let directory = tempfile::tempdir().expect("temporary store");
2305 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2306 for id in 1..=100 {
2307 store.insert_auto("rows", row(id, 1)).expect("seed");
2308 }
2309 store.persist().expect("initial checkpoint");
2310 let tree = store
2311 .btree_indices
2312 .read()
2313 .get("rows")
2314 .cloned()
2315 .expect("tree");
2316 let mut cursor = tree.cursor_first().expect("cursor");
2317 drop(tree);
2318 let manager = store.get_collection("rows").expect("rows");
2319 for value in 2..=6 {
2320 manager.update(row(1, value)).expect("change");
2321 store.persist().expect("checkpoint with old reader");
2322 }
2323 let mut count = 0;
2324 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
2325 let (entity, _) =
2326 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2327 .expect("decode");
2328 assert_eq!(
2329 entity.data.as_row().expect("row").columns,
2330 [Value::Integer(1)]
2331 );
2332 count += 1;
2333 }
2334 assert_eq!(count, 100);
2335 drop(cursor);
2336 store.persist().expect("reclaim after reader release");
2337 assert!(store.retired_btrees.lock().is_empty());
2338 }
2339
2340 #[test]
2341 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
2342 let directory = tempfile::tempdir().expect("temporary store");
2343 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2344 store.create_collection("rows").expect("collection");
2345 let manager = store.get_collection("rows").expect("rows");
2346 for id in (1..=1200).rev() {
2347 manager
2348 .insert(UnifiedEntity::table_row(
2349 EntityId::new(id),
2350 "rows",
2351 id,
2352 vec![Value::text("x".repeat(4096))],
2353 ))
2354 .expect("seed");
2355 }
2356 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
2357 store.persist().expect("bounded checkpoint");
2358 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
2359 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
2360 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
2361 }
2362
2363 #[test]
2364 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
2365 let directory = tempfile::tempdir().expect("temporary store");
2366 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2367 store.create_collection("rows").expect("collection");
2368 for id in 1..=40 {
2369 store
2370 .insert(
2371 "rows",
2372 UnifiedEntity::table_row(
2373 EntityId::new(id),
2374 "rows",
2375 id,
2376 vec![Value::Integer(id as i64)],
2377 ),
2378 )
2379 .expect("seed row");
2380 }
2381 store.persist().expect("initial checkpoint");
2382 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2383 store.persist().expect("unchanged checkpoint");
2384 assert_eq!(
2385 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
2386 0,
2387 "unchanged collections must not hold the writer fence while serializing every row"
2388 );
2389 drop(store);
2390 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
2391 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2392 reopened.persist().expect("unchanged recovered generation");
2393 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2394 }
2395
2396 #[test]
2397 fn sparse_wal_replay_does_not_reencode_complete_collections() {
2398 let directory = tempfile::tempdir().expect("temporary store");
2399 let path = directory.path().join("data.rdb");
2400 let store = UnifiedStore::open(&path).expect("paged store");
2401 for (name, first) in [("rows", 1), ("cold", 129)] {
2402 store.create_collection(name).expect("collection");
2403 for id in first..first + 128 {
2404 store
2405 .insert(
2406 name,
2407 UnifiedEntity::table_row(
2408 EntityId::new(id),
2409 name,
2410 id,
2411 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2412 ),
2413 )
2414 .expect("seed");
2415 }
2416 }
2417 store.persist().expect("durable baseline");
2418 let action = super::super::commit::StoreWalAction::upsert_entity(
2419 "rows",
2420 &row(1, 42),
2421 None,
2422 STORE_VERSION_V11,
2423 );
2424 store
2426 .commit
2427 .as_ref()
2428 .unwrap()
2429 .append_actions(&[action])
2430 .unwrap();
2431 drop(store);
2432
2433 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2434 let recovered = UnifiedStore::open(&path).expect("replay committed change");
2435 assert_eq!(
2436 recovered
2437 .get("rows", EntityId::new(1))
2438 .unwrap()
2439 .data
2440 .as_row()
2441 .unwrap()
2442 .columns,
2443 vec![Value::Integer(42)]
2444 );
2445 for name in ["rows", "cold"] {
2446 assert_eq!(recovered.get_collection(name).unwrap().count(), 128);
2447 }
2448 assert_eq!(
2449 REPLAY_SERIALIZED_ROWS.with(|count| count.get()),
2450 0,
2451 "one WAL-backed row must not reencode its whole collection or unrelated cold rows"
2452 );
2453 for (name, first) in [("rows", 2), ("cold", 129)] {
2454 let end = if name == "rows" { 129 } else { 257 };
2455 for id in first..end {
2456 assert_eq!(
2457 recovered
2458 .get(name, EntityId::new(id))
2459 .unwrap()
2460 .data
2461 .as_row()
2462 .unwrap()
2463 .columns,
2464 vec![Value::text(format!("{id}:{}", "x".repeat(8192)))],
2465 );
2466 }
2467 }
2468
2469 drop(recovered);
2470 let reopened = UnifiedStore::open(&path).expect("durable sparse replay");
2471 assert_eq!(
2472 reopened
2473 .get("rows", EntityId::new(1))
2474 .unwrap()
2475 .data
2476 .as_row()
2477 .unwrap()
2478 .columns,
2479 vec![Value::Integer(42)]
2480 );
2481 }
2482
2483 fn replay_test_payload(bytes: usize) -> String {
2484 let mut state = 0x1234_5678_u32;
2485 (0..bytes)
2486 .map(|_| {
2487 state ^= state << 13;
2488 state ^= state >> 17;
2489 state ^= state << 5;
2490 char::from(b'!' + (state % 90) as u8)
2491 })
2492 .collect()
2493 }
2494
2495 #[test]
2496 fn wal_replay_with_exhausted_row_proof_keeps_full_validation() {
2497 let directory = tempfile::tempdir().unwrap();
2498 let path = directory.path().join("data.rdb");
2499 let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
2500 let store = UnifiedStore::open_with_config(&path, config()).unwrap();
2501 for id in 1..=16 {
2502 store.insert_auto("rows", row(id, 1)).unwrap();
2503 }
2504 store.persist().unwrap();
2505 let payload = replay_test_payload(64 * 1024);
2507 let changed =
2508 UnifiedEntity::table_row(EntityId::new(1), "rows", 1, vec![Value::text(payload)]);
2509 let action = StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2510 store
2511 .commit
2512 .as_ref()
2513 .unwrap()
2514 .append_actions(&[action])
2515 .unwrap();
2516 drop(store);
2517 REPLAY_SERIALIZED_ROWS.with(|count| count.set(0));
2518 let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
2519 assert!(REPLAY_SERIALIZED_ROWS.with(|count| count.get()) >= 16);
2520 assert_eq!(
2521 recovered
2522 .get("rows", EntityId::new(1))
2523 .unwrap()
2524 .data
2525 .as_row()
2526 .unwrap()
2527 .columns,
2528 changed.data.as_row().unwrap().columns
2529 );
2530 drop(recovered);
2531 let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
2532 assert_eq!(reopened.get_collection("rows").unwrap().count(), 16);
2533 assert_eq!(
2534 reopened
2535 .get("rows", EntityId::new(1))
2536 .unwrap()
2537 .data
2538 .as_row()
2539 .unwrap()
2540 .columns,
2541 changed.data.as_row().unwrap().columns
2542 );
2543 }
2544
2545 #[cfg(target_os = "linux")]
2546 #[test]
2547 fn interrupted_sparse_replay_preserves_rows_and_bounds_growth() {
2548 const CHILD: &str = "REDDB_TEST_SPARSE_REPLAY_PATH";
2549 if let Some(path) = std::env::var_os(CHILD) {
2550 let _store = UnifiedStore::open(path).unwrap();
2551 panic!("replay crash hook was not reached");
2552 }
2553 for overflow in [false, true] {
2554 for (hook, code) in [
2555 ("REDDB_TEST_EXIT_AFTER_SPARSE_BATCH", 193),
2556 ("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", 182),
2557 ] {
2558 let directory = tempfile::tempdir().unwrap();
2559 let path = directory.path().join("data.rdb");
2560 let store = UnifiedStore::open(&path).unwrap();
2561 for id in 1..=128 {
2562 store.insert_auto("rows", row(id, 1)).unwrap();
2563 }
2564 store.persist().unwrap();
2565 let changed = if overflow {
2566 UnifiedEntity::table_row(
2567 EntityId::new(1),
2568 "rows",
2569 1,
2570 vec![Value::text(replay_test_payload(16 * 1024))],
2571 )
2572 } else {
2573 row(1, 42)
2574 };
2575 let action =
2576 StoreWalAction::upsert_entity("rows", &changed, None, store.format_version());
2577 store
2578 .commit
2579 .as_ref()
2580 .unwrap()
2581 .append_actions(&[action])
2582 .unwrap();
2583 drop(store);
2584 let mut first_size = 0;
2585 for cycle in 0..12 {
2586 let output = std::process::Command::new(std::env::current_exe().unwrap())
2587 .args(["--exact", "storage::unified::store::impl_pages::checkpoint_work_tests::interrupted_sparse_replay_preserves_rows_and_bounds_growth", "--nocapture"])
2588 .env(CHILD, &path).env(hook, "1").output().unwrap();
2589 assert_eq!(
2590 output.status.code(),
2591 Some(code),
2592 "overflow={overflow} hook={hook} cycle={cycle}: {}",
2593 String::from_utf8_lossy(&output.stderr)
2594 );
2595 let size = std::fs::metadata(&path).unwrap().len();
2596 if cycle == 0 {
2597 first_size = size;
2598 }
2599 assert!(
2600 size <= first_size + 2 * crate::storage::engine::PAGE_SIZE as u64,
2601 "repeated recovery must reuse reservations: {first_size} -> {size}"
2602 );
2603 }
2604 for _ in 0..2 {
2605 let recovered = UnifiedStore::open(&path).unwrap();
2606 assert_eq!(recovered.get_collection("rows").unwrap().count(), 128);
2607 for id in 1..=128 {
2608 let expected = if id == 1 { changed.clone() } else { row(id, 1) };
2609 assert_eq!(
2610 recovered
2611 .get("rows", EntityId::new(id))
2612 .unwrap()
2613 .data
2614 .as_row()
2615 .unwrap()
2616 .columns,
2617 expected.data.as_row().unwrap().columns
2618 );
2619 }
2620 }
2621 }
2622 }
2623 }
2624
2625 #[test]
2626 fn paged_reopen_preserves_stored_sequence_identity() {
2627 let directory = tempfile::tempdir().expect("temporary store");
2628 let path = directory.path().join("data.rdb");
2629 let store = UnifiedStore::open(&path).expect("paged store");
2630 store.create_collection("rows").expect("collection");
2631 for id in [30, 10, 20] {
2632 store.insert("rows", row(id, id as i64)).expect("insert");
2633 }
2634 store.persist().expect("checkpoint");
2635 let sequences = [10, 20, 30].map(|id| {
2636 store
2637 .get("rows", EntityId::new(id))
2638 .expect("row")
2639 .sequence_id
2640 });
2641 drop(store);
2642 let reopened = UnifiedStore::open(&path).expect("reopen");
2643 let recovered = [10, 20, 30].map(|id| {
2644 reopened
2645 .get("rows", EntityId::new(id))
2646 .expect("row")
2647 .sequence_id
2648 });
2649 assert_eq!(
2650 recovered, sequences,
2651 "opening a checkpoint must not rewrite stored row identities"
2652 );
2653 }
2654
2655 #[test]
2656 fn auto_insert_persists_the_assigned_sequence_identity() {
2657 let directory = tempfile::tempdir().expect("temporary store");
2658 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2659 for id in 1u64..=3 {
2660 store
2661 .insert_auto("rows", row(id, id as i64))
2662 .expect("insert");
2663 }
2664 let manager = store.get_collection("rows").expect("rows");
2665 let tree = store
2666 .btree_indices
2667 .read()
2668 .get("rows")
2669 .cloned()
2670 .expect("tree");
2671 for id in 1u64..=3 {
2672 let bytes = tree
2673 .get(&id.to_be_bytes())
2674 .expect("lookup")
2675 .expect("stored row");
2676 let (persisted, _) =
2677 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
2678 .expect("decode");
2679 assert_eq!(
2680 persisted.sequence_id,
2681 manager
2682 .get(EntityId::new(id))
2683 .expect("live row")
2684 .sequence_id
2685 );
2686 }
2687 }
2688
2689 #[test]
2690 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
2691 let directory = tempfile::tempdir().expect("temporary store");
2692 let path = directory.path().join("data.rdb");
2693 let store = UnifiedStore::open(&path).expect("paged store");
2694 store.insert_auto("rows", row(1, 1)).expect("seed");
2695 store.persist().expect("checkpoint");
2696 store.insert_auto("rows", row(2, 2)).expect("append");
2697 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2698 store.persist().expect("inline append checkpoint");
2699 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2700 store
2701 .get_collection("rows")
2702 .expect("rows")
2703 .update(row(1, 99))
2704 .expect("untracked edit");
2705 store.insert_auto("rows", row(3, 3)).expect("next append");
2706 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2707 store.persist().expect("untracked edit checkpoint");
2708 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
2709 drop(store);
2710 let reopened = UnifiedStore::open(&path).expect("reopen");
2711 assert_eq!(
2712 reopened
2713 .get("rows", EntityId::new(1))
2714 .expect("row")
2715 .data
2716 .as_row()
2717 .expect("table")
2718 .columns,
2719 vec![Value::Integer(99)]
2720 );
2721 }
2722
2723 #[test]
2724 fn replayed_trees_are_reusable_after_durable_reconciliation() {
2725 let directory = tempfile::tempdir().expect("temporary store");
2726 let path = directory.path().join("data.rdb");
2727 let store = UnifiedStore::open(&path).expect("paged store");
2728 store.insert_auto("rows", row(1, 1)).expect("seed");
2729 store.persist().expect("checkpoint");
2730 store
2731 .insert_auto("rows", row(2, 2))
2732 .expect("WAL-backed append");
2733 drop(store);
2734 let reopened = UnifiedStore::open(&path).expect("replay");
2735 assert!(reopened.get("rows", EntityId::new(2)).is_some());
2736 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2737 reopened.persist().expect("checkpoint reconciled tree");
2738 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2739 }
2740
2741 fn row(id: u64, value: i64) -> UnifiedEntity {
2742 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
2743 }
2744
2745 #[test]
2746 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
2747 let directory = tempfile::tempdir().expect("temporary store");
2748 let path = directory.path().join("data.rdb");
2749 let store = UnifiedStore::open(&path).expect("paged store");
2750 store.create_collection("rows").expect("collection");
2751 store.insert("rows", row(1, 1)).expect("seed");
2752 store.persist().expect("initial checkpoint");
2753 let manager = store.get_collection("rows").expect("rows");
2754 manager.update(row(1, 2)).expect("direct update");
2755 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2756 store.persist().expect("updated row checkpoint");
2757 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2758 let mut metadata = Metadata::new();
2759 metadata.set("verified", MetadataValue::Bool(true));
2760 manager
2761 .set_metadata(EntityId::new(1), metadata)
2762 .expect("metadata");
2763 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2764 store.persist().expect("metadata-only checkpoint");
2765 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2766 manager
2767 .bulk_insert(vec![row(2, 2), row(3, 3)])
2768 .expect("bulk insert");
2769 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2770 store.persist().expect("bulk insert checkpoint");
2771 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
2772 manager.delete(EntityId::new(2)).expect("delete");
2773 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2774 store.persist().expect("changed checkpoint");
2775 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
2776 drop(store);
2777 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
2778 let manager = recovered.get_collection("rows").expect("rows");
2779 assert_eq!(manager.count(), 2);
2780 assert_eq!(
2781 manager
2782 .get(EntityId::new(1))
2783 .expect("row")
2784 .data
2785 .as_row()
2786 .expect("table")
2787 .columns,
2788 vec![Value::Integer(2)]
2789 );
2790 assert!(manager.get(EntityId::new(2)).is_none());
2791 assert!(manager
2792 .get_metadata(EntityId::new(1))
2793 .expect("metadata")
2794 .get("verified")
2795 .is_some());
2796 }
2797
2798 #[test]
2799 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
2800 let directory = tempfile::tempdir().expect("temporary store");
2801 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2802 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2803 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2804 store.create_collection("rows").expect("collection");
2805 for id in 1..=40 {
2806 store.insert("rows", row(id, id as i64)).expect("seed");
2807 }
2808 store.persist().expect("initial checkpoint");
2809 let unrelated = snapshots.begin();
2810 snapshots.rollback(unrelated);
2811 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2812 store.persist().expect("unrelated rollback checkpoint");
2813 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2814
2815 store
2816 .pending_vault_versions
2817 .write()
2818 .insert(EntityId::new(100));
2819 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2820 store.persist().expect("unrelated vault checkpoint");
2821 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2822 store.pending_vault_versions.write().clear();
2823 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2824 store
2825 .persist()
2826 .expect("unrelated vault publication checkpoint");
2827 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2828 }
2829
2830 #[test]
2831 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
2832 let directory = tempfile::tempdir().expect("temporary store");
2833 let path = directory.path().join("data.rdb");
2834 let store = UnifiedStore::open(&path).expect("paged store");
2835 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2836 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2837 store.create_collection("rows").expect("collection");
2838 let xid = snapshots.begin();
2839 let mut entity = row(1, 1);
2840 entity.xmin = xid;
2841 store.insert("rows", entity).expect("committed row");
2842 snapshots.commit(xid);
2843 store.persist().expect("committed baseline");
2844 snapshots.rollback(xid);
2845 store
2846 .persist()
2847 .expect("untracked outcome requires validation");
2848 drop(store);
2849 let recovered = UnifiedStore::open(&path).expect("recover");
2850 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2851 }
2852
2853 #[test]
2854 fn exhausted_deferred_row_cache_keeps_full_validation() {
2855 let directory = tempfile::tempdir().expect("temporary store");
2856 let path = directory.path().join("data.rdb");
2857 let store = UnifiedStore::open(&path).expect("paged store");
2858 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2859 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2860 store.create_collection("rows").expect("collection");
2861 let xid = snapshots.begin();
2862 for id in 1..=1025 {
2863 let mut entity = row(id, id as i64);
2864 entity.xmin = xid;
2865 store.insert("rows", entity).expect("pending row");
2866 }
2867 store.persist().expect("exclude pending rows");
2868 assert!(store.checkpoint_versions.read()["rows"]
2869 .deferred_rows
2870 .is_none());
2871 snapshots.commit(xid);
2872 store
2873 .persist()
2874 .expect("publish committed rows conservatively");
2875 drop(store);
2876 let recovered = UnifiedStore::open(&path).expect("recover");
2877 assert_eq!(
2878 recovered.get_collection("rows").expect("rows").count(),
2879 1025
2880 );
2881 }
2882
2883 #[test]
2884 fn rollback_invalidates_reuse_without_a_segment_mutation() {
2885 let directory = tempfile::tempdir().expect("temporary store");
2886 let path = directory.path().join("data.rdb");
2887 let store = UnifiedStore::open(&path).expect("paged store");
2888 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2889 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2890 store.create_collection("rows").expect("collection");
2891 let xid = snapshots.begin();
2892 let mut pending = row(1, 1);
2893 pending.xmin = xid;
2894 store.insert("rows", pending).expect("version");
2895 store.persist().expect("before rollback");
2896 snapshots.rollback(xid);
2897 store.persist().expect("after rollback");
2898 drop(store);
2899 let recovered = UnifiedStore::open(&path).expect("recover");
2900 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2901 }
2902
2903 #[test]
2904 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
2905 let directory = tempfile::tempdir().expect("temporary store");
2906 let path = directory.path().join("data.rdb");
2907 let store = UnifiedStore::open(&path).expect("paged store");
2908 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2909 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2910 store.create_collection("rows").expect("collection");
2911 let xid = snapshots.begin();
2912 let mut deleted = row(1, 1);
2913 deleted.xmax = xid;
2914 store.insert("rows", deleted).expect("version");
2915 store.persist().expect("before rollback");
2916 snapshots.rollback(xid);
2917 store.persist().expect("after rollback");
2918 drop(store);
2919 let recovered = UnifiedStore::open(&path).expect("recover");
2920 let manager = recovered.get_collection("rows").expect("rows");
2921 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
2922 }
2923
2924 #[test]
2925 fn publishing_a_pending_vault_version_invalidates_reuse() {
2926 let directory = tempfile::tempdir().expect("temporary store");
2927 let path = directory.path().join("data.rdb");
2928 let store = UnifiedStore::open(&path).expect("paged store");
2929 store.create_collection("rows").expect("collection");
2930 store.insert("rows", row(1, 1)).expect("row");
2931 store
2932 .pending_vault_versions
2933 .write()
2934 .insert(EntityId::new(1));
2935 store.persist().expect("exclude pending vault version");
2936 store
2937 .pending_vault_versions
2938 .write()
2939 .remove(&EntityId::new(1));
2940 store.persist().expect("include published vault version");
2941 drop(store);
2942 let recovered = UnifiedStore::open(&path).expect("recover");
2943 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
2944 }
2945}