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 CHECKPOINT_BUFFERED_BYTES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
67}
68
69fn checkpoint_visibility_matches(
70 previous: &CheckpointVersion,
71 current: &CheckpointVersion,
72) -> bool {
73 (previous.visibility_revision == current.visibility_revision
74 || previous.excluded_xids == current.excluded_xids)
75 && previous.pending_vault == current.pending_vault
76}
77
78const META_PAGE_CONTENT_CAP: usize =
97 crate::storage::engine::PAGE_SIZE - crate::storage::engine::HEADER_SIZE;
98const META_V3_PAGE1_HEADER: usize = reddb_file::METADATA_OVERFLOW_HEADER_BYTES;
99const META_V3_OVERFLOW_HEADER: usize = reddb_file::METADATA_OVERFLOW_CONTINUATION_HEADER_BYTES;
100const META_V3_FIRST_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_PAGE1_HEADER;
101const META_V3_OVERFLOW_PAYLOAD_CAP: usize = META_PAGE_CONTENT_CAP - META_V3_OVERFLOW_HEADER;
102
103fn existing_overflow_chain(pager: &Pager) -> Result<Vec<u32>, PagerError> {
104 let cs = crate::storage::engine::HEADER_SIZE;
105 let page = match pager.read_page(1) {
106 Ok(p) => p,
107 Err(_) => return Ok(Vec::new()),
108 };
109 let bytes = page.as_bytes();
110 if bytes.len() < cs + META_V3_PAGE1_HEADER {
111 return Ok(Vec::new());
112 }
113 let Some(header) =
114 reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
115 PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
116 })?
117 else {
118 return Ok(Vec::new());
119 };
120 let mut next = header.next_overflow_page_id;
121 let mut pages = Vec::new();
122 let mut seen = std::collections::HashSet::new();
123 while next != 0 {
124 if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
125 return Err(PagerError::InvalidDatabase(
126 "cyclic metadata overflow chain".into(),
127 ));
128 }
129 let ov = match pager.read_page(next) {
130 Ok(p) => p,
131 Err(_) => break,
132 };
133 let ob = ov.as_bytes();
134 let nn = match reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..]) {
135 Ok(header) => header.next_overflow_page_id,
136 Err(_) => 0,
137 };
138 pages.push(next);
139 next = nn;
140 }
141 Ok(pages)
142}
143
144fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
145 let retired = existing_overflow_chain(pager)?;
146 let page = build_meta_page1_with_overflow(pager, meta_data)?;
147 pager.sync()?;
149 pager.write_page(1, page)?;
150 pager.sync()?;
151 for id in retired {
152 pager.free_page(id)?;
153 }
154 Ok(())
155}
156
157fn build_meta_page1_with_overflow(
158 pager: &Pager,
159 meta_data: &[u8],
160) -> Result<crate::storage::engine::Page, PagerError> {
161 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
162
163 let mut page1 = Page::new(PageType::Header, 1);
164 let cs = HEADER_SIZE;
165
166 if meta_data.len() <= META_PAGE_CONTENT_CAP {
167 let buf = page1.as_bytes_mut();
169 buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
170 return Ok(page1);
171 }
172
173 let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
177 let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
178 let mut chunks: Vec<&[u8]> = Vec::new();
179 while !tail.is_empty() {
180 let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
181 chunks.push(&tail[..take]);
182 tail = &tail[take..];
183 }
184
185 let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
186 let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
187 for _ in 0..chunks.len() {
188 let pg = pager.allocate_page(PageType::Overflow)?;
189 overflow_ids.push(pg.page_id());
190 overflow_pages.push(pg);
191 }
192
193 for i in 0..chunks.len() {
194 let next = if i + 1 < chunks.len() {
195 overflow_ids[i + 1]
196 } else {
197 0u32
198 };
199 let len = chunks[i].len() as u32;
200 let buf = overflow_pages[i].as_bytes_mut();
201 reddb_file::encode_native_metadata_overflow_continuation_header(
202 &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
203 reddb_file::NativeMetadataOverflowContinuationHeader {
204 next_overflow_page_id: next,
205 chunk_bytes: len,
206 },
207 )
208 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
209 buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
210 }
211 for (idx, page) in overflow_pages.into_iter().enumerate() {
212 let id = overflow_ids[idx];
213 pager.write_page(id, page)?;
214 }
215
216 let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
218 .ok()
219 .flatten()
220 .map_or(0, |header| header.format_version);
221
222 let buf = page1.as_bytes_mut();
223 reddb_file::encode_native_metadata_overflow_header(
224 &mut buf[cs..cs + META_V3_PAGE1_HEADER],
225 reddb_file::NativeMetadataOverflowHeader {
226 format_version,
227 total_payload_bytes: meta_data.len() as u32,
228 next_overflow_page_id: overflow_ids[0],
229 },
230 )
231 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
232 buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
233 .copy_from_slice(first_chunk);
234
235 Ok(page1)
236}
237
238fn storage_integrity(
239 zone: &str,
240 id: impl Into<String>,
241 collection: Option<String>,
242 detail: impl Into<String>,
243) -> StoreError {
244 StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
245 zone, id, collection, detail,
246 ))
247}
248
249fn page_integrity(
250 page_id: u32,
251 collection: Option<String>,
252 detail: impl Into<String>,
253) -> StoreError {
254 storage_integrity("page", page_id.to_string(), collection, detail)
255}
256
257fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
269 let cs = crate::storage::engine::HEADER_SIZE;
270 let meta_page = pager.read_manifest_page().map_err(|err| {
271 storage_integrity(
272 "manifest",
273 "manifest",
274 None,
275 format!("internal manifest zone read failed ({err})"),
276 )
277 })?;
278 let bytes = meta_page.as_bytes();
279 if bytes.len() < cs + 4 {
280 return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
281 }
282 let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
283 .map_err(|err| page_integrity(1, None, err.to_string()))?
284 {
285 Some(header) => header,
286 None => {
287 return Ok(Some(bytes[cs..].to_vec()));
288 }
289 };
290 if bytes.len() < cs + META_V3_PAGE1_HEADER {
291 return Err(page_integrity(
292 1,
293 None,
294 "metadata overflow header is truncated",
295 ));
296 }
297 let total = header.total_payload_bytes as usize;
298 let mut next = header.next_overflow_page_id;
299 let mut payload: Vec<u8> = Vec::with_capacity(total);
300 let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
301 payload.extend_from_slice(
302 &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
303 );
304 while next != 0 && payload.len() < total {
305 let ov = pager
306 .read_page(next)
307 .map_err(|err| page_integrity(next, None, err.to_string()))?;
308 let ob = ov.as_bytes();
309 if ob.len() < cs + META_V3_OVERFLOW_HEADER {
310 return Err(page_integrity(
311 next,
312 None,
313 "metadata overflow continuation is truncated",
314 ));
315 }
316 let continuation =
317 reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
318 .map_err(|err| page_integrity(next, None, err.to_string()))?;
319 let nn = continuation.next_overflow_page_id;
320 let len = continuation.chunk_bytes as usize;
321 let remaining = total - payload.len();
322 let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
323 payload.extend_from_slice(
324 &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
325 );
326 next = nn;
327 }
328 Ok(Some(payload))
329}
330
331impl UnifiedStore {
332 fn checkpoint_tree(
336 &self,
337 pager: &Arc<Pager>,
338 manager: &SegmentManager,
339 previous: Option<&Arc<BTree>>,
340 mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
341 ) -> Result<Arc<BTree>, StoreError> {
342 let mut cursor = previous
343 .map(|tree| tree.cursor_first())
344 .transpose()
345 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
346 let mut matched = 0;
347 let mut builder = None;
348 let mut failure = None;
349 manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
350 let Some(value) = encode(entity, metadata) else {
351 return true;
352 };
353 let record = (entity.id.raw().to_be_bytes().to_vec(), value);
354 let result = (|| {
355 if builder.is_none() {
356 if let Some(cursor) = cursor.as_mut() {
357 if cursor
358 .next()
359 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
360 .as_ref()
361 == Some(&record)
362 {
363 matched += 1;
364 return Ok(());
365 }
366 }
367 let mut replacement = CheckpointTreeBuilder::new(pager);
368 if let Some(previous) = previous {
369 replacement.copy_prefix(previous, matched)?;
370 }
371 builder = Some(replacement);
372 }
373 builder
374 .as_mut()
375 .expect("replacement initialized")
376 .push(record)
377 })();
378 if let Err(error) = result {
379 failure = Some(error);
380 return false;
381 }
382 true
383 });
384 if let Some(error) = failure {
385 return Err(error);
386 }
387 if builder.is_none() {
388 if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
389 if cursor
390 .next()
391 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
392 .is_none()
393 {
394 return Ok(Arc::clone(previous));
395 }
396 }
397 let mut replacement = CheckpointTreeBuilder::new(pager);
398 if let Some(previous) = previous {
399 replacement.copy_prefix(previous, matched)?;
400 }
401 builder = Some(replacement);
402 }
403 let mut builder = builder.expect("replacement or reused tree");
404 builder.flush()?;
405 Ok(builder.tree)
406 }
407
408 fn publish_reclaimable_trees(
412 &self,
413 pager: &Pager,
414 metadata: &mut Vec<u8>,
415 live: &HashMap<String, Arc<BTree>>,
416 ) -> Result<(), StoreError> {
417 let mut retired = self.retired_btrees.lock();
418 let mut roots = Vec::new();
419 let mut reclaim = Vec::new();
420 let mut remove = std::collections::HashSet::new();
421 for (index, entry) in retired.iter().enumerate() {
422 if live.values().any(|tree| Arc::ptr_eq(tree, &entry.tree)) {
423 remove.insert(index);
424 continue;
425 }
426 if entry.published
427 && Arc::strong_count(&entry.tree) == 1
428 && !entry.tree.has_retirement_readers()
429 {
430 reclaim.extend(
431 entry
432 .tree
433 .retired_pages()
434 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
435 );
436 remove.insert(index);
437 } else if entry.tree.root_page_id() != 0 {
438 roots.push(entry.tree.root_page_id());
439 }
440 }
441 roots.sort_unstable();
442 roots.dedup();
443 reddb_file::encode_native_paged_retired_trees(metadata, &roots)
444 .map_err(|e| StoreError::Serialization(e.to_string()))?;
445 let (pending_pages, published_pages) = pager
446 .deferred_reclamation_pages()
447 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
448 let readers = live.values().any(|tree| tree.has_retirement_readers())
449 || retired
450 .iter()
451 .any(|entry| entry.tree.has_retirement_readers());
452 let released_pages = if readers { Vec::new() } else { published_pages };
453 let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
454 let retained_pages: Vec<_> = pending_pages
455 .into_iter()
456 .filter(|id| !released.contains(id))
457 .collect();
458 reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
459 .map_err(|e| StoreError::Serialization(e.to_string()))?;
460 publish_paged_metadata(pager, metadata)
463 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
464 let mut index = 0;
465 retired.retain_mut(|entry| {
466 let keep = !remove.contains(&index);
467 entry.published = true;
468 index += 1;
469 keep
470 });
471 if !reclaim.is_empty() {
472 pager
473 .free_retired_pages(&reclaim)
474 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
475 pager
476 .sync()
477 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
478 }
479 pager
480 .complete_deferred_publication(&retained_pages, &released_pages)
481 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
482 if !released_pages.is_empty() {
483 pager
484 .sync()
485 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
486 }
487 Ok(())
488 }
489
490 pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
491 let mut retired = self.retired_btrees.lock();
492 if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
493 {
494 retired.push(RetiredBTree {
495 tree,
496 published: false,
497 });
498 }
499 }
500
501 pub(crate) fn note_paged_insert(
505 &self,
506 collection: &str,
507 manager: &Arc<SegmentManager>,
508 before: &[(u64, u64)],
509 id: EntityId,
510 ) {
511 let after = manager.checkpoint_revisions();
512 if before.len() != after.len() {
513 return;
514 }
515 let mut changed = 0;
516 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
517 if old_id != new_id {
518 return;
519 }
520 if old_revision != new_revision {
521 if old_revision.checked_add(1) != Some(*new_revision) {
522 return;
523 }
524 changed += 1;
525 }
526 }
527 if changed != 1 {
528 return;
529 }
530 let mut versions = self.checkpoint_versions.write();
531 let Some(previous) = versions.get_mut(collection) else {
532 return;
533 };
534 if previous.manager.ptr_eq(&Arc::downgrade(manager))
535 && previous.segments == before
536 && previous.format_version == self.format_version()
537 && !previous.pending_vault.contains(&id)
538 && !previous.excluded_xids.contains(&0)
539 {
540 previous.segments = after;
541 }
542 }
543
544 pub(crate) fn note_paged_delete(
548 &self,
549 collection: &str,
550 manager: &Arc<SegmentManager>,
551 before: &[(u64, u64)],
552 deleted: &[EntityId],
553 ) {
554 let after = manager.checkpoint_revisions();
555 if before.len() != after.len() {
556 return;
557 }
558 let mut changes = 0_u64;
559 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
560 if old_id != new_id {
561 return;
562 }
563 let Some(delta) = new_revision.checked_sub(*old_revision) else {
564 return;
565 };
566 let Some(total) = changes.checked_add(delta) else {
567 return;
568 };
569 changes = total;
570 }
571 let Ok(expected) = u64::try_from(deleted.len()) else {
572 return;
573 };
574 if changes != expected {
575 return;
576 }
577 let mut versions = self.checkpoint_versions.write();
578 let Some(previous) = versions.get_mut(collection) else {
579 return;
580 };
581 if !previous.manager.ptr_eq(&Arc::downgrade(manager))
582 || previous.format_version != self.format_version()
583 || deleted.iter().any(|id| previous.pending_vault.contains(id))
584 || previous.excluded_xids.contains(&0)
585 {
586 return;
587 }
588 if previous.segments != before {
589 let Some(deferred) = previous.deferred_rows.as_ref() else {
590 return;
591 };
592 let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
593 return;
594 };
595 let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
596 let pending: std::collections::BTreeSet<_> = deferred
597 .iter()
598 .copied()
599 .chain(changed)
600 .filter(|id| !removed.contains(id))
601 .collect();
602 if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
603 return;
604 }
605 previous.has_unprojected_rows = !pending.is_empty();
610 previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
611 }
612 previous.segments = after;
613 }
614
615 pub(crate) fn mark_paged_registry_dirty(&self) {
616 self.resident.mark_paged_registry_dirty()
617 }
618
619 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
625 let pager = self.pager.as_ref()?;
626 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
627 return Some(btree);
628 }
629 let mut write = self.btree_indices.write();
630 let btree = write
631 .entry(collection.to_string())
632 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
633 .clone();
634 Some(btree)
635 }
636
637 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
638 let Some(pager) = &self.pager else {
639 return Ok(());
640 };
641
642 if self.resident.paged_registry_dirty.load(Ordering::Acquire) {
643 self.flush_paged_registry()?;
644 self.resident
645 .paged_registry_dirty
646 .store(false, Ordering::Release);
647 return Ok(());
648 }
649
650 pager
651 .flush()
652 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
653 }
654
655 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
656 let _vault_guard = self.vault_publication_lock.lock();
657 let Some(pager) = &self.pager else {
658 return Ok(());
659 };
660
661 match pager.read_page(1) {
662 Ok(_) => {}
663 Err(PagerError::PageNotFound(_)) => {
664 let meta_page = pager
665 .allocate_page(crate::storage::engine::PageType::Header)
666 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
667 pager
668 .write_page(meta_page.page_id(), meta_page)
669 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
670 }
671 Err(e) => {
672 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
673 }
674 }
675
676 let format_version = STORE_VERSION_V11;
677 self.set_format_version(format_version);
678
679 let collections = self.resident.collections.read();
680 let btree_indices = self.btree_indices.read();
681 let mut collection_roots = Vec::with_capacity(collections.len());
682 for (name, _) in collections.iter() {
683 let root_page = btree_indices
684 .get(name)
685 .map_or(0, |btree| btree.root_page_id());
686 collection_roots.push((name.clone(), root_page));
687 }
688 drop(btree_indices);
689 drop(collections);
690
691 let mut meta_data = Vec::with_capacity(4096);
692 reddb_file::encode_native_paged_metadata_header(
693 &mut meta_data,
694 reddb_file::NativePagedMetadataHeader {
695 format_version,
696 collection_count: collection_roots.len() as u32,
697 },
698 );
699 for (name, root_page) in &collection_roots {
700 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
701 }
702
703 let cross_refs = self.resident.cross_refs.read();
704 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
705 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
706 for (source_id, refs) in cross_refs.iter() {
707 for (target_id, ref_type, collection) in refs {
708 reddb_file::encode_native_paged_cross_ref(
709 &mut meta_data,
710 source_id.raw(),
711 target_id.raw(),
712 ref_type.to_byte(),
713 collection,
714 );
715 }
716 }
717
718 self.publish_reclaimable_trees(pager, &mut meta_data, &self.btree_indices.read())?;
719
720 Ok(())
721 }
722
723 pub fn pager(&self) -> Option<&Arc<Pager>> {
725 self.pager.as_ref()
726 }
727
728 pub fn page_cache_bytes_in_use(&self) -> u64 {
732 self.pager.as_ref().map_or(0, |pager| {
733 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
734 })
735 }
736
737 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
740 self.commit
741 .as_ref()
742 .map_or(0, |commit| commit.buffered_bytes())
743 }
744
745 pub fn segment_memory_bytes(&self) -> u64 {
749 let managers: Vec<Arc<SegmentManager>> = self
750 .resident
751 .collections
752 .read()
753 .values()
754 .map(Arc::clone)
755 .collect();
756
757 managers
758 .iter()
759 .map(|manager| manager.resident_bytes())
760 .fold(0, u64::saturating_add)
761 }
762
763 pub fn config(&self) -> &UnifiedStoreConfig {
767 &self.config
768 }
769
770 pub fn with_config(config: UnifiedStoreConfig) -> Self {
771 Self {
772 config,
773 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
774 format_version: AtomicU32::new(STORE_VERSION_V11),
775 pager: None,
776 db_path: None,
777 btree_indices: RwLock::new(HashMap::new()),
778 checkpoint_versions: RwLock::new(HashMap::new()),
779 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
780 retired_btrees: parking_lot::Mutex::new(Vec::new()),
781 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
782 commit: None,
783 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
784 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
785 snapshot_manager: std::sync::OnceLock::new(),
786 embedded_commit_session: std::sync::OnceLock::new(),
787 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
788 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
789 }
790 }
791
792 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
810 Self::open_with_config(path, UnifiedStoreConfig::default())
811 }
812
813 pub fn open_with_config(
814 path: impl AsRef<Path>,
815 config: UnifiedStoreConfig,
816 ) -> Result<Self, StoreError> {
817 let path = path.as_ref();
818 let mut pager_config = PagerConfig::default();
819 if let Some(slots) = config.page_cache_slots {
823 pager_config.cache_size = slots;
824 }
825 if matches!(
833 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
834 Some("0") | Some("false") | Some("off")
835 ) {
836 pager_config.double_write = false;
837 }
838 let pager = Pager::open(path, pager_config)
839 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
840 pager
841 .enable_deferred_reclamation()
842 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
843
844 let wal_path = reddb_file::layout::unified_wal_path(path);
845 let commit = Some(Arc::new(
848 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
849 .map_err(StoreError::Io)?,
850 ));
851
852 let store = Self {
853 config,
854 resident: reddb_engine::storage::resident_store::ResidentStore::new(),
855 format_version: AtomicU32::new(STORE_VERSION_V11),
856 pager: Some(Arc::new(pager)),
857 db_path: Some(path.to_path_buf()),
858 btree_indices: RwLock::new(HashMap::new()),
859 checkpoint_versions: RwLock::new(HashMap::new()),
860 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
861 retired_btrees: parking_lot::Mutex::new(Vec::new()),
862 detached_native_blob_pages: parking_lot::Mutex::new(Default::default()),
863 commit,
864 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
865 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
866 snapshot_manager: std::sync::OnceLock::new(),
867 embedded_commit_session: std::sync::OnceLock::new(),
868 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
869 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
870 };
871
872 store.load_from_pages()?;
874 if let Some(commit) = &store.commit {
875 commit.replay_into(&store).map_err(StoreError::Io)?;
876 }
877 store.recover_operational_manifest()?;
878
879 Ok(store)
880 }
881
882 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
886 let Some(pager) = &self.pager else {
887 return Ok(());
888 };
889 let collections = self.resident.collections.read();
890 let mut indices = self.btree_indices.write();
891 for (name, manager) in collections.iter() {
892 let tree =
893 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
894 Some(Self::serialize_entity_record(
895 entity,
896 metadata.as_ref(),
897 self.format_version(),
898 ))
899 })?;
900 if indices
901 .get(name)
902 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
903 {
904 continue;
905 }
906 if let Some(previous) = indices.insert(name.clone(), tree) {
907 self.retire_btree(previous);
908 }
909 self.mark_paged_registry_dirty();
910 }
911 drop(indices);
912 drop(collections);
913 self.flush_paged_state()?;
914 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
917 let collections = self.resident.collections.read();
918 let mut versions = self.checkpoint_versions.write();
919 for (name, manager) in collections.iter() {
920 versions.insert(
921 name.clone(),
922 CheckpointVersion {
923 manager: Arc::downgrade(manager),
924 segments: manager.checkpoint_revisions(),
925 visibility_revision: 0,
926 untracked_visibility_revision: 0,
927 excluded_xids: Arc::default(),
928 pending_vault: Arc::default(),
929 format_version: self.format_version(),
930 deferred_rows: Some(Arc::default()),
931 has_unprojected_rows: false,
932 },
933 );
934 }
935 }
936 Ok(())
937 }
938
939 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
940 let Some(path) = &self.db_path else {
941 return Ok(());
942 };
943 let mut collections = self.list_collections();
944 collections.sort();
945 let pending_drops =
946 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
947 .recover_or_bootstrap(&collections)
948 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
949 for name in pending_drops {
950 if self.get_collection(&name).is_some() {
951 self.drop_collection(&name)?;
952 }
953 }
954 Ok(())
955 }
956
957 pub(crate) fn prepare_operational_collection_create(
958 &self,
959 name: &str,
960 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
961 let Some(path) = &self.db_path else {
962 return Ok(None);
963 };
964 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
965 .prepare_collection_create(name)
966 .map(Some)
967 .map_err(StoreError::Io)
968 }
969
970 pub(crate) fn prepare_operational_collection_drop(
971 &self,
972 name: &str,
973 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
974 let Some(path) = &self.db_path else {
975 return Ok(None);
976 };
977 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
978 .prepare_collection_drop(name)
979 .map(Some)
980 .map_err(StoreError::Io)
981 }
982
983 fn load_from_pages(&self) -> Result<(), StoreError> {
987 let pager = match &self.pager {
988 Some(p) => p,
989 None => return Ok(()), };
991
992 let page_count = pager.page_count().map_err(|e| {
994 StoreError::Io(std::io::Error::other(format!(
995 "failed to read page count: {}",
996 e
997 )))
998 })?;
999 if page_count <= 1 {
1000 return Ok(());
1002 }
1003
1004 if let Some(content_vec) = read_meta_payload(pager)? {
1009 let content: &[u8] = &content_vec;
1010 if content.len() >= 4 {
1011 let mut pos = 0;
1012 let mut format_version = STORE_VERSION_V1;
1013
1014 let collection_count = if let Some(header) =
1015 reddb_file::decode_native_paged_metadata_header(content)
1016 .map_err(|err| StoreError::Serialization(err.to_string()))?
1017 {
1018 format_version = header.format_version;
1019 pos += reddb_file::METADATA_HEADER_BYTES;
1020 header.collection_count as usize
1021 } else {
1022 let count = u32::from_le_bytes([
1023 content[pos],
1024 content[pos + 1],
1025 content[pos + 2],
1026 content[pos + 3],
1027 ]) as usize;
1028 pos += 4;
1029 count
1030 };
1031
1032 self.set_format_version(format_version);
1033
1034 if pos > content.len() {
1035 return Ok(());
1036 }
1037
1038 for _ in 0..collection_count {
1040 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1041 .map_err(|err| page_integrity(1, None, err.to_string()))?;
1042 let root_page = root.root_page;
1044 let name = root.collection;
1045
1046 let _ = self.create_collection_in_memory(&name);
1050
1051 if root_page > 0 {
1053 let btree = BTree::with_root(Arc::clone(pager), root_page);
1054
1055 let mut cursor = btree.cursor_first().map_err(|err| {
1057 page_integrity(root_page, Some(name.clone()), err.to_string())
1058 })?;
1059 let manager = self.get_collection(&name);
1060 loop {
1061 let next = cursor.next().map_err(|err| {
1062 page_integrity(root_page, Some(name.clone()), err.to_string())
1063 })?;
1064 let Some((key, value)) = next else {
1065 break;
1066 };
1067 let (entity, metadata) =
1069 Self::deserialize_entity_record(&value, self.format_version())
1070 .map_err(|err| {
1071 page_integrity(
1072 root_page,
1073 Some(name.clone()),
1074 err.to_string(),
1075 )
1076 })?;
1077 if let Some(m) = &manager {
1078 let id = entity.id;
1079 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1080 m.register_row_id(*row_id);
1081 }
1082 self.resident.context_index.index_entity(&name, &entity);
1083 m.restore_snapshot_entity(entity.clone())?;
1084 if let Some(metadata) = metadata {
1085 let _ = m.set_metadata(id, metadata);
1086 }
1087 self.register_entity_id(id);
1088 if self.config.auto_index_refs {
1089 self.index_cross_refs(&entity, &name)?;
1090 }
1091 }
1092 }
1093
1094 self.btree_indices
1096 .write()
1097 .insert(name.clone(), Arc::new(btree));
1098 if format_version == STORE_VERSION_V11 {
1099 if let Some(manager) = &manager {
1100 self.checkpoint_versions.write().insert(
1101 name,
1102 CheckpointVersion {
1103 manager: Arc::downgrade(manager),
1104 segments: manager.checkpoint_revisions(),
1105 visibility_revision: 0,
1106 untracked_visibility_revision: 0,
1107 excluded_xids: Arc::default(),
1108 pending_vault: Arc::default(),
1109 format_version,
1110 deferred_rows: Some(Arc::default()),
1111 has_unprojected_rows: false,
1112 },
1113 );
1114 }
1115 }
1116 }
1117 }
1118
1119 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1120 let cross_ref_count = u32::from_le_bytes([
1121 content[pos],
1122 content[pos + 1],
1123 content[pos + 2],
1124 content[pos + 3],
1125 ]) as usize;
1126 pos += 4;
1127
1128 for _ in 0..cross_ref_count {
1129 let Ok(cross_ref) =
1130 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1131 else {
1132 break;
1133 };
1134 let source_id = EntityId::new(cross_ref.source_id);
1135 let target_id = EntityId::new(cross_ref.target_id);
1136 let ref_type = RefType::from_byte(cross_ref.ref_type);
1137 let target_collection = cross_ref.target_collection;
1138
1139 self.resident
1140 .cross_refs
1141 .write()
1142 .entry(source_id)
1143 .or_default()
1144 .push((target_id, ref_type, target_collection.clone()));
1145
1146 if let Some((collection, mut entity)) = self.get_any(source_id) {
1147 let exists = entity.cross_refs().iter().any(|xref| {
1148 xref.target == target_id
1149 && xref.ref_type == ref_type
1150 && xref.target_collection == target_collection
1151 });
1152 if !exists {
1153 entity.cross_refs_mut().push(CrossRef::new(
1154 source_id,
1155 target_id,
1156 target_collection.clone(),
1157 ref_type,
1158 ));
1159 if let Some(manager) = self.get_collection(&collection) {
1160 let _ = manager.update(entity);
1161 }
1162 }
1163 }
1164 }
1165 }
1166 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1167 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1168 let live_roots: std::collections::HashSet<_> = self
1169 .btree_indices
1170 .read()
1171 .values()
1172 .map(|tree| tree.root_page_id())
1173 .collect();
1174 for root in retired {
1175 if live_roots.contains(&root)
1176 || root
1177 >= pager
1178 .page_count()
1179 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1180 {
1181 return Err(page_integrity(
1182 root,
1183 None,
1184 "retired root aliases a live or missing page",
1185 ));
1186 }
1187 self.retired_btrees.lock().push(RetiredBTree {
1188 tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1189 published: true,
1190 });
1191 }
1192 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1193 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1194 if deferred.iter().any(|id| live_roots.contains(id)) {
1195 return Err(page_integrity(1, None, "deferred page aliases a live root"));
1196 }
1197 pager
1198 .restore_deferred_reclamation(&deferred)
1199 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1200 }
1201 }
1202
1203 if self.format_version() < STORE_VERSION_V11 {
1204 self.set_format_version(STORE_VERSION_V11);
1205 }
1206
1207 Ok(())
1208 }
1209
1210 pub(crate) fn deserialize_entity(
1212 data: &[u8],
1213 format_version: u32,
1214 ) -> Result<UnifiedEntity, StoreError> {
1215 let mut pos = 0;
1216 Self::read_entity_binary(data, &mut pos, format_version)
1217 .map_err(|e| StoreError::Serialization(e.to_string()))
1218 }
1219
1220 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1222 let mut buf = Vec::with_capacity(256);
1227 Self::write_entity_binary(&mut buf, entity, format_version);
1228 buf
1229 }
1230
1231 pub(crate) fn serialize_entity_record(
1232 entity: &UnifiedEntity,
1233 metadata: Option<&Metadata>,
1234 format_version: u32,
1235 ) -> Vec<u8> {
1236 reddb_engine::storage::metadata_codec::serialize_entity_record(
1237 entity,
1238 metadata,
1239 format_version,
1240 )
1241 }
1242
1243 pub(crate) fn deserialize_entity_record(
1244 data: &[u8],
1245 format_version: u32,
1246 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1247 reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
1248 .map_err(StoreError::Serialization)
1249 }
1250
1251 pub fn persist(&self) -> Result<(), StoreError> {
1256 let _vault_guard = self.vault_publication_lock.lock();
1257 let pending_vault_versions = self.pending_vault_versions.read();
1258 let pager = match &self.pager {
1259 Some(p) => p,
1260 None => {
1261 if let Some(path) = &self.db_path {
1263 return self
1264 .save_to_file(path)
1265 .map_err(|e| StoreError::Serialization(e.to_string()));
1266 }
1267 return Err(StoreError::Io(std::io::Error::other(
1268 "No pager or path configured for persistence",
1269 )));
1270 }
1271 };
1272
1273 match pager.read_page(1) {
1274 Ok(_) => {}
1275 Err(PagerError::PageNotFound(_)) => {
1276 let meta_page = pager
1277 .allocate_page(crate::storage::engine::PageType::Header)
1278 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1279 pager
1280 .write_page(meta_page.page_id(), meta_page)
1281 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1282 }
1283 Err(e) => {
1284 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1285 }
1286 }
1287
1288 if let Some(commit) = &self.commit {
1289 commit.force_sync().map_err(StoreError::Io)?;
1290 }
1291
1292 let collections = self.resident.collections.read();
1293 let mut btree_indices = self.btree_indices.write();
1294
1295 let mut collection_roots: Vec<(String, u32)> = Vec::new();
1297
1298 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1301 let previous_versions = self.checkpoint_versions.read();
1302 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1303 let pending_vault = Arc::new(pending_vault_versions.clone());
1304 let (visibility_revision, excluded, untracked_visibility_revision) =
1305 self.snapshot_manager.get().map_or_else(
1306 || (0, Default::default(), 0),
1307 |manager| manager.checkpoint_projection_state(),
1308 );
1309 let excluded = Arc::new(excluded);
1310 for (name, manager) in collections.iter() {
1311 let mut version = CheckpointVersion {
1312 manager: Arc::downgrade(manager),
1313 segments: manager.checkpoint_revisions(),
1314 visibility_revision,
1315 untracked_visibility_revision,
1316 excluded_xids: Arc::clone(&excluded),
1317 pending_vault: Arc::clone(&pending_vault),
1318 format_version: self.format_version(),
1319 deferred_rows: Some(Arc::default()),
1320 has_unprojected_rows: false,
1321 };
1322 if let (Some(previous), Some(tree)) =
1323 (previous_versions.get(name), btree_indices.get(name))
1324 {
1325 if previous.manager.ptr_eq(&version.manager)
1326 && previous.segments == version.segments
1327 && previous.format_version == version.format_version
1328 && !previous.has_unprojected_rows
1329 && checkpoint_visibility_matches(previous, &version)
1330 {
1331 version.deferred_rows = previous.deferred_rows.clone();
1332 collection_roots.push((name.clone(), tree.root_page_id()));
1333 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1334 checkpoint_versions.insert(name.clone(), version);
1335 continue;
1336 }
1337 }
1338 if let (Some(previous), Some(tree)) =
1339 (previous_versions.get(name), btree_indices.get(name))
1340 {
1341 if previous.manager.ptr_eq(&version.manager)
1342 && previous.format_version == version.format_version
1343 {
1344 if let Some(tree) =
1345 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
1346 {
1347 collection_roots.push((name.clone(), tree.root_page_id()));
1348 rebuilt_indices.insert(name.clone(), tree);
1349 checkpoint_versions.insert(name.clone(), version);
1350 continue;
1351 }
1352 }
1353 }
1354 let btree = self.checkpoint_tree(
1355 pager,
1356 manager,
1357 btree_indices.get(name),
1358 |entity, metadata| {
1359 #[cfg(test)]
1360 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1361 let hidden = pending_vault_versions.contains(&entity.id)
1362 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1363 && excluded.contains(&entity.xmin));
1364 let normalize_xmax = matches!(entity.data, EntityData::Row(_))
1365 && excluded.contains(&entity.xmax);
1366 if hidden || normalize_xmax {
1367 version.remember_deferred_row(entity.id);
1368 }
1369 if hidden {
1370 return None;
1371 }
1372 let mut normalized = std::borrow::Cow::Borrowed(entity);
1373 if normalize_xmax {
1374 normalized.to_mut().xmax = 0;
1375 }
1376 Some(Self::serialize_entity_record(
1377 &normalized,
1378 metadata.as_ref(),
1379 self.format_version(),
1380 ))
1381 },
1382 )?;
1383 checkpoint_versions.insert(name.clone(), version);
1384
1385 collection_roots.push((name.clone(), btree.root_page_id()));
1386 rebuilt_indices.insert(name.clone(), btree);
1387 }
1388
1389 let mut meta_data = Vec::with_capacity(4096);
1391
1392 let format_version = STORE_VERSION_V11;
1393 self.set_format_version(format_version);
1394
1395 reddb_file::encode_native_paged_metadata_header(
1396 &mut meta_data,
1397 reddb_file::NativePagedMetadataHeader {
1398 format_version,
1399 collection_count: collection_roots.len() as u32,
1400 },
1401 );
1402
1403 for (name, root_page) in &collection_roots {
1405 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1406 }
1407
1408 let cross_refs = self.resident.cross_refs.read();
1410 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1411 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1412 for (source_id, refs) in cross_refs.iter() {
1413 for (target_id, ref_type, collection) in refs {
1414 reddb_file::encode_native_paged_cross_ref(
1415 &mut meta_data,
1416 source_id.raw(),
1417 target_id.raw(),
1418 ref_type.to_byte(),
1419 collection,
1420 );
1421 }
1422 }
1423
1424 for (name, previous) in btree_indices.iter() {
1425 if !rebuilt_indices
1426 .get(name)
1427 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1428 {
1429 self.retire_btree(Arc::clone(previous));
1430 }
1431 }
1432 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices)?;
1433
1434 *btree_indices = rebuilt_indices;
1437 drop(previous_versions);
1438 *self.checkpoint_versions.write() = checkpoint_versions;
1439
1440 if let Some(commit) = &self.commit {
1441 commit.truncate().map_err(StoreError::Io)?;
1442 }
1443 *self.checkpoint_durable_rows.write() = Default::default();
1444
1445 Ok(())
1446 }
1447
1448 pub fn is_paged(&self) -> bool {
1450 self.pager.is_some()
1451 }
1452
1453 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1456 self.btree_indices
1457 .read()
1458 .get(collection)
1459 .map(|btree| btree.root_page_id())
1460 .filter(|root| *root != 0)
1461 }
1462
1463 pub fn db_path(&self) -> Option<&Path> {
1465 self.db_path.as_deref()
1466 }
1467}
1468
1469#[cfg(test)]
1470mod checkpoint_work_tests {
1471 use super::*;
1472
1473 #[test]
1474 fn retired_tree_pages_remain_pinned_by_a_cursor() {
1475 let directory = tempfile::tempdir().expect("temporary store");
1476 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1477 for id in 1..=100 {
1478 store.insert_auto("rows", row(id, 1)).expect("seed");
1479 }
1480 store.persist().expect("initial checkpoint");
1481 let tree = store
1482 .btree_indices
1483 .read()
1484 .get("rows")
1485 .cloned()
1486 .expect("tree");
1487 let mut cursor = tree.cursor_first().expect("cursor");
1488 drop(tree);
1489 let manager = store.get_collection("rows").expect("rows");
1490 for value in 2..=6 {
1491 manager.update(row(1, value)).expect("change");
1492 store.persist().expect("checkpoint with old reader");
1493 }
1494 let mut count = 0;
1495 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
1496 let (entity, _) =
1497 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1498 .expect("decode");
1499 assert_eq!(
1500 entity.data.as_row().expect("row").columns,
1501 [Value::Integer(1)]
1502 );
1503 count += 1;
1504 }
1505 assert_eq!(count, 100);
1506 drop(cursor);
1507 store.persist().expect("reclaim after reader release");
1508 assert!(store.retired_btrees.lock().is_empty());
1509 }
1510
1511 #[test]
1512 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
1513 let directory = tempfile::tempdir().expect("temporary store");
1514 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1515 store.create_collection("rows").expect("collection");
1516 let manager = store.get_collection("rows").expect("rows");
1517 for id in (1..=1200).rev() {
1518 manager
1519 .insert(UnifiedEntity::table_row(
1520 EntityId::new(id),
1521 "rows",
1522 id,
1523 vec![Value::text("x".repeat(4096))],
1524 ))
1525 .expect("seed");
1526 }
1527 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
1528 store.persist().expect("bounded checkpoint");
1529 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
1530 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
1531 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
1532 }
1533
1534 #[test]
1535 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1536 let directory = tempfile::tempdir().expect("temporary store");
1537 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1538 store.create_collection("rows").expect("collection");
1539 for id in 1..=40 {
1540 store
1541 .insert(
1542 "rows",
1543 UnifiedEntity::table_row(
1544 EntityId::new(id),
1545 "rows",
1546 id,
1547 vec![Value::Integer(id as i64)],
1548 ),
1549 )
1550 .expect("seed row");
1551 }
1552 store.persist().expect("initial checkpoint");
1553 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1554 store.persist().expect("unchanged checkpoint");
1555 assert_eq!(
1556 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1557 0,
1558 "unchanged collections must not hold the writer fence while serializing every row"
1559 );
1560 drop(store);
1561 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1562 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1563 reopened.persist().expect("unchanged recovered generation");
1564 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1565 }
1566
1567 #[test]
1568 fn paged_reopen_preserves_stored_sequence_identity() {
1569 let directory = tempfile::tempdir().expect("temporary store");
1570 let path = directory.path().join("data.rdb");
1571 let store = UnifiedStore::open(&path).expect("paged store");
1572 store.create_collection("rows").expect("collection");
1573 for id in [30, 10, 20] {
1574 store.insert("rows", row(id, id as i64)).expect("insert");
1575 }
1576 store.persist().expect("checkpoint");
1577 let sequences = [10, 20, 30].map(|id| {
1578 store
1579 .get("rows", EntityId::new(id))
1580 .expect("row")
1581 .sequence_id
1582 });
1583 drop(store);
1584 let reopened = UnifiedStore::open(&path).expect("reopen");
1585 let recovered = [10, 20, 30].map(|id| {
1586 reopened
1587 .get("rows", EntityId::new(id))
1588 .expect("row")
1589 .sequence_id
1590 });
1591 assert_eq!(
1592 recovered, sequences,
1593 "opening a checkpoint must not rewrite stored row identities"
1594 );
1595 }
1596
1597 #[test]
1598 fn auto_insert_persists_the_assigned_sequence_identity() {
1599 let directory = tempfile::tempdir().expect("temporary store");
1600 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1601 for id in 1u64..=3 {
1602 store
1603 .insert_auto("rows", row(id, id as i64))
1604 .expect("insert");
1605 }
1606 let manager = store.get_collection("rows").expect("rows");
1607 let tree = store
1608 .btree_indices
1609 .read()
1610 .get("rows")
1611 .cloned()
1612 .expect("tree");
1613 for id in 1u64..=3 {
1614 let bytes = tree
1615 .get(&id.to_be_bytes())
1616 .expect("lookup")
1617 .expect("stored row");
1618 let (persisted, _) =
1619 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1620 .expect("decode");
1621 assert_eq!(
1622 persisted.sequence_id,
1623 manager
1624 .get(EntityId::new(id))
1625 .expect("live row")
1626 .sequence_id
1627 );
1628 }
1629 }
1630
1631 #[test]
1632 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
1633 let directory = tempfile::tempdir().expect("temporary store");
1634 let path = directory.path().join("data.rdb");
1635 let store = UnifiedStore::open(&path).expect("paged store");
1636 store.insert_auto("rows", row(1, 1)).expect("seed");
1637 store.persist().expect("checkpoint");
1638 store.insert_auto("rows", row(2, 2)).expect("append");
1639 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1640 store.persist().expect("inline append checkpoint");
1641 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1642 store
1643 .get_collection("rows")
1644 .expect("rows")
1645 .update(row(1, 99))
1646 .expect("untracked edit");
1647 store.insert_auto("rows", row(3, 3)).expect("next append");
1648 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1649 store.persist().expect("untracked edit checkpoint");
1650 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
1651 drop(store);
1652 let reopened = UnifiedStore::open(&path).expect("reopen");
1653 assert_eq!(
1654 reopened
1655 .get("rows", EntityId::new(1))
1656 .expect("row")
1657 .data
1658 .as_row()
1659 .expect("table")
1660 .columns,
1661 vec![Value::Integer(99)]
1662 );
1663 }
1664
1665 #[test]
1666 fn replayed_trees_are_reusable_after_durable_reconciliation() {
1667 let directory = tempfile::tempdir().expect("temporary store");
1668 let path = directory.path().join("data.rdb");
1669 let store = UnifiedStore::open(&path).expect("paged store");
1670 store.insert_auto("rows", row(1, 1)).expect("seed");
1671 store.persist().expect("checkpoint");
1672 store
1673 .insert_auto("rows", row(2, 2))
1674 .expect("WAL-backed append");
1675 drop(store);
1676 let reopened = UnifiedStore::open(&path).expect("replay");
1677 assert!(reopened.get("rows", EntityId::new(2)).is_some());
1678 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1679 reopened.persist().expect("checkpoint reconciled tree");
1680 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1681 }
1682
1683 fn row(id: u64, value: i64) -> UnifiedEntity {
1684 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
1685 }
1686
1687 #[test]
1688 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
1689 let directory = tempfile::tempdir().expect("temporary store");
1690 let path = directory.path().join("data.rdb");
1691 let store = UnifiedStore::open(&path).expect("paged store");
1692 store.create_collection("rows").expect("collection");
1693 store.insert("rows", row(1, 1)).expect("seed");
1694 store.persist().expect("initial checkpoint");
1695 let manager = store.get_collection("rows").expect("rows");
1696 manager.update(row(1, 2)).expect("direct update");
1697 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1698 store.persist().expect("updated row checkpoint");
1699 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1700 let mut metadata = Metadata::new();
1701 metadata.set("verified", MetadataValue::Bool(true));
1702 manager
1703 .set_metadata(EntityId::new(1), metadata)
1704 .expect("metadata");
1705 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1706 store.persist().expect("metadata-only checkpoint");
1707 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1708 manager
1709 .bulk_insert(vec![row(2, 2), row(3, 3)])
1710 .expect("bulk insert");
1711 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1712 store.persist().expect("bulk insert checkpoint");
1713 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
1714 manager.delete(EntityId::new(2)).expect("delete");
1715 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1716 store.persist().expect("changed checkpoint");
1717 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
1718 drop(store);
1719 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
1720 let manager = recovered.get_collection("rows").expect("rows");
1721 assert_eq!(manager.count(), 2);
1722 assert_eq!(
1723 manager
1724 .get(EntityId::new(1))
1725 .expect("row")
1726 .data
1727 .as_row()
1728 .expect("table")
1729 .columns,
1730 vec![Value::Integer(2)]
1731 );
1732 assert!(manager.get(EntityId::new(2)).is_none());
1733 assert!(manager
1734 .get_metadata(EntityId::new(1))
1735 .expect("metadata")
1736 .get("verified")
1737 .is_some());
1738 }
1739
1740 #[test]
1741 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
1742 let directory = tempfile::tempdir().expect("temporary store");
1743 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1744 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1745 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1746 store.create_collection("rows").expect("collection");
1747 for id in 1..=40 {
1748 store.insert("rows", row(id, id as i64)).expect("seed");
1749 }
1750 store.persist().expect("initial checkpoint");
1751 let unrelated = snapshots.begin();
1752 snapshots.rollback(unrelated);
1753 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1754 store.persist().expect("unrelated rollback checkpoint");
1755 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1756
1757 store
1758 .pending_vault_versions
1759 .write()
1760 .insert(EntityId::new(100));
1761 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1762 store.persist().expect("unrelated vault checkpoint");
1763 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1764 store.pending_vault_versions.write().clear();
1765 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1766 store
1767 .persist()
1768 .expect("unrelated vault publication checkpoint");
1769 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1770 }
1771
1772 #[test]
1773 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
1774 let directory = tempfile::tempdir().expect("temporary store");
1775 let path = directory.path().join("data.rdb");
1776 let store = UnifiedStore::open(&path).expect("paged store");
1777 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1778 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1779 store.create_collection("rows").expect("collection");
1780 let xid = snapshots.begin();
1781 let mut entity = row(1, 1);
1782 entity.xmin = xid;
1783 store.insert("rows", entity).expect("committed row");
1784 snapshots.commit(xid);
1785 store.persist().expect("committed baseline");
1786 snapshots.rollback(xid);
1787 store
1788 .persist()
1789 .expect("untracked outcome requires validation");
1790 drop(store);
1791 let recovered = UnifiedStore::open(&path).expect("recover");
1792 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1793 }
1794
1795 #[test]
1796 fn exhausted_deferred_row_cache_keeps_full_validation() {
1797 let directory = tempfile::tempdir().expect("temporary store");
1798 let path = directory.path().join("data.rdb");
1799 let store = UnifiedStore::open(&path).expect("paged store");
1800 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1801 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1802 store.create_collection("rows").expect("collection");
1803 let xid = snapshots.begin();
1804 for id in 1..=1025 {
1805 let mut entity = row(id, id as i64);
1806 entity.xmin = xid;
1807 store.insert("rows", entity).expect("pending row");
1808 }
1809 store.persist().expect("exclude pending rows");
1810 assert!(store.checkpoint_versions.read()["rows"]
1811 .deferred_rows
1812 .is_none());
1813 snapshots.commit(xid);
1814 store
1815 .persist()
1816 .expect("publish committed rows conservatively");
1817 drop(store);
1818 let recovered = UnifiedStore::open(&path).expect("recover");
1819 assert_eq!(
1820 recovered.get_collection("rows").expect("rows").count(),
1821 1025
1822 );
1823 }
1824
1825 #[test]
1826 fn rollback_invalidates_reuse_without_a_segment_mutation() {
1827 let directory = tempfile::tempdir().expect("temporary store");
1828 let path = directory.path().join("data.rdb");
1829 let store = UnifiedStore::open(&path).expect("paged store");
1830 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1831 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1832 store.create_collection("rows").expect("collection");
1833 let xid = snapshots.begin();
1834 let mut pending = row(1, 1);
1835 pending.xmin = xid;
1836 store.insert("rows", pending).expect("version");
1837 store.persist().expect("before rollback");
1838 snapshots.rollback(xid);
1839 store.persist().expect("after rollback");
1840 drop(store);
1841 let recovered = UnifiedStore::open(&path).expect("recover");
1842 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1843 }
1844
1845 #[test]
1846 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
1847 let directory = tempfile::tempdir().expect("temporary store");
1848 let path = directory.path().join("data.rdb");
1849 let store = UnifiedStore::open(&path).expect("paged store");
1850 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1851 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1852 store.create_collection("rows").expect("collection");
1853 let xid = snapshots.begin();
1854 let mut deleted = row(1, 1);
1855 deleted.xmax = xid;
1856 store.insert("rows", deleted).expect("version");
1857 store.persist().expect("before rollback");
1858 snapshots.rollback(xid);
1859 store.persist().expect("after rollback");
1860 drop(store);
1861 let recovered = UnifiedStore::open(&path).expect("recover");
1862 let manager = recovered.get_collection("rows").expect("rows");
1863 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
1864 }
1865
1866 #[test]
1867 fn publishing_a_pending_vault_version_invalidates_reuse() {
1868 let directory = tempfile::tempdir().expect("temporary store");
1869 let path = directory.path().join("data.rdb");
1870 let store = UnifiedStore::open(&path).expect("paged store");
1871 store.create_collection("rows").expect("collection");
1872 store.insert("rows", row(1, 1)).expect("row");
1873 store
1874 .pending_vault_versions
1875 .write()
1876 .insert(EntityId::new(1));
1877 store.persist().expect("exclude pending vault version");
1878 store
1879 .pending_vault_versions
1880 .write()
1881 .remove(&EntityId::new(1));
1882 store.persist().expect("include published vault version");
1883 drop(store);
1884 let recovered = UnifiedStore::open(&path).expect("recover");
1885 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
1886 }
1887}