1use super::*;
2use crate::storage::unified::entity_cache::EntityCache;
3use parking_lot::RwLock;
4
5const CHECKPOINT_BATCH_BYTES: usize = 1024 * 1024;
6const CHECKPOINT_BATCH_ROWS: usize = 512;
7
8struct CheckpointTreeBuilder {
11 tree: Arc<BTree>,
12 records: Vec<(Vec<u8>, Vec<u8>)>,
13 bytes: usize,
14}
15
16impl CheckpointTreeBuilder {
17 fn new(pager: &Arc<Pager>) -> Self {
18 Self {
19 tree: Arc::new(BTree::new(Arc::clone(pager))),
20 records: Vec::new(),
21 bytes: 0,
22 }
23 }
24
25 fn push(&mut self, record: (Vec<u8>, Vec<u8>)) -> Result<(), StoreError> {
26 let bytes = record.0.len() + record.1.len();
27 if !self.records.is_empty()
28 && (self.bytes.saturating_add(bytes) > CHECKPOINT_BATCH_BYTES
29 || self.records.len() >= CHECKPOINT_BATCH_ROWS)
30 {
31 self.flush()?;
32 }
33 self.bytes += bytes;
34 self.records.push(record);
35 #[cfg(test)]
36 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(peak.get().max(self.bytes)));
37 Ok(())
38 }
39
40 fn flush(&mut self) -> Result<(), StoreError> {
41 self.tree
42 .bulk_insert_sorted(&self.records)
43 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
44 self.records.clear();
45 self.bytes = 0;
46 Ok(())
47 }
48
49 fn copy_prefix(&mut self, previous: &BTree, count: usize) -> Result<(), StoreError> {
50 let mut cursor = previous
51 .cursor_first()
52 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
53 for _ in 0..count {
54 let record = cursor
55 .next()
56 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
57 .ok_or_else(|| StoreError::Serialization("checkpoint prefix disappeared".into()))?;
58 self.push(record)?;
59 }
60 Ok(())
61 }
62}
63
64#[cfg(test)]
65thread_local! {
66 static CHECKPOINT_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 = match pager.read_page(1) {
107 Ok(p) => p,
108 Err(_) => return Ok(Vec::new()),
109 };
110 let bytes = page.as_bytes();
111 if bytes.len() < cs + META_V3_PAGE1_HEADER {
112 return Ok(Vec::new());
113 }
114 let Some(header) =
115 reddb_file::decode_native_metadata_overflow_header(&bytes[cs..]).map_err(|err| {
116 PagerError::InvalidDatabase(format!("invalid metadata overflow header: {err}"))
117 })?
118 else {
119 return Ok(Vec::new());
120 };
121 let mut next = header.next_overflow_page_id;
122 let mut pages = Vec::new();
123 let mut seen = std::collections::HashSet::new();
124 while next != 0 {
125 if !seen.insert(next) || pages.len() >= pager.page_count()? as usize {
126 return Err(PagerError::InvalidDatabase(
127 "cyclic metadata overflow chain".into(),
128 ));
129 }
130 let ov = match pager.read_page(next) {
131 Ok(p) => p,
132 Err(_) => break,
133 };
134 let ob = ov.as_bytes();
135 let nn = match reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..]) {
136 Ok(header) => header.next_overflow_page_id,
137 Err(_) => 0,
138 };
139 pages.push(next);
140 next = nn;
141 }
142 Ok(pages)
143}
144
145fn publish_paged_metadata(pager: &Pager, meta_data: &[u8]) -> Result<(), PagerError> {
146 let retired = existing_overflow_chain(pager)?;
147 let page = build_meta_page1_with_overflow(pager, meta_data)?;
148 pager.sync()?;
150 pager.write_page(1, page)?;
151 pager.sync()?;
152 for id in retired {
153 pager.free_page(id)?;
154 }
155 Ok(())
156}
157
158fn build_meta_page1_with_overflow(
159 pager: &Pager,
160 meta_data: &[u8],
161) -> Result<crate::storage::engine::Page, PagerError> {
162 use crate::storage::engine::{Page, PageType, HEADER_SIZE};
163
164 let mut page1 = Page::new(PageType::Header, 1);
165 let cs = HEADER_SIZE;
166
167 if meta_data.len() <= META_PAGE_CONTENT_CAP {
168 let buf = page1.as_bytes_mut();
170 buf[cs..cs + meta_data.len()].copy_from_slice(meta_data);
171 return Ok(page1);
172 }
173
174 let first_chunk = &meta_data[..META_V3_FIRST_PAYLOAD_CAP];
178 let mut tail = &meta_data[META_V3_FIRST_PAYLOAD_CAP..];
179 let mut chunks: Vec<&[u8]> = Vec::new();
180 while !tail.is_empty() {
181 let take = tail.len().min(META_V3_OVERFLOW_PAYLOAD_CAP);
182 chunks.push(&tail[..take]);
183 tail = &tail[take..];
184 }
185
186 let mut overflow_pages: Vec<Page> = Vec::with_capacity(chunks.len());
187 let mut overflow_ids: Vec<u32> = Vec::with_capacity(chunks.len());
188 for _ in 0..chunks.len() {
189 let pg = pager.allocate_page(PageType::Overflow)?;
190 overflow_ids.push(pg.page_id());
191 overflow_pages.push(pg);
192 }
193
194 for i in 0..chunks.len() {
195 let next = if i + 1 < chunks.len() {
196 overflow_ids[i + 1]
197 } else {
198 0u32
199 };
200 let len = chunks[i].len() as u32;
201 let buf = overflow_pages[i].as_bytes_mut();
202 reddb_file::encode_native_metadata_overflow_continuation_header(
203 &mut buf[cs..cs + META_V3_OVERFLOW_HEADER],
204 reddb_file::NativeMetadataOverflowContinuationHeader {
205 next_overflow_page_id: next,
206 chunk_bytes: len,
207 },
208 )
209 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
210 buf[cs + 8..cs + 8 + chunks[i].len()].copy_from_slice(chunks[i]);
211 }
212 for (idx, page) in overflow_pages.into_iter().enumerate() {
213 let id = overflow_ids[idx];
214 pager.write_page(id, page)?;
215 }
216
217 let format_version = reddb_file::decode_native_paged_metadata_header(meta_data)
219 .ok()
220 .flatten()
221 .map_or(0, |header| header.format_version);
222
223 let buf = page1.as_bytes_mut();
224 reddb_file::encode_native_metadata_overflow_header(
225 &mut buf[cs..cs + META_V3_PAGE1_HEADER],
226 reddb_file::NativeMetadataOverflowHeader {
227 format_version,
228 total_payload_bytes: meta_data.len() as u32,
229 next_overflow_page_id: overflow_ids[0],
230 },
231 )
232 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
233 buf[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_chunk.len()]
234 .copy_from_slice(first_chunk);
235
236 Ok(page1)
237}
238
239fn storage_integrity(
240 zone: &str,
241 id: impl Into<String>,
242 collection: Option<String>,
243 detail: impl Into<String>,
244) -> StoreError {
245 StoreError::StorageIntegrity(crate::api::StorageIntegrityError::new(
246 zone, id, collection, detail,
247 ))
248}
249
250fn page_integrity(
251 page_id: u32,
252 collection: Option<String>,
253 detail: impl Into<String>,
254) -> StoreError {
255 storage_integrity("page", page_id.to_string(), collection, detail)
256}
257
258fn read_meta_payload(pager: &Pager) -> Result<Option<Vec<u8>>, StoreError> {
270 let cs = crate::storage::engine::HEADER_SIZE;
271 let meta_page = pager.read_manifest_page().map_err(|err| {
272 storage_integrity(
273 "manifest",
274 "manifest",
275 None,
276 format!("internal manifest zone read failed ({err})"),
277 )
278 })?;
279 let bytes = meta_page.as_bytes();
280 if bytes.len() < cs + 4 {
281 return Ok(Some(bytes.get(cs..).unwrap_or(&[]).to_vec()));
282 }
283 let header = match reddb_file::decode_native_metadata_overflow_header(&bytes[cs..])
284 .map_err(|err| page_integrity(1, None, err.to_string()))?
285 {
286 Some(header) => header,
287 None => {
288 return Ok(Some(bytes[cs..].to_vec()));
289 }
290 };
291 if bytes.len() < cs + META_V3_PAGE1_HEADER {
292 return Err(page_integrity(
293 1,
294 None,
295 "metadata overflow header is truncated",
296 ));
297 }
298 let total = header.total_payload_bytes as usize;
299 let mut next = header.next_overflow_page_id;
300 let mut payload: Vec<u8> = Vec::with_capacity(total);
301 let first_take = total.min(META_V3_FIRST_PAYLOAD_CAP);
302 payload.extend_from_slice(
303 &bytes[cs + META_V3_PAGE1_HEADER..cs + META_V3_PAGE1_HEADER + first_take],
304 );
305 while next != 0 && payload.len() < total {
306 let ov = pager
307 .read_page(next)
308 .map_err(|err| page_integrity(next, None, err.to_string()))?;
309 let ob = ov.as_bytes();
310 if ob.len() < cs + META_V3_OVERFLOW_HEADER {
311 return Err(page_integrity(
312 next,
313 None,
314 "metadata overflow continuation is truncated",
315 ));
316 }
317 let continuation =
318 reddb_file::decode_native_metadata_overflow_continuation_header(&ob[cs..])
319 .map_err(|err| page_integrity(next, None, err.to_string()))?;
320 let nn = continuation.next_overflow_page_id;
321 let len = continuation.chunk_bytes as usize;
322 let remaining = total - payload.len();
323 let take = len.min(remaining).min(META_V3_OVERFLOW_PAYLOAD_CAP);
324 payload.extend_from_slice(
325 &ob[cs + META_V3_OVERFLOW_HEADER..cs + META_V3_OVERFLOW_HEADER + take],
326 );
327 next = nn;
328 }
329 Ok(Some(payload))
330}
331
332impl UnifiedStore {
333 fn checkpoint_tree(
337 &self,
338 pager: &Arc<Pager>,
339 manager: &SegmentManager,
340 previous: Option<&Arc<BTree>>,
341 mut encode: impl FnMut(&UnifiedEntity, Option<Metadata>) -> Option<Vec<u8>>,
342 ) -> Result<Arc<BTree>, StoreError> {
343 let mut cursor = previous
344 .map(|tree| tree.cursor_first())
345 .transpose()
346 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
347 let mut matched = 0;
348 let mut builder = None;
349 let mut failure = None;
350 manager.for_each_entity_with_metadata_in_id_order(|entity, metadata| {
351 let Some(value) = encode(entity, metadata) else {
352 return true;
353 };
354 let record = (entity.id.raw().to_be_bytes().to_vec(), value);
355 let result = (|| {
356 if builder.is_none() {
357 if let Some(cursor) = cursor.as_mut() {
358 if cursor
359 .next()
360 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
361 .as_ref()
362 == Some(&record)
363 {
364 matched += 1;
365 return Ok(());
366 }
367 }
368 let mut replacement = CheckpointTreeBuilder::new(pager);
369 if let Some(previous) = previous {
370 replacement.copy_prefix(previous, matched)?;
371 }
372 builder = Some(replacement);
373 }
374 builder
375 .as_mut()
376 .expect("replacement initialized")
377 .push(record)
378 })();
379 if let Err(error) = result {
380 failure = Some(error);
381 return false;
382 }
383 true
384 });
385 if let Some(error) = failure {
386 return Err(error);
387 }
388 if builder.is_none() {
389 if let (Some(previous), Some(cursor)) = (previous, cursor.as_mut()) {
390 if cursor
391 .next()
392 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
393 .is_none()
394 {
395 return Ok(Arc::clone(previous));
396 }
397 }
398 let mut replacement = CheckpointTreeBuilder::new(pager);
399 if let Some(previous) = previous {
400 replacement.copy_prefix(previous, matched)?;
401 }
402 builder = Some(replacement);
403 }
404 let mut builder = builder.expect("replacement or reused tree");
405 builder.flush()?;
406 Ok(builder.tree)
407 }
408
409 fn publish_reclaimable_trees(
413 &self,
414 pager: &Pager,
415 metadata: &mut Vec<u8>,
416 live: &HashMap<String, Arc<BTree>>,
417 ) -> Result<(), StoreError> {
418 let mut retired = self.retired_btrees.lock();
419 let mut roots = Vec::new();
420 let mut reclaim = Vec::new();
421 let mut remove = std::collections::HashSet::new();
422 for (index, entry) in retired.iter().enumerate() {
423 if live.values().any(|tree| Arc::ptr_eq(tree, &entry.tree)) {
424 remove.insert(index);
425 continue;
426 }
427 if entry.published
428 && Arc::strong_count(&entry.tree) == 1
429 && !entry.tree.has_retirement_readers()
430 {
431 reclaim.extend(
432 entry
433 .tree
434 .retired_pages()
435 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?,
436 );
437 remove.insert(index);
438 } else if entry.tree.root_page_id() != 0 {
439 roots.push(entry.tree.root_page_id());
440 }
441 }
442 roots.sort_unstable();
443 roots.dedup();
444 reddb_file::encode_native_paged_retired_trees(metadata, &roots)
445 .map_err(|e| StoreError::Serialization(e.to_string()))?;
446 let (pending_pages, published_pages) = pager
447 .deferred_reclamation_pages()
448 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
449 let readers = live.values().any(|tree| tree.has_retirement_readers())
450 || retired
451 .iter()
452 .any(|entry| entry.tree.has_retirement_readers());
453 let released_pages = if readers { Vec::new() } else { published_pages };
454 let released: std::collections::HashSet<_> = released_pages.iter().copied().collect();
455 let retained_pages: Vec<_> = pending_pages
456 .into_iter()
457 .filter(|id| !released.contains(id))
458 .collect();
459 reddb_file::encode_native_paged_deferred_pages(metadata, &retained_pages)
460 .map_err(|e| StoreError::Serialization(e.to_string()))?;
461 publish_paged_metadata(pager, metadata)
464 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
465 let mut index = 0;
466 retired.retain_mut(|entry| {
467 let keep = !remove.contains(&index);
468 entry.published = true;
469 index += 1;
470 keep
471 });
472 if !reclaim.is_empty() {
473 pager
474 .free_retired_pages(&reclaim)
475 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
476 pager
477 .sync()
478 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
479 }
480 pager
481 .complete_deferred_publication(&retained_pages, &released_pages)
482 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
483 if !released_pages.is_empty() {
484 pager
485 .sync()
486 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
487 }
488 Ok(())
489 }
490
491 pub(super) fn retire_btree(&self, tree: Arc<BTree>) {
492 let mut retired = self.retired_btrees.lock();
493 if tree.root_page_id() != 0 && !retired.iter().any(|entry| Arc::ptr_eq(&entry.tree, &tree))
494 {
495 retired.push(RetiredBTree {
496 tree,
497 published: false,
498 });
499 }
500 }
501
502 pub(crate) fn note_paged_insert(
506 &self,
507 collection: &str,
508 manager: &Arc<SegmentManager>,
509 before: &[(u64, u64)],
510 id: EntityId,
511 ) {
512 let after = manager.checkpoint_revisions();
513 if before.len() != after.len() {
514 return;
515 }
516 let mut changed = 0;
517 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
518 if old_id != new_id {
519 return;
520 }
521 if old_revision != new_revision {
522 if old_revision.checked_add(1) != Some(*new_revision) {
523 return;
524 }
525 changed += 1;
526 }
527 }
528 if changed != 1 {
529 return;
530 }
531 let mut versions = self.checkpoint_versions.write();
532 let Some(previous) = versions.get_mut(collection) else {
533 return;
534 };
535 if previous.manager.ptr_eq(&Arc::downgrade(manager))
536 && previous.segments == before
537 && previous.format_version == self.format_version()
538 && !previous.pending_vault.contains(&id)
539 && !previous.excluded_xids.contains(&0)
540 {
541 previous.segments = after;
542 }
543 }
544
545 pub(crate) fn note_paged_delete(
549 &self,
550 collection: &str,
551 manager: &Arc<SegmentManager>,
552 before: &[(u64, u64)],
553 deleted: &[EntityId],
554 ) {
555 let after = manager.checkpoint_revisions();
556 if before.len() != after.len() {
557 return;
558 }
559 let mut changes = 0_u64;
560 for ((old_id, old_revision), (new_id, new_revision)) in before.iter().zip(&after) {
561 if old_id != new_id {
562 return;
563 }
564 let Some(delta) = new_revision.checked_sub(*old_revision) else {
565 return;
566 };
567 let Some(total) = changes.checked_add(delta) else {
568 return;
569 };
570 changes = total;
571 }
572 let Ok(expected) = u64::try_from(deleted.len()) else {
573 return;
574 };
575 if changes != expected {
576 return;
577 }
578 let mut versions = self.checkpoint_versions.write();
579 let Some(previous) = versions.get_mut(collection) else {
580 return;
581 };
582 if !previous.manager.ptr_eq(&Arc::downgrade(manager))
583 || previous.format_version != self.format_version()
584 || deleted.iter().any(|id| previous.pending_vault.contains(id))
585 || previous.excluded_xids.contains(&0)
586 {
587 return;
588 }
589 if previous.segments != before {
590 let Some(deferred) = previous.deferred_rows.as_ref() else {
591 return;
592 };
593 let Some(changed) = manager.checkpoint_mutations_since(&previous.segments) else {
594 return;
595 };
596 let removed: std::collections::HashSet<_> = deleted.iter().copied().collect();
597 let pending: std::collections::BTreeSet<_> = deferred
598 .iter()
599 .copied()
600 .chain(changed)
601 .filter(|id| !removed.contains(id))
602 .collect();
603 if pending.len() > crate::storage::unified::segment::CHECKPOINT_MUTATIONS_MAX {
604 return;
605 }
606 previous.has_unprojected_rows = !pending.is_empty();
611 previous.deferred_rows = Some(Arc::new(pending.into_iter().collect()));
612 }
613 previous.segments = after;
614 }
615
616 pub(crate) fn mark_paged_registry_dirty(&self) {
617 self.paged_registry_dirty.store(true, Ordering::Release);
618 }
619
620 pub(crate) fn get_or_create_btree(&self, collection: &str) -> Option<Arc<BTree>> {
626 let pager = self.pager.as_ref()?;
627 if let Some(btree) = self.btree_indices.read().get(collection).cloned() {
628 return Some(btree);
629 }
630 let mut write = self.btree_indices.write();
631 let btree = write
632 .entry(collection.to_string())
633 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))))
634 .clone();
635 Some(btree)
636 }
637
638 pub(crate) fn flush_paged_state(&self) -> Result<(), StoreError> {
639 let Some(pager) = &self.pager else {
640 return Ok(());
641 };
642
643 if self.paged_registry_dirty.load(Ordering::Acquire) {
644 self.flush_paged_registry()?;
645 self.paged_registry_dirty.store(false, Ordering::Release);
646 return Ok(());
647 }
648
649 pager
650 .flush()
651 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))
652 }
653
654 pub(crate) fn flush_paged_registry(&self) -> Result<(), StoreError> {
655 let _vault_guard = self.vault_publication_lock.lock();
656 let Some(pager) = &self.pager else {
657 return Ok(());
658 };
659
660 match pager.read_page(1) {
661 Ok(_) => {}
662 Err(PagerError::PageNotFound(_)) => {
663 let meta_page = pager
664 .allocate_page(crate::storage::engine::PageType::Header)
665 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
666 pager
667 .write_page(meta_page.page_id(), meta_page)
668 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
669 }
670 Err(e) => {
671 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
672 }
673 }
674
675 let format_version = STORE_VERSION_V11;
676 self.set_format_version(format_version);
677
678 let collections = self.collections.read();
679 let btree_indices = self.btree_indices.read();
680 let mut collection_roots = Vec::with_capacity(collections.len());
681 for (name, _) in collections.iter() {
682 let root_page = btree_indices
683 .get(name)
684 .map_or(0, |btree| btree.root_page_id());
685 collection_roots.push((name.clone(), root_page));
686 }
687 drop(btree_indices);
688 drop(collections);
689
690 let mut meta_data = Vec::with_capacity(4096);
691 reddb_file::encode_native_paged_metadata_header(
692 &mut meta_data,
693 reddb_file::NativePagedMetadataHeader {
694 format_version,
695 collection_count: collection_roots.len() as u32,
696 },
697 );
698 for (name, root_page) in &collection_roots {
699 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
700 }
701
702 let cross_refs = self.cross_refs.read();
703 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
704 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
705 for (source_id, refs) in cross_refs.iter() {
706 for (target_id, ref_type, collection) in refs {
707 reddb_file::encode_native_paged_cross_ref(
708 &mut meta_data,
709 source_id.raw(),
710 target_id.raw(),
711 ref_type.to_byte(),
712 collection,
713 );
714 }
715 }
716
717 self.publish_reclaimable_trees(pager, &mut meta_data, &self.btree_indices.read())?;
718
719 Ok(())
720 }
721
722 pub fn pager(&self) -> Option<&Arc<Pager>> {
724 self.pager.as_ref()
725 }
726
727 pub fn page_cache_bytes_in_use(&self) -> u64 {
731 self.pager.as_ref().map_or(0, |pager| {
732 pager.cache_len() as u64 * crate::storage::memory_pools::PAGE_CACHE_PAGE_SIZE_BYTES
733 })
734 }
735
736 pub fn wal_buffer_bytes_in_use(&self) -> u64 {
739 self.commit
740 .as_ref()
741 .map_or(0, |commit| commit.buffered_bytes())
742 }
743
744 pub fn segment_memory_bytes(&self) -> u64 {
748 let managers: Vec<Arc<SegmentManager>> =
749 self.collections.read().values().map(Arc::clone).collect();
750
751 managers
752 .iter()
753 .map(|manager| manager.resident_bytes())
754 .fold(0, u64::saturating_add)
755 }
756
757 pub fn config(&self) -> &UnifiedStoreConfig {
761 &self.config
762 }
763
764 pub fn with_config(config: UnifiedStoreConfig) -> Self {
765 Self {
766 config,
767 format_version: AtomicU32::new(STORE_VERSION_V11),
768 next_entity_id: AtomicU64::new(1),
769 collections: RwLock::new(HashMap::new()),
770 cross_refs: RwLock::new(HashMap::new()),
771 reverse_refs: RwLock::new(HashMap::new()),
772 pager: None,
773 db_path: None,
774 btree_indices: RwLock::new(HashMap::new()),
775 checkpoint_versions: RwLock::new(HashMap::new()),
776 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
777 retired_btrees: parking_lot::Mutex::new(Vec::new()),
778 context_index: ContextIndex::new(),
779 entity_cache: EntityCache::new(),
780 graph_label_index: RwLock::new(HashMap::new()),
781 paged_registry_dirty: AtomicBool::new(false),
782 commit: None,
783 unindex_cross_refs_fast_path: AtomicU64::new(0),
784 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
785 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
786 aux_metadata: RwLock::new(Vec::new()),
787 snapshot_manager: std::sync::OnceLock::new(),
788 embedded_commit_session: std::sync::OnceLock::new(),
789 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
790 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
791 }
792 }
793
794 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
812 Self::open_with_config(path, UnifiedStoreConfig::default())
813 }
814
815 pub fn open_with_config(
816 path: impl AsRef<Path>,
817 config: UnifiedStoreConfig,
818 ) -> Result<Self, StoreError> {
819 let path = path.as_ref();
820 let mut pager_config = PagerConfig::default();
821 if let Some(slots) = config.page_cache_slots {
825 pager_config.cache_size = slots;
826 }
827 if matches!(
835 std::env::var("REDDB_DOUBLE_WRITE").ok().as_deref(),
836 Some("0") | Some("false") | Some("off")
837 ) {
838 pager_config.double_write = false;
839 }
840 let pager = Pager::open(path, pager_config)
841 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
842 pager
843 .enable_deferred_reclamation()
844 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
845
846 let wal_path = reddb_file::layout::unified_wal_path(path);
847 let commit = Some(Arc::new(
850 StoreCommitCoordinator::open(wal_path, config.durability_mode, config.group_commit)
851 .map_err(StoreError::Io)?,
852 ));
853
854 let store = Self {
855 config,
856 format_version: AtomicU32::new(STORE_VERSION_V11),
857 next_entity_id: AtomicU64::new(1),
858 collections: RwLock::new(HashMap::new()),
859 cross_refs: RwLock::new(HashMap::new()),
860 reverse_refs: RwLock::new(HashMap::new()),
861 pager: Some(Arc::new(pager)),
862 db_path: Some(path.to_path_buf()),
863 btree_indices: RwLock::new(HashMap::new()),
864 checkpoint_versions: RwLock::new(HashMap::new()),
865 checkpoint_durable_rows: RwLock::new(super::checkpoint_delta::DurableRows::default()),
866 retired_btrees: parking_lot::Mutex::new(Vec::new()),
867 context_index: ContextIndex::new(),
868 entity_cache: EntityCache::new(),
869 graph_label_index: RwLock::new(HashMap::new()),
870 paged_registry_dirty: AtomicBool::new(false),
871 commit,
872 unindex_cross_refs_fast_path: AtomicU64::new(0),
873 replayed_turbo_inserts: parking_lot::Mutex::new(HashMap::new()),
874 replayed_probabilistic_deltas: parking_lot::Mutex::new(Vec::new()),
875 aux_metadata: RwLock::new(Vec::new()),
876 snapshot_manager: std::sync::OnceLock::new(),
877 embedded_commit_session: std::sync::OnceLock::new(),
878 vault_publication_lock: parking_lot::ReentrantMutex::new(()),
879 pending_vault_versions: RwLock::new(std::collections::HashSet::new()),
880 };
881
882 store.load_from_pages()?;
884 if let Some(commit) = &store.commit {
885 commit.replay_into(&store).map_err(StoreError::Io)?;
886 }
887 store.recover_operational_manifest()?;
888
889 Ok(store)
890 }
891
892 pub(crate) fn reconcile_replayed_btrees(&self) -> Result<(), StoreError> {
896 let Some(pager) = &self.pager else {
897 return Ok(());
898 };
899 let collections = self.collections.read();
900 let mut indices = self.btree_indices.write();
901 for (name, manager) in collections.iter() {
902 let tree =
903 self.checkpoint_tree(pager, manager, indices.get(name), |entity, metadata| {
904 Some(Self::serialize_entity_record(
905 entity,
906 metadata.as_ref(),
907 self.format_version(),
908 ))
909 })?;
910 if indices
911 .get(name)
912 .is_some_and(|previous| Arc::ptr_eq(previous, &tree))
913 {
914 continue;
915 }
916 if let Some(previous) = indices.insert(name.clone(), tree) {
917 self.retire_btree(previous);
918 }
919 self.mark_paged_registry_dirty();
920 }
921 drop(indices);
922 drop(collections);
923 self.flush_paged_state()?;
924 if self.snapshot_manager.get().is_none() && self.pending_vault_versions.read().is_empty() {
927 let collections = self.collections.read();
928 let mut versions = self.checkpoint_versions.write();
929 for (name, manager) in collections.iter() {
930 versions.insert(
931 name.clone(),
932 CheckpointVersion {
933 manager: Arc::downgrade(manager),
934 segments: manager.checkpoint_revisions(),
935 visibility_revision: 0,
936 untracked_visibility_revision: 0,
937 excluded_xids: Arc::default(),
938 pending_vault: Arc::default(),
939 format_version: self.format_version(),
940 deferred_rows: Some(Arc::default()),
941 has_unprojected_rows: false,
942 },
943 );
944 }
945 }
946 Ok(())
947 }
948
949 pub(crate) fn recover_operational_manifest(&self) -> Result<(), StoreError> {
950 let Some(path) = &self.db_path else {
951 return Ok(());
952 };
953 let mut collections = self.list_collections();
954 collections.sort();
955 let pending_drops =
956 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
957 .recover_or_bootstrap(&collections)
958 .map_err(|err| storage_integrity("manifest", "current", None, err.to_string()))?;
959 for name in pending_drops {
960 if self.get_collection(&name).is_some() {
961 self.drop_collection(&name)?;
962 }
963 }
964 Ok(())
965 }
966
967 pub(crate) fn prepare_operational_collection_create(
968 &self,
969 name: &str,
970 ) -> Result<Option<reddb_file::PreparedCollectionCreate<reddb_file::StdVfs>>, StoreError> {
971 let Some(path) = &self.db_path else {
972 return Ok(None);
973 };
974 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
975 .prepare_collection_create(name)
976 .map(Some)
977 .map_err(StoreError::Io)
978 }
979
980 pub(crate) fn prepare_operational_collection_drop(
981 &self,
982 name: &str,
983 ) -> Result<Option<reddb_file::PreparedCollectionDrop<reddb_file::StdVfs>>, StoreError> {
984 let Some(path) = &self.db_path else {
985 return Ok(None);
986 };
987 crate::storage::operational_manifest::OperationalManifest::for_db_path(path)
988 .prepare_collection_drop(name)
989 .map(Some)
990 .map_err(StoreError::Io)
991 }
992
993 fn load_from_pages(&self) -> Result<(), StoreError> {
997 let pager = match &self.pager {
998 Some(p) => p,
999 None => return Ok(()), };
1001
1002 let page_count = pager.page_count().map_err(|e| {
1004 StoreError::Io(std::io::Error::other(format!(
1005 "failed to read page count: {}",
1006 e
1007 )))
1008 })?;
1009 if page_count <= 1 {
1010 return Ok(());
1012 }
1013
1014 if let Some(content_vec) = read_meta_payload(pager)? {
1019 let content: &[u8] = &content_vec;
1020 if content.len() >= 4 {
1021 let mut pos = 0;
1022 let mut format_version = STORE_VERSION_V1;
1023
1024 let collection_count = if let Some(header) =
1025 reddb_file::decode_native_paged_metadata_header(content)
1026 .map_err(|err| StoreError::Serialization(err.to_string()))?
1027 {
1028 format_version = header.format_version;
1029 pos += reddb_file::METADATA_HEADER_BYTES;
1030 header.collection_count as usize
1031 } else {
1032 let count = u32::from_le_bytes([
1033 content[pos],
1034 content[pos + 1],
1035 content[pos + 2],
1036 content[pos + 3],
1037 ]) as usize;
1038 pos += 4;
1039 count
1040 };
1041
1042 self.set_format_version(format_version);
1043
1044 if pos > content.len() {
1045 return Ok(());
1046 }
1047
1048 for _ in 0..collection_count {
1050 let root = reddb_file::decode_native_paged_collection_root(content, &mut pos)
1051 .map_err(|err| page_integrity(1, None, err.to_string()))?;
1052 let root_page = root.root_page;
1054 let name = root.collection;
1055
1056 let _ = self.create_collection_in_memory(&name);
1060
1061 if root_page > 0 {
1063 let btree = BTree::with_root(Arc::clone(pager), root_page);
1064
1065 let mut cursor = btree.cursor_first().map_err(|err| {
1067 page_integrity(root_page, Some(name.clone()), err.to_string())
1068 })?;
1069 let manager = self.get_collection(&name);
1070 loop {
1071 let next = cursor.next().map_err(|err| {
1072 page_integrity(root_page, Some(name.clone()), err.to_string())
1073 })?;
1074 let Some((key, value)) = next else {
1075 break;
1076 };
1077 let (entity, metadata) =
1079 Self::deserialize_entity_record(&value, self.format_version())
1080 .map_err(|err| {
1081 page_integrity(
1082 root_page,
1083 Some(name.clone()),
1084 err.to_string(),
1085 )
1086 })?;
1087 if let Some(m) = &manager {
1088 let id = entity.id;
1089 if let EntityKind::TableRow { row_id, .. } = &entity.kind {
1090 m.register_row_id(*row_id);
1091 }
1092 self.context_index.index_entity(&name, &entity);
1093 m.restore_snapshot_entity(entity.clone())?;
1094 if let Some(metadata) = metadata {
1095 let _ = m.set_metadata(id, metadata);
1096 }
1097 self.register_entity_id(id);
1098 if self.config.auto_index_refs {
1099 self.index_cross_refs(&entity, &name)?;
1100 }
1101 }
1102 }
1103
1104 self.btree_indices
1106 .write()
1107 .insert(name.clone(), Arc::new(btree));
1108 if format_version == STORE_VERSION_V11 {
1109 if let Some(manager) = &manager {
1110 self.checkpoint_versions.write().insert(
1111 name,
1112 CheckpointVersion {
1113 manager: Arc::downgrade(manager),
1114 segments: manager.checkpoint_revisions(),
1115 visibility_revision: 0,
1116 untracked_visibility_revision: 0,
1117 excluded_xids: Arc::default(),
1118 pending_vault: Arc::default(),
1119 format_version,
1120 deferred_rows: Some(Arc::default()),
1121 has_unprojected_rows: false,
1122 },
1123 );
1124 }
1125 }
1126 }
1127 }
1128
1129 if format_version >= STORE_VERSION_V2 && pos + 4 <= content.len() {
1130 let cross_ref_count = u32::from_le_bytes([
1131 content[pos],
1132 content[pos + 1],
1133 content[pos + 2],
1134 content[pos + 3],
1135 ]) as usize;
1136 pos += 4;
1137
1138 for _ in 0..cross_ref_count {
1139 let Ok(cross_ref) =
1140 reddb_file::decode_native_paged_cross_ref(content, &mut pos)
1141 else {
1142 break;
1143 };
1144 let source_id = EntityId::new(cross_ref.source_id);
1145 let target_id = EntityId::new(cross_ref.target_id);
1146 let ref_type = RefType::from_byte(cross_ref.ref_type);
1147 let target_collection = cross_ref.target_collection;
1148
1149 self.cross_refs.write().entry(source_id).or_default().push((
1150 target_id,
1151 ref_type,
1152 target_collection.clone(),
1153 ));
1154
1155 if let Some((collection, mut entity)) = self.get_any(source_id) {
1156 let exists = entity.cross_refs().iter().any(|xref| {
1157 xref.target == target_id
1158 && xref.ref_type == ref_type
1159 && xref.target_collection == target_collection
1160 });
1161 if !exists {
1162 entity.cross_refs_mut().push(CrossRef::new(
1163 source_id,
1164 target_id,
1165 target_collection.clone(),
1166 ref_type,
1167 ));
1168 if let Some(manager) = self.get_collection(&collection) {
1169 let _ = manager.update(entity);
1170 }
1171 }
1172 }
1173 }
1174 }
1175 let retired = reddb_file::decode_native_paged_retired_trees(content, &mut pos)
1176 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1177 let live_roots: std::collections::HashSet<_> = self
1178 .btree_indices
1179 .read()
1180 .values()
1181 .map(|tree| tree.root_page_id())
1182 .collect();
1183 for root in retired {
1184 if live_roots.contains(&root)
1185 || root
1186 >= pager
1187 .page_count()
1188 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?
1189 {
1190 return Err(page_integrity(
1191 root,
1192 None,
1193 "retired root aliases a live or missing page",
1194 ));
1195 }
1196 self.retired_btrees.lock().push(RetiredBTree {
1197 tree: Arc::new(BTree::with_root(Arc::clone(pager), root)),
1198 published: true,
1199 });
1200 }
1201 let deferred = reddb_file::decode_native_paged_deferred_pages(content, &mut pos)
1202 .map_err(|e| StoreError::Serialization(e.to_string()))?;
1203 if deferred.iter().any(|id| live_roots.contains(id)) {
1204 return Err(page_integrity(1, None, "deferred page aliases a live root"));
1205 }
1206 pager
1207 .restore_deferred_reclamation(&deferred)
1208 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1209 }
1210 }
1211
1212 if self.format_version() < STORE_VERSION_V11 {
1213 self.set_format_version(STORE_VERSION_V11);
1214 }
1215
1216 Ok(())
1217 }
1218
1219 pub(crate) fn deserialize_entity(
1221 data: &[u8],
1222 format_version: u32,
1223 ) -> Result<UnifiedEntity, StoreError> {
1224 let mut pos = 0;
1225 Self::read_entity_binary(data, &mut pos, format_version)
1226 .map_err(|e| StoreError::Serialization(e.to_string()))
1227 }
1228
1229 pub(crate) fn serialize_entity(entity: &UnifiedEntity, format_version: u32) -> Vec<u8> {
1231 let mut buf = Vec::with_capacity(256);
1236 Self::write_entity_binary(&mut buf, entity, format_version);
1237 buf
1238 }
1239
1240 pub(crate) fn serialize_entity_record(
1241 entity: &UnifiedEntity,
1242 metadata: Option<&Metadata>,
1243 format_version: u32,
1244 ) -> Vec<u8> {
1245 let entity_bytes = Self::serialize_entity(entity, format_version);
1246 let has_meta = matches!(metadata, Some(m) if !m.fields.is_empty());
1252 if has_meta {
1253 let metadata_bytes = serialize_metadata(metadata);
1254 reddb_file::encode_native_entity_record_frame(&entity_bytes, Some(&metadata_bytes))
1255 } else {
1256 reddb_file::encode_native_entity_record_frame(&entity_bytes, None)
1257 }
1258 }
1259
1260 pub(crate) fn deserialize_entity_record(
1261 data: &[u8],
1262 format_version: u32,
1263 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1264 let Some(frame) = reddb_file::decode_native_entity_record_frame(data)
1265 .map_err(|err| StoreError::Serialization(err.to_string()))?
1266 else {
1267 return Self::deserialize_entity(data, format_version).map(|entity| (entity, None));
1268 };
1269
1270 let entity = Self::deserialize_entity(frame.entity, format_version)?;
1271 let metadata = if frame.metadata.is_empty() {
1272 None
1273 } else {
1274 let metadata = deserialize_metadata(frame.metadata)?;
1275 if metadata.is_empty() {
1276 None
1277 } else {
1278 Some(metadata)
1279 }
1280 };
1281
1282 Ok((entity, metadata))
1283 }
1284
1285 pub fn persist(&self) -> Result<(), StoreError> {
1290 let _vault_guard = self.vault_publication_lock.lock();
1291 let pending_vault_versions = self.pending_vault_versions.read();
1292 let pager = match &self.pager {
1293 Some(p) => p,
1294 None => {
1295 if let Some(path) = &self.db_path {
1297 return self
1298 .save_to_file(path)
1299 .map_err(|e| StoreError::Serialization(e.to_string()));
1300 }
1301 return Err(StoreError::Io(std::io::Error::other(
1302 "No pager or path configured for persistence",
1303 )));
1304 }
1305 };
1306
1307 match pager.read_page(1) {
1308 Ok(_) => {}
1309 Err(PagerError::PageNotFound(_)) => {
1310 let meta_page = pager
1311 .allocate_page(crate::storage::engine::PageType::Header)
1312 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1313 pager
1314 .write_page(meta_page.page_id(), meta_page)
1315 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1316 }
1317 Err(e) => {
1318 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1319 }
1320 }
1321
1322 if let Some(commit) = &self.commit {
1323 commit.force_sync().map_err(StoreError::Io)?;
1324 }
1325
1326 let collections = self.collections.read();
1327 let mut btree_indices = self.btree_indices.write();
1328
1329 let mut collection_roots: Vec<(String, u32)> = Vec::new();
1331
1332 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1335 let previous_versions = self.checkpoint_versions.read();
1336 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1337 let pending_vault = Arc::new(pending_vault_versions.clone());
1338 let (visibility_revision, excluded, untracked_visibility_revision) =
1339 self.snapshot_manager.get().map_or_else(
1340 || (0, Default::default(), 0),
1341 |manager| manager.checkpoint_projection_state(),
1342 );
1343 let excluded = Arc::new(excluded);
1344 for (name, manager) in collections.iter() {
1345 let mut version = CheckpointVersion {
1346 manager: Arc::downgrade(manager),
1347 segments: manager.checkpoint_revisions(),
1348 visibility_revision,
1349 untracked_visibility_revision,
1350 excluded_xids: Arc::clone(&excluded),
1351 pending_vault: Arc::clone(&pending_vault),
1352 format_version: self.format_version(),
1353 deferred_rows: Some(Arc::default()),
1354 has_unprojected_rows: false,
1355 };
1356 if let (Some(previous), Some(tree)) =
1357 (previous_versions.get(name), btree_indices.get(name))
1358 {
1359 if previous.manager.ptr_eq(&version.manager)
1360 && previous.segments == version.segments
1361 && previous.format_version == version.format_version
1362 && !previous.has_unprojected_rows
1363 && checkpoint_visibility_matches(previous, &version)
1364 {
1365 version.deferred_rows = previous.deferred_rows.clone();
1366 collection_roots.push((name.clone(), tree.root_page_id()));
1367 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1368 checkpoint_versions.insert(name.clone(), version);
1369 continue;
1370 }
1371 }
1372 if let (Some(previous), Some(tree)) =
1373 (previous_versions.get(name), btree_indices.get(name))
1374 {
1375 if previous.manager.ptr_eq(&version.manager)
1376 && previous.format_version == version.format_version
1377 {
1378 if let Some(tree) =
1379 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
1380 {
1381 collection_roots.push((name.clone(), tree.root_page_id()));
1382 rebuilt_indices.insert(name.clone(), tree);
1383 checkpoint_versions.insert(name.clone(), version);
1384 continue;
1385 }
1386 }
1387 }
1388 let btree = self.checkpoint_tree(
1389 pager,
1390 manager,
1391 btree_indices.get(name),
1392 |entity, metadata| {
1393 #[cfg(test)]
1394 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1395 let hidden = pending_vault_versions.contains(&entity.id)
1396 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1397 && excluded.contains(&entity.xmin));
1398 let normalize_xmax = matches!(entity.data, EntityData::Row(_))
1399 && excluded.contains(&entity.xmax);
1400 if hidden || normalize_xmax {
1401 version.remember_deferred_row(entity.id);
1402 }
1403 if hidden {
1404 return None;
1405 }
1406 let mut normalized = std::borrow::Cow::Borrowed(entity);
1407 if normalize_xmax {
1408 normalized.to_mut().xmax = 0;
1409 }
1410 Some(Self::serialize_entity_record(
1411 &normalized,
1412 metadata.as_ref(),
1413 self.format_version(),
1414 ))
1415 },
1416 )?;
1417 checkpoint_versions.insert(name.clone(), version);
1418
1419 collection_roots.push((name.clone(), btree.root_page_id()));
1420 rebuilt_indices.insert(name.clone(), btree);
1421 }
1422
1423 let mut meta_data = Vec::with_capacity(4096);
1425
1426 let format_version = STORE_VERSION_V11;
1427 self.set_format_version(format_version);
1428
1429 reddb_file::encode_native_paged_metadata_header(
1430 &mut meta_data,
1431 reddb_file::NativePagedMetadataHeader {
1432 format_version,
1433 collection_count: collection_roots.len() as u32,
1434 },
1435 );
1436
1437 for (name, root_page) in &collection_roots {
1439 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1440 }
1441
1442 let cross_refs = self.cross_refs.read();
1444 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1445 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1446 for (source_id, refs) in cross_refs.iter() {
1447 for (target_id, ref_type, collection) in refs {
1448 reddb_file::encode_native_paged_cross_ref(
1449 &mut meta_data,
1450 source_id.raw(),
1451 target_id.raw(),
1452 ref_type.to_byte(),
1453 collection,
1454 );
1455 }
1456 }
1457
1458 for (name, previous) in btree_indices.iter() {
1459 if !rebuilt_indices
1460 .get(name)
1461 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1462 {
1463 self.retire_btree(Arc::clone(previous));
1464 }
1465 }
1466 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices)?;
1467
1468 *btree_indices = rebuilt_indices;
1471 drop(previous_versions);
1472 *self.checkpoint_versions.write() = checkpoint_versions;
1473
1474 if let Some(commit) = &self.commit {
1475 commit.truncate().map_err(StoreError::Io)?;
1476 }
1477 *self.checkpoint_durable_rows.write() = Default::default();
1478
1479 Ok(())
1480 }
1481
1482 pub fn is_paged(&self) -> bool {
1484 self.pager.is_some()
1485 }
1486
1487 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1490 self.btree_indices
1491 .read()
1492 .get(collection)
1493 .map(|btree| btree.root_page_id())
1494 .filter(|root| *root != 0)
1495 }
1496
1497 pub fn db_path(&self) -> Option<&Path> {
1499 self.db_path.as_deref()
1500 }
1501}
1502
1503fn serialize_metadata(metadata: Option<&Metadata>) -> Vec<u8> {
1504 let Some(metadata) = metadata else {
1505 return Vec::new();
1506 };
1507 if metadata.is_empty() {
1508 return Vec::new();
1509 }
1510
1511 let mut entries: Vec<_> = metadata.iter().collect();
1512 entries.sort_by_key(|(a, _)| *a);
1513
1514 let mut buf = Vec::new();
1515 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1516 for (key, value) in entries {
1517 write_string(&mut buf, key);
1518 write_metadata_value(&mut buf, value);
1519 }
1520 buf
1521}
1522
1523fn deserialize_metadata(data: &[u8]) -> Result<Metadata, StoreError> {
1524 let mut pos = 0usize;
1525 let count = read_u32(data, &mut pos)? as usize;
1526 let mut metadata = Metadata::new();
1527 for _ in 0..count {
1528 let key = read_string(data, &mut pos)?;
1529 let value = read_metadata_value(data, &mut pos)?;
1530 metadata.set(key, value);
1531 }
1532 Ok(metadata)
1533}
1534
1535fn write_string(buf: &mut Vec<u8>, value: &str) {
1536 reddb_file::encode_native_len_prefixed_str(buf, value);
1537}
1538
1539fn write_bytes(buf: &mut Vec<u8>, value: &[u8]) {
1540 reddb_file::encode_native_len_prefixed_bytes(buf, value);
1541}
1542
1543fn write_ref_target(buf: &mut Vec<u8>, target: &crate::storage::unified::RefTarget) {
1544 use crate::storage::unified::RefTarget;
1545
1546 match target {
1547 RefTarget::TableRow { table, row_id } => {
1548 buf.push(0);
1549 write_string(buf, table);
1550 buf.extend_from_slice(&row_id.to_le_bytes());
1551 }
1552 RefTarget::Node {
1553 collection,
1554 node_id,
1555 } => {
1556 buf.push(1);
1557 write_string(buf, collection);
1558 buf.extend_from_slice(&node_id.raw().to_le_bytes());
1559 }
1560 RefTarget::Edge {
1561 collection,
1562 edge_id,
1563 } => {
1564 buf.push(2);
1565 write_string(buf, collection);
1566 buf.extend_from_slice(&edge_id.raw().to_le_bytes());
1567 }
1568 RefTarget::Vector {
1569 collection,
1570 vector_id,
1571 } => {
1572 buf.push(3);
1573 write_string(buf, collection);
1574 buf.extend_from_slice(&vector_id.raw().to_le_bytes());
1575 }
1576 RefTarget::Entity {
1577 collection,
1578 entity_id,
1579 } => {
1580 buf.push(4);
1581 write_string(buf, collection);
1582 buf.extend_from_slice(&entity_id.raw().to_le_bytes());
1583 }
1584 }
1585}
1586
1587fn write_metadata_value(buf: &mut Vec<u8>, value: &MetadataValue) {
1588 match value {
1589 MetadataValue::Null => buf.push(0),
1590 MetadataValue::Bool(v) => {
1591 buf.push(1);
1592 buf.push(u8::from(*v));
1593 }
1594 MetadataValue::Int(v) => {
1595 buf.push(2);
1596 buf.extend_from_slice(&v.to_le_bytes());
1597 }
1598 MetadataValue::Float(v) => {
1599 buf.push(3);
1600 buf.extend_from_slice(&v.to_le_bytes());
1601 }
1602 MetadataValue::String(v) => {
1603 buf.push(4);
1604 write_string(buf, v);
1605 }
1606 MetadataValue::Bytes(v) => {
1607 buf.push(5);
1608 write_bytes(buf, v);
1609 }
1610 MetadataValue::Array(values) => {
1611 buf.push(6);
1612 buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
1613 for value in values {
1614 write_metadata_value(buf, value);
1615 }
1616 }
1617 MetadataValue::Object(values) => {
1618 buf.push(7);
1619 let mut entries: Vec<_> = values.iter().collect();
1620 entries.sort_by_key(|(a, _)| *a);
1621 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1622 for (key, value) in entries {
1623 write_string(buf, key);
1624 write_metadata_value(buf, value);
1625 }
1626 }
1627 MetadataValue::Timestamp(v) => {
1628 buf.push(8);
1629 buf.extend_from_slice(&v.to_le_bytes());
1630 }
1631 MetadataValue::Geo { lat, lon } => {
1632 buf.push(9);
1633 buf.extend_from_slice(&lat.to_le_bytes());
1634 buf.extend_from_slice(&lon.to_le_bytes());
1635 }
1636 MetadataValue::Reference(target) => {
1637 buf.push(10);
1638 write_ref_target(buf, target);
1639 }
1640 MetadataValue::References(targets) => {
1641 buf.push(11);
1642 buf.extend_from_slice(&(targets.len() as u32).to_le_bytes());
1643 for target in targets {
1644 write_ref_target(buf, target);
1645 }
1646 }
1647 }
1648}
1649
1650fn read_exact_slice<'a>(
1651 data: &'a [u8],
1652 pos: &mut usize,
1653 len: usize,
1654) -> Result<&'a [u8], StoreError> {
1655 if *pos + len > data.len() {
1656 return Err(StoreError::Serialization(
1657 "truncated metadata payload".to_string(),
1658 ));
1659 }
1660 let slice = &data[*pos..*pos + len];
1661 *pos += len;
1662 Ok(slice)
1663}
1664
1665fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, StoreError> {
1666 let bytes = read_exact_slice(data, pos, 4)?;
1667 let mut raw = [0u8; 4];
1668 raw.copy_from_slice(bytes);
1669 Ok(u32::from_le_bytes(raw))
1670}
1671
1672fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, StoreError> {
1673 let bytes = read_exact_slice(data, pos, 8)?;
1674 let mut raw = [0u8; 8];
1675 raw.copy_from_slice(bytes);
1676 Ok(u64::from_le_bytes(raw))
1677}
1678
1679fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, StoreError> {
1680 let bytes = read_exact_slice(data, pos, 8)?;
1681 let mut raw = [0u8; 8];
1682 raw.copy_from_slice(bytes);
1683 Ok(i64::from_le_bytes(raw))
1684}
1685
1686fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, StoreError> {
1687 let bytes = read_exact_slice(data, pos, 8)?;
1688 let mut raw = [0u8; 8];
1689 raw.copy_from_slice(bytes);
1690 Ok(f64::from_le_bytes(raw))
1691}
1692
1693fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, StoreError> {
1694 let bytes = read_exact_slice(data, pos, 1)?;
1695 Ok(bytes[0])
1696}
1697
1698fn read_string(data: &[u8], pos: &mut usize) -> Result<String, StoreError> {
1699 reddb_file::decode_native_len_prefixed_string(data, pos)
1700 .map_err(|err| StoreError::Serialization(err.to_string()))
1701}
1702
1703fn read_bytes(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, StoreError> {
1704 reddb_file::decode_native_len_prefixed_bytes(data, pos)
1705 .map(|bytes| bytes.to_vec())
1706 .map_err(|err| StoreError::Serialization(err.to_string()))
1707}
1708
1709fn read_ref_target(
1710 data: &[u8],
1711 pos: &mut usize,
1712) -> Result<crate::storage::unified::RefTarget, StoreError> {
1713 use crate::storage::unified::RefTarget;
1714
1715 match read_u8(data, pos)? {
1716 0 => Ok(RefTarget::TableRow {
1717 table: read_string(data, pos)?,
1718 row_id: read_u64(data, pos)?,
1719 }),
1720 1 => Ok(RefTarget::Node {
1721 collection: read_string(data, pos)?,
1722 node_id: EntityId::new(read_u64(data, pos)?),
1723 }),
1724 2 => Ok(RefTarget::Edge {
1725 collection: read_string(data, pos)?,
1726 edge_id: EntityId::new(read_u64(data, pos)?),
1727 }),
1728 3 => Ok(RefTarget::Vector {
1729 collection: read_string(data, pos)?,
1730 vector_id: EntityId::new(read_u64(data, pos)?),
1731 }),
1732 4 => Ok(RefTarget::Entity {
1733 collection: read_string(data, pos)?,
1734 entity_id: EntityId::new(read_u64(data, pos)?),
1735 }),
1736 tag => Err(StoreError::Serialization(format!(
1737 "unknown metadata ref target tag {tag}"
1738 ))),
1739 }
1740}
1741
1742fn read_metadata_value(data: &[u8], pos: &mut usize) -> Result<MetadataValue, StoreError> {
1743 match read_u8(data, pos)? {
1744 0 => Ok(MetadataValue::Null),
1745 1 => Ok(MetadataValue::Bool(read_u8(data, pos)? != 0)),
1746 2 => Ok(MetadataValue::Int(read_i64(data, pos)?)),
1747 3 => Ok(MetadataValue::Float(read_f64(data, pos)?)),
1748 4 => Ok(MetadataValue::String(read_string(data, pos)?)),
1749 5 => Ok(MetadataValue::Bytes(read_bytes(data, pos)?)),
1750 6 => {
1751 let count = read_u32(data, pos)? as usize;
1752 let mut values = Vec::with_capacity(count);
1753 for _ in 0..count {
1754 values.push(read_metadata_value(data, pos)?);
1755 }
1756 Ok(MetadataValue::Array(values))
1757 }
1758 7 => {
1759 let count = read_u32(data, pos)? as usize;
1760 let mut values = std::collections::HashMap::with_capacity(count);
1761 for _ in 0..count {
1762 let key = read_string(data, pos)?;
1763 let value = read_metadata_value(data, pos)?;
1764 values.insert(key, value);
1765 }
1766 Ok(MetadataValue::Object(values))
1767 }
1768 8 => Ok(MetadataValue::Timestamp(read_u64(data, pos)?)),
1769 9 => Ok(MetadataValue::Geo {
1770 lat: read_f64(data, pos)?,
1771 lon: read_f64(data, pos)?,
1772 }),
1773 10 => Ok(MetadataValue::Reference(read_ref_target(data, pos)?)),
1774 11 => {
1775 let count = read_u32(data, pos)? as usize;
1776 let mut targets = Vec::with_capacity(count);
1777 for _ in 0..count {
1778 targets.push(read_ref_target(data, pos)?);
1779 }
1780 Ok(MetadataValue::References(targets))
1781 }
1782 tag => Err(StoreError::Serialization(format!(
1783 "unknown metadata value tag {tag}"
1784 ))),
1785 }
1786}
1787
1788#[cfg(test)]
1789mod checkpoint_work_tests {
1790 use super::*;
1791
1792 #[test]
1793 fn retired_tree_pages_remain_pinned_by_a_cursor() {
1794 let directory = tempfile::tempdir().expect("temporary store");
1795 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1796 for id in 1..=100 {
1797 store.insert_auto("rows", row(id, 1)).expect("seed");
1798 }
1799 store.persist().expect("initial checkpoint");
1800 let tree = store
1801 .btree_indices
1802 .read()
1803 .get("rows")
1804 .cloned()
1805 .expect("tree");
1806 let mut cursor = tree.cursor_first().expect("cursor");
1807 drop(tree);
1808 let manager = store.get_collection("rows").expect("rows");
1809 for value in 2..=6 {
1810 manager.update(row(1, value)).expect("change");
1811 store.persist().expect("checkpoint with old reader");
1812 }
1813 let mut count = 0;
1814 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
1815 let (entity, _) =
1816 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1817 .expect("decode");
1818 assert_eq!(
1819 entity.data.as_row().expect("row").columns,
1820 [Value::Integer(1)]
1821 );
1822 count += 1;
1823 }
1824 assert_eq!(count, 100);
1825 drop(cursor);
1826 store.persist().expect("reclaim after reader release");
1827 assert!(store.retired_btrees.lock().is_empty());
1828 }
1829
1830 #[test]
1831 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
1832 let directory = tempfile::tempdir().expect("temporary store");
1833 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1834 store.create_collection("rows").expect("collection");
1835 let manager = store.get_collection("rows").expect("rows");
1836 for id in (1..=1200).rev() {
1837 manager
1838 .insert(UnifiedEntity::table_row(
1839 EntityId::new(id),
1840 "rows",
1841 id,
1842 vec![Value::text("x".repeat(4096))],
1843 ))
1844 .expect("seed");
1845 }
1846 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
1847 store.persist().expect("bounded checkpoint");
1848 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
1849 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
1850 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
1851 }
1852
1853 #[test]
1854 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1855 let directory = tempfile::tempdir().expect("temporary store");
1856 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1857 store.create_collection("rows").expect("collection");
1858 for id in 1..=40 {
1859 store
1860 .insert(
1861 "rows",
1862 UnifiedEntity::table_row(
1863 EntityId::new(id),
1864 "rows",
1865 id,
1866 vec![Value::Integer(id as i64)],
1867 ),
1868 )
1869 .expect("seed row");
1870 }
1871 store.persist().expect("initial checkpoint");
1872 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1873 store.persist().expect("unchanged checkpoint");
1874 assert_eq!(
1875 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1876 0,
1877 "unchanged collections must not hold the writer fence while serializing every row"
1878 );
1879 drop(store);
1880 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1881 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1882 reopened.persist().expect("unchanged recovered generation");
1883 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1884 }
1885
1886 #[test]
1887 fn paged_reopen_preserves_stored_sequence_identity() {
1888 let directory = tempfile::tempdir().expect("temporary store");
1889 let path = directory.path().join("data.rdb");
1890 let store = UnifiedStore::open(&path).expect("paged store");
1891 store.create_collection("rows").expect("collection");
1892 for id in [30, 10, 20] {
1893 store.insert("rows", row(id, id as i64)).expect("insert");
1894 }
1895 store.persist().expect("checkpoint");
1896 let sequences = [10, 20, 30].map(|id| {
1897 store
1898 .get("rows", EntityId::new(id))
1899 .expect("row")
1900 .sequence_id
1901 });
1902 drop(store);
1903 let reopened = UnifiedStore::open(&path).expect("reopen");
1904 let recovered = [10, 20, 30].map(|id| {
1905 reopened
1906 .get("rows", EntityId::new(id))
1907 .expect("row")
1908 .sequence_id
1909 });
1910 assert_eq!(
1911 recovered, sequences,
1912 "opening a checkpoint must not rewrite stored row identities"
1913 );
1914 }
1915
1916 #[test]
1917 fn auto_insert_persists_the_assigned_sequence_identity() {
1918 let directory = tempfile::tempdir().expect("temporary store");
1919 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1920 for id in 1u64..=3 {
1921 store
1922 .insert_auto("rows", row(id, id as i64))
1923 .expect("insert");
1924 }
1925 let manager = store.get_collection("rows").expect("rows");
1926 let tree = store
1927 .btree_indices
1928 .read()
1929 .get("rows")
1930 .cloned()
1931 .expect("tree");
1932 for id in 1u64..=3 {
1933 let bytes = tree
1934 .get(&id.to_be_bytes())
1935 .expect("lookup")
1936 .expect("stored row");
1937 let (persisted, _) =
1938 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1939 .expect("decode");
1940 assert_eq!(
1941 persisted.sequence_id,
1942 manager
1943 .get(EntityId::new(id))
1944 .expect("live row")
1945 .sequence_id
1946 );
1947 }
1948 }
1949
1950 #[test]
1951 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
1952 let directory = tempfile::tempdir().expect("temporary store");
1953 let path = directory.path().join("data.rdb");
1954 let store = UnifiedStore::open(&path).expect("paged store");
1955 store.insert_auto("rows", row(1, 1)).expect("seed");
1956 store.persist().expect("checkpoint");
1957 store.insert_auto("rows", row(2, 2)).expect("append");
1958 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1959 store.persist().expect("inline append checkpoint");
1960 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1961 store
1962 .get_collection("rows")
1963 .expect("rows")
1964 .update(row(1, 99))
1965 .expect("untracked edit");
1966 store.insert_auto("rows", row(3, 3)).expect("next append");
1967 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1968 store.persist().expect("untracked edit checkpoint");
1969 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
1970 drop(store);
1971 let reopened = UnifiedStore::open(&path).expect("reopen");
1972 assert_eq!(
1973 reopened
1974 .get("rows", EntityId::new(1))
1975 .expect("row")
1976 .data
1977 .as_row()
1978 .expect("table")
1979 .columns,
1980 vec![Value::Integer(99)]
1981 );
1982 }
1983
1984 #[test]
1985 fn replayed_trees_are_reusable_after_durable_reconciliation() {
1986 let directory = tempfile::tempdir().expect("temporary store");
1987 let path = directory.path().join("data.rdb");
1988 let store = UnifiedStore::open(&path).expect("paged store");
1989 store.insert_auto("rows", row(1, 1)).expect("seed");
1990 store.persist().expect("checkpoint");
1991 store
1992 .insert_auto("rows", row(2, 2))
1993 .expect("WAL-backed append");
1994 drop(store);
1995 let reopened = UnifiedStore::open(&path).expect("replay");
1996 assert!(reopened.get("rows", EntityId::new(2)).is_some());
1997 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1998 reopened.persist().expect("checkpoint reconciled tree");
1999 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2000 }
2001
2002 fn row(id: u64, value: i64) -> UnifiedEntity {
2003 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
2004 }
2005
2006 #[test]
2007 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
2008 let directory = tempfile::tempdir().expect("temporary store");
2009 let path = directory.path().join("data.rdb");
2010 let store = UnifiedStore::open(&path).expect("paged store");
2011 store.create_collection("rows").expect("collection");
2012 store.insert("rows", row(1, 1)).expect("seed");
2013 store.persist().expect("initial checkpoint");
2014 let manager = store.get_collection("rows").expect("rows");
2015 manager.update(row(1, 2)).expect("direct update");
2016 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2017 store.persist().expect("updated row checkpoint");
2018 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2019 let mut metadata = Metadata::new();
2020 metadata.set("verified", MetadataValue::Bool(true));
2021 manager
2022 .set_metadata(EntityId::new(1), metadata)
2023 .expect("metadata");
2024 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2025 store.persist().expect("metadata-only checkpoint");
2026 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
2027 manager
2028 .bulk_insert(vec![row(2, 2), row(3, 3)])
2029 .expect("bulk insert");
2030 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2031 store.persist().expect("bulk insert checkpoint");
2032 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
2033 manager.delete(EntityId::new(2)).expect("delete");
2034 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2035 store.persist().expect("changed checkpoint");
2036 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
2037 drop(store);
2038 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
2039 let manager = recovered.get_collection("rows").expect("rows");
2040 assert_eq!(manager.count(), 2);
2041 assert_eq!(
2042 manager
2043 .get(EntityId::new(1))
2044 .expect("row")
2045 .data
2046 .as_row()
2047 .expect("table")
2048 .columns,
2049 vec![Value::Integer(2)]
2050 );
2051 assert!(manager.get(EntityId::new(2)).is_none());
2052 assert!(manager
2053 .get_metadata(EntityId::new(1))
2054 .expect("metadata")
2055 .get("verified")
2056 .is_some());
2057 }
2058
2059 #[test]
2060 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
2061 let directory = tempfile::tempdir().expect("temporary store");
2062 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
2063 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2064 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2065 store.create_collection("rows").expect("collection");
2066 for id in 1..=40 {
2067 store.insert("rows", row(id, id as i64)).expect("seed");
2068 }
2069 store.persist().expect("initial checkpoint");
2070 let unrelated = snapshots.begin();
2071 snapshots.rollback(unrelated);
2072 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2073 store.persist().expect("unrelated rollback checkpoint");
2074 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2075
2076 store
2077 .pending_vault_versions
2078 .write()
2079 .insert(EntityId::new(100));
2080 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2081 store.persist().expect("unrelated vault checkpoint");
2082 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2083 store.pending_vault_versions.write().clear();
2084 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
2085 store
2086 .persist()
2087 .expect("unrelated vault publication checkpoint");
2088 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
2089 }
2090
2091 #[test]
2092 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
2093 let directory = tempfile::tempdir().expect("temporary store");
2094 let path = directory.path().join("data.rdb");
2095 let store = UnifiedStore::open(&path).expect("paged store");
2096 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2097 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2098 store.create_collection("rows").expect("collection");
2099 let xid = snapshots.begin();
2100 let mut entity = row(1, 1);
2101 entity.xmin = xid;
2102 store.insert("rows", entity).expect("committed row");
2103 snapshots.commit(xid);
2104 store.persist().expect("committed baseline");
2105 snapshots.rollback(xid);
2106 store
2107 .persist()
2108 .expect("untracked outcome requires validation");
2109 drop(store);
2110 let recovered = UnifiedStore::open(&path).expect("recover");
2111 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2112 }
2113
2114 #[test]
2115 fn exhausted_deferred_row_cache_keeps_full_validation() {
2116 let directory = tempfile::tempdir().expect("temporary store");
2117 let path = directory.path().join("data.rdb");
2118 let store = UnifiedStore::open(&path).expect("paged store");
2119 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2120 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2121 store.create_collection("rows").expect("collection");
2122 let xid = snapshots.begin();
2123 for id in 1..=1025 {
2124 let mut entity = row(id, id as i64);
2125 entity.xmin = xid;
2126 store.insert("rows", entity).expect("pending row");
2127 }
2128 store.persist().expect("exclude pending rows");
2129 assert!(store.checkpoint_versions.read()["rows"]
2130 .deferred_rows
2131 .is_none());
2132 snapshots.commit(xid);
2133 store
2134 .persist()
2135 .expect("publish committed rows conservatively");
2136 drop(store);
2137 let recovered = UnifiedStore::open(&path).expect("recover");
2138 assert_eq!(
2139 recovered.get_collection("rows").expect("rows").count(),
2140 1025
2141 );
2142 }
2143
2144 #[test]
2145 fn rollback_invalidates_reuse_without_a_segment_mutation() {
2146 let directory = tempfile::tempdir().expect("temporary store");
2147 let path = directory.path().join("data.rdb");
2148 let store = UnifiedStore::open(&path).expect("paged store");
2149 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2150 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2151 store.create_collection("rows").expect("collection");
2152 let xid = snapshots.begin();
2153 let mut pending = row(1, 1);
2154 pending.xmin = xid;
2155 store.insert("rows", pending).expect("version");
2156 store.persist().expect("before rollback");
2157 snapshots.rollback(xid);
2158 store.persist().expect("after rollback");
2159 drop(store);
2160 let recovered = UnifiedStore::open(&path).expect("recover");
2161 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
2162 }
2163
2164 #[test]
2165 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
2166 let directory = tempfile::tempdir().expect("temporary store");
2167 let path = directory.path().join("data.rdb");
2168 let store = UnifiedStore::open(&path).expect("paged store");
2169 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
2170 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
2171 store.create_collection("rows").expect("collection");
2172 let xid = snapshots.begin();
2173 let mut deleted = row(1, 1);
2174 deleted.xmax = xid;
2175 store.insert("rows", deleted).expect("version");
2176 store.persist().expect("before rollback");
2177 snapshots.rollback(xid);
2178 store.persist().expect("after rollback");
2179 drop(store);
2180 let recovered = UnifiedStore::open(&path).expect("recover");
2181 let manager = recovered.get_collection("rows").expect("rows");
2182 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
2183 }
2184
2185 #[test]
2186 fn publishing_a_pending_vault_version_invalidates_reuse() {
2187 let directory = tempfile::tempdir().expect("temporary store");
2188 let path = directory.path().join("data.rdb");
2189 let store = UnifiedStore::open(&path).expect("paged store");
2190 store.create_collection("rows").expect("collection");
2191 store.insert("rows", row(1, 1)).expect("row");
2192 store
2193 .pending_vault_versions
2194 .write()
2195 .insert(EntityId::new(1));
2196 store.persist().expect("exclude pending vault version");
2197 store
2198 .pending_vault_versions
2199 .write()
2200 .remove(&EntityId::new(1));
2201 store.persist().expect("include published vault version");
2202 drop(store);
2203 let recovered = UnifiedStore::open(&path).expect("recover");
2204 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
2205 }
2206}