1#[cfg(test)]
36use super::page::PageType;
37use super::page::{Page, PAGE_SIZE};
38use crate::storage::wal::writer::WalWriter;
39use fs2::FileExt;
40pub use reddb_engine::storage::pager::{DatabaseHeader, ExtentId, PagerError, PhysicalFileHeader};
41use reddb_engine::storage::pager::{
42 Pager as SharedPager, PagerConfig as SharedPagerConfig, WalBarrier,
43};
44#[cfg(test)]
45use reddb_engine::storage::pager_io::{
46 DWB_ZONE_BYTES, DWB_ZONE_START_PAGE, FIRST_ALLOCATABLE_PAGE,
47};
48use std::fs::{File, OpenOptions};
49#[cfg(test)]
50use std::io::{Read, Seek, SeekFrom, Write};
51use std::path::{Path, PathBuf};
52#[cfg(test)]
53use std::sync::atomic::{AtomicU8, Ordering};
54use std::sync::{Arc, Mutex, RwLock};
55const DEFAULT_CACHE_SIZE: usize = 10_000;
56#[cfg(test)]
57static COW_ATOMIC_WRITE_TEST_OVERRIDE: AtomicU8 = AtomicU8::new(0);
58
59#[derive(Debug, Clone)]
61pub struct PagerConfig {
62 pub cache_size: usize,
64 pub read_only: bool,
66 pub create: bool,
68 pub verify_checksums: bool,
70 pub double_write: bool,
72 pub encryption: Option<crate::storage::encryption::SecureKey>,
78}
79
80impl Default for PagerConfig {
81 fn default() -> Self {
82 Self {
83 cache_size: DEFAULT_CACHE_SIZE,
84 read_only: false,
85 create: true,
86 verify_checksums: true,
87 double_write: true,
88 encryption: None,
89 }
90 }
91}
92
93pub struct Pager {
95 core: SharedPager<NativePagerFile, NativeWalBarrier>,
96 _lock_file: Option<File>,
97 config: PagerConfig,
98 pub(crate) encryption: Option<(
99 crate::storage::encryption::PageEncryptor,
100 crate::storage::encryption::EncryptionHeader,
101 )>,
102}
103
104#[doc(hidden)]
105pub struct NativePagerFile(File);
106
107impl reddb_file::VfsFile for NativePagerFile {
108 fn try_clone(&self) -> std::io::Result<Self> {
109 Err(std::io::ErrorKind::Unsupported.into())
110 }
111 fn file_len(&self) -> std::io::Result<u64> {
112 Ok(self.0.metadata()?.len())
113 }
114 fn set_len(&self, len: u64) -> std::io::Result<()> {
115 self.0.set_len(len)
116 }
117 fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
118 std::io::Write::write_all(&mut self.0, bytes)
119 }
120 fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
121 std::io::Read::read(&mut self.0, bytes)
122 }
123 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
124 std::io::Seek::seek(&mut self.0, pos)
125 }
126 fn sync_all(&self) -> std::io::Result<()> {
127 self.0.sync_all()
128 }
129}
130
131#[doc(hidden)]
132#[derive(Default)]
133pub struct NativeWalBarrier(RwLock<Option<Arc<Mutex<WalWriter>>>>);
134
135impl WalBarrier for NativeWalBarrier {
136 fn flush_until(&self, lsn: u64) -> std::io::Result<()> {
137 let wal = self
138 .0
139 .read()
140 .map_err(|_| std::io::Error::other("WAL slot lock poisoned"))?
141 .clone();
142 if let Some(wal) = wal {
143 let mut writer = wal.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
144 writer.flush_until(lsn)?;
145 }
146 Ok(())
147 }
148}
149
150impl std::ops::Deref for Pager {
151 type Target = SharedPager<NativePagerFile, NativeWalBarrier>;
152 fn deref(&self) -> &Self::Target {
153 &self.core
154 }
155}
156
157impl reddb_engine::storage::page_store::PageStore for Pager {
158 fn read_page(&self, id: u32) -> Result<Page, PagerError> {
159 self.core.read_page(id)
160 }
161 fn write_page(&self, id: u32, page: Page) -> Result<(), PagerError> {
162 self.core.write_page(id, page)
163 }
164 fn allocate_page(&self, kind: super::page::PageType) -> Result<Page, PagerError> {
165 self.core.allocate_page(kind)
166 }
167 fn free_page(&self, id: u32) -> Result<(), PagerError> {
168 self.core.free_page(id)
169 }
170 fn page_count(&self) -> Result<u32, PagerError> {
171 self.core.page_count()
172 }
173 fn prefetch_hint(&self, id: u32) {
174 self.prefetch_hint(id)
175 }
176}
177
178#[path = "pager/impl.rs"]
179mod pager_impl;
180impl Drop for Pager {
181 fn drop(&mut self) {
182 let _ = self.flush();
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 #[cfg(target_os = "linux")]
191 use pager_impl::parse_mountinfo_options_for_path;
192 use pager_impl::{
193 classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
194 };
195 use std::fs;
196 use std::io::Write;
197
198 fn temp_db_path() -> PathBuf {
199 use std::sync::atomic::{AtomicU64, Ordering};
200 static COUNTER: AtomicU64 = AtomicU64::new(0);
201 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
202 let mut path = std::env::temp_dir();
203 path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
204 path
205 }
206
207 fn cleanup(path: &Path) {
208 let _ = fs::remove_file(path);
216 for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
217 let _ = fs::remove_file(sidecar);
218 }
219 }
220
221 fn dwb_path_for(path: &Path) -> PathBuf {
222 reddb_file::layout::pager_dwb_shadow_path(path)
223 }
224
225 fn dwb_zone_offset_for_test() -> u64 {
226 3 * PAGE_SIZE as u64
227 }
228
229 static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
230
231 struct CowAtomicWriteOverrideGuard {
232 _guard: std::sync::MutexGuard<'static, ()>,
233 }
234
235 impl Drop for CowAtomicWriteOverrideGuard {
236 fn drop(&mut self) {
237 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
238 }
239 }
240
241 fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
242 let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
243 .lock()
244 .unwrap_or_else(|err| err.into_inner());
245 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
246 CowAtomicWriteOverrideGuard { _guard: guard }
247 }
248
249 fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
250 let pages: Vec<_> = pages
251 .iter()
252 .map(|(page_id, page)| {
253 let mut page = page.clone();
254 page.update_checksum();
255 (*page_id, page)
256 })
257 .collect();
258 let buf = reddb_file::encode_paged_dwb_frame(
259 pages
260 .iter()
261 .map(|(page_id, page)| (*page_id, page.as_bytes())),
262 );
263
264 let mut file = OpenOptions::new()
265 .read(true)
266 .write(true)
267 .open(path)
268 .expect("open() should succeed");
269 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
270 .expect("seek() should succeed");
271 file.write_all(&buf).expect("write_all() should succeed");
272 file.sync_all().expect("sync_all() should succeed");
273 }
274
275 fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
276 let mut file = OpenOptions::new()
277 .write(true)
278 .open(path)
279 .expect("open() should succeed");
280 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
281 .expect("value is present");
282 file.write_all(page.as_bytes())
283 .expect("write_all() should succeed");
284 file.sync_all().expect("sync_all() should succeed");
285 }
286
287 fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
288 let mut torn = *before.as_bytes();
289 torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
290
291 let mut file = OpenOptions::new()
292 .write(true)
293 .open(path)
294 .expect("open() should succeed");
295 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
296 .expect("value is present");
297 file.write_all(&torn).expect("write_all() should succeed");
298 file.sync_all().expect("sync_all() should succeed");
299 }
300
301 #[test]
302 fn test_pager_create_new() {
303 let path = temp_db_path();
304 cleanup(&path);
305
306 {
307 let pager = Pager::open_default(&path).expect("open_default() should succeed");
308 assert_eq!(
309 pager.page_count().expect("page_count() should succeed"),
310 FIRST_ALLOCATABLE_PAGE
311 );
312 }
314
315 cleanup(&path);
316 }
317
318 #[test]
319 fn test_pager_reopen() {
320 let path = temp_db_path();
321 cleanup(&path);
322
323 {
325 let pager = Pager::open_default(&path).expect("open_default() should succeed");
326
327 let page = pager
329 .allocate_page(PageType::BTreeLeaf)
330 .expect("allocate_page() should succeed");
331 assert_eq!(page.page_id(), FIRST_ALLOCATABLE_PAGE);
332
333 pager.sync().expect("sync() should succeed");
334 }
335
336 {
338 let pager = Pager::open_default(&path).expect("open_default() should succeed");
339 assert_eq!(
340 pager.page_count().expect("page_count() should succeed"),
341 FIRST_ALLOCATABLE_PAGE + 1
342 );
343 }
345
346 cleanup(&path);
347 }
348
349 #[test]
350 fn test_pager_read_write() {
351 let path = temp_db_path();
352 cleanup(&path);
353
354 {
355 let pager = Pager::open_default(&path).expect("open_default() should succeed");
356
357 let mut page = pager
359 .allocate_page(PageType::BTreeLeaf)
360 .expect("allocate_page() should succeed");
361 let page_id = page.page_id();
362
363 page.insert_cell(b"key", b"value")
364 .expect("insert_cell() should succeed");
365 pager
366 .write_page(page_id, page)
367 .expect("write_page() should succeed");
368
369 let read_page = pager
371 .read_page(page_id)
372 .expect("read_page() should succeed");
373 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
374 assert_eq!(key, b"key");
375 assert_eq!(value, b"value");
376 }
377
378 cleanup(&path);
379 }
380
381 #[test]
382 fn test_pager_cache() {
383 let path = temp_db_path();
384 cleanup(&path);
385
386 {
387 let pager = Pager::open_default(&path).expect("open_default() should succeed");
388
389 let page = pager
391 .allocate_page(PageType::BTreeLeaf)
392 .expect("allocate_page() should succeed");
393 let page_id = page.page_id();
394
395 let _ = pager
397 .read_page(page_id)
398 .expect("read_page() should succeed");
399
400 let _ = pager
402 .read_page(page_id)
403 .expect("read_page() should succeed");
404
405 let stats = pager.cache_stats();
406 assert!(stats.hits >= 1);
407 }
408
409 cleanup(&path);
410 }
411
412 #[test]
413 fn test_pager_free_page() {
414 let path = temp_db_path();
415 cleanup(&path);
416
417 {
418 let pager = Pager::open_default(&path).expect("open_default() should succeed");
419
420 let page1 = pager
422 .allocate_page(PageType::BTreeLeaf)
423 .expect("allocate_page() should succeed");
424 let page2 = pager
425 .allocate_page(PageType::BTreeLeaf)
426 .expect("allocate_page() should succeed");
427
428 let id1 = page1.page_id();
429 let id2 = page2.page_id();
430
431 pager.free_page(id1).expect("free_page() should succeed");
433
434 let page3 = pager
436 .allocate_page(PageType::BTreeLeaf)
437 .expect("allocate_page() should succeed");
438 assert_eq!(page3.page_id(), id1);
439 }
440
441 cleanup(&path);
442 }
443
444 #[test]
445 fn test_freelist_persistence() {
446 let path = temp_db_path();
447 cleanup(&path);
448
449 let freed_id;
450 {
451 let pager = Pager::open_default(&path).expect("open_default() should succeed");
452 let page1 = pager
453 .allocate_page(PageType::BTreeLeaf)
454 .expect("allocate_page() should succeed");
455 let _page2 = pager
456 .allocate_page(PageType::BTreeLeaf)
457 .expect("allocate_page() should succeed");
458 freed_id = page1.page_id();
459
460 pager
461 .free_page(freed_id)
462 .expect("free_page() should succeed");
463 pager.sync().expect("sync() should succeed");
464 }
465
466 {
467 let pager = Pager::open_default(&path).expect("open_default() should succeed");
468 let page = pager
469 .allocate_page(PageType::BTreeLeaf)
470 .expect("allocate_page() should succeed");
471 assert_eq!(page.page_id(), freed_id);
472 }
473
474 cleanup(&path);
475 }
476
477 #[test]
478 fn test_pager_read_only() {
479 let path = temp_db_path();
480 cleanup(&path);
481
482 {
484 let pager = Pager::open_default(&path).expect("open_default() should succeed");
485 pager.sync().expect("sync() should succeed");
486 }
487
488 {
490 let config = PagerConfig {
491 read_only: true,
492 ..Default::default()
493 };
494
495 let pager = Pager::open(&path, config).expect("open() should succeed");
496 assert!(pager.is_read_only());
497
498 assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
500 }
501
502 cleanup(&path);
503 }
504
505 #[test]
506 fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
507 let path = temp_db_path();
508 cleanup(&path);
509
510 let config = PagerConfig {
511 double_write: true,
512 ..Default::default()
513 };
514
515 let page_id;
516 {
517 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
518 let page = pager
519 .allocate_page(PageType::BTreeLeaf)
520 .expect("allocate_page() should succeed");
521 page_id = page.page_id();
522 pager.sync().expect("sync() should succeed");
523 }
524
525 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
526 recovered_page
527 .insert_cell(b"key", b"value")
528 .expect("insert_cell() should succeed");
529 write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
530
531 assert!(!dwb_path_for(&path).exists());
532
533 {
534 let pager = Pager::open(&path, config).expect("open() should succeed");
535
536 let read_page = pager
537 .read_page(page_id)
538 .expect("read_page() should succeed");
539 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
540 assert_eq!(key, b"key");
541 assert_eq!(value, b"value");
542
543 assert!(!dwb_path_for(&path).exists());
544
545 let mut updated_page = recovered_page.clone();
546 updated_page
547 .insert_cell(b"key2", b"value2")
548 .expect("insert_cell() should succeed");
549 pager
550 .write_page(page_id, updated_page)
551 .expect("write_page() should succeed");
552 pager.flush().expect("flush() should succeed");
553
554 assert!(!dwb_path_for(&path).exists());
555 }
556
557 cleanup(&path);
558 }
559
560 #[test]
561 fn retired_dwb_sidecar_routes_through_offline_migration() {
562 let path = temp_db_path();
563 cleanup(&path);
564
565 {
566 let pager = Pager::open_default(&path).expect("open_default() should succeed");
567 pager.sync().expect("sync() should succeed");
568 }
569
570 let dwb = dwb_path_for(&path);
571 fs::write(&dwb, b"legacy dwb sidecar").expect("write legacy sidecar");
572
573 let err = match Pager::open_default(&path) {
574 Ok(_) => panic!("retired DWB sidecar must refuse open"),
575 Err(err) => err,
576 };
577 assert!(matches!(err, PagerError::LegacySidecarStore { .. }));
578
579 cleanup(&path);
580 }
581
582 #[test]
583 fn interrupted_dwb_staging_write_is_discarded_on_open() {
584 let path = temp_db_path();
585 cleanup(&path);
586
587 {
588 let pager = Pager::open_default(&path).expect("open_default() should succeed");
589 pager.sync().expect("sync() should succeed");
590 }
591
592 let mut file = OpenOptions::new()
593 .read(true)
594 .write(true)
595 .open(&path)
596 .expect("open() should succeed");
597 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
598 .expect("seek() should succeed");
599 file.write_all(b"RDDW")
600 .expect("write corrupt DWB magic should succeed");
601 file.sync_all().expect("sync_all() should succeed");
602
603 {
604 let pager = Pager::open_default(&path).expect("torn staging frame should be discarded");
605 pager.sync().expect("sync() should succeed");
606 }
607
608 let mut cleared = [0u8; 4];
609 let mut file = OpenOptions::new()
610 .read(true)
611 .open(&path)
612 .expect("open() should succeed");
613 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
614 .expect("seek() should succeed");
615 file.read_exact(&mut cleared)
616 .expect("read_exact() should succeed");
617 assert_ne!(cleared, reddb_file::DWB_MAGIC);
618
619 cleanup(&path);
620 }
621
622 #[test]
623 fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
624 assert_eq!(
625 classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
626 Some(CowFilesystemKind::Zfs),
627 "ZFS is always CoW"
628 );
629 assert_eq!(
630 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
631 Some(CowFilesystemKind::BtrfsDataCow),
632 "btrfs qualifies only when datacow remains enabled"
633 );
634 assert_eq!(
635 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
636 None,
637 "btrfs nodatacow mount option must reject DWB skip"
638 );
639 assert_eq!(
640 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
641 None,
642 "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
643 );
644 assert_eq!(
645 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
646 None,
647 "missing btrfs inode flags are uncertain and must fail closed"
648 );
649 assert_eq!(
650 classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
651 None,
652 "missing btrfs mount options are uncertain and must fail closed"
653 );
654 }
655
656 #[cfg(target_os = "linux")]
657 #[test]
658 fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
659 let mountinfo = "\
66024 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
66135 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
66236 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
663";
664
665 assert_eq!(
666 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
667 .as_deref(),
668 Some("rw,relatime,rw,space_cache=v2")
669 );
670 assert_eq!(
671 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
672 .as_deref(),
673 Some("rw,relatime,rw,nodatacow")
674 );
675 }
676
677 #[test]
678 fn double_write_false_keeps_dwb_when_cow_probe_denies() {
679 let _override = cow_atomic_write_override(false);
680 let path = temp_db_path();
681 cleanup(&path);
682
683 {
684 let config = PagerConfig {
685 double_write: false,
686 ..Default::default()
687 };
688 let pager = Pager::open(&path, config).expect("open() should succeed");
689 let page = pager
690 .allocate_page(PageType::BTreeLeaf)
691 .expect("allocate_page() should succeed");
692 pager
693 .write_page(page.page_id(), page)
694 .expect("write_page() should succeed");
695 pager.flush().expect("flush() should succeed");
696 }
697
698 assert!(
699 !dwb_path_for(&path).exists(),
700 "DWB must stay enabled in-file when double_write=false is not proven safe"
701 );
702
703 cleanup(&path);
704 }
705
706 #[test]
707 fn double_write_false_skips_dwb_when_cow_probe_allows() {
708 let _override = cow_atomic_write_override(true);
709 let path = temp_db_path();
710 cleanup(&path);
711
712 {
713 let config = PagerConfig {
714 double_write: false,
715 ..Default::default()
716 };
717 let pager = Pager::open(&path, config).expect("open() should succeed");
718 let page = pager
719 .allocate_page(PageType::BTreeLeaf)
720 .expect("allocate_page() should succeed");
721 pager
722 .write_page(page.page_id(), page)
723 .expect("write_page() should succeed");
724 pager.flush().expect("flush() should succeed");
725 }
726
727 assert!(
728 !dwb_path_for(&path).exists(),
729 "DWB may be skipped only after the CoW probe allows it"
730 );
731
732 cleanup(&path);
733 }
734
735 #[test]
736 fn double_write_false_on_cow_recovers_from_in_file_dwb_zone() {
737 let _override = cow_atomic_write_override(true);
738 let path = temp_db_path();
739 cleanup(&path);
740
741 let page_id;
742 {
743 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
744 let page = pager
745 .allocate_page(PageType::BTreeLeaf)
746 .expect("allocate_page() should succeed");
747 page_id = page.page_id();
748 pager.sync().expect("sync() should succeed");
749 }
750
751 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
752 recovered_page
753 .insert_cell(b"key", b"value")
754 .expect("insert_cell() should succeed");
755 write_dwb_fixture(&path, &[(page_id, recovered_page)]);
756
757 {
758 let config = PagerConfig {
759 double_write: false,
760 ..Default::default()
761 };
762 let pager = Pager::open(&path, config).expect("open() should succeed");
763 let read_page = pager
764 .read_page(page_id)
765 .expect("read_page() should succeed");
766 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
767 assert_eq!(key, b"key");
768 assert_eq!(value, b"value");
769 }
770
771 assert!(!dwb_path_for(&path).exists());
772
773 cleanup(&path);
774 }
775
776 #[test]
777 fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
778 let _override = cow_atomic_write_override(true);
779 let path = temp_db_path();
780 cleanup(&path);
781
782 let config = PagerConfig {
783 double_write: false,
784 ..Default::default()
785 };
786
787 let page_id;
788 let before;
789 let after;
790 {
791 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
792 let mut page = pager
793 .allocate_page(PageType::BTreeLeaf)
794 .expect("allocate_page() should succeed");
795 page_id = page.page_id();
796 page.insert_cell(b"phase", b"before")
797 .expect("insert_cell() should succeed");
798 pager
799 .write_page(page_id, page)
800 .expect("write_page() should succeed");
801 pager.sync().expect("sync() should succeed");
802 before = pager
803 .read_page(page_id)
804 .expect("read_page() should succeed");
805
806 let mut page = before.clone();
807 page.insert_cell(b"phase2", b"after")
808 .expect("insert_cell() should succeed");
809 pager
810 .write_page(page_id, page)
811 .expect("write_page() should succeed");
812 pager.flush().expect("flush() should succeed");
813 after = pager
814 .read_page(page_id)
815 .expect("read_page() should succeed");
816 }
817
818 for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
821 write_page_bytes(&path, page_id, whole_page);
822
823 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
824 let recovered = pager
825 .read_page(page_id)
826 .expect("read_page() should succeed");
827 assert_eq!(recovered.cell_count(), expected_cells);
828 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
829 assert_eq!(key, b"phase");
830 assert_eq!(value, b"before");
831 if expected_cells == 2 {
832 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
833 assert_eq!(key, b"phase2");
834 assert_eq!(value, b"after");
835 }
836 drop(pager);
837 }
838
839 cleanup(&path);
840 }
841
842 #[test]
843 fn same_mid_write_without_cow_recovers_from_dwb() {
844 let _override = cow_atomic_write_override(false);
845 let path = temp_db_path();
846 cleanup(&path);
847
848 let config = PagerConfig {
849 double_write: false,
850 ..Default::default()
851 };
852
853 let page_id;
854 let before;
855 let after;
856 {
857 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
858 let mut page = pager
859 .allocate_page(PageType::BTreeLeaf)
860 .expect("allocate_page() should succeed");
861 page_id = page.page_id();
862 page.insert_cell(b"phase", b"before")
863 .expect("insert_cell() should succeed");
864 pager
865 .write_page(page_id, page)
866 .expect("write_page() should succeed");
867 pager.sync().expect("sync() should succeed");
868 before = pager
869 .read_page(page_id)
870 .expect("read_page() should succeed");
871
872 let mut page = before.clone();
873 page.insert_cell(b"phase2", b"after")
874 .expect("insert_cell() should succeed");
875 pager
876 .write_page(page_id, page)
877 .expect("write_page() should succeed");
878 pager.flush().expect("flush() should succeed");
879 after = pager
880 .read_page(page_id)
881 .expect("read_page() should succeed");
882 }
883
884 write_dwb_fixture(&path, &[(page_id, after.clone())]);
885 write_torn_page_bytes(&path, page_id, &before, &after);
886
887 {
888 let pager = Pager::open(&path, config).expect("open() should succeed");
889 let recovered = pager
890 .read_page(page_id)
891 .expect("read_page() should succeed");
892 assert_eq!(recovered.cell_count(), 2);
893
894 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
895 assert_eq!(key, b"phase");
896 assert_eq!(value, b"before");
897
898 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
899 assert_eq!(key, b"phase2");
900 assert_eq!(value, b"after");
901 }
902
903 assert!(!dwb_path_for(&path).exists());
904 cleanup(&path);
905 }
906
907 #[test]
912 fn pager_starts_without_wal_writer() {
913 let path = temp_db_path();
914 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
915 assert!(!pager.has_wal_writer());
916 drop(pager);
917 cleanup(&path);
918 }
919
920 #[test]
921 fn set_wal_writer_attaches_handle() {
922 use crate::storage::wal::writer::WalWriter;
923 use std::sync::{Arc, Mutex};
924
925 let db_path = temp_db_path();
926 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
927 let _ = fs::remove_file(&wal_path);
928
929 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
930 let wal = Arc::new(Mutex::new(
931 WalWriter::open(&wal_path).expect("open() should succeed"),
932 ));
933 pager.set_wal_writer(Arc::clone(&wal));
934 assert!(pager.has_wal_writer());
935
936 pager.clear_wal_writer();
937 assert!(!pager.has_wal_writer());
938
939 drop(pager);
940 let _ = fs::remove_file(&wal_path);
941 cleanup(&db_path);
942 }
943
944 #[test]
945 fn flush_with_lsn_zero_pages_skips_wal_call() {
946 use crate::storage::wal::writer::WalWriter;
952 use std::sync::{Arc, Mutex};
953
954 let db_path = temp_db_path();
955 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
956 let _ = fs::remove_file(&wal_path);
957
958 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
959 let wal = Arc::new(Mutex::new(
960 WalWriter::open(&wal_path).expect("open() should succeed"),
961 ));
962 let initial_durable = {
963 let g = wal.lock().expect("lock() should succeed");
964 g.durable_lsn()
965 };
966 pager.set_wal_writer(Arc::clone(&wal));
967
968 let mut page = pager
970 .allocate_page(PageType::BTreeLeaf)
971 .expect("allocate_page() should succeed");
972 page.insert_cell(b"k", b"v")
973 .expect("insert_cell() should succeed");
974 pager
976 .write_page(page.page_id(), page)
977 .expect("write_page() should succeed");
978 pager.flush().expect("flush() should succeed");
979
980 let after_flush = {
983 let g = wal.lock().expect("lock() should succeed");
984 g.durable_lsn()
985 };
986 assert_eq!(after_flush, initial_durable);
987
988 drop(pager);
989 let _ = fs::remove_file(&wal_path);
990 cleanup(&db_path);
991 }
992
993 #[test]
994 fn flush_advances_wal_durable_when_pages_carry_lsn() {
995 use crate::storage::wal::record::WalRecord;
999 use crate::storage::wal::writer::WalWriter;
1000 use std::sync::{Arc, Mutex};
1001
1002 let db_path = temp_db_path();
1003 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1004 let _ = fs::remove_file(&wal_path);
1005
1006 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1007 let wal = Arc::new(Mutex::new(
1008 WalWriter::open(&wal_path).expect("open() should succeed"),
1009 ));
1010 pager.set_wal_writer(Arc::clone(&wal));
1011
1012 let stamped_lsn = {
1014 let mut wal_guard = wal.lock().expect("lock() should succeed");
1015 wal_guard
1016 .append(&WalRecord::Begin { tx_id: 1 })
1017 .expect("append() should succeed");
1018 wal_guard
1019 .append(&WalRecord::Commit { tx_id: 1 })
1020 .expect("append() should succeed");
1021 wal_guard.current_lsn()
1022 };
1023 let mut page = pager
1024 .allocate_page(PageType::BTreeLeaf)
1025 .expect("allocate_page() should succeed");
1026 page.insert_cell(b"k", b"v")
1027 .expect("insert_cell() should succeed");
1028 page.set_lsn(stamped_lsn);
1030 pager
1031 .write_page(page.page_id(), page)
1032 .expect("write_page() should succeed");
1033 pager.flush().expect("flush() should succeed");
1034
1035 let after_flush = {
1037 let g = wal.lock().expect("lock() should succeed");
1038 g.durable_lsn()
1039 };
1040 assert!(
1041 after_flush >= stamped_lsn,
1042 "after flush durable_lsn {} must be >= stamped {}",
1043 after_flush,
1044 stamped_lsn
1045 );
1046
1047 drop(pager);
1048 let _ = fs::remove_file(&wal_path);
1049 cleanup(&db_path);
1050 }
1051
1052 #[test]
1057 fn block_size_warn_fires_for_mismatched_block_size() {
1058 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1062 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1064 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1066 }
1067
1068 #[test]
1069 fn block_size_silent_for_divisor() {
1070 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1072 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1073 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1074 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1075 }
1076
1077 #[test]
1078 fn block_size_unavailable_is_silent() {
1079 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1081 }
1082
1083 #[test]
1084 fn page_size_is_unchanged_16kib() {
1085 assert_eq!(PAGE_SIZE, 16 * 1024);
1087 }
1088}