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 reddb_engine::storage::metadata_codec::serialize_entity_record(
1246 entity,
1247 metadata,
1248 format_version,
1249 )
1250 }
1251
1252 pub(crate) fn deserialize_entity_record(
1253 data: &[u8],
1254 format_version: u32,
1255 ) -> Result<(UnifiedEntity, Option<Metadata>), StoreError> {
1256 reddb_engine::storage::metadata_codec::deserialize_entity_record(data, format_version)
1257 .map_err(StoreError::Serialization)
1258 }
1259
1260 pub fn persist(&self) -> Result<(), StoreError> {
1265 let _vault_guard = self.vault_publication_lock.lock();
1266 let pending_vault_versions = self.pending_vault_versions.read();
1267 let pager = match &self.pager {
1268 Some(p) => p,
1269 None => {
1270 if let Some(path) = &self.db_path {
1272 return self
1273 .save_to_file(path)
1274 .map_err(|e| StoreError::Serialization(e.to_string()));
1275 }
1276 return Err(StoreError::Io(std::io::Error::other(
1277 "No pager or path configured for persistence",
1278 )));
1279 }
1280 };
1281
1282 match pager.read_page(1) {
1283 Ok(_) => {}
1284 Err(PagerError::PageNotFound(_)) => {
1285 let meta_page = pager
1286 .allocate_page(crate::storage::engine::PageType::Header)
1287 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1288 pager
1289 .write_page(meta_page.page_id(), meta_page)
1290 .map_err(|e| StoreError::Io(std::io::Error::other(e.to_string())))?;
1291 }
1292 Err(e) => {
1293 return Err(StoreError::Io(std::io::Error::other(e.to_string())));
1294 }
1295 }
1296
1297 if let Some(commit) = &self.commit {
1298 commit.force_sync().map_err(StoreError::Io)?;
1299 }
1300
1301 let collections = self.collections.read();
1302 let mut btree_indices = self.btree_indices.write();
1303
1304 let mut collection_roots: Vec<(String, u32)> = Vec::new();
1306
1307 let mut rebuilt_indices = HashMap::with_capacity(collections.len());
1310 let previous_versions = self.checkpoint_versions.read();
1311 let mut checkpoint_versions = HashMap::with_capacity(collections.len());
1312 let pending_vault = Arc::new(pending_vault_versions.clone());
1313 let (visibility_revision, excluded, untracked_visibility_revision) =
1314 self.snapshot_manager.get().map_or_else(
1315 || (0, Default::default(), 0),
1316 |manager| manager.checkpoint_projection_state(),
1317 );
1318 let excluded = Arc::new(excluded);
1319 for (name, manager) in collections.iter() {
1320 let mut version = CheckpointVersion {
1321 manager: Arc::downgrade(manager),
1322 segments: manager.checkpoint_revisions(),
1323 visibility_revision,
1324 untracked_visibility_revision,
1325 excluded_xids: Arc::clone(&excluded),
1326 pending_vault: Arc::clone(&pending_vault),
1327 format_version: self.format_version(),
1328 deferred_rows: Some(Arc::default()),
1329 has_unprojected_rows: false,
1330 };
1331 if let (Some(previous), Some(tree)) =
1332 (previous_versions.get(name), btree_indices.get(name))
1333 {
1334 if previous.manager.ptr_eq(&version.manager)
1335 && previous.segments == version.segments
1336 && previous.format_version == version.format_version
1337 && !previous.has_unprojected_rows
1338 && checkpoint_visibility_matches(previous, &version)
1339 {
1340 version.deferred_rows = previous.deferred_rows.clone();
1341 collection_roots.push((name.clone(), tree.root_page_id()));
1342 rebuilt_indices.insert(name.clone(), Arc::clone(tree));
1343 checkpoint_versions.insert(name.clone(), version);
1344 continue;
1345 }
1346 }
1347 if let (Some(previous), Some(tree)) =
1348 (previous_versions.get(name), btree_indices.get(name))
1349 {
1350 if previous.manager.ptr_eq(&version.manager)
1351 && previous.format_version == version.format_version
1352 {
1353 if let Some(tree) =
1354 self.sparse_checkpoint_tree(name, manager, tree, previous, &mut version)?
1355 {
1356 collection_roots.push((name.clone(), tree.root_page_id()));
1357 rebuilt_indices.insert(name.clone(), tree);
1358 checkpoint_versions.insert(name.clone(), version);
1359 continue;
1360 }
1361 }
1362 }
1363 let btree = self.checkpoint_tree(
1364 pager,
1365 manager,
1366 btree_indices.get(name),
1367 |entity, metadata| {
1368 #[cfg(test)]
1369 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(count.get() + 1));
1370 let hidden = pending_vault_versions.contains(&entity.id)
1371 || (matches!(entity.data, EntityData::Row(_) | EntityData::Vector(_))
1372 && excluded.contains(&entity.xmin));
1373 let normalize_xmax = matches!(entity.data, EntityData::Row(_))
1374 && excluded.contains(&entity.xmax);
1375 if hidden || normalize_xmax {
1376 version.remember_deferred_row(entity.id);
1377 }
1378 if hidden {
1379 return None;
1380 }
1381 let mut normalized = std::borrow::Cow::Borrowed(entity);
1382 if normalize_xmax {
1383 normalized.to_mut().xmax = 0;
1384 }
1385 Some(Self::serialize_entity_record(
1386 &normalized,
1387 metadata.as_ref(),
1388 self.format_version(),
1389 ))
1390 },
1391 )?;
1392 checkpoint_versions.insert(name.clone(), version);
1393
1394 collection_roots.push((name.clone(), btree.root_page_id()));
1395 rebuilt_indices.insert(name.clone(), btree);
1396 }
1397
1398 let mut meta_data = Vec::with_capacity(4096);
1400
1401 let format_version = STORE_VERSION_V11;
1402 self.set_format_version(format_version);
1403
1404 reddb_file::encode_native_paged_metadata_header(
1405 &mut meta_data,
1406 reddb_file::NativePagedMetadataHeader {
1407 format_version,
1408 collection_count: collection_roots.len() as u32,
1409 },
1410 );
1411
1412 for (name, root_page) in &collection_roots {
1414 reddb_file::encode_native_paged_collection_root(&mut meta_data, name, *root_page);
1415 }
1416
1417 let cross_refs = self.cross_refs.read();
1419 let total_refs: usize = cross_refs.values().map(|v| v.len()).sum();
1420 meta_data.extend_from_slice(&(total_refs as u32).to_le_bytes());
1421 for (source_id, refs) in cross_refs.iter() {
1422 for (target_id, ref_type, collection) in refs {
1423 reddb_file::encode_native_paged_cross_ref(
1424 &mut meta_data,
1425 source_id.raw(),
1426 target_id.raw(),
1427 ref_type.to_byte(),
1428 collection,
1429 );
1430 }
1431 }
1432
1433 for (name, previous) in btree_indices.iter() {
1434 if !rebuilt_indices
1435 .get(name)
1436 .is_some_and(|tree| Arc::ptr_eq(tree, previous))
1437 {
1438 self.retire_btree(Arc::clone(previous));
1439 }
1440 }
1441 self.publish_reclaimable_trees(pager, &mut meta_data, &rebuilt_indices)?;
1442
1443 *btree_indices = rebuilt_indices;
1446 drop(previous_versions);
1447 *self.checkpoint_versions.write() = checkpoint_versions;
1448
1449 if let Some(commit) = &self.commit {
1450 commit.truncate().map_err(StoreError::Io)?;
1451 }
1452 *self.checkpoint_durable_rows.write() = Default::default();
1453
1454 Ok(())
1455 }
1456
1457 pub fn is_paged(&self) -> bool {
1459 self.pager.is_some()
1460 }
1461
1462 pub(crate) fn collection_root_page(&self, collection: &str) -> Option<u32> {
1465 self.btree_indices
1466 .read()
1467 .get(collection)
1468 .map(|btree| btree.root_page_id())
1469 .filter(|root| *root != 0)
1470 }
1471
1472 pub fn db_path(&self) -> Option<&Path> {
1474 self.db_path.as_deref()
1475 }
1476}
1477
1478#[cfg(test)]
1479mod checkpoint_work_tests {
1480 use super::*;
1481
1482 #[test]
1483 fn retired_tree_pages_remain_pinned_by_a_cursor() {
1484 let directory = tempfile::tempdir().expect("temporary store");
1485 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1486 for id in 1..=100 {
1487 store.insert_auto("rows", row(id, 1)).expect("seed");
1488 }
1489 store.persist().expect("initial checkpoint");
1490 let tree = store
1491 .btree_indices
1492 .read()
1493 .get("rows")
1494 .cloned()
1495 .expect("tree");
1496 let mut cursor = tree.cursor_first().expect("cursor");
1497 drop(tree);
1498 let manager = store.get_collection("rows").expect("rows");
1499 for value in 2..=6 {
1500 manager.update(row(1, value)).expect("change");
1501 store.persist().expect("checkpoint with old reader");
1502 }
1503 let mut count = 0;
1504 while let Some((_, bytes)) = cursor.next().expect("pinned tree must stay readable") {
1505 let (entity, _) =
1506 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1507 .expect("decode");
1508 assert_eq!(
1509 entity.data.as_row().expect("row").columns,
1510 [Value::Integer(1)]
1511 );
1512 count += 1;
1513 }
1514 assert_eq!(count, 100);
1515 drop(cursor);
1516 store.persist().expect("reclaim after reader release");
1517 assert!(store.retired_btrees.lock().is_empty());
1518 }
1519
1520 #[test]
1521 fn checkpoint_payload_buffer_is_bounded_for_a_large_collection() {
1522 let directory = tempfile::tempdir().expect("temporary store");
1523 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1524 store.create_collection("rows").expect("collection");
1525 let manager = store.get_collection("rows").expect("rows");
1526 for id in (1..=1200).rev() {
1527 manager
1528 .insert(UnifiedEntity::table_row(
1529 EntityId::new(id),
1530 "rows",
1531 id,
1532 vec![Value::text("x".repeat(4096))],
1533 ))
1534 .expect("seed");
1535 }
1536 CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.set(0));
1537 store.persist().expect("bounded checkpoint");
1538 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) <= CHECKPOINT_BATCH_BYTES);
1539 assert!(CHECKPOINT_BUFFERED_BYTES.with(|peak| peak.get()) > 0);
1540 assert_eq!(store.get_collection("rows").expect("rows").count(), 1200);
1541 }
1542
1543 #[test]
1544 fn unchanged_checkpoint_does_not_serialize_collection_rows() {
1545 let directory = tempfile::tempdir().expect("temporary store");
1546 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1547 store.create_collection("rows").expect("collection");
1548 for id in 1..=40 {
1549 store
1550 .insert(
1551 "rows",
1552 UnifiedEntity::table_row(
1553 EntityId::new(id),
1554 "rows",
1555 id,
1556 vec![Value::Integer(id as i64)],
1557 ),
1558 )
1559 .expect("seed row");
1560 }
1561 store.persist().expect("initial checkpoint");
1562 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1563 store.persist().expect("unchanged checkpoint");
1564 assert_eq!(
1565 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()),
1566 0,
1567 "unchanged collections must not hold the writer fence while serializing every row"
1568 );
1569 drop(store);
1570 let reopened = UnifiedStore::open(&directory.path().join("data.rdb")).expect("reopen");
1571 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1572 reopened.persist().expect("unchanged recovered generation");
1573 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1574 }
1575
1576 #[test]
1577 fn paged_reopen_preserves_stored_sequence_identity() {
1578 let directory = tempfile::tempdir().expect("temporary store");
1579 let path = directory.path().join("data.rdb");
1580 let store = UnifiedStore::open(&path).expect("paged store");
1581 store.create_collection("rows").expect("collection");
1582 for id in [30, 10, 20] {
1583 store.insert("rows", row(id, id as i64)).expect("insert");
1584 }
1585 store.persist().expect("checkpoint");
1586 let sequences = [10, 20, 30].map(|id| {
1587 store
1588 .get("rows", EntityId::new(id))
1589 .expect("row")
1590 .sequence_id
1591 });
1592 drop(store);
1593 let reopened = UnifiedStore::open(&path).expect("reopen");
1594 let recovered = [10, 20, 30].map(|id| {
1595 reopened
1596 .get("rows", EntityId::new(id))
1597 .expect("row")
1598 .sequence_id
1599 });
1600 assert_eq!(
1601 recovered, sequences,
1602 "opening a checkpoint must not rewrite stored row identities"
1603 );
1604 }
1605
1606 #[test]
1607 fn auto_insert_persists_the_assigned_sequence_identity() {
1608 let directory = tempfile::tempdir().expect("temporary store");
1609 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1610 for id in 1u64..=3 {
1611 store
1612 .insert_auto("rows", row(id, id as i64))
1613 .expect("insert");
1614 }
1615 let manager = store.get_collection("rows").expect("rows");
1616 let tree = store
1617 .btree_indices
1618 .read()
1619 .get("rows")
1620 .cloned()
1621 .expect("tree");
1622 for id in 1u64..=3 {
1623 let bytes = tree
1624 .get(&id.to_be_bytes())
1625 .expect("lookup")
1626 .expect("stored row");
1627 let (persisted, _) =
1628 UnifiedStore::deserialize_entity_record(&bytes, store.format_version())
1629 .expect("decode");
1630 assert_eq!(
1631 persisted.sequence_id,
1632 manager
1633 .get(EntityId::new(id))
1634 .expect("live row")
1635 .sequence_id
1636 );
1637 }
1638 }
1639
1640 #[test]
1641 fn completed_inline_insert_reuses_history_but_not_untracked_edits() {
1642 let directory = tempfile::tempdir().expect("temporary store");
1643 let path = directory.path().join("data.rdb");
1644 let store = UnifiedStore::open(&path).expect("paged store");
1645 store.insert_auto("rows", row(1, 1)).expect("seed");
1646 store.persist().expect("checkpoint");
1647 store.insert_auto("rows", row(2, 2)).expect("append");
1648 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1649 store.persist().expect("inline append checkpoint");
1650 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1651 store
1652 .get_collection("rows")
1653 .expect("rows")
1654 .update(row(1, 99))
1655 .expect("untracked edit");
1656 store.insert_auto("rows", row(3, 3)).expect("next append");
1657 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1658 store.persist().expect("untracked edit checkpoint");
1659 assert!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()) > 0);
1660 drop(store);
1661 let reopened = UnifiedStore::open(&path).expect("reopen");
1662 assert_eq!(
1663 reopened
1664 .get("rows", EntityId::new(1))
1665 .expect("row")
1666 .data
1667 .as_row()
1668 .expect("table")
1669 .columns,
1670 vec![Value::Integer(99)]
1671 );
1672 }
1673
1674 #[test]
1675 fn replayed_trees_are_reusable_after_durable_reconciliation() {
1676 let directory = tempfile::tempdir().expect("temporary store");
1677 let path = directory.path().join("data.rdb");
1678 let store = UnifiedStore::open(&path).expect("paged store");
1679 store.insert_auto("rows", row(1, 1)).expect("seed");
1680 store.persist().expect("checkpoint");
1681 store
1682 .insert_auto("rows", row(2, 2))
1683 .expect("WAL-backed append");
1684 drop(store);
1685 let reopened = UnifiedStore::open(&path).expect("replay");
1686 assert!(reopened.get("rows", EntityId::new(2)).is_some());
1687 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1688 reopened.persist().expect("checkpoint reconciled tree");
1689 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1690 }
1691
1692 fn row(id: u64, value: i64) -> UnifiedEntity {
1693 UnifiedEntity::table_row(EntityId::new(id), "rows", id, vec![Value::Integer(value)])
1694 }
1695
1696 #[test]
1697 fn checkpoint_reuse_tracks_manager_updates_metadata_deletes_and_bulk_inserts() {
1698 let directory = tempfile::tempdir().expect("temporary store");
1699 let path = directory.path().join("data.rdb");
1700 let store = UnifiedStore::open(&path).expect("paged store");
1701 store.create_collection("rows").expect("collection");
1702 store.insert("rows", row(1, 1)).expect("seed");
1703 store.persist().expect("initial checkpoint");
1704 let manager = store.get_collection("rows").expect("rows");
1705 manager.update(row(1, 2)).expect("direct update");
1706 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1707 store.persist().expect("updated row checkpoint");
1708 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1709 let mut metadata = Metadata::new();
1710 metadata.set("verified", MetadataValue::Bool(true));
1711 manager
1712 .set_metadata(EntityId::new(1), metadata)
1713 .expect("metadata");
1714 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1715 store.persist().expect("metadata-only checkpoint");
1716 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 1);
1717 manager
1718 .bulk_insert(vec![row(2, 2), row(3, 3)])
1719 .expect("bulk insert");
1720 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1721 store.persist().expect("bulk insert checkpoint");
1722 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 3);
1723 manager.delete(EntityId::new(2)).expect("delete");
1724 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1725 store.persist().expect("changed checkpoint");
1726 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 2);
1727 drop(store);
1728 let recovered = UnifiedStore::open(&path).expect("recover changed rows");
1729 let manager = recovered.get_collection("rows").expect("rows");
1730 assert_eq!(manager.count(), 2);
1731 assert_eq!(
1732 manager
1733 .get(EntityId::new(1))
1734 .expect("row")
1735 .data
1736 .as_row()
1737 .expect("table")
1738 .columns,
1739 vec![Value::Integer(2)]
1740 );
1741 assert!(manager.get(EntityId::new(2)).is_none());
1742 assert!(manager
1743 .get_metadata(EntityId::new(1))
1744 .expect("metadata")
1745 .get("verified")
1746 .is_some());
1747 }
1748
1749 #[test]
1750 fn unrelated_visibility_changes_do_not_serialize_collection_rows() {
1751 let directory = tempfile::tempdir().expect("temporary store");
1752 let store = UnifiedStore::open(&directory.path().join("data.rdb")).expect("paged store");
1753 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1754 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1755 store.create_collection("rows").expect("collection");
1756 for id in 1..=40 {
1757 store.insert("rows", row(id, id as i64)).expect("seed");
1758 }
1759 store.persist().expect("initial checkpoint");
1760 let unrelated = snapshots.begin();
1761 snapshots.rollback(unrelated);
1762 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1763 store.persist().expect("unrelated rollback checkpoint");
1764 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1765
1766 store
1767 .pending_vault_versions
1768 .write()
1769 .insert(EntityId::new(100));
1770 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1771 store.persist().expect("unrelated vault checkpoint");
1772 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1773 store.pending_vault_versions.write().clear();
1774 CHECKPOINT_SERIALIZED_ROWS.with(|count| count.set(0));
1775 store
1776 .persist()
1777 .expect("unrelated vault publication checkpoint");
1778 assert_eq!(CHECKPOINT_SERIALIZED_ROWS.with(|count| count.get()), 0);
1779 }
1780
1781 #[test]
1782 fn low_level_rollback_of_a_non_active_xid_invalidates_sparse_visibility() {
1783 let directory = tempfile::tempdir().expect("temporary store");
1784 let path = directory.path().join("data.rdb");
1785 let store = UnifiedStore::open(&path).expect("paged store");
1786 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1787 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1788 store.create_collection("rows").expect("collection");
1789 let xid = snapshots.begin();
1790 let mut entity = row(1, 1);
1791 entity.xmin = xid;
1792 store.insert("rows", entity).expect("committed row");
1793 snapshots.commit(xid);
1794 store.persist().expect("committed baseline");
1795 snapshots.rollback(xid);
1796 store
1797 .persist()
1798 .expect("untracked outcome requires validation");
1799 drop(store);
1800 let recovered = UnifiedStore::open(&path).expect("recover");
1801 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1802 }
1803
1804 #[test]
1805 fn exhausted_deferred_row_cache_keeps_full_validation() {
1806 let directory = tempfile::tempdir().expect("temporary store");
1807 let path = directory.path().join("data.rdb");
1808 let store = UnifiedStore::open(&path).expect("paged store");
1809 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1810 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1811 store.create_collection("rows").expect("collection");
1812 let xid = snapshots.begin();
1813 for id in 1..=1025 {
1814 let mut entity = row(id, id as i64);
1815 entity.xmin = xid;
1816 store.insert("rows", entity).expect("pending row");
1817 }
1818 store.persist().expect("exclude pending rows");
1819 assert!(store.checkpoint_versions.read()["rows"]
1820 .deferred_rows
1821 .is_none());
1822 snapshots.commit(xid);
1823 store
1824 .persist()
1825 .expect("publish committed rows conservatively");
1826 drop(store);
1827 let recovered = UnifiedStore::open(&path).expect("recover");
1828 assert_eq!(
1829 recovered.get_collection("rows").expect("rows").count(),
1830 1025
1831 );
1832 }
1833
1834 #[test]
1835 fn rollback_invalidates_reuse_without_a_segment_mutation() {
1836 let directory = tempfile::tempdir().expect("temporary store");
1837 let path = directory.path().join("data.rdb");
1838 let store = UnifiedStore::open(&path).expect("paged store");
1839 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1840 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1841 store.create_collection("rows").expect("collection");
1842 let xid = snapshots.begin();
1843 let mut pending = row(1, 1);
1844 pending.xmin = xid;
1845 store.insert("rows", pending).expect("version");
1846 store.persist().expect("before rollback");
1847 snapshots.rollback(xid);
1848 store.persist().expect("after rollback");
1849 drop(store);
1850 let recovered = UnifiedStore::open(&path).expect("recover");
1851 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 0);
1852 }
1853
1854 #[test]
1855 fn rolled_back_delete_is_normalized_without_a_segment_mutation() {
1856 let directory = tempfile::tempdir().expect("temporary store");
1857 let path = directory.path().join("data.rdb");
1858 let store = UnifiedStore::open(&path).expect("paged store");
1859 let snapshots = Arc::new(crate::storage::transaction::snapshot::SnapshotManager::new());
1860 assert!(store.snapshot_manager.set(Arc::clone(&snapshots)).is_ok());
1861 store.create_collection("rows").expect("collection");
1862 let xid = snapshots.begin();
1863 let mut deleted = row(1, 1);
1864 deleted.xmax = xid;
1865 store.insert("rows", deleted).expect("version");
1866 store.persist().expect("before rollback");
1867 snapshots.rollback(xid);
1868 store.persist().expect("after rollback");
1869 drop(store);
1870 let recovered = UnifiedStore::open(&path).expect("recover");
1871 let manager = recovered.get_collection("rows").expect("rows");
1872 assert_eq!(manager.get(EntityId::new(1)).expect("retained row").xmax, 0);
1873 }
1874
1875 #[test]
1876 fn publishing_a_pending_vault_version_invalidates_reuse() {
1877 let directory = tempfile::tempdir().expect("temporary store");
1878 let path = directory.path().join("data.rdb");
1879 let store = UnifiedStore::open(&path).expect("paged store");
1880 store.create_collection("rows").expect("collection");
1881 store.insert("rows", row(1, 1)).expect("row");
1882 store
1883 .pending_vault_versions
1884 .write()
1885 .insert(EntityId::new(1));
1886 store.persist().expect("exclude pending vault version");
1887 store
1888 .pending_vault_versions
1889 .write()
1890 .remove(&EntityId::new(1));
1891 store.persist().expect("include published vault version");
1892 drop(store);
1893 let recovered = UnifiedStore::open(&path).expect("recover");
1894 assert_eq!(recovered.get_collection("rows").expect("rows").count(), 1);
1895 }
1896}