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}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct ExtentId {
83 pub start_page: u32,
84 pub n_pages: u32,
85}
86
87impl std::fmt::Display for PagerError {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 Self::Io(e) => write!(f, "I/O error: {}", e),
91 Self::Page(e) => write!(f, "Page error: {}", e),
92 Self::InvalidDatabase(msg) => write!(f, "Invalid database: {}", msg),
93 Self::ReadOnly => write!(f, "Database is read-only"),
94 Self::PageNotFound(id) => write!(f, "Page {} not found", id),
95 Self::Locked => write!(f, "Database is locked"),
96 Self::LockPoisoned => write!(f, "Internal lock poisoned (concurrent thread panicked)"),
97 Self::EncryptionRequired => write!(
98 f,
99 "Database is encrypted but no key was supplied (set PagerConfig::encryption)"
100 ),
101 Self::PlainDatabaseRefusesKey => write!(
102 f,
103 "Plain (unencrypted) database opened with an encryption key — refusing"
104 ),
105 Self::InvalidKey => write!(f, "Encryption key validation failed for this database"),
106 }
107 }
108}
109
110impl std::error::Error for PagerError {
111 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
112 match self {
113 Self::Io(e) => Some(e),
114 Self::Page(e) => Some(e),
115 _ => None,
116 }
117 }
118}
119
120impl From<std::io::Error> for PagerError {
121 fn from(e: std::io::Error) -> Self {
122 Self::Io(e)
123 }
124}
125
126impl From<PageError> for PagerError {
127 fn from(e: PageError) -> Self {
128 Self::Page(e)
129 }
130}
131
132#[derive(Debug, Clone)]
134pub struct PagerConfig {
135 pub cache_size: usize,
137 pub read_only: bool,
139 pub create: bool,
141 pub verify_checksums: bool,
143 pub double_write: bool,
145 pub encryption: Option<crate::storage::encryption::SecureKey>,
151}
152
153impl Default for PagerConfig {
154 fn default() -> Self {
155 Self {
156 cache_size: DEFAULT_CACHE_SIZE,
157 read_only: false,
158 create: true,
159 verify_checksums: true,
160 double_write: true,
161 encryption: None,
162 }
163 }
164}
165
166pub struct Pager {
170 path: PathBuf,
172 file: Mutex<File>,
174 _lock_file: Option<File>,
176 dwb_file: Option<Mutex<File>>,
178 cache: PageCache,
180 freelist: RwLock<FreeList>,
182 header: RwLock<DatabaseHeader>,
184 config: PagerConfig,
186 header_dirty: Mutex<bool>,
188 wal: RwLock<Option<Arc<Mutex<WalWriter>>>>,
200 pub(crate) encryption: Option<(
207 crate::storage::encryption::PageEncryptor,
208 crate::storage::encryption::EncryptionHeader,
209 )>,
210}
211
212#[path = "pager/impl.rs"]
213mod pager_impl;
214impl Drop for Pager {
215 fn drop(&mut self) {
216 let _ = self.flush();
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 #[cfg(target_os = "linux")]
225 use pager_impl::parse_mountinfo_options_for_path;
226 use pager_impl::{
227 classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
228 };
229 use std::fs;
230 use std::io::Write;
231
232 fn temp_db_path() -> PathBuf {
233 use std::sync::atomic::{AtomicU64, Ordering};
234 static COUNTER: AtomicU64 = AtomicU64::new(0);
235 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
236 let mut path = std::env::temp_dir();
237 path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
238 path
239 }
240
241 fn cleanup(path: &Path) {
242 let _ = fs::remove_file(path);
250 for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
251 let _ = fs::remove_file(sidecar);
252 }
253 }
254
255 fn dwb_path_for(path: &Path) -> PathBuf {
256 reddb_file::layout::pager_dwb_shadow_path(path)
257 }
258
259 static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
260
261 struct CowAtomicWriteOverrideGuard {
262 _guard: std::sync::MutexGuard<'static, ()>,
263 }
264
265 impl Drop for CowAtomicWriteOverrideGuard {
266 fn drop(&mut self) {
267 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
268 }
269 }
270
271 fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
272 let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
273 .lock()
274 .unwrap_or_else(|err| err.into_inner());
275 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
276 CowAtomicWriteOverrideGuard { _guard: guard }
277 }
278
279 fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
280 let pages: Vec<_> = pages
281 .iter()
282 .map(|(page_id, page)| {
283 let mut page = page.clone();
284 page.update_checksum();
285 (*page_id, page)
286 })
287 .collect();
288 let buf = reddb_file::encode_paged_dwb_frame(
289 pages
290 .iter()
291 .map(|(page_id, page)| (*page_id, page.as_bytes())),
292 );
293
294 let dwb_path = dwb_path_for(path);
295 let mut file = fs::File::create(&dwb_path).expect("create() should succeed");
296 file.write_all(&buf).expect("write_all() should succeed");
297 file.sync_all().expect("sync_all() should succeed");
298 }
299
300 fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
301 let mut file = OpenOptions::new()
302 .write(true)
303 .open(path)
304 .expect("open() should succeed");
305 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
306 .expect("value is present");
307 file.write_all(page.as_bytes())
308 .expect("write_all() should succeed");
309 file.sync_all().expect("sync_all() should succeed");
310 }
311
312 fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
313 let mut torn = *before.as_bytes();
314 torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
315
316 let mut file = OpenOptions::new()
317 .write(true)
318 .open(path)
319 .expect("open() should succeed");
320 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
321 .expect("value is present");
322 file.write_all(&torn).expect("write_all() should succeed");
323 file.sync_all().expect("sync_all() should succeed");
324 }
325
326 #[test]
327 fn test_pager_create_new() {
328 let path = temp_db_path();
329 cleanup(&path);
330
331 {
332 let pager = Pager::open_default(&path).expect("open_default() should succeed");
333 assert_eq!(pager.page_count().expect("page_count() should succeed"), 3);
334 }
336
337 cleanup(&path);
338 }
339
340 #[test]
341 fn test_pager_reopen() {
342 let path = temp_db_path();
343 cleanup(&path);
344
345 {
347 let pager = Pager::open_default(&path).expect("open_default() should succeed");
348
349 let page = pager
351 .allocate_page(PageType::BTreeLeaf)
352 .expect("allocate_page() should succeed");
353 assert_eq!(page.page_id(), 3);
354
355 pager.sync().expect("sync() should succeed");
356 }
357
358 {
360 let pager = Pager::open_default(&path).expect("open_default() should succeed");
361 assert_eq!(pager.page_count().expect("page_count() should succeed"), 4);
362 }
364
365 cleanup(&path);
366 }
367
368 #[test]
369 fn test_pager_read_write() {
370 let path = temp_db_path();
371 cleanup(&path);
372
373 {
374 let pager = Pager::open_default(&path).expect("open_default() should succeed");
375
376 let mut page = pager
378 .allocate_page(PageType::BTreeLeaf)
379 .expect("allocate_page() should succeed");
380 let page_id = page.page_id();
381
382 page.insert_cell(b"key", b"value")
383 .expect("insert_cell() should succeed");
384 pager
385 .write_page(page_id, page)
386 .expect("write_page() should succeed");
387
388 let read_page = pager
390 .read_page(page_id)
391 .expect("read_page() should succeed");
392 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
393 assert_eq!(key, b"key");
394 assert_eq!(value, b"value");
395 }
396
397 cleanup(&path);
398 }
399
400 #[test]
401 fn test_pager_cache() {
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 page = pager
410 .allocate_page(PageType::BTreeLeaf)
411 .expect("allocate_page() should succeed");
412 let page_id = page.page_id();
413
414 let _ = pager
416 .read_page(page_id)
417 .expect("read_page() should succeed");
418
419 let _ = pager
421 .read_page(page_id)
422 .expect("read_page() should succeed");
423
424 let stats = pager.cache_stats();
425 assert!(stats.hits >= 1);
426 }
427
428 cleanup(&path);
429 }
430
431 #[test]
432 fn test_pager_free_page() {
433 let path = temp_db_path();
434 cleanup(&path);
435
436 {
437 let pager = Pager::open_default(&path).expect("open_default() should succeed");
438
439 let page1 = pager
441 .allocate_page(PageType::BTreeLeaf)
442 .expect("allocate_page() should succeed");
443 let page2 = pager
444 .allocate_page(PageType::BTreeLeaf)
445 .expect("allocate_page() should succeed");
446
447 let id1 = page1.page_id();
448 let id2 = page2.page_id();
449
450 pager.free_page(id1).expect("free_page() should succeed");
452
453 let page3 = pager
455 .allocate_page(PageType::BTreeLeaf)
456 .expect("allocate_page() should succeed");
457 assert_eq!(page3.page_id(), id1);
458 }
459
460 cleanup(&path);
461 }
462
463 #[test]
464 fn test_freelist_persistence() {
465 let path = temp_db_path();
466 cleanup(&path);
467
468 let freed_id;
469 {
470 let pager = Pager::open_default(&path).expect("open_default() should succeed");
471 let page1 = pager
472 .allocate_page(PageType::BTreeLeaf)
473 .expect("allocate_page() should succeed");
474 let _page2 = pager
475 .allocate_page(PageType::BTreeLeaf)
476 .expect("allocate_page() should succeed");
477 freed_id = page1.page_id();
478
479 pager
480 .free_page(freed_id)
481 .expect("free_page() should succeed");
482 pager.sync().expect("sync() should succeed");
483 }
484
485 {
486 let pager = Pager::open_default(&path).expect("open_default() should succeed");
487 let page = pager
488 .allocate_page(PageType::BTreeLeaf)
489 .expect("allocate_page() should succeed");
490 assert_eq!(page.page_id(), freed_id);
491 }
492
493 cleanup(&path);
494 }
495
496 #[test]
497 fn test_pager_read_only() {
498 let path = temp_db_path();
499 cleanup(&path);
500
501 {
503 let pager = Pager::open_default(&path).expect("open_default() should succeed");
504 pager.sync().expect("sync() should succeed");
505 }
506
507 {
509 let config = PagerConfig {
510 read_only: true,
511 ..Default::default()
512 };
513
514 let pager = Pager::open(&path, config).expect("open() should succeed");
515 assert!(pager.is_read_only());
516
517 assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
519 }
520
521 cleanup(&path);
522 }
523
524 #[test]
525 fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
526 let path = temp_db_path();
527 cleanup(&path);
528
529 let config = PagerConfig {
530 double_write: true,
531 ..Default::default()
532 };
533
534 let page_id;
535 {
536 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
537 let page = pager
538 .allocate_page(PageType::BTreeLeaf)
539 .expect("allocate_page() should succeed");
540 page_id = page.page_id();
541 pager.sync().expect("sync() should succeed");
542 }
543
544 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
545 recovered_page
546 .insert_cell(b"key", b"value")
547 .expect("insert_cell() should succeed");
548 write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
549
550 let dwb_path = dwb_path_for(&path);
551 assert!(dwb_path.exists());
552 assert!(
553 fs::metadata(&dwb_path)
554 .expect("metadata() should succeed")
555 .len()
556 > 0
557 );
558
559 {
560 let pager = Pager::open(&path, config).expect("open() should succeed");
561
562 let read_page = pager
563 .read_page(page_id)
564 .expect("read_page() should succeed");
565 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
566 assert_eq!(key, b"key");
567 assert_eq!(value, b"value");
568
569 assert!(dwb_path.exists());
570 assert_eq!(
571 fs::metadata(&dwb_path)
572 .expect("metadata() should succeed")
573 .len(),
574 0
575 );
576
577 let mut updated_page = recovered_page.clone();
578 updated_page
579 .insert_cell(b"key2", b"value2")
580 .expect("insert_cell() should succeed");
581 pager
582 .write_page(page_id, updated_page)
583 .expect("write_page() should succeed");
584 pager.flush().expect("flush() should succeed");
585
586 assert!(dwb_path.exists());
587 assert_eq!(
588 fs::metadata(&dwb_path)
589 .expect("metadata() should succeed")
590 .len(),
591 0
592 );
593 }
594
595 cleanup(&path);
596 }
597
598 #[test]
599 fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
600 assert_eq!(
601 classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
602 Some(CowFilesystemKind::Zfs),
603 "ZFS is always CoW"
604 );
605 assert_eq!(
606 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
607 Some(CowFilesystemKind::BtrfsDataCow),
608 "btrfs qualifies only when datacow remains enabled"
609 );
610 assert_eq!(
611 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
612 None,
613 "btrfs nodatacow mount option must reject DWB skip"
614 );
615 assert_eq!(
616 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
617 None,
618 "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
619 );
620 assert_eq!(
621 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
622 None,
623 "missing btrfs inode flags are uncertain and must fail closed"
624 );
625 assert_eq!(
626 classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
627 None,
628 "missing btrfs mount options are uncertain and must fail closed"
629 );
630 }
631
632 #[cfg(target_os = "linux")]
633 #[test]
634 fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
635 let mountinfo = "\
63624 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
63735 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
63836 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
639";
640
641 assert_eq!(
642 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
643 .as_deref(),
644 Some("rw,relatime,rw,space_cache=v2")
645 );
646 assert_eq!(
647 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
648 .as_deref(),
649 Some("rw,relatime,rw,nodatacow")
650 );
651 }
652
653 #[test]
654 fn double_write_false_keeps_dwb_when_cow_probe_denies() {
655 let _override = cow_atomic_write_override(false);
656 let path = temp_db_path();
657 cleanup(&path);
658
659 {
660 let config = PagerConfig {
661 double_write: false,
662 ..Default::default()
663 };
664 let pager = Pager::open(&path, config).expect("open() should succeed");
665 let page = pager
666 .allocate_page(PageType::BTreeLeaf)
667 .expect("allocate_page() should succeed");
668 pager
669 .write_page(page.page_id(), page)
670 .expect("write_page() should succeed");
671 pager.flush().expect("flush() should succeed");
672 }
673
674 assert!(
675 dwb_path_for(&path).exists(),
676 "DWB must stay enabled when double_write=false is not proven safe"
677 );
678
679 cleanup(&path);
680 }
681
682 #[test]
683 fn double_write_false_skips_dwb_when_cow_probe_allows() {
684 let _override = cow_atomic_write_override(true);
685 let path = temp_db_path();
686 cleanup(&path);
687
688 {
689 let config = PagerConfig {
690 double_write: false,
691 ..Default::default()
692 };
693 let pager = Pager::open(&path, config).expect("open() should succeed");
694 let page = pager
695 .allocate_page(PageType::BTreeLeaf)
696 .expect("allocate_page() should succeed");
697 pager
698 .write_page(page.page_id(), page)
699 .expect("write_page() should succeed");
700 pager.flush().expect("flush() should succeed");
701 }
702
703 assert!(
704 !dwb_path_for(&path).exists(),
705 "DWB may be skipped only after the CoW probe allows it"
706 );
707
708 cleanup(&path);
709 }
710
711 #[test]
712 fn double_write_false_on_cow_replays_then_removes_existing_dwb() {
713 let _override = cow_atomic_write_override(true);
714 let path = temp_db_path();
715 cleanup(&path);
716
717 let page_id;
718 {
719 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
720 let page = pager
721 .allocate_page(PageType::BTreeLeaf)
722 .expect("allocate_page() should succeed");
723 page_id = page.page_id();
724 pager.sync().expect("sync() should succeed");
725 }
726
727 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
728 recovered_page
729 .insert_cell(b"key", b"value")
730 .expect("insert_cell() should succeed");
731 write_dwb_fixture(&path, &[(page_id, recovered_page)]);
732
733 {
734 let config = PagerConfig {
735 double_write: false,
736 ..Default::default()
737 };
738 let pager = Pager::open(&path, config).expect("open() should succeed");
739 let read_page = pager
740 .read_page(page_id)
741 .expect("read_page() should succeed");
742 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
743 assert_eq!(key, b"key");
744 assert_eq!(value, b"value");
745 }
746
747 assert!(
748 !dwb_path_for(&path).exists(),
749 "CoW DWB-skip must replay any existing DWB before removing the sidecar"
750 );
751
752 cleanup(&path);
753 }
754
755 #[test]
756 fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
757 let _override = cow_atomic_write_override(true);
758 let path = temp_db_path();
759 cleanup(&path);
760
761 let config = PagerConfig {
762 double_write: false,
763 ..Default::default()
764 };
765
766 let page_id;
767 let before;
768 let after;
769 {
770 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
771 let mut page = pager
772 .allocate_page(PageType::BTreeLeaf)
773 .expect("allocate_page() should succeed");
774 page_id = page.page_id();
775 page.insert_cell(b"phase", b"before")
776 .expect("insert_cell() should succeed");
777 pager
778 .write_page(page_id, page)
779 .expect("write_page() should succeed");
780 pager.sync().expect("sync() should succeed");
781 before = pager
782 .read_page(page_id)
783 .expect("read_page() should succeed");
784
785 let mut page = before.clone();
786 page.insert_cell(b"phase2", b"after")
787 .expect("insert_cell() should succeed");
788 pager
789 .write_page(page_id, page)
790 .expect("write_page() should succeed");
791 pager.flush().expect("flush() should succeed");
792 after = pager
793 .read_page(page_id)
794 .expect("read_page() should succeed");
795 }
796
797 for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
800 write_page_bytes(&path, page_id, whole_page);
801
802 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
803 let recovered = pager
804 .read_page(page_id)
805 .expect("read_page() should succeed");
806 assert_eq!(recovered.cell_count(), expected_cells);
807 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
808 assert_eq!(key, b"phase");
809 assert_eq!(value, b"before");
810 if expected_cells == 2 {
811 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
812 assert_eq!(key, b"phase2");
813 assert_eq!(value, b"after");
814 }
815 drop(pager);
816 }
817
818 cleanup(&path);
819 }
820
821 #[test]
822 fn same_mid_write_without_cow_recovers_from_dwb() {
823 let _override = cow_atomic_write_override(false);
824 let path = temp_db_path();
825 cleanup(&path);
826
827 let config = PagerConfig {
828 double_write: false,
829 ..Default::default()
830 };
831
832 let page_id;
833 let before;
834 let after;
835 {
836 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
837 let mut page = pager
838 .allocate_page(PageType::BTreeLeaf)
839 .expect("allocate_page() should succeed");
840 page_id = page.page_id();
841 page.insert_cell(b"phase", b"before")
842 .expect("insert_cell() should succeed");
843 pager
844 .write_page(page_id, page)
845 .expect("write_page() should succeed");
846 pager.sync().expect("sync() should succeed");
847 before = pager
848 .read_page(page_id)
849 .expect("read_page() should succeed");
850
851 let mut page = before.clone();
852 page.insert_cell(b"phase2", b"after")
853 .expect("insert_cell() should succeed");
854 pager
855 .write_page(page_id, page)
856 .expect("write_page() should succeed");
857 pager.flush().expect("flush() should succeed");
858 after = pager
859 .read_page(page_id)
860 .expect("read_page() should succeed");
861 }
862
863 write_dwb_fixture(&path, &[(page_id, after.clone())]);
864 write_torn_page_bytes(&path, page_id, &before, &after);
865
866 {
867 let pager = Pager::open(&path, config).expect("open() should succeed");
868 let recovered = pager
869 .read_page(page_id)
870 .expect("read_page() should succeed");
871 assert_eq!(recovered.cell_count(), 2);
872
873 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
874 assert_eq!(key, b"phase");
875 assert_eq!(value, b"before");
876
877 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
878 assert_eq!(key, b"phase2");
879 assert_eq!(value, b"after");
880 }
881
882 assert_eq!(
883 fs::metadata(dwb_path_for(&path))
884 .expect("metadata() should succeed")
885 .len(),
886 0
887 );
888 cleanup(&path);
889 }
890
891 #[test]
896 fn pager_starts_without_wal_writer() {
897 let path = temp_db_path();
898 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
899 assert!(!pager.has_wal_writer());
900 drop(pager);
901 cleanup(&path);
902 }
903
904 #[test]
905 fn set_wal_writer_attaches_handle() {
906 use crate::storage::wal::writer::WalWriter;
907 use std::sync::{Arc, Mutex};
908
909 let db_path = temp_db_path();
910 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
911 let _ = fs::remove_file(&wal_path);
912
913 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
914 let wal = Arc::new(Mutex::new(
915 WalWriter::open(&wal_path).expect("open() should succeed"),
916 ));
917 pager.set_wal_writer(Arc::clone(&wal));
918 assert!(pager.has_wal_writer());
919
920 pager.clear_wal_writer();
921 assert!(!pager.has_wal_writer());
922
923 drop(pager);
924 let _ = fs::remove_file(&wal_path);
925 cleanup(&db_path);
926 }
927
928 #[test]
929 fn flush_with_lsn_zero_pages_skips_wal_call() {
930 use crate::storage::wal::writer::WalWriter;
936 use std::sync::{Arc, Mutex};
937
938 let db_path = temp_db_path();
939 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
940 let _ = fs::remove_file(&wal_path);
941
942 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
943 let wal = Arc::new(Mutex::new(
944 WalWriter::open(&wal_path).expect("open() should succeed"),
945 ));
946 let initial_durable = {
947 let g = wal.lock().expect("lock() should succeed");
948 g.durable_lsn()
949 };
950 pager.set_wal_writer(Arc::clone(&wal));
951
952 let mut page = pager
954 .allocate_page(PageType::BTreeLeaf)
955 .expect("allocate_page() should succeed");
956 page.insert_cell(b"k", b"v")
957 .expect("insert_cell() should succeed");
958 pager
960 .write_page(page.page_id(), page)
961 .expect("write_page() should succeed");
962 pager.flush().expect("flush() should succeed");
963
964 let after_flush = {
967 let g = wal.lock().expect("lock() should succeed");
968 g.durable_lsn()
969 };
970 assert_eq!(after_flush, initial_durable);
971
972 drop(pager);
973 let _ = fs::remove_file(&wal_path);
974 cleanup(&db_path);
975 }
976
977 #[test]
978 fn flush_advances_wal_durable_when_pages_carry_lsn() {
979 use crate::storage::wal::record::WalRecord;
983 use crate::storage::wal::writer::WalWriter;
984 use std::sync::{Arc, Mutex};
985
986 let db_path = temp_db_path();
987 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
988 let _ = fs::remove_file(&wal_path);
989
990 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
991 let wal = Arc::new(Mutex::new(
992 WalWriter::open(&wal_path).expect("open() should succeed"),
993 ));
994 pager.set_wal_writer(Arc::clone(&wal));
995
996 let stamped_lsn = {
998 let mut wal_guard = wal.lock().expect("lock() should succeed");
999 wal_guard
1000 .append(&WalRecord::Begin { tx_id: 1 })
1001 .expect("append() should succeed");
1002 wal_guard
1003 .append(&WalRecord::Commit { tx_id: 1 })
1004 .expect("append() should succeed");
1005 wal_guard.current_lsn()
1006 };
1007 let mut page = pager
1008 .allocate_page(PageType::BTreeLeaf)
1009 .expect("allocate_page() should succeed");
1010 page.insert_cell(b"k", b"v")
1011 .expect("insert_cell() should succeed");
1012 page.set_lsn(stamped_lsn);
1014 pager
1015 .write_page(page.page_id(), page)
1016 .expect("write_page() should succeed");
1017 pager.flush().expect("flush() should succeed");
1018
1019 let after_flush = {
1021 let g = wal.lock().expect("lock() should succeed");
1022 g.durable_lsn()
1023 };
1024 assert!(
1025 after_flush >= stamped_lsn,
1026 "after flush durable_lsn {} must be >= stamped {}",
1027 after_flush,
1028 stamped_lsn
1029 );
1030
1031 drop(pager);
1032 let _ = fs::remove_file(&wal_path);
1033 cleanup(&db_path);
1034 }
1035
1036 #[test]
1041 fn block_size_warn_fires_for_mismatched_block_size() {
1042 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1046 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1048 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1050 }
1051
1052 #[test]
1053 fn block_size_silent_for_divisor() {
1054 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1056 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1057 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1058 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1059 }
1060
1061 #[test]
1062 fn block_size_unavailable_is_silent() {
1063 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1065 }
1066
1067 #[test]
1068 fn page_size_is_unchanged_16kib() {
1069 assert_eq!(PAGE_SIZE, 16 * 1024);
1071 }
1072}