Skip to main content

reddb_server/storage/engine/
pager.rs

1//! Pager - Page I/O Manager
2//!
3//! The Pager is responsible for reading and writing pages to/from disk.
4//! It integrates with the PageCache for efficient caching and the FreeList
5//! for page allocation.
6//!
7//! # Responsibilities
8//!
9//! 1. **Page I/O**: Read/write 16 KiB pages from/to disk
10//! 2. **Caching**: Integrate with SIEVE PageCache
11//! 3. **Allocation**: Manage free page allocation via FreeList
12//! 4. **Header Management**: Maintain database header (page 0)
13//!
14//! # File Layout
15//!
16//! ```text
17//! ┌─────────────────────────────────────────────────────────────┐
18//! │ Page 0: Database Header                                     │
19//! │   - Magic bytes "RDDB"                                      │
20//! │   - Version                                                 │
21//! │   - Page count                                              │
22//! │   - Freelist head                                           │
23//! ├─────────────────────────────────────────────────────────────┤
24//! │ Page 1: Root B-tree page (or first data page)              │
25//! ├─────────────────────────────────────────────────────────────┤
26//! │ Page 2..N: Data pages                                       │
27//! └─────────────────────────────────────────────────────────────┘
28//! ```
29//!
30//! # References
31//!
32//! - Turso `core/storage/pager.rs:54-134` - HeaderRef::from_pager()
33//! - Turso `core/storage/pager.rs:120` - pager.add_dirty(&page)
34
35#[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/// Pager configuration
60#[derive(Debug, Clone)]
61pub struct PagerConfig {
62    /// Page cache capacity
63    pub cache_size: usize,
64    /// Whether to open read-only
65    pub read_only: bool,
66    /// Whether to create if not exists
67    pub create: bool,
68    /// Whether to verify checksums on read
69    pub verify_checksums: bool,
70    /// Enable double-write buffer for torn page protection
71    pub double_write: bool,
72    /// Optional encryption key. When set, `Pager::open` writes/reads
73    /// pages through `PageEncryptor` and rejects any DB whose
74    /// encryption-marker disagrees with the supplied key (or its
75    /// absence). When `None`, the pager refuses to open a DB whose
76    /// header carries the `RDBE` encryption marker.
77    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
93/// Native host adapter for the shared pager.
94pub struct Pager {
95    core: SharedPager<NativePagerFile, NativeWalBarrier>,
96    config: PagerConfig,
97    pub(crate) encryption: Option<(
98        crate::storage::encryption::PageEncryptor,
99        crate::storage::encryption::EncryptionHeader,
100    )>,
101}
102
103#[doc(hidden)]
104pub struct NativePagerFile(File);
105
106impl reddb_file::VfsFile for NativePagerFile {
107    fn try_clone(&self) -> std::io::Result<Self> {
108        Err(std::io::ErrorKind::Unsupported.into())
109    }
110    fn file_len(&self) -> std::io::Result<u64> {
111        Ok(self.0.metadata()?.len())
112    }
113    fn set_len(&self, len: u64) -> std::io::Result<()> {
114        self.0.set_len(len)
115    }
116    fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
117        std::io::Write::write_all(&mut self.0, bytes)
118    }
119    fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
120        std::io::Read::read(&mut self.0, bytes)
121    }
122    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
123        std::io::Seek::seek(&mut self.0, pos)
124    }
125    fn sync_all(&self) -> std::io::Result<()> {
126        self.0.sync_all()
127    }
128}
129
130/// Durable ownership callbacks installed only while a fenced epoch is active.
131pub(crate) trait PageOwnershipRecorder: Send + Sync {
132    fn reserve_pages(&self, start: u32, count: u32) -> std::io::Result<()>;
133    fn detach_free_pages(&self, pages: &[u32]) -> std::io::Result<()>;
134    fn retire_pages(&self, pages: &[u32]) -> std::io::Result<()>;
135    fn needs_page_image(&self, _page_id: u32) -> std::io::Result<bool> {
136        Ok(false)
137    }
138    fn capture_page_image(&self, _page: &Page) -> std::io::Result<()> {
139        Ok(())
140    }
141}
142
143#[doc(hidden)]
144#[derive(Default)]
145pub struct NativeWalBarrier(
146    RwLock<Option<Arc<Mutex<WalWriter>>>>,
147    RwLock<Option<Arc<dyn PageOwnershipRecorder>>>,
148);
149
150impl NativeWalBarrier {
151    fn ownership_recorder(&self) -> std::io::Result<Option<Arc<dyn PageOwnershipRecorder>>> {
152        Ok(self
153            .1
154            .read()
155            .map_err(|_| std::io::Error::other("ownership recorder lock poisoned"))?
156            .clone())
157    }
158}
159
160impl WalBarrier for NativeWalBarrier {
161    fn needs_page_image(&self, page_id: u32) -> std::io::Result<bool> {
162        self.ownership_recorder()?
163            .map_or(Ok(false), |recorder| recorder.needs_page_image(page_id))
164    }
165
166    fn capture_page_image(&self, page: &Page) -> std::io::Result<()> {
167        if let Some(recorder) = self.ownership_recorder()? {
168            recorder.capture_page_image(page)?;
169        }
170        Ok(())
171    }
172
173    fn flush_until(&self, lsn: u64) -> std::io::Result<()> {
174        let wal = self
175            .0
176            .read()
177            .map_err(|_| std::io::Error::other("WAL slot lock poisoned"))?
178            .clone();
179        if let Some(wal) = wal {
180            let mut writer = wal.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
181            writer.flush_until(lsn)?;
182        } else if lsn != 0 {
183            return Err(std::io::Error::other(
184                "nonzero page LSN requires a WAL durability barrier",
185            ));
186        }
187        Ok(())
188    }
189
190    fn reserve_pages(&self, start: u32, count: u32) -> std::io::Result<()> {
191        if let Some(recorder) = self.ownership_recorder()? {
192            recorder.reserve_pages(start, count)?;
193        }
194        Ok(())
195    }
196
197    fn detach_free_pages(&self, pages: &[u32]) -> std::io::Result<()> {
198        if let Some(recorder) = self.ownership_recorder()? {
199            recorder.detach_free_pages(pages)?;
200        }
201        Ok(())
202    }
203
204    fn retire_pages(&self, pages: &[u32]) -> std::io::Result<()> {
205        if let Some(recorder) = self.ownership_recorder()? {
206            recorder.retire_pages(pages)?;
207        }
208        Ok(())
209    }
210}
211
212impl std::ops::Deref for Pager {
213    type Target = SharedPager<NativePagerFile, NativeWalBarrier>;
214    fn deref(&self) -> &Self::Target {
215        &self.core
216    }
217}
218
219impl reddb_engine::storage::page_store::PageStore for Pager {
220    fn read_page(&self, id: u32) -> Result<Page, PagerError> {
221        self.core.read_page(id)
222    }
223    fn write_page(&self, id: u32, page: Page) -> Result<(), PagerError> {
224        self.core.write_page(id, page)
225    }
226    fn allocate_page(&self, kind: super::page::PageType) -> Result<Page, PagerError> {
227        self.core.allocate_page(kind)
228    }
229    fn free_page(&self, id: u32) -> Result<(), PagerError> {
230        self.core.free_page(id)
231    }
232    fn page_count(&self) -> Result<u32, PagerError> {
233        self.core.page_count()
234    }
235    fn prefetch_hint(&self, id: u32) {
236        self.prefetch_hint(id)
237    }
238}
239
240#[path = "pager/impl.rs"]
241mod pager_impl;
242impl Drop for Pager {
243    fn drop(&mut self) {
244        // Try to flush on drop
245        let _ = self.flush();
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    #[cfg(target_os = "linux")]
253    use pager_impl::parse_mountinfo_options_for_path;
254    use pager_impl::{
255        classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
256    };
257    use std::fs;
258    use std::io::Write;
259
260    fn temp_db_path() -> PathBuf {
261        use std::sync::atomic::{AtomicU64, Ordering};
262        static COUNTER: AtomicU64 = AtomicU64::new(0);
263        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
264        let mut path = std::env::temp_dir();
265        path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
266        path
267    }
268
269    fn cleanup(path: &Path) {
270        // Remove the data file AND every pager shadow sidecar via reddb-file's
271        // authoritative path helpers (the pager writes only these three shadows
272        // next to the data file). Deriving the paths through reddb-file keeps the
273        // file-suffix contract out of the server source (enforced by reddb-file's
274        // `server_source_does_not_embed_owned_file_suffixes`). Tests drop the
275        // pager BEFORE calling this (every call site uses an inner block) so the
276        // drop-time flush cannot re-create a sidecar after removal.
277        let _ = fs::remove_file(path);
278        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
279            let _ = fs::remove_file(sidecar);
280        }
281    }
282
283    fn dwb_path_for(path: &Path) -> PathBuf {
284        reddb_file::layout::pager_dwb_shadow_path(path)
285    }
286
287    fn dwb_zone_offset_for_test() -> u64 {
288        3 * PAGE_SIZE as u64
289    }
290
291    static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
292
293    struct CowAtomicWriteOverrideGuard {
294        _guard: std::sync::MutexGuard<'static, ()>,
295    }
296
297    impl Drop for CowAtomicWriteOverrideGuard {
298        fn drop(&mut self) {
299            COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
300        }
301    }
302
303    fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
304        let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
305            .lock()
306            .unwrap_or_else(|err| err.into_inner());
307        COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
308        CowAtomicWriteOverrideGuard { _guard: guard }
309    }
310
311    fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
312        let pages: Vec<_> = pages
313            .iter()
314            .map(|(page_id, page)| {
315                let mut page = page.clone();
316                page.update_checksum();
317                (*page_id, page)
318            })
319            .collect();
320        let buf = reddb_file::encode_paged_dwb_frame(
321            pages
322                .iter()
323                .map(|(page_id, page)| (*page_id, page.as_bytes())),
324        );
325
326        let mut file = OpenOptions::new()
327            .read(true)
328            .write(true)
329            .open(path)
330            .expect("open() should succeed");
331        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
332            .expect("seek() should succeed");
333        file.write_all(&buf).expect("write_all() should succeed");
334        file.sync_all().expect("sync_all() should succeed");
335    }
336
337    fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
338        let mut file = OpenOptions::new()
339            .write(true)
340            .open(path)
341            .expect("open() should succeed");
342        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
343            .expect("value is present");
344        file.write_all(page.as_bytes())
345            .expect("write_all() should succeed");
346        file.sync_all().expect("sync_all() should succeed");
347    }
348
349    fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
350        let mut torn = *before.as_bytes();
351        torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
352
353        let mut file = OpenOptions::new()
354            .write(true)
355            .open(path)
356            .expect("open() should succeed");
357        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
358            .expect("value is present");
359        file.write_all(&torn).expect("write_all() should succeed");
360        file.sync_all().expect("sync_all() should succeed");
361    }
362
363    #[test]
364    fn test_pager_create_new() {
365        let path = temp_db_path();
366        cleanup(&path);
367
368        {
369            let pager = Pager::open_default(&path).expect("open_default() should succeed");
370            assert_eq!(
371                pager.page_count().expect("page_count() should succeed"),
372                FIRST_ALLOCATABLE_PAGE
373            );
374            // Header + reserved metadata pages + DWB zone
375        }
376
377        cleanup(&path);
378    }
379
380    #[test]
381    fn native_pager_lock_guards_own_io_and_releases_on_drop() {
382        let directory = tempfile::tempdir().expect("directory");
383        let path = directory.path().join("locked-io.rdb");
384        let pager = Pager::open_default(&path).expect("locked pager opens");
385        let mut page = pager
386            .allocate_page(PageType::BTreeLeaf)
387            .expect("allocate under lock");
388        let id = page.page_id();
389        page.insert_cell(b"key", b"kept").expect("cell");
390        pager.write_page(id, page).expect("write under lock");
391        pager.sync().expect("sync under lock");
392        assert_eq!(
393            pager
394                .read_page(id)
395                .expect("read under lock")
396                .read_cell(0)
397                .expect("cell"),
398            (b"key".to_vec(), b"kept".to_vec())
399        );
400        assert!(
401            matches!(Pager::open_default(&path), Err(PagerError::Locked)),
402            "the writer fence remains exclusive"
403        );
404        drop(pager);
405        let reopened = Pager::open_default(&path).expect("last I/O handle releases writer fence");
406        assert_eq!(
407            reopened
408                .read_page(id)
409                .expect("durable page")
410                .read_cell(0)
411                .expect("durable cell"),
412            (b"key".to_vec(), b"kept".to_vec())
413        );
414    }
415
416    #[test]
417    fn test_pager_reopen() {
418        let path = temp_db_path();
419        cleanup(&path);
420
421        // Create and write
422        {
423            let pager = Pager::open_default(&path).expect("open_default() should succeed");
424
425            // Allocate a page
426            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        // Reopen and verify
435        {
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            // Header + reserved metadata pages + DWB zone + 1 data page
442        }
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            // Allocate and write
456            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            // Read back
468            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            // Allocate a page
488            let page = pager
489                .allocate_page(PageType::BTreeLeaf)
490                .expect("allocate_page() should succeed");
491            let page_id = page.page_id();
492
493            // First read - should be cached from allocate
494            let _ = pager
495                .read_page(page_id)
496                .expect("read_page() should succeed");
497
498            // Second read - should hit cache
499            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            // Allocate pages
519            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            // Free page 1
530            pager.free_page(id1).expect("free_page() should succeed");
531
532            // Next allocation should reuse page 1
533            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        // Create database
581        {
582            let pager = Pager::open_default(&path).expect("open_default() should succeed");
583            pager.sync().expect("sync() should succeed");
584        }
585
586        // Open read-only
587        {
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            // Should fail to allocate
597            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        // CoW crash model: the interrupted write leaves either the old full
917        // page or the new full page, never a torn mix. Exercise both outcomes.
918        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    // -----------------------------------------------------------------
1006    // Target 3: WAL-first flush ordering
1007    // -----------------------------------------------------------------
1008
1009    #[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 flush_refuses_nonzero_lsn_without_a_wal_writer() {
1020        let path = temp_db_path();
1021        let pager = Pager::open(&path, PagerConfig::default()).expect("open pager");
1022        let mut page = pager
1023            .allocate_page(PageType::BTreeLeaf)
1024            .expect("allocate page");
1025        page.set_lsn(42);
1026        pager.write_page(page.page_id(), page).expect("write page");
1027
1028        assert!(
1029            pager.flush().is_err(),
1030            "a page with an LSN needs a WAL barrier"
1031        );
1032        assert!(
1033            pager.dirty_page_count() > 0,
1034            "failed flush must retain dirty pages"
1035        );
1036        assert!(
1037            pager.flush().is_err(),
1038            "failed writeback must remain fenced"
1039        );
1040        drop(pager);
1041        cleanup(&path);
1042    }
1043
1044    #[test]
1045    fn set_wal_writer_attaches_handle() {
1046        use crate::storage::wal::writer::WalWriter;
1047        use std::sync::{Arc, Mutex};
1048
1049        let db_path = temp_db_path();
1050        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1051        let _ = fs::remove_file(&wal_path);
1052
1053        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1054        let wal = Arc::new(Mutex::new(
1055            WalWriter::open(&wal_path).expect("open() should succeed"),
1056        ));
1057        pager.set_wal_writer(Arc::clone(&wal));
1058        assert!(pager.has_wal_writer());
1059
1060        pager.clear_wal_writer();
1061        assert!(!pager.has_wal_writer());
1062
1063        drop(pager);
1064        let _ = fs::remove_file(&wal_path);
1065        cleanup(&db_path);
1066    }
1067
1068    #[test]
1069    fn flush_with_lsn_zero_pages_skips_wal_call() {
1070        // When every dirty page has lsn == 0 (the legacy auto-commit
1071        // path), flush() must NOT call wal.flush_until — there is no
1072        // WAL record to wait for. We verify this by attaching a WAL
1073        // whose durable_lsn starts at 8 and confirming flush() does
1074        // not advance it (no append, no flush).
1075        use crate::storage::wal::writer::WalWriter;
1076        use std::sync::{Arc, Mutex};
1077
1078        let db_path = temp_db_path();
1079        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1080        let _ = fs::remove_file(&wal_path);
1081
1082        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1083        let wal = Arc::new(Mutex::new(
1084            WalWriter::open(&wal_path).expect("open() should succeed"),
1085        ));
1086        let initial_durable = {
1087            let g = wal.lock().expect("lock() should succeed");
1088            g.durable_lsn()
1089        };
1090        pager.set_wal_writer(Arc::clone(&wal));
1091
1092        // Allocate and write a page with lsn = 0.
1093        let mut page = pager
1094            .allocate_page(PageType::BTreeLeaf)
1095            .expect("allocate_page() should succeed");
1096        page.insert_cell(b"k", b"v")
1097            .expect("insert_cell() should succeed");
1098        // header.lsn stays at 0 — caller did not stamp.
1099        pager
1100            .write_page(page.page_id(), page)
1101            .expect("write_page() should succeed");
1102        pager.flush().expect("flush() should succeed");
1103
1104        // WAL durable_lsn must be unchanged because flush_until was
1105        // never called (max lsn over dirty pages was 0).
1106        let after_flush = {
1107            let g = wal.lock().expect("lock() should succeed");
1108            g.durable_lsn()
1109        };
1110        assert_eq!(after_flush, initial_durable);
1111
1112        drop(pager);
1113        let _ = fs::remove_file(&wal_path);
1114        cleanup(&db_path);
1115    }
1116
1117    #[test]
1118    fn flush_advances_wal_durable_when_pages_carry_lsn() {
1119        // The full WAL-first dance: append a record, capture the
1120        // returned LSN, stamp it on a page, flush — afterwards the
1121        // WAL must be durable up to at least that LSN.
1122        use crate::storage::wal::record::WalRecord;
1123        use crate::storage::wal::writer::WalWriter;
1124        use std::sync::{Arc, Mutex};
1125
1126        let db_path = temp_db_path();
1127        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1128        let _ = fs::remove_file(&wal_path);
1129
1130        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1131        let wal = Arc::new(Mutex::new(
1132            WalWriter::open(&wal_path).expect("open() should succeed"),
1133        ));
1134        pager.set_wal_writer(Arc::clone(&wal));
1135
1136        // Stamp two dirty pages with a real WAL LSN.
1137        let stamped_lsn = {
1138            let mut wal_guard = wal.lock().expect("lock() should succeed");
1139            wal_guard
1140                .append(&WalRecord::Begin { tx_id: 1 })
1141                .expect("append() should succeed");
1142            wal_guard
1143                .append(&WalRecord::Commit { tx_id: 1 })
1144                .expect("append() should succeed");
1145            wal_guard.current_lsn()
1146        };
1147        let mut page = pager
1148            .allocate_page(PageType::BTreeLeaf)
1149            .expect("allocate_page() should succeed");
1150        page.insert_cell(b"k", b"v")
1151            .expect("insert_cell() should succeed");
1152        // Use the public Page API to set the LSN.
1153        page.set_lsn(stamped_lsn);
1154        pager
1155            .write_page(page.page_id(), page)
1156            .expect("write_page() should succeed");
1157        pager.flush().expect("flush() should succeed");
1158
1159        // After flush, the WAL is durable at least up to our stamp.
1160        let after_flush = {
1161            let g = wal.lock().expect("lock() should succeed");
1162            g.durable_lsn()
1163        };
1164        assert!(
1165            after_flush >= stamped_lsn,
1166            "after flush durable_lsn {} must be >= stamped {}",
1167            after_flush,
1168            stamped_lsn
1169        );
1170
1171        drop(pager);
1172        let _ = fs::remove_file(&wal_path);
1173        cleanup(&db_path);
1174    }
1175
1176    // -----------------------------------------------------------------
1177    // gh-892: filesystem block-size alignment diagnostic
1178    // -----------------------------------------------------------------
1179
1180    #[test]
1181    fn block_size_warn_fires_for_mismatched_block_size() {
1182        // A block size that does not divide the 16 KiB page size means a
1183        // page write straddles FS blocks — the predicate must report a
1184        // misalignment so `open()` emits the warning.
1185        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1186        // Block larger than the page (e.g. 1 MiB): 16384 % 1048576 != 0.
1187        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1188        // 6 KiB also fails to divide 16 KiB.
1189        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1190    }
1191
1192    #[test]
1193    fn block_size_silent_for_divisor() {
1194        // Block sizes that evenly divide the page size: no straddle, no warn.
1195        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1196        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1197        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1198        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1199    }
1200
1201    #[test]
1202    fn block_size_unavailable_is_silent() {
1203        // st_blksize == 0 means the probe is unavailable; never warn on it.
1204        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1205    }
1206
1207    #[test]
1208    fn page_size_is_unchanged_16kib() {
1209        // The diagnostic must never alter the compile-time page size.
1210        assert_eq!(PAGE_SIZE, 16 * 1024);
1211    }
1212}