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