1use super::freelist::FreeList;
36use super::page::{Page, PageError, PageType, PAGE_SIZE};
37use super::page_cache::PageCache;
38use crate::storage::wal::writer::WalWriter;
39use fs2::FileExt;
40use std::fs::{File, OpenOptions};
41use std::io::{Read, Seek, SeekFrom, Write};
42use std::path::{Path, PathBuf};
43#[cfg(test)]
44use std::sync::atomic::{AtomicU8, Ordering};
45use std::sync::{Arc, Mutex, RwLock};
46
47pub use reddb_file::{DatabaseHeader, PhysicalFileHeader};
48
49const DEFAULT_CACHE_SIZE: usize = 10_000;
51
52#[cfg(test)]
53static COW_ATOMIC_WRITE_TEST_OVERRIDE: AtomicU8 = AtomicU8::new(0);
54
55#[derive(Debug)]
57pub enum PagerError {
58 Io(std::io::Error),
60 Page(PageError),
62 InvalidDatabase(String),
64 ReadOnly,
66 PageNotFound(u32),
68 Locked,
70 LockPoisoned,
72 EncryptionRequired,
74 PlainDatabaseRefusesKey,
76 InvalidKey,
78 SuperblockZoneUnrecoverable(PathBuf),
80 ManifestZoneCorrupt(PathBuf),
82 LegacySidecarStore { sidecar: PathBuf },
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct ExtentId {
89 pub start_page: u32,
90 pub n_pages: u32,
91}
92
93impl std::fmt::Display for PagerError {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Self::Io(e) => write!(f, "I/O error: {}", e),
97 Self::Page(e) => write!(f, "Page error: {}", e),
98 Self::InvalidDatabase(msg) => write!(f, "Invalid database: {}", msg),
99 Self::ReadOnly => write!(f, "Database is read-only"),
100 Self::PageNotFound(id) => write!(f, "Page {} not found", id),
101 Self::Locked => write!(f, "Database is locked"),
102 Self::LockPoisoned => write!(f, "Internal lock poisoned (concurrent thread panicked)"),
103 Self::EncryptionRequired => write!(
104 f,
105 "Database is encrypted but no key was supplied (set PagerConfig::encryption)"
106 ),
107 Self::PlainDatabaseRefusesKey => write!(
108 f,
109 "Plain (unencrypted) database opened with an encryption key — refusing"
110 ),
111 Self::InvalidKey => write!(f, "Encryption key validation failed for this database"),
112 Self::SuperblockZoneUnrecoverable(path) => write!(
113 f,
114 "superblock zone of {} is unrecoverable: both ping-pong copies failed \
115 validation, so no generation can be trusted to root the store. The store \
116 will not be opened. Recover what survives with red salvage \
117 (ADR 0074 §4); it reads the damaged file without writing to it and \
118 reports what it could not verify.",
119 path.display()
120 ),
121 Self::ManifestZoneCorrupt(path) => write!(
122 f,
123 "internal manifest zone of {} failed its checksum: the zone that names \
124 collections, indexes and the checkpoint boundary cannot be trusted, so no \
125 rows are returned rather than garbage ones. Run scrub to classify the fault \
126 and red salvage to extract what survives (ADR 0074 §2/§4).",
127 path.display()
128 ),
129 Self::LegacySidecarStore { sidecar } => write!(
130 f,
131 "refusing to open a legacy sidecar-backed store: found {}. The superblock \
132 pair and the internal manifest now live inside the .rdb file (ADR 0038 §2), \
133 and the engine never reads the retired sidecars silently. Convert the store \
134 first with the offline migration tool \
135 (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is reversible.",
136 sidecar.display()
137 ),
138 }
139 }
140}
141
142impl std::error::Error for PagerError {
143 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
144 match self {
145 Self::Io(e) => Some(e),
146 Self::Page(e) => Some(e),
147 _ => None,
148 }
149 }
150}
151
152impl From<std::io::Error> for PagerError {
153 fn from(e: std::io::Error) -> Self {
154 Self::Io(e)
155 }
156}
157
158impl From<PageError> for PagerError {
159 fn from(e: PageError) -> Self {
160 Self::Page(e)
161 }
162}
163
164#[derive(Debug, Clone)]
166pub struct PagerConfig {
167 pub cache_size: usize,
169 pub read_only: bool,
171 pub create: bool,
173 pub verify_checksums: bool,
175 pub double_write: bool,
177 pub encryption: Option<crate::storage::encryption::SecureKey>,
183}
184
185impl Default for PagerConfig {
186 fn default() -> Self {
187 Self {
188 cache_size: DEFAULT_CACHE_SIZE,
189 read_only: false,
190 create: true,
191 verify_checksums: true,
192 double_write: true,
193 encryption: None,
194 }
195 }
196}
197
198pub struct Pager {
202 path: PathBuf,
204 file: Mutex<File>,
206 _lock_file: Option<File>,
208 dwb_file: Option<Mutex<File>>,
210 cache: PageCache,
212 freelist: RwLock<FreeList>,
214 header: RwLock<DatabaseHeader>,
216 config: PagerConfig,
218 header_dirty: Mutex<bool>,
220 wal: RwLock<Option<Arc<Mutex<WalWriter>>>>,
232 pub(crate) encryption: Option<(
239 crate::storage::encryption::PageEncryptor,
240 crate::storage::encryption::EncryptionHeader,
241 )>,
242}
243
244#[path = "pager/impl.rs"]
245mod pager_impl;
246impl Drop for Pager {
247 fn drop(&mut self) {
248 let _ = self.flush();
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 #[cfg(target_os = "linux")]
257 use pager_impl::parse_mountinfo_options_for_path;
258 use pager_impl::{
259 classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
260 };
261 use std::fs;
262 use std::io::Write;
263
264 fn temp_db_path() -> PathBuf {
265 use std::sync::atomic::{AtomicU64, Ordering};
266 static COUNTER: AtomicU64 = AtomicU64::new(0);
267 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
268 let mut path = std::env::temp_dir();
269 path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
270 path
271 }
272
273 fn cleanup(path: &Path) {
274 let _ = fs::remove_file(path);
282 for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
283 let _ = fs::remove_file(sidecar);
284 }
285 }
286
287 fn dwb_path_for(path: &Path) -> PathBuf {
288 reddb_file::layout::pager_dwb_shadow_path(path)
289 }
290
291 static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
292
293 struct CowAtomicWriteOverrideGuard {
294 _guard: std::sync::MutexGuard<'static, ()>,
295 }
296
297 impl Drop for CowAtomicWriteOverrideGuard {
298 fn drop(&mut self) {
299 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
300 }
301 }
302
303 fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
304 let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
305 .lock()
306 .unwrap_or_else(|err| err.into_inner());
307 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
308 CowAtomicWriteOverrideGuard { _guard: guard }
309 }
310
311 fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
312 let pages: Vec<_> = pages
313 .iter()
314 .map(|(page_id, page)| {
315 let mut page = page.clone();
316 page.update_checksum();
317 (*page_id, page)
318 })
319 .collect();
320 let buf = reddb_file::encode_paged_dwb_frame(
321 pages
322 .iter()
323 .map(|(page_id, page)| (*page_id, page.as_bytes())),
324 );
325
326 let dwb_path = dwb_path_for(path);
327 let mut file = fs::File::create(&dwb_path).expect("create() should succeed");
328 file.write_all(&buf).expect("write_all() should succeed");
329 file.sync_all().expect("sync_all() should succeed");
330 }
331
332 fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
333 let mut file = OpenOptions::new()
334 .write(true)
335 .open(path)
336 .expect("open() should succeed");
337 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
338 .expect("value is present");
339 file.write_all(page.as_bytes())
340 .expect("write_all() should succeed");
341 file.sync_all().expect("sync_all() should succeed");
342 }
343
344 fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
345 let mut torn = *before.as_bytes();
346 torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
347
348 let mut file = OpenOptions::new()
349 .write(true)
350 .open(path)
351 .expect("open() should succeed");
352 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
353 .expect("value is present");
354 file.write_all(&torn).expect("write_all() should succeed");
355 file.sync_all().expect("sync_all() should succeed");
356 }
357
358 #[test]
359 fn test_pager_create_new() {
360 let path = temp_db_path();
361 cleanup(&path);
362
363 {
364 let pager = Pager::open_default(&path).expect("open_default() should succeed");
365 assert_eq!(pager.page_count().expect("page_count() should succeed"), 3);
366 }
368
369 cleanup(&path);
370 }
371
372 #[test]
373 fn test_pager_reopen() {
374 let path = temp_db_path();
375 cleanup(&path);
376
377 {
379 let pager = Pager::open_default(&path).expect("open_default() should succeed");
380
381 let page = pager
383 .allocate_page(PageType::BTreeLeaf)
384 .expect("allocate_page() should succeed");
385 assert_eq!(page.page_id(), 3);
386
387 pager.sync().expect("sync() should succeed");
388 }
389
390 {
392 let pager = Pager::open_default(&path).expect("open_default() should succeed");
393 assert_eq!(pager.page_count().expect("page_count() should succeed"), 4);
394 }
396
397 cleanup(&path);
398 }
399
400 #[test]
401 fn test_pager_read_write() {
402 let path = temp_db_path();
403 cleanup(&path);
404
405 {
406 let pager = Pager::open_default(&path).expect("open_default() should succeed");
407
408 let mut page = pager
410 .allocate_page(PageType::BTreeLeaf)
411 .expect("allocate_page() should succeed");
412 let page_id = page.page_id();
413
414 page.insert_cell(b"key", b"value")
415 .expect("insert_cell() should succeed");
416 pager
417 .write_page(page_id, page)
418 .expect("write_page() should succeed");
419
420 let read_page = pager
422 .read_page(page_id)
423 .expect("read_page() should succeed");
424 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
425 assert_eq!(key, b"key");
426 assert_eq!(value, b"value");
427 }
428
429 cleanup(&path);
430 }
431
432 #[test]
433 fn test_pager_cache() {
434 let path = temp_db_path();
435 cleanup(&path);
436
437 {
438 let pager = Pager::open_default(&path).expect("open_default() should succeed");
439
440 let page = pager
442 .allocate_page(PageType::BTreeLeaf)
443 .expect("allocate_page() should succeed");
444 let page_id = page.page_id();
445
446 let _ = pager
448 .read_page(page_id)
449 .expect("read_page() should succeed");
450
451 let _ = pager
453 .read_page(page_id)
454 .expect("read_page() should succeed");
455
456 let stats = pager.cache_stats();
457 assert!(stats.hits >= 1);
458 }
459
460 cleanup(&path);
461 }
462
463 #[test]
464 fn test_pager_free_page() {
465 let path = temp_db_path();
466 cleanup(&path);
467
468 {
469 let pager = Pager::open_default(&path).expect("open_default() should succeed");
470
471 let page1 = pager
473 .allocate_page(PageType::BTreeLeaf)
474 .expect("allocate_page() should succeed");
475 let page2 = pager
476 .allocate_page(PageType::BTreeLeaf)
477 .expect("allocate_page() should succeed");
478
479 let id1 = page1.page_id();
480 let id2 = page2.page_id();
481
482 pager.free_page(id1).expect("free_page() should succeed");
484
485 let page3 = pager
487 .allocate_page(PageType::BTreeLeaf)
488 .expect("allocate_page() should succeed");
489 assert_eq!(page3.page_id(), id1);
490 }
491
492 cleanup(&path);
493 }
494
495 #[test]
496 fn test_freelist_persistence() {
497 let path = temp_db_path();
498 cleanup(&path);
499
500 let freed_id;
501 {
502 let pager = Pager::open_default(&path).expect("open_default() should succeed");
503 let page1 = pager
504 .allocate_page(PageType::BTreeLeaf)
505 .expect("allocate_page() should succeed");
506 let _page2 = pager
507 .allocate_page(PageType::BTreeLeaf)
508 .expect("allocate_page() should succeed");
509 freed_id = page1.page_id();
510
511 pager
512 .free_page(freed_id)
513 .expect("free_page() should succeed");
514 pager.sync().expect("sync() should succeed");
515 }
516
517 {
518 let pager = Pager::open_default(&path).expect("open_default() should succeed");
519 let page = pager
520 .allocate_page(PageType::BTreeLeaf)
521 .expect("allocate_page() should succeed");
522 assert_eq!(page.page_id(), freed_id);
523 }
524
525 cleanup(&path);
526 }
527
528 #[test]
529 fn test_pager_read_only() {
530 let path = temp_db_path();
531 cleanup(&path);
532
533 {
535 let pager = Pager::open_default(&path).expect("open_default() should succeed");
536 pager.sync().expect("sync() should succeed");
537 }
538
539 {
541 let config = PagerConfig {
542 read_only: true,
543 ..Default::default()
544 };
545
546 let pager = Pager::open(&path, config).expect("open() should succeed");
547 assert!(pager.is_read_only());
548
549 assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
551 }
552
553 cleanup(&path);
554 }
555
556 #[test]
557 fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
558 let path = temp_db_path();
559 cleanup(&path);
560
561 let config = PagerConfig {
562 double_write: true,
563 ..Default::default()
564 };
565
566 let page_id;
567 {
568 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
569 let page = pager
570 .allocate_page(PageType::BTreeLeaf)
571 .expect("allocate_page() should succeed");
572 page_id = page.page_id();
573 pager.sync().expect("sync() should succeed");
574 }
575
576 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
577 recovered_page
578 .insert_cell(b"key", b"value")
579 .expect("insert_cell() should succeed");
580 write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
581
582 let dwb_path = dwb_path_for(&path);
583 assert!(dwb_path.exists());
584 assert!(
585 fs::metadata(&dwb_path)
586 .expect("metadata() should succeed")
587 .len()
588 > 0
589 );
590
591 {
592 let pager = Pager::open(&path, config).expect("open() should succeed");
593
594 let read_page = pager
595 .read_page(page_id)
596 .expect("read_page() should succeed");
597 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
598 assert_eq!(key, b"key");
599 assert_eq!(value, b"value");
600
601 assert!(dwb_path.exists());
602 assert_eq!(
603 fs::metadata(&dwb_path)
604 .expect("metadata() should succeed")
605 .len(),
606 0
607 );
608
609 let mut updated_page = recovered_page.clone();
610 updated_page
611 .insert_cell(b"key2", b"value2")
612 .expect("insert_cell() should succeed");
613 pager
614 .write_page(page_id, updated_page)
615 .expect("write_page() should succeed");
616 pager.flush().expect("flush() should succeed");
617
618 assert!(dwb_path.exists());
619 assert_eq!(
620 fs::metadata(&dwb_path)
621 .expect("metadata() should succeed")
622 .len(),
623 0
624 );
625 }
626
627 cleanup(&path);
628 }
629
630 #[test]
631 fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
632 assert_eq!(
633 classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
634 Some(CowFilesystemKind::Zfs),
635 "ZFS is always CoW"
636 );
637 assert_eq!(
638 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
639 Some(CowFilesystemKind::BtrfsDataCow),
640 "btrfs qualifies only when datacow remains enabled"
641 );
642 assert_eq!(
643 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
644 None,
645 "btrfs nodatacow mount option must reject DWB skip"
646 );
647 assert_eq!(
648 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
649 None,
650 "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
651 );
652 assert_eq!(
653 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
654 None,
655 "missing btrfs inode flags are uncertain and must fail closed"
656 );
657 assert_eq!(
658 classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
659 None,
660 "missing btrfs mount options are uncertain and must fail closed"
661 );
662 }
663
664 #[cfg(target_os = "linux")]
665 #[test]
666 fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
667 let mountinfo = "\
66824 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
66935 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
67036 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
671";
672
673 assert_eq!(
674 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
675 .as_deref(),
676 Some("rw,relatime,rw,space_cache=v2")
677 );
678 assert_eq!(
679 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
680 .as_deref(),
681 Some("rw,relatime,rw,nodatacow")
682 );
683 }
684
685 #[test]
686 fn double_write_false_keeps_dwb_when_cow_probe_denies() {
687 let _override = cow_atomic_write_override(false);
688 let path = temp_db_path();
689 cleanup(&path);
690
691 {
692 let config = PagerConfig {
693 double_write: false,
694 ..Default::default()
695 };
696 let pager = Pager::open(&path, config).expect("open() should succeed");
697 let page = pager
698 .allocate_page(PageType::BTreeLeaf)
699 .expect("allocate_page() should succeed");
700 pager
701 .write_page(page.page_id(), page)
702 .expect("write_page() should succeed");
703 pager.flush().expect("flush() should succeed");
704 }
705
706 assert!(
707 dwb_path_for(&path).exists(),
708 "DWB must stay enabled when double_write=false is not proven safe"
709 );
710
711 cleanup(&path);
712 }
713
714 #[test]
715 fn double_write_false_skips_dwb_when_cow_probe_allows() {
716 let _override = cow_atomic_write_override(true);
717 let path = temp_db_path();
718 cleanup(&path);
719
720 {
721 let config = PagerConfig {
722 double_write: false,
723 ..Default::default()
724 };
725 let pager = Pager::open(&path, config).expect("open() should succeed");
726 let page = pager
727 .allocate_page(PageType::BTreeLeaf)
728 .expect("allocate_page() should succeed");
729 pager
730 .write_page(page.page_id(), page)
731 .expect("write_page() should succeed");
732 pager.flush().expect("flush() should succeed");
733 }
734
735 assert!(
736 !dwb_path_for(&path).exists(),
737 "DWB may be skipped only after the CoW probe allows it"
738 );
739
740 cleanup(&path);
741 }
742
743 #[test]
744 fn double_write_false_on_cow_replays_then_removes_existing_dwb() {
745 let _override = cow_atomic_write_override(true);
746 let path = temp_db_path();
747 cleanup(&path);
748
749 let page_id;
750 {
751 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
752 let page = pager
753 .allocate_page(PageType::BTreeLeaf)
754 .expect("allocate_page() should succeed");
755 page_id = page.page_id();
756 pager.sync().expect("sync() should succeed");
757 }
758
759 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
760 recovered_page
761 .insert_cell(b"key", b"value")
762 .expect("insert_cell() should succeed");
763 write_dwb_fixture(&path, &[(page_id, recovered_page)]);
764
765 {
766 let config = PagerConfig {
767 double_write: false,
768 ..Default::default()
769 };
770 let pager = Pager::open(&path, config).expect("open() should succeed");
771 let read_page = pager
772 .read_page(page_id)
773 .expect("read_page() should succeed");
774 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
775 assert_eq!(key, b"key");
776 assert_eq!(value, b"value");
777 }
778
779 assert!(
780 !dwb_path_for(&path).exists(),
781 "CoW DWB-skip must replay any existing DWB before removing the sidecar"
782 );
783
784 cleanup(&path);
785 }
786
787 #[test]
788 fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
789 let _override = cow_atomic_write_override(true);
790 let path = temp_db_path();
791 cleanup(&path);
792
793 let config = PagerConfig {
794 double_write: false,
795 ..Default::default()
796 };
797
798 let page_id;
799 let before;
800 let after;
801 {
802 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
803 let mut page = pager
804 .allocate_page(PageType::BTreeLeaf)
805 .expect("allocate_page() should succeed");
806 page_id = page.page_id();
807 page.insert_cell(b"phase", b"before")
808 .expect("insert_cell() should succeed");
809 pager
810 .write_page(page_id, page)
811 .expect("write_page() should succeed");
812 pager.sync().expect("sync() should succeed");
813 before = pager
814 .read_page(page_id)
815 .expect("read_page() should succeed");
816
817 let mut page = before.clone();
818 page.insert_cell(b"phase2", b"after")
819 .expect("insert_cell() should succeed");
820 pager
821 .write_page(page_id, page)
822 .expect("write_page() should succeed");
823 pager.flush().expect("flush() should succeed");
824 after = pager
825 .read_page(page_id)
826 .expect("read_page() should succeed");
827 }
828
829 for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
832 write_page_bytes(&path, page_id, whole_page);
833
834 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
835 let recovered = pager
836 .read_page(page_id)
837 .expect("read_page() should succeed");
838 assert_eq!(recovered.cell_count(), expected_cells);
839 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
840 assert_eq!(key, b"phase");
841 assert_eq!(value, b"before");
842 if expected_cells == 2 {
843 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
844 assert_eq!(key, b"phase2");
845 assert_eq!(value, b"after");
846 }
847 drop(pager);
848 }
849
850 cleanup(&path);
851 }
852
853 #[test]
854 fn same_mid_write_without_cow_recovers_from_dwb() {
855 let _override = cow_atomic_write_override(false);
856 let path = temp_db_path();
857 cleanup(&path);
858
859 let config = PagerConfig {
860 double_write: false,
861 ..Default::default()
862 };
863
864 let page_id;
865 let before;
866 let after;
867 {
868 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
869 let mut page = pager
870 .allocate_page(PageType::BTreeLeaf)
871 .expect("allocate_page() should succeed");
872 page_id = page.page_id();
873 page.insert_cell(b"phase", b"before")
874 .expect("insert_cell() should succeed");
875 pager
876 .write_page(page_id, page)
877 .expect("write_page() should succeed");
878 pager.sync().expect("sync() should succeed");
879 before = pager
880 .read_page(page_id)
881 .expect("read_page() should succeed");
882
883 let mut page = before.clone();
884 page.insert_cell(b"phase2", b"after")
885 .expect("insert_cell() should succeed");
886 pager
887 .write_page(page_id, page)
888 .expect("write_page() should succeed");
889 pager.flush().expect("flush() should succeed");
890 after = pager
891 .read_page(page_id)
892 .expect("read_page() should succeed");
893 }
894
895 write_dwb_fixture(&path, &[(page_id, after.clone())]);
896 write_torn_page_bytes(&path, page_id, &before, &after);
897
898 {
899 let pager = Pager::open(&path, config).expect("open() should succeed");
900 let recovered = pager
901 .read_page(page_id)
902 .expect("read_page() should succeed");
903 assert_eq!(recovered.cell_count(), 2);
904
905 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
906 assert_eq!(key, b"phase");
907 assert_eq!(value, b"before");
908
909 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
910 assert_eq!(key, b"phase2");
911 assert_eq!(value, b"after");
912 }
913
914 assert_eq!(
915 fs::metadata(dwb_path_for(&path))
916 .expect("metadata() should succeed")
917 .len(),
918 0
919 );
920 cleanup(&path);
921 }
922
923 #[test]
928 fn pager_starts_without_wal_writer() {
929 let path = temp_db_path();
930 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
931 assert!(!pager.has_wal_writer());
932 drop(pager);
933 cleanup(&path);
934 }
935
936 #[test]
937 fn set_wal_writer_attaches_handle() {
938 use crate::storage::wal::writer::WalWriter;
939 use std::sync::{Arc, Mutex};
940
941 let db_path = temp_db_path();
942 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
943 let _ = fs::remove_file(&wal_path);
944
945 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
946 let wal = Arc::new(Mutex::new(
947 WalWriter::open(&wal_path).expect("open() should succeed"),
948 ));
949 pager.set_wal_writer(Arc::clone(&wal));
950 assert!(pager.has_wal_writer());
951
952 pager.clear_wal_writer();
953 assert!(!pager.has_wal_writer());
954
955 drop(pager);
956 let _ = fs::remove_file(&wal_path);
957 cleanup(&db_path);
958 }
959
960 #[test]
961 fn flush_with_lsn_zero_pages_skips_wal_call() {
962 use crate::storage::wal::writer::WalWriter;
968 use std::sync::{Arc, Mutex};
969
970 let db_path = temp_db_path();
971 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
972 let _ = fs::remove_file(&wal_path);
973
974 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
975 let wal = Arc::new(Mutex::new(
976 WalWriter::open(&wal_path).expect("open() should succeed"),
977 ));
978 let initial_durable = {
979 let g = wal.lock().expect("lock() should succeed");
980 g.durable_lsn()
981 };
982 pager.set_wal_writer(Arc::clone(&wal));
983
984 let mut page = pager
986 .allocate_page(PageType::BTreeLeaf)
987 .expect("allocate_page() should succeed");
988 page.insert_cell(b"k", b"v")
989 .expect("insert_cell() should succeed");
990 pager
992 .write_page(page.page_id(), page)
993 .expect("write_page() should succeed");
994 pager.flush().expect("flush() should succeed");
995
996 let after_flush = {
999 let g = wal.lock().expect("lock() should succeed");
1000 g.durable_lsn()
1001 };
1002 assert_eq!(after_flush, initial_durable);
1003
1004 drop(pager);
1005 let _ = fs::remove_file(&wal_path);
1006 cleanup(&db_path);
1007 }
1008
1009 #[test]
1010 fn flush_advances_wal_durable_when_pages_carry_lsn() {
1011 use crate::storage::wal::record::WalRecord;
1015 use crate::storage::wal::writer::WalWriter;
1016 use std::sync::{Arc, Mutex};
1017
1018 let db_path = temp_db_path();
1019 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1020 let _ = fs::remove_file(&wal_path);
1021
1022 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1023 let wal = Arc::new(Mutex::new(
1024 WalWriter::open(&wal_path).expect("open() should succeed"),
1025 ));
1026 pager.set_wal_writer(Arc::clone(&wal));
1027
1028 let stamped_lsn = {
1030 let mut wal_guard = wal.lock().expect("lock() should succeed");
1031 wal_guard
1032 .append(&WalRecord::Begin { tx_id: 1 })
1033 .expect("append() should succeed");
1034 wal_guard
1035 .append(&WalRecord::Commit { tx_id: 1 })
1036 .expect("append() should succeed");
1037 wal_guard.current_lsn()
1038 };
1039 let mut page = pager
1040 .allocate_page(PageType::BTreeLeaf)
1041 .expect("allocate_page() should succeed");
1042 page.insert_cell(b"k", b"v")
1043 .expect("insert_cell() should succeed");
1044 page.set_lsn(stamped_lsn);
1046 pager
1047 .write_page(page.page_id(), page)
1048 .expect("write_page() should succeed");
1049 pager.flush().expect("flush() should succeed");
1050
1051 let after_flush = {
1053 let g = wal.lock().expect("lock() should succeed");
1054 g.durable_lsn()
1055 };
1056 assert!(
1057 after_flush >= stamped_lsn,
1058 "after flush durable_lsn {} must be >= stamped {}",
1059 after_flush,
1060 stamped_lsn
1061 );
1062
1063 drop(pager);
1064 let _ = fs::remove_file(&wal_path);
1065 cleanup(&db_path);
1066 }
1067
1068 #[test]
1073 fn block_size_warn_fires_for_mismatched_block_size() {
1074 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1078 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1080 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1082 }
1083
1084 #[test]
1085 fn block_size_silent_for_divisor() {
1086 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1088 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1089 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1090 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1091 }
1092
1093 #[test]
1094 fn block_size_unavailable_is_silent() {
1095 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1097 }
1098
1099 #[test]
1100 fn page_size_is_unchanged_16kib() {
1101 assert_eq!(PAGE_SIZE, 16 * 1024);
1103 }
1104}