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 scrub 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 scrub 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 header: RwLock<DatabaseHeader>,
243 config: PagerConfig,
245 header_dirty: Mutex<bool>,
247 wal: RwLock<Option<Arc<Mutex<WalWriter>>>>,
259 pub(crate) encryption: Option<(
266 crate::storage::encryption::PageEncryptor,
267 crate::storage::encryption::EncryptionHeader,
268 )>,
269}
270
271#[path = "pager/impl.rs"]
272mod pager_impl;
273impl Drop for Pager {
274 fn drop(&mut self) {
275 let _ = self.flush();
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 #[cfg(target_os = "linux")]
284 use pager_impl::parse_mountinfo_options_for_path;
285 use pager_impl::{
286 classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
287 };
288 use std::fs;
289 use std::io::Write;
290
291 fn temp_db_path() -> PathBuf {
292 use std::sync::atomic::{AtomicU64, Ordering};
293 static COUNTER: AtomicU64 = AtomicU64::new(0);
294 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
295 let mut path = std::env::temp_dir();
296 path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
297 path
298 }
299
300 fn cleanup(path: &Path) {
301 let _ = fs::remove_file(path);
309 for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
310 let _ = fs::remove_file(sidecar);
311 }
312 }
313
314 fn dwb_path_for(path: &Path) -> PathBuf {
315 reddb_file::layout::pager_dwb_shadow_path(path)
316 }
317
318 fn dwb_zone_offset_for_test() -> u64 {
319 3 * PAGE_SIZE as u64
320 }
321
322 static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
323
324 struct CowAtomicWriteOverrideGuard {
325 _guard: std::sync::MutexGuard<'static, ()>,
326 }
327
328 impl Drop for CowAtomicWriteOverrideGuard {
329 fn drop(&mut self) {
330 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
331 }
332 }
333
334 fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
335 let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
336 .lock()
337 .unwrap_or_else(|err| err.into_inner());
338 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
339 CowAtomicWriteOverrideGuard { _guard: guard }
340 }
341
342 fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
343 let pages: Vec<_> = pages
344 .iter()
345 .map(|(page_id, page)| {
346 let mut page = page.clone();
347 page.update_checksum();
348 (*page_id, page)
349 })
350 .collect();
351 let buf = reddb_file::encode_paged_dwb_frame(
352 pages
353 .iter()
354 .map(|(page_id, page)| (*page_id, page.as_bytes())),
355 );
356
357 let mut file = OpenOptions::new()
358 .read(true)
359 .write(true)
360 .open(path)
361 .expect("open() should succeed");
362 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
363 .expect("seek() should succeed");
364 file.write_all(&buf).expect("write_all() should succeed");
365 file.sync_all().expect("sync_all() should succeed");
366 }
367
368 fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
369 let mut file = OpenOptions::new()
370 .write(true)
371 .open(path)
372 .expect("open() should succeed");
373 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
374 .expect("value is present");
375 file.write_all(page.as_bytes())
376 .expect("write_all() should succeed");
377 file.sync_all().expect("sync_all() should succeed");
378 }
379
380 fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
381 let mut torn = *before.as_bytes();
382 torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
383
384 let mut file = OpenOptions::new()
385 .write(true)
386 .open(path)
387 .expect("open() should succeed");
388 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
389 .expect("value is present");
390 file.write_all(&torn).expect("write_all() should succeed");
391 file.sync_all().expect("sync_all() should succeed");
392 }
393
394 #[test]
395 fn test_pager_create_new() {
396 let path = temp_db_path();
397 cleanup(&path);
398
399 {
400 let pager = Pager::open_default(&path).expect("open_default() should succeed");
401 assert_eq!(
402 pager.page_count().expect("page_count() should succeed"),
403 FIRST_ALLOCATABLE_PAGE
404 );
405 }
407
408 cleanup(&path);
409 }
410
411 #[test]
412 fn test_pager_reopen() {
413 let path = temp_db_path();
414 cleanup(&path);
415
416 {
418 let pager = Pager::open_default(&path).expect("open_default() should succeed");
419
420 let page = pager
422 .allocate_page(PageType::BTreeLeaf)
423 .expect("allocate_page() should succeed");
424 assert_eq!(page.page_id(), FIRST_ALLOCATABLE_PAGE);
425
426 pager.sync().expect("sync() should succeed");
427 }
428
429 {
431 let pager = Pager::open_default(&path).expect("open_default() should succeed");
432 assert_eq!(
433 pager.page_count().expect("page_count() should succeed"),
434 FIRST_ALLOCATABLE_PAGE + 1
435 );
436 }
438
439 cleanup(&path);
440 }
441
442 #[test]
443 fn test_pager_read_write() {
444 let path = temp_db_path();
445 cleanup(&path);
446
447 {
448 let pager = Pager::open_default(&path).expect("open_default() should succeed");
449
450 let mut page = pager
452 .allocate_page(PageType::BTreeLeaf)
453 .expect("allocate_page() should succeed");
454 let page_id = page.page_id();
455
456 page.insert_cell(b"key", b"value")
457 .expect("insert_cell() should succeed");
458 pager
459 .write_page(page_id, page)
460 .expect("write_page() should succeed");
461
462 let read_page = pager
464 .read_page(page_id)
465 .expect("read_page() should succeed");
466 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
467 assert_eq!(key, b"key");
468 assert_eq!(value, b"value");
469 }
470
471 cleanup(&path);
472 }
473
474 #[test]
475 fn test_pager_cache() {
476 let path = temp_db_path();
477 cleanup(&path);
478
479 {
480 let pager = Pager::open_default(&path).expect("open_default() should succeed");
481
482 let page = pager
484 .allocate_page(PageType::BTreeLeaf)
485 .expect("allocate_page() should succeed");
486 let page_id = page.page_id();
487
488 let _ = pager
490 .read_page(page_id)
491 .expect("read_page() should succeed");
492
493 let _ = pager
495 .read_page(page_id)
496 .expect("read_page() should succeed");
497
498 let stats = pager.cache_stats();
499 assert!(stats.hits >= 1);
500 }
501
502 cleanup(&path);
503 }
504
505 #[test]
506 fn test_pager_free_page() {
507 let path = temp_db_path();
508 cleanup(&path);
509
510 {
511 let pager = Pager::open_default(&path).expect("open_default() should succeed");
512
513 let page1 = pager
515 .allocate_page(PageType::BTreeLeaf)
516 .expect("allocate_page() should succeed");
517 let page2 = pager
518 .allocate_page(PageType::BTreeLeaf)
519 .expect("allocate_page() should succeed");
520
521 let id1 = page1.page_id();
522 let id2 = page2.page_id();
523
524 pager.free_page(id1).expect("free_page() should succeed");
526
527 let page3 = pager
529 .allocate_page(PageType::BTreeLeaf)
530 .expect("allocate_page() should succeed");
531 assert_eq!(page3.page_id(), id1);
532 }
533
534 cleanup(&path);
535 }
536
537 #[test]
538 fn test_freelist_persistence() {
539 let path = temp_db_path();
540 cleanup(&path);
541
542 let freed_id;
543 {
544 let pager = Pager::open_default(&path).expect("open_default() should succeed");
545 let page1 = pager
546 .allocate_page(PageType::BTreeLeaf)
547 .expect("allocate_page() should succeed");
548 let _page2 = pager
549 .allocate_page(PageType::BTreeLeaf)
550 .expect("allocate_page() should succeed");
551 freed_id = page1.page_id();
552
553 pager
554 .free_page(freed_id)
555 .expect("free_page() should succeed");
556 pager.sync().expect("sync() should succeed");
557 }
558
559 {
560 let pager = Pager::open_default(&path).expect("open_default() should succeed");
561 let page = pager
562 .allocate_page(PageType::BTreeLeaf)
563 .expect("allocate_page() should succeed");
564 assert_eq!(page.page_id(), freed_id);
565 }
566
567 cleanup(&path);
568 }
569
570 #[test]
571 fn test_pager_read_only() {
572 let path = temp_db_path();
573 cleanup(&path);
574
575 {
577 let pager = Pager::open_default(&path).expect("open_default() should succeed");
578 pager.sync().expect("sync() should succeed");
579 }
580
581 {
583 let config = PagerConfig {
584 read_only: true,
585 ..Default::default()
586 };
587
588 let pager = Pager::open(&path, config).expect("open() should succeed");
589 assert!(pager.is_read_only());
590
591 assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
593 }
594
595 cleanup(&path);
596 }
597
598 #[test]
599 fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
600 let path = temp_db_path();
601 cleanup(&path);
602
603 let config = PagerConfig {
604 double_write: true,
605 ..Default::default()
606 };
607
608 let page_id;
609 {
610 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
611 let page = pager
612 .allocate_page(PageType::BTreeLeaf)
613 .expect("allocate_page() should succeed");
614 page_id = page.page_id();
615 pager.sync().expect("sync() should succeed");
616 }
617
618 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
619 recovered_page
620 .insert_cell(b"key", b"value")
621 .expect("insert_cell() should succeed");
622 write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
623
624 assert!(!dwb_path_for(&path).exists());
625
626 {
627 let pager = Pager::open(&path, config).expect("open() should succeed");
628
629 let read_page = pager
630 .read_page(page_id)
631 .expect("read_page() should succeed");
632 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
633 assert_eq!(key, b"key");
634 assert_eq!(value, b"value");
635
636 assert!(!dwb_path_for(&path).exists());
637
638 let mut updated_page = recovered_page.clone();
639 updated_page
640 .insert_cell(b"key2", b"value2")
641 .expect("insert_cell() should succeed");
642 pager
643 .write_page(page_id, updated_page)
644 .expect("write_page() should succeed");
645 pager.flush().expect("flush() should succeed");
646
647 assert!(!dwb_path_for(&path).exists());
648 }
649
650 cleanup(&path);
651 }
652
653 #[test]
654 fn retired_dwb_sidecar_routes_through_offline_migration() {
655 let path = temp_db_path();
656 cleanup(&path);
657
658 {
659 let pager = Pager::open_default(&path).expect("open_default() should succeed");
660 pager.sync().expect("sync() should succeed");
661 }
662
663 let dwb = dwb_path_for(&path);
664 fs::write(&dwb, b"legacy dwb sidecar").expect("write legacy sidecar");
665
666 let err = match Pager::open_default(&path) {
667 Ok(_) => panic!("retired DWB sidecar must refuse open"),
668 Err(err) => err,
669 };
670 assert!(matches!(err, PagerError::LegacySidecarStore { .. }));
671
672 cleanup(&path);
673 }
674
675 #[test]
676 fn interrupted_dwb_staging_write_is_discarded_on_open() {
677 let path = temp_db_path();
678 cleanup(&path);
679
680 {
681 let pager = Pager::open_default(&path).expect("open_default() should succeed");
682 pager.sync().expect("sync() should succeed");
683 }
684
685 let mut file = OpenOptions::new()
686 .read(true)
687 .write(true)
688 .open(&path)
689 .expect("open() should succeed");
690 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
691 .expect("seek() should succeed");
692 file.write_all(b"RDDW")
693 .expect("write corrupt DWB magic should succeed");
694 file.sync_all().expect("sync_all() should succeed");
695
696 {
697 let pager = Pager::open_default(&path).expect("torn staging frame should be discarded");
698 pager.sync().expect("sync() should succeed");
699 }
700
701 let mut cleared = [0u8; 4];
702 let mut file = OpenOptions::new()
703 .read(true)
704 .open(&path)
705 .expect("open() should succeed");
706 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
707 .expect("seek() should succeed");
708 file.read_exact(&mut cleared)
709 .expect("read_exact() should succeed");
710 assert_ne!(cleared, reddb_file::DWB_MAGIC);
711
712 cleanup(&path);
713 }
714
715 #[test]
716 fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
717 assert_eq!(
718 classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
719 Some(CowFilesystemKind::Zfs),
720 "ZFS is always CoW"
721 );
722 assert_eq!(
723 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
724 Some(CowFilesystemKind::BtrfsDataCow),
725 "btrfs qualifies only when datacow remains enabled"
726 );
727 assert_eq!(
728 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
729 None,
730 "btrfs nodatacow mount option must reject DWB skip"
731 );
732 assert_eq!(
733 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
734 None,
735 "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
736 );
737 assert_eq!(
738 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
739 None,
740 "missing btrfs inode flags are uncertain and must fail closed"
741 );
742 assert_eq!(
743 classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
744 None,
745 "missing btrfs mount options are uncertain and must fail closed"
746 );
747 }
748
749 #[cfg(target_os = "linux")]
750 #[test]
751 fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
752 let mountinfo = "\
75324 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
75435 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
75536 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
756";
757
758 assert_eq!(
759 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
760 .as_deref(),
761 Some("rw,relatime,rw,space_cache=v2")
762 );
763 assert_eq!(
764 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
765 .as_deref(),
766 Some("rw,relatime,rw,nodatacow")
767 );
768 }
769
770 #[test]
771 fn double_write_false_keeps_dwb_when_cow_probe_denies() {
772 let _override = cow_atomic_write_override(false);
773 let path = temp_db_path();
774 cleanup(&path);
775
776 {
777 let config = PagerConfig {
778 double_write: false,
779 ..Default::default()
780 };
781 let pager = Pager::open(&path, config).expect("open() should succeed");
782 let page = pager
783 .allocate_page(PageType::BTreeLeaf)
784 .expect("allocate_page() should succeed");
785 pager
786 .write_page(page.page_id(), page)
787 .expect("write_page() should succeed");
788 pager.flush().expect("flush() should succeed");
789 }
790
791 assert!(
792 !dwb_path_for(&path).exists(),
793 "DWB must stay enabled in-file when double_write=false is not proven safe"
794 );
795
796 cleanup(&path);
797 }
798
799 #[test]
800 fn double_write_false_skips_dwb_when_cow_probe_allows() {
801 let _override = cow_atomic_write_override(true);
802 let path = temp_db_path();
803 cleanup(&path);
804
805 {
806 let config = PagerConfig {
807 double_write: false,
808 ..Default::default()
809 };
810 let pager = Pager::open(&path, config).expect("open() should succeed");
811 let page = pager
812 .allocate_page(PageType::BTreeLeaf)
813 .expect("allocate_page() should succeed");
814 pager
815 .write_page(page.page_id(), page)
816 .expect("write_page() should succeed");
817 pager.flush().expect("flush() should succeed");
818 }
819
820 assert!(
821 !dwb_path_for(&path).exists(),
822 "DWB may be skipped only after the CoW probe allows it"
823 );
824
825 cleanup(&path);
826 }
827
828 #[test]
829 fn double_write_false_on_cow_recovers_from_in_file_dwb_zone() {
830 let _override = cow_atomic_write_override(true);
831 let path = temp_db_path();
832 cleanup(&path);
833
834 let page_id;
835 {
836 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
837 let page = pager
838 .allocate_page(PageType::BTreeLeaf)
839 .expect("allocate_page() should succeed");
840 page_id = page.page_id();
841 pager.sync().expect("sync() should succeed");
842 }
843
844 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
845 recovered_page
846 .insert_cell(b"key", b"value")
847 .expect("insert_cell() should succeed");
848 write_dwb_fixture(&path, &[(page_id, recovered_page)]);
849
850 {
851 let config = PagerConfig {
852 double_write: false,
853 ..Default::default()
854 };
855 let pager = Pager::open(&path, config).expect("open() should succeed");
856 let read_page = pager
857 .read_page(page_id)
858 .expect("read_page() should succeed");
859 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
860 assert_eq!(key, b"key");
861 assert_eq!(value, b"value");
862 }
863
864 assert!(!dwb_path_for(&path).exists());
865
866 cleanup(&path);
867 }
868
869 #[test]
870 fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
871 let _override = cow_atomic_write_override(true);
872 let path = temp_db_path();
873 cleanup(&path);
874
875 let config = PagerConfig {
876 double_write: false,
877 ..Default::default()
878 };
879
880 let page_id;
881 let before;
882 let after;
883 {
884 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
885 let mut page = pager
886 .allocate_page(PageType::BTreeLeaf)
887 .expect("allocate_page() should succeed");
888 page_id = page.page_id();
889 page.insert_cell(b"phase", b"before")
890 .expect("insert_cell() should succeed");
891 pager
892 .write_page(page_id, page)
893 .expect("write_page() should succeed");
894 pager.sync().expect("sync() should succeed");
895 before = pager
896 .read_page(page_id)
897 .expect("read_page() should succeed");
898
899 let mut page = before.clone();
900 page.insert_cell(b"phase2", b"after")
901 .expect("insert_cell() should succeed");
902 pager
903 .write_page(page_id, page)
904 .expect("write_page() should succeed");
905 pager.flush().expect("flush() should succeed");
906 after = pager
907 .read_page(page_id)
908 .expect("read_page() should succeed");
909 }
910
911 for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
914 write_page_bytes(&path, page_id, whole_page);
915
916 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
917 let recovered = pager
918 .read_page(page_id)
919 .expect("read_page() should succeed");
920 assert_eq!(recovered.cell_count(), expected_cells);
921 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
922 assert_eq!(key, b"phase");
923 assert_eq!(value, b"before");
924 if expected_cells == 2 {
925 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
926 assert_eq!(key, b"phase2");
927 assert_eq!(value, b"after");
928 }
929 drop(pager);
930 }
931
932 cleanup(&path);
933 }
934
935 #[test]
936 fn same_mid_write_without_cow_recovers_from_dwb() {
937 let _override = cow_atomic_write_override(false);
938 let path = temp_db_path();
939 cleanup(&path);
940
941 let config = PagerConfig {
942 double_write: false,
943 ..Default::default()
944 };
945
946 let page_id;
947 let before;
948 let after;
949 {
950 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
951 let mut page = pager
952 .allocate_page(PageType::BTreeLeaf)
953 .expect("allocate_page() should succeed");
954 page_id = page.page_id();
955 page.insert_cell(b"phase", b"before")
956 .expect("insert_cell() should succeed");
957 pager
958 .write_page(page_id, page)
959 .expect("write_page() should succeed");
960 pager.sync().expect("sync() should succeed");
961 before = pager
962 .read_page(page_id)
963 .expect("read_page() should succeed");
964
965 let mut page = before.clone();
966 page.insert_cell(b"phase2", b"after")
967 .expect("insert_cell() should succeed");
968 pager
969 .write_page(page_id, page)
970 .expect("write_page() should succeed");
971 pager.flush().expect("flush() should succeed");
972 after = pager
973 .read_page(page_id)
974 .expect("read_page() should succeed");
975 }
976
977 write_dwb_fixture(&path, &[(page_id, after.clone())]);
978 write_torn_page_bytes(&path, page_id, &before, &after);
979
980 {
981 let pager = Pager::open(&path, config).expect("open() should succeed");
982 let recovered = pager
983 .read_page(page_id)
984 .expect("read_page() should succeed");
985 assert_eq!(recovered.cell_count(), 2);
986
987 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
988 assert_eq!(key, b"phase");
989 assert_eq!(value, b"before");
990
991 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
992 assert_eq!(key, b"phase2");
993 assert_eq!(value, b"after");
994 }
995
996 assert!(!dwb_path_for(&path).exists());
997 cleanup(&path);
998 }
999
1000 #[test]
1005 fn pager_starts_without_wal_writer() {
1006 let path = temp_db_path();
1007 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
1008 assert!(!pager.has_wal_writer());
1009 drop(pager);
1010 cleanup(&path);
1011 }
1012
1013 #[test]
1014 fn set_wal_writer_attaches_handle() {
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 assert!(pager.has_wal_writer());
1028
1029 pager.clear_wal_writer();
1030 assert!(!pager.has_wal_writer());
1031
1032 drop(pager);
1033 let _ = fs::remove_file(&wal_path);
1034 cleanup(&db_path);
1035 }
1036
1037 #[test]
1038 fn flush_with_lsn_zero_pages_skips_wal_call() {
1039 use crate::storage::wal::writer::WalWriter;
1045 use std::sync::{Arc, Mutex};
1046
1047 let db_path = temp_db_path();
1048 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1049 let _ = fs::remove_file(&wal_path);
1050
1051 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1052 let wal = Arc::new(Mutex::new(
1053 WalWriter::open(&wal_path).expect("open() should succeed"),
1054 ));
1055 let initial_durable = {
1056 let g = wal.lock().expect("lock() should succeed");
1057 g.durable_lsn()
1058 };
1059 pager.set_wal_writer(Arc::clone(&wal));
1060
1061 let mut page = pager
1063 .allocate_page(PageType::BTreeLeaf)
1064 .expect("allocate_page() should succeed");
1065 page.insert_cell(b"k", b"v")
1066 .expect("insert_cell() should succeed");
1067 pager
1069 .write_page(page.page_id(), page)
1070 .expect("write_page() should succeed");
1071 pager.flush().expect("flush() should succeed");
1072
1073 let after_flush = {
1076 let g = wal.lock().expect("lock() should succeed");
1077 g.durable_lsn()
1078 };
1079 assert_eq!(after_flush, initial_durable);
1080
1081 drop(pager);
1082 let _ = fs::remove_file(&wal_path);
1083 cleanup(&db_path);
1084 }
1085
1086 #[test]
1087 fn flush_advances_wal_durable_when_pages_carry_lsn() {
1088 use crate::storage::wal::record::WalRecord;
1092 use crate::storage::wal::writer::WalWriter;
1093 use std::sync::{Arc, Mutex};
1094
1095 let db_path = temp_db_path();
1096 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1097 let _ = fs::remove_file(&wal_path);
1098
1099 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1100 let wal = Arc::new(Mutex::new(
1101 WalWriter::open(&wal_path).expect("open() should succeed"),
1102 ));
1103 pager.set_wal_writer(Arc::clone(&wal));
1104
1105 let stamped_lsn = {
1107 let mut wal_guard = wal.lock().expect("lock() should succeed");
1108 wal_guard
1109 .append(&WalRecord::Begin { tx_id: 1 })
1110 .expect("append() should succeed");
1111 wal_guard
1112 .append(&WalRecord::Commit { tx_id: 1 })
1113 .expect("append() should succeed");
1114 wal_guard.current_lsn()
1115 };
1116 let mut page = pager
1117 .allocate_page(PageType::BTreeLeaf)
1118 .expect("allocate_page() should succeed");
1119 page.insert_cell(b"k", b"v")
1120 .expect("insert_cell() should succeed");
1121 page.set_lsn(stamped_lsn);
1123 pager
1124 .write_page(page.page_id(), page)
1125 .expect("write_page() should succeed");
1126 pager.flush().expect("flush() should succeed");
1127
1128 let after_flush = {
1130 let g = wal.lock().expect("lock() should succeed");
1131 g.durable_lsn()
1132 };
1133 assert!(
1134 after_flush >= stamped_lsn,
1135 "after flush durable_lsn {} must be >= stamped {}",
1136 after_flush,
1137 stamped_lsn
1138 );
1139
1140 drop(pager);
1141 let _ = fs::remove_file(&wal_path);
1142 cleanup(&db_path);
1143 }
1144
1145 #[test]
1150 fn block_size_warn_fires_for_mismatched_block_size() {
1151 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1155 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1157 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1159 }
1160
1161 #[test]
1162 fn block_size_silent_for_divisor() {
1163 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1165 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1166 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1167 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1168 }
1169
1170 #[test]
1171 fn block_size_unavailable_is_silent() {
1172 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1174 }
1175
1176 #[test]
1177 fn page_size_is_unchanged_16kib() {
1178 assert_eq!(PAGE_SIZE, 16 * 1024);
1180 }
1181}