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