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    /// Host guarantees immutable published trees and durable resume counters.
63    pub disk_residency: bool,
64    /// Page cache capacity
65    pub cache_size: usize,
66    /// Whether to open read-only
67    pub read_only: bool,
68    /// Whether to create if not exists
69    pub create: bool,
70    /// Whether to verify checksums on read
71    pub verify_checksums: bool,
72    /// Enable double-write buffer for torn page protection
73    pub double_write: bool,
74    /// Optional encryption key. When set, `Pager::open` writes/reads
75    /// pages through `PageEncryptor` and rejects any DB whose
76    /// encryption-marker disagrees with the supplied key (or its
77    /// absence). When `None`, the pager refuses to open a DB whose
78    /// header carries the `RDBE` encryption marker.
79    pub encryption: Option<crate::storage::encryption::SecureKey>,
80}
81
82impl Default for PagerConfig {
83    fn default() -> Self {
84        Self {
85            disk_residency: false,
86            cache_size: DEFAULT_CACHE_SIZE,
87            read_only: false,
88            create: true,
89            verify_checksums: true,
90            double_write: true,
91            encryption: None,
92        }
93    }
94}
95
96/// Native host adapter for the shared pager.
97pub struct Pager {
98    core: SharedPager<NativePagerFile, NativeWalBarrier>,
99    config: PagerConfig,
100    pub(crate) encryption: Option<(
101        crate::storage::encryption::PageEncryptor,
102        crate::storage::encryption::EncryptionHeader,
103    )>,
104}
105
106#[doc(hidden)]
107pub struct NativePagerFile(File);
108
109#[cfg(test)]
110thread_local! {
111    static PAGER_READ_BYTES: std::cell::Cell<Option<u64>> = const {
112        std::cell::Cell::new(None)
113    };
114}
115
116#[cfg(test)]
117pub(crate) fn measure_pager_read_bytes<T>(operation: impl FnOnce() -> T) -> (T, u64) {
118    PAGER_READ_BYTES.with(|bytes| {
119        assert!(bytes.get().is_none(), "pager read measurement cannot nest");
120        bytes.set(Some(0));
121    });
122    let result = operation();
123    let bytes = PAGER_READ_BYTES.with(|bytes| {
124        bytes
125            .take()
126            .expect("pager read measurement remains active until completion")
127    });
128    (result, bytes)
129}
130
131#[cfg(unix)]
132impl Drop for NativePagerFile {
133    fn drop(&mut self) {
134        // flock belongs to the open file description: a concurrent spawn can
135        // retain it until exec closes the inherited CLOEXEC descriptor. That
136        // descriptor is not a pager owner. Release our fence at the end of the
137        // actual owner's lifetime, rather than waiting for every inherited FD.
138        // File close remains the fallback if the explicit unlock fails.
139        let _ = fs2::FileExt::unlock(&self.0);
140    }
141}
142
143impl reddb_file::VfsFile for NativePagerFile {
144    fn try_clone(&self) -> std::io::Result<Self> {
145        Err(std::io::ErrorKind::Unsupported.into())
146    }
147    fn file_len(&self) -> std::io::Result<u64> {
148        Ok(self.0.metadata()?.len())
149    }
150    fn set_len(&self, len: u64) -> std::io::Result<()> {
151        self.0.set_len(len)
152    }
153    fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
154        std::io::Write::write_all(&mut self.0, bytes)
155    }
156    fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
157        let count = std::io::Read::read(&mut self.0, bytes)?;
158        #[cfg(test)]
159        PAGER_READ_BYTES.with(|bytes| {
160            let Some(current) = bytes.get() else {
161                return;
162            };
163            bytes.set(Some(
164                current
165                    .checked_add(u64::try_from(count).expect("read size fits u64"))
166                    .expect("test pager read byte counter does not overflow"),
167            ));
168        });
169        Ok(count)
170    }
171    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
172        std::io::Seek::seek(&mut self.0, pos)
173    }
174    fn sync_all(&self) -> std::io::Result<()> {
175        self.0.sync_all()
176    }
177}
178
179/// Durable ownership callbacks installed only while a fenced epoch is active.
180pub(crate) trait PageOwnershipRecorder: Send + Sync {
181    fn reserve_pages(&self, start: u32, count: u32) -> std::io::Result<()>;
182    fn detach_free_pages(&self, pages: &[u32]) -> std::io::Result<()>;
183    fn retire_pages(&self, pages: &[u32]) -> std::io::Result<()>;
184    fn needs_page_image(&self, _page_id: u32) -> std::io::Result<bool> {
185        Ok(false)
186    }
187    fn capture_page_image(&self, _page: &Page) -> std::io::Result<()> {
188        Ok(())
189    }
190}
191
192#[doc(hidden)]
193#[derive(Default)]
194pub struct NativeWalBarrier(
195    RwLock<Option<Arc<Mutex<WalWriter>>>>,
196    RwLock<Option<Arc<dyn PageOwnershipRecorder>>>,
197);
198
199impl NativeWalBarrier {
200    fn ownership_recorder(&self) -> std::io::Result<Option<Arc<dyn PageOwnershipRecorder>>> {
201        Ok(self
202            .1
203            .read()
204            .map_err(|_| std::io::Error::other("ownership recorder lock poisoned"))?
205            .clone())
206    }
207}
208
209impl WalBarrier for NativeWalBarrier {
210    fn has_page_ownership_journal(&self) -> std::io::Result<bool> {
211        Ok(self.ownership_recorder()?.is_some())
212    }
213
214    fn needs_page_image(&self, page_id: u32) -> std::io::Result<bool> {
215        self.ownership_recorder()?
216            .map_or(Ok(false), |recorder| recorder.needs_page_image(page_id))
217    }
218
219    fn capture_page_image(&self, page: &Page) -> std::io::Result<()> {
220        if let Some(recorder) = self.ownership_recorder()? {
221            recorder.capture_page_image(page)?;
222        }
223        Ok(())
224    }
225
226    fn flush_until(&self, lsn: u64) -> std::io::Result<()> {
227        let wal = self
228            .0
229            .read()
230            .map_err(|_| std::io::Error::other("WAL slot lock poisoned"))?
231            .clone();
232        if let Some(wal) = wal {
233            let mut writer = wal.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
234            writer.flush_until(lsn)?;
235        } else if lsn != 0 {
236            return Err(std::io::Error::other(
237                "nonzero page LSN requires a WAL durability barrier",
238            ));
239        }
240        Ok(())
241    }
242
243    fn reserve_pages(&self, start: u32, count: u32) -> std::io::Result<()> {
244        if let Some(recorder) = self.ownership_recorder()? {
245            recorder.reserve_pages(start, count)?;
246        }
247        Ok(())
248    }
249
250    fn detach_free_pages(&self, pages: &[u32]) -> std::io::Result<()> {
251        if let Some(recorder) = self.ownership_recorder()? {
252            recorder.detach_free_pages(pages)?;
253        }
254        Ok(())
255    }
256
257    fn retire_pages(&self, pages: &[u32]) -> std::io::Result<()> {
258        if let Some(recorder) = self.ownership_recorder()? {
259            recorder.retire_pages(pages)?;
260        }
261        Ok(())
262    }
263}
264
265impl std::ops::Deref for Pager {
266    type Target = SharedPager<NativePagerFile, NativeWalBarrier>;
267    fn deref(&self) -> &Self::Target {
268        &self.core
269    }
270}
271
272impl reddb_engine::storage::page_store::PageStore for Pager {
273    fn read_page(&self, id: u32) -> Result<Page, PagerError> {
274        self.core.read_page(id)
275    }
276    fn write_page(&self, id: u32, page: Page) -> Result<(), PagerError> {
277        self.core.write_page(id, page)
278    }
279    fn allocate_page(&self, kind: super::page::PageType) -> Result<Page, PagerError> {
280        self.core.allocate_page(kind)
281    }
282    fn free_page(&self, id: u32) -> Result<(), PagerError> {
283        self.core.free_page(id)
284    }
285    fn page_count(&self) -> Result<u32, PagerError> {
286        self.core.page_count()
287    }
288    fn prefetch_hint(&self, id: u32) {
289        self.prefetch_hint(id)
290    }
291}
292
293#[path = "pager/impl.rs"]
294mod pager_impl;
295impl Drop for Pager {
296    fn drop(&mut self) {
297        // Try to flush on drop
298        let _ = self.flush();
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    #[cfg(target_os = "linux")]
306    use pager_impl::parse_mountinfo_options_for_path;
307    use pager_impl::{
308        classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
309    };
310    use std::fs;
311    use std::io::Write;
312
313    #[cfg(unix)]
314    #[test]
315    fn native_pager_drop_releases_lock_with_inherited_descriptor() {
316        use fs2::FileExt;
317        let directory = tempfile::tempdir().expect("lock fixture");
318        let path = directory.path().join("owner.rdb");
319        let file = std::fs::OpenOptions::new()
320            .read(true)
321            .write(true)
322            .create_new(true)
323            .open(&path)
324            .expect("owner file");
325        file.try_lock_exclusive().expect("owner lock");
326        let owner = NativePagerFile(file);
327        // dup and fork retain the same open file description. A concurrent
328        // spawn may hold this non-owner descriptor until the child execs,
329        // even though Rust opens it with CLOEXEC.
330        let inherited = owner.0.try_clone().expect("inherited descriptor");
331        let contender = std::fs::OpenOptions::new()
332            .read(true)
333            .write(true)
334            .open(&path)
335            .expect("independent contender");
336        assert!(
337            contender.try_lock_exclusive().is_err(),
338            "live owner must exclude a second writer"
339        );
340        drop(owner);
341        contender
342            .try_lock_exclusive()
343            .expect("closed owner must permit immediate reopen, before the child execs");
344        // Releasing the inherited descriptor must not unlock the new owner.
345        drop(inherited);
346        let third = std::fs::OpenOptions::new()
347            .read(true)
348            .write(true)
349            .open(&path)
350            .expect("third handle");
351        assert!(
352            third.try_lock_exclusive().is_err(),
353            "replacement writer retains its independent fence"
354        );
355    }
356
357    fn temp_db_path() -> PathBuf {
358        use std::sync::atomic::{AtomicU64, Ordering};
359        static COUNTER: AtomicU64 = AtomicU64::new(0);
360        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
361        let mut path = std::env::temp_dir();
362        path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
363        path
364    }
365
366    fn cleanup(path: &Path) {
367        // Remove the data file AND every pager shadow sidecar via reddb-file's
368        // authoritative path helpers (the pager writes only these three shadows
369        // next to the data file). Deriving the paths through reddb-file keeps the
370        // file-suffix contract out of the server source (enforced by reddb-file's
371        // `server_source_does_not_embed_owned_file_suffixes`). Tests drop the
372        // pager BEFORE calling this (every call site uses an inner block) so the
373        // drop-time flush cannot re-create a sidecar after removal.
374        let _ = fs::remove_file(path);
375        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
376            let _ = fs::remove_file(sidecar);
377        }
378    }
379
380    fn dwb_path_for(path: &Path) -> PathBuf {
381        reddb_file::layout::pager_dwb_shadow_path(path)
382    }
383
384    fn dwb_zone_offset_for_test() -> u64 {
385        3 * PAGE_SIZE as u64
386    }
387
388    static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
389
390    struct CowAtomicWriteOverrideGuard {
391        _guard: std::sync::MutexGuard<'static, ()>,
392    }
393
394    impl Drop for CowAtomicWriteOverrideGuard {
395        fn drop(&mut self) {
396            COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
397        }
398    }
399
400    fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
401        let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
402            .lock()
403            .unwrap_or_else(|err| err.into_inner());
404        COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
405        CowAtomicWriteOverrideGuard { _guard: guard }
406    }
407
408    fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
409        let pages: Vec<_> = pages
410            .iter()
411            .map(|(page_id, page)| {
412                let mut page = page.clone();
413                page.update_checksum();
414                (*page_id, page)
415            })
416            .collect();
417        let buf = reddb_file::encode_paged_dwb_frame(
418            pages
419                .iter()
420                .map(|(page_id, page)| (*page_id, page.as_bytes())),
421        );
422
423        let mut file = OpenOptions::new()
424            .read(true)
425            .write(true)
426            .open(path)
427            .expect("open() should succeed");
428        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
429            .expect("seek() should succeed");
430        file.write_all(&buf).expect("write_all() should succeed");
431        file.sync_all().expect("sync_all() should succeed");
432    }
433
434    fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
435        let mut file = OpenOptions::new()
436            .write(true)
437            .open(path)
438            .expect("open() should succeed");
439        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
440            .expect("value is present");
441        file.write_all(page.as_bytes())
442            .expect("write_all() should succeed");
443        file.sync_all().expect("sync_all() should succeed");
444    }
445
446    fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
447        let mut torn = *before.as_bytes();
448        torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
449
450        let mut file = OpenOptions::new()
451            .write(true)
452            .open(path)
453            .expect("open() should succeed");
454        file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
455            .expect("value is present");
456        file.write_all(&torn).expect("write_all() should succeed");
457        file.sync_all().expect("sync_all() should succeed");
458    }
459
460    #[test]
461    fn test_pager_create_new() {
462        let path = temp_db_path();
463        cleanup(&path);
464
465        {
466            let pager = Pager::open_default(&path).expect("open_default() should succeed");
467            assert_eq!(
468                pager.page_count().expect("page_count() should succeed"),
469                FIRST_ALLOCATABLE_PAGE
470            );
471            // Header + reserved metadata pages + DWB zone
472        }
473
474        cleanup(&path);
475    }
476
477    #[test]
478    fn native_pager_lock_guards_own_io_and_releases_on_drop() {
479        let directory = tempfile::tempdir().expect("directory");
480        let path = directory.path().join("locked-io.rdb");
481        let pager = Pager::open_default(&path).expect("locked pager opens");
482        let mut page = pager
483            .allocate_page(PageType::BTreeLeaf)
484            .expect("allocate under lock");
485        let id = page.page_id();
486        page.insert_cell(b"key", b"kept").expect("cell");
487        pager.write_page(id, page).expect("write under lock");
488        pager.sync().expect("sync under lock");
489        assert_eq!(
490            pager
491                .read_page(id)
492                .expect("read under lock")
493                .read_cell(0)
494                .expect("cell"),
495            (b"key".to_vec(), b"kept".to_vec())
496        );
497        assert!(
498            matches!(Pager::open_default(&path), Err(PagerError::Locked)),
499            "the writer fence remains exclusive"
500        );
501        drop(pager);
502        let reopened = Pager::open_default(&path).expect("last I/O handle releases writer fence");
503        assert_eq!(
504            reopened
505                .read_page(id)
506                .expect("durable page")
507                .read_cell(0)
508                .expect("durable cell"),
509            (b"key".to_vec(), b"kept".to_vec())
510        );
511    }
512
513    #[test]
514    fn test_pager_reopen() {
515        let path = temp_db_path();
516        cleanup(&path);
517
518        // Create and write
519        {
520            let pager = Pager::open_default(&path).expect("open_default() should succeed");
521
522            // Allocate a page
523            let page = pager
524                .allocate_page(PageType::BTreeLeaf)
525                .expect("allocate_page() should succeed");
526            assert_eq!(page.page_id(), FIRST_ALLOCATABLE_PAGE);
527
528            pager.sync().expect("sync() should succeed");
529        }
530
531        // Reopen and verify
532        {
533            let pager = Pager::open_default(&path).expect("open_default() should succeed");
534            assert_eq!(
535                pager.page_count().expect("page_count() should succeed"),
536                FIRST_ALLOCATABLE_PAGE + 1
537            );
538            // Header + reserved metadata pages + DWB zone + 1 data page
539        }
540
541        cleanup(&path);
542    }
543
544    #[test]
545    fn test_pager_read_write() {
546        let path = temp_db_path();
547        cleanup(&path);
548
549        {
550            let pager = Pager::open_default(&path).expect("open_default() should succeed");
551
552            // Allocate and write
553            let mut page = pager
554                .allocate_page(PageType::BTreeLeaf)
555                .expect("allocate_page() should succeed");
556            let page_id = page.page_id();
557
558            page.insert_cell(b"key", b"value")
559                .expect("insert_cell() should succeed");
560            pager
561                .write_page(page_id, page)
562                .expect("write_page() should succeed");
563
564            // Read back
565            let read_page = pager
566                .read_page(page_id)
567                .expect("read_page() should succeed");
568            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
569            assert_eq!(key, b"key");
570            assert_eq!(value, b"value");
571        }
572
573        cleanup(&path);
574    }
575
576    #[test]
577    fn test_pager_cache() {
578        let path = temp_db_path();
579        cleanup(&path);
580
581        {
582            let pager = Pager::open_default(&path).expect("open_default() should succeed");
583
584            // Allocate a page
585            let page = pager
586                .allocate_page(PageType::BTreeLeaf)
587                .expect("allocate_page() should succeed");
588            let page_id = page.page_id();
589
590            // First read - should be cached from allocate
591            let _ = pager
592                .read_page(page_id)
593                .expect("read_page() should succeed");
594
595            // Second read - should hit cache
596            let _ = pager
597                .read_page(page_id)
598                .expect("read_page() should succeed");
599
600            let stats = pager.cache_stats();
601            assert!(stats.hits >= 1);
602        }
603
604        cleanup(&path);
605    }
606
607    #[test]
608    fn test_pager_free_page() {
609        let path = temp_db_path();
610        cleanup(&path);
611
612        {
613            let pager = Pager::open_default(&path).expect("open_default() should succeed");
614
615            // Allocate pages
616            let page1 = pager
617                .allocate_page(PageType::BTreeLeaf)
618                .expect("allocate_page() should succeed");
619            pager
620                .allocate_page(PageType::BTreeLeaf)
621                .expect("allocate_page() should succeed");
622
623            let id1 = page1.page_id();
624
625            // Free page 1
626            pager.free_page(id1).expect("free_page() should succeed");
627
628            // Next allocation should reuse page 1
629            let page3 = pager
630                .allocate_page(PageType::BTreeLeaf)
631                .expect("allocate_page() should succeed");
632            assert_eq!(page3.page_id(), id1);
633        }
634
635        cleanup(&path);
636    }
637
638    #[test]
639    fn test_freelist_persistence() {
640        let path = temp_db_path();
641        cleanup(&path);
642
643        let freed_id;
644        {
645            let pager = Pager::open_default(&path).expect("open_default() should succeed");
646            let page1 = pager
647                .allocate_page(PageType::BTreeLeaf)
648                .expect("allocate_page() should succeed");
649            let _page2 = pager
650                .allocate_page(PageType::BTreeLeaf)
651                .expect("allocate_page() should succeed");
652            freed_id = page1.page_id();
653
654            pager
655                .free_page(freed_id)
656                .expect("free_page() should succeed");
657            pager.sync().expect("sync() should succeed");
658        }
659
660        {
661            let pager = Pager::open_default(&path).expect("open_default() should succeed");
662            let page = pager
663                .allocate_page(PageType::BTreeLeaf)
664                .expect("allocate_page() should succeed");
665            assert_eq!(page.page_id(), freed_id);
666        }
667
668        cleanup(&path);
669    }
670
671    #[test]
672    fn test_pager_read_only() {
673        let path = temp_db_path();
674        cleanup(&path);
675
676        // Create database
677        {
678            let pager = Pager::open_default(&path).expect("open_default() should succeed");
679            pager.sync().expect("sync() should succeed");
680        }
681
682        // Open read-only
683        {
684            let config = PagerConfig {
685                read_only: true,
686                ..Default::default()
687            };
688
689            let pager = Pager::open(&path, config).expect("open() should succeed");
690            assert!(pager.is_read_only());
691
692            // Should fail to allocate
693            assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
694        }
695
696        cleanup(&path);
697    }
698
699    #[test]
700    fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
701        let path = temp_db_path();
702        cleanup(&path);
703
704        let config = PagerConfig {
705            double_write: true,
706            ..Default::default()
707        };
708
709        let page_id;
710        {
711            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
712            let page = pager
713                .allocate_page(PageType::BTreeLeaf)
714                .expect("allocate_page() should succeed");
715            page_id = page.page_id();
716            pager.sync().expect("sync() should succeed");
717        }
718
719        let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
720        recovered_page
721            .insert_cell(b"key", b"value")
722            .expect("insert_cell() should succeed");
723        write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
724
725        assert!(!dwb_path_for(&path).exists());
726
727        {
728            let pager = Pager::open(&path, config).expect("open() should succeed");
729
730            let read_page = pager
731                .read_page(page_id)
732                .expect("read_page() should succeed");
733            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
734            assert_eq!(key, b"key");
735            assert_eq!(value, b"value");
736
737            assert!(!dwb_path_for(&path).exists());
738
739            let mut updated_page = recovered_page.clone();
740            updated_page
741                .insert_cell(b"key2", b"value2")
742                .expect("insert_cell() should succeed");
743            pager
744                .write_page(page_id, updated_page)
745                .expect("write_page() should succeed");
746            pager.flush().expect("flush() should succeed");
747
748            assert!(!dwb_path_for(&path).exists());
749        }
750
751        cleanup(&path);
752    }
753
754    #[test]
755    fn retired_dwb_sidecar_routes_through_offline_migration() {
756        let path = temp_db_path();
757        cleanup(&path);
758
759        {
760            let pager = Pager::open_default(&path).expect("open_default() should succeed");
761            pager.sync().expect("sync() should succeed");
762        }
763
764        let dwb = dwb_path_for(&path);
765        fs::write(&dwb, b"legacy dwb sidecar").expect("write legacy sidecar");
766
767        let err = match Pager::open_default(&path) {
768            Ok(_) => panic!("retired DWB sidecar must refuse open"),
769            Err(err) => err,
770        };
771        assert!(matches!(err, PagerError::LegacySidecarStore { .. }));
772
773        cleanup(&path);
774    }
775
776    #[test]
777    fn interrupted_dwb_staging_write_is_discarded_on_open() {
778        let path = temp_db_path();
779        cleanup(&path);
780
781        {
782            let pager = Pager::open_default(&path).expect("open_default() should succeed");
783            pager.sync().expect("sync() should succeed");
784        }
785
786        let mut file = OpenOptions::new()
787            .read(true)
788            .write(true)
789            .open(&path)
790            .expect("open() should succeed");
791        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
792            .expect("seek() should succeed");
793        file.write_all(b"RDDW")
794            .expect("write corrupt DWB magic should succeed");
795        file.sync_all().expect("sync_all() should succeed");
796
797        {
798            let pager = Pager::open_default(&path).expect("torn staging frame should be discarded");
799            pager.sync().expect("sync() should succeed");
800        }
801
802        let mut cleared = [0u8; 4];
803        let mut file = OpenOptions::new()
804            .read(true)
805            .open(&path)
806            .expect("open() should succeed");
807        file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
808            .expect("seek() should succeed");
809        file.read_exact(&mut cleared)
810            .expect("read_exact() should succeed");
811        assert_ne!(cleared, reddb_file::DWB_MAGIC);
812
813        cleanup(&path);
814    }
815
816    #[test]
817    fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
818        assert_eq!(
819            classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
820            Some(CowFilesystemKind::Zfs),
821            "ZFS is always CoW"
822        );
823        assert_eq!(
824            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
825            Some(CowFilesystemKind::BtrfsDataCow),
826            "btrfs qualifies only when datacow remains enabled"
827        );
828        assert_eq!(
829            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
830            None,
831            "btrfs nodatacow mount option must reject DWB skip"
832        );
833        assert_eq!(
834            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
835            None,
836            "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
837        );
838        assert_eq!(
839            classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
840            None,
841            "missing btrfs inode flags are uncertain and must fail closed"
842        );
843        assert_eq!(
844            classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
845            None,
846            "missing btrfs mount options are uncertain and must fail closed"
847        );
848    }
849
850    #[cfg(target_os = "linux")]
851    #[test]
852    fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
853        let mountinfo = "\
85424 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
85535 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
85636 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
857";
858
859        assert_eq!(
860            parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
861                .as_deref(),
862            Some("rw,relatime,rw,space_cache=v2")
863        );
864        assert_eq!(
865            parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
866                .as_deref(),
867            Some("rw,relatime,rw,nodatacow")
868        );
869    }
870
871    #[test]
872    fn double_write_false_keeps_dwb_when_cow_probe_denies() {
873        let _override = cow_atomic_write_override(false);
874        let path = temp_db_path();
875        cleanup(&path);
876
877        {
878            let config = PagerConfig {
879                double_write: false,
880                ..Default::default()
881            };
882            let pager = Pager::open(&path, config).expect("open() should succeed");
883            let page = pager
884                .allocate_page(PageType::BTreeLeaf)
885                .expect("allocate_page() should succeed");
886            pager
887                .write_page(page.page_id(), page)
888                .expect("write_page() should succeed");
889            pager.flush().expect("flush() should succeed");
890        }
891
892        assert!(
893            !dwb_path_for(&path).exists(),
894            "DWB must stay enabled in-file when double_write=false is not proven safe"
895        );
896
897        cleanup(&path);
898    }
899
900    #[test]
901    fn double_write_false_skips_dwb_when_cow_probe_allows() {
902        let _override = cow_atomic_write_override(true);
903        let path = temp_db_path();
904        cleanup(&path);
905
906        {
907            let config = PagerConfig {
908                double_write: false,
909                ..Default::default()
910            };
911            let pager = Pager::open(&path, config).expect("open() should succeed");
912            let page = pager
913                .allocate_page(PageType::BTreeLeaf)
914                .expect("allocate_page() should succeed");
915            pager
916                .write_page(page.page_id(), page)
917                .expect("write_page() should succeed");
918            pager.flush().expect("flush() should succeed");
919        }
920
921        assert!(
922            !dwb_path_for(&path).exists(),
923            "DWB may be skipped only after the CoW probe allows it"
924        );
925
926        cleanup(&path);
927    }
928
929    #[test]
930    fn double_write_false_on_cow_recovers_from_in_file_dwb_zone() {
931        let _override = cow_atomic_write_override(true);
932        let path = temp_db_path();
933        cleanup(&path);
934
935        let page_id;
936        {
937            let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
938            let page = pager
939                .allocate_page(PageType::BTreeLeaf)
940                .expect("allocate_page() should succeed");
941            page_id = page.page_id();
942            pager.sync().expect("sync() should succeed");
943        }
944
945        let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
946        recovered_page
947            .insert_cell(b"key", b"value")
948            .expect("insert_cell() should succeed");
949        write_dwb_fixture(&path, &[(page_id, recovered_page)]);
950
951        {
952            let config = PagerConfig {
953                double_write: false,
954                ..Default::default()
955            };
956            let pager = Pager::open(&path, config).expect("open() should succeed");
957            let read_page = pager
958                .read_page(page_id)
959                .expect("read_page() should succeed");
960            let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
961            assert_eq!(key, b"key");
962            assert_eq!(value, b"value");
963        }
964
965        assert!(!dwb_path_for(&path).exists());
966
967        cleanup(&path);
968    }
969
970    #[test]
971    fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
972        let _override = cow_atomic_write_override(true);
973        let path = temp_db_path();
974        cleanup(&path);
975
976        let config = PagerConfig {
977            double_write: false,
978            ..Default::default()
979        };
980
981        let page_id;
982        let before;
983        let after;
984        {
985            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
986            let mut page = pager
987                .allocate_page(PageType::BTreeLeaf)
988                .expect("allocate_page() should succeed");
989            page_id = page.page_id();
990            page.insert_cell(b"phase", b"before")
991                .expect("insert_cell() should succeed");
992            pager
993                .write_page(page_id, page)
994                .expect("write_page() should succeed");
995            pager.sync().expect("sync() should succeed");
996            before = pager
997                .read_page(page_id)
998                .expect("read_page() should succeed");
999
1000            let mut page = before.clone();
1001            page.insert_cell(b"phase2", b"after")
1002                .expect("insert_cell() should succeed");
1003            pager
1004                .write_page(page_id, page)
1005                .expect("write_page() should succeed");
1006            pager.flush().expect("flush() should succeed");
1007            after = pager
1008                .read_page(page_id)
1009                .expect("read_page() should succeed");
1010        }
1011
1012        // CoW crash model: the interrupted write leaves either the old full
1013        // page or the new full page, never a torn mix. Exercise both outcomes.
1014        for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
1015            write_page_bytes(&path, page_id, whole_page);
1016
1017            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
1018            let recovered = pager
1019                .read_page(page_id)
1020                .expect("read_page() should succeed");
1021            assert_eq!(recovered.cell_count(), expected_cells);
1022            let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
1023            assert_eq!(key, b"phase");
1024            assert_eq!(value, b"before");
1025            if expected_cells == 2 {
1026                let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
1027                assert_eq!(key, b"phase2");
1028                assert_eq!(value, b"after");
1029            }
1030            drop(pager);
1031        }
1032
1033        cleanup(&path);
1034    }
1035
1036    #[test]
1037    fn same_mid_write_without_cow_recovers_from_dwb() {
1038        let _override = cow_atomic_write_override(false);
1039        let path = temp_db_path();
1040        cleanup(&path);
1041
1042        let config = PagerConfig {
1043            double_write: false,
1044            ..Default::default()
1045        };
1046
1047        let page_id;
1048        let before;
1049        let after;
1050        {
1051            let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
1052            let mut page = pager
1053                .allocate_page(PageType::BTreeLeaf)
1054                .expect("allocate_page() should succeed");
1055            page_id = page.page_id();
1056            page.insert_cell(b"phase", b"before")
1057                .expect("insert_cell() should succeed");
1058            pager
1059                .write_page(page_id, page)
1060                .expect("write_page() should succeed");
1061            pager.sync().expect("sync() should succeed");
1062            before = pager
1063                .read_page(page_id)
1064                .expect("read_page() should succeed");
1065
1066            let mut page = before.clone();
1067            page.insert_cell(b"phase2", b"after")
1068                .expect("insert_cell() should succeed");
1069            pager
1070                .write_page(page_id, page)
1071                .expect("write_page() should succeed");
1072            pager.flush().expect("flush() should succeed");
1073            after = pager
1074                .read_page(page_id)
1075                .expect("read_page() should succeed");
1076        }
1077
1078        write_dwb_fixture(&path, &[(page_id, after.clone())]);
1079        write_torn_page_bytes(&path, page_id, &before, &after);
1080
1081        {
1082            let pager = Pager::open(&path, config).expect("open() should succeed");
1083            let recovered = pager
1084                .read_page(page_id)
1085                .expect("read_page() should succeed");
1086            assert_eq!(recovered.cell_count(), 2);
1087
1088            let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
1089            assert_eq!(key, b"phase");
1090            assert_eq!(value, b"before");
1091
1092            let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
1093            assert_eq!(key, b"phase2");
1094            assert_eq!(value, b"after");
1095        }
1096
1097        assert!(!dwb_path_for(&path).exists());
1098        cleanup(&path);
1099    }
1100
1101    // -----------------------------------------------------------------
1102    // Target 3: WAL-first flush ordering
1103    // -----------------------------------------------------------------
1104
1105    #[test]
1106    fn pager_starts_without_wal_writer() {
1107        let path = temp_db_path();
1108        let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
1109        assert!(!pager.has_wal_writer());
1110        drop(pager);
1111        cleanup(&path);
1112    }
1113
1114    #[test]
1115    fn flush_refuses_nonzero_lsn_without_a_wal_writer() {
1116        let path = temp_db_path();
1117        let pager = Pager::open(&path, PagerConfig::default()).expect("open pager");
1118        let mut page = pager
1119            .allocate_page(PageType::BTreeLeaf)
1120            .expect("allocate page");
1121        page.set_lsn(42);
1122        pager.write_page(page.page_id(), page).expect("write page");
1123
1124        assert!(
1125            pager.flush().is_err(),
1126            "a page with an LSN needs a WAL barrier"
1127        );
1128        assert!(
1129            pager.dirty_page_count() > 0,
1130            "failed flush must retain dirty pages"
1131        );
1132        assert!(
1133            pager.flush().is_err(),
1134            "failed writeback must remain fenced"
1135        );
1136        drop(pager);
1137        cleanup(&path);
1138    }
1139
1140    #[test]
1141    fn set_wal_writer_attaches_handle() {
1142        use crate::storage::wal::writer::WalWriter;
1143        use std::sync::{Arc, Mutex};
1144
1145        let db_path = temp_db_path();
1146        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1147        let _ = fs::remove_file(&wal_path);
1148
1149        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1150        let wal = Arc::new(Mutex::new(
1151            WalWriter::open(&wal_path).expect("open() should succeed"),
1152        ));
1153        pager.set_wal_writer(Arc::clone(&wal));
1154        assert!(pager.has_wal_writer());
1155
1156        pager.clear_wal_writer();
1157        assert!(!pager.has_wal_writer());
1158
1159        drop(pager);
1160        let _ = fs::remove_file(&wal_path);
1161        cleanup(&db_path);
1162    }
1163
1164    #[test]
1165    fn flush_with_lsn_zero_pages_skips_wal_call() {
1166        // When every dirty page has lsn == 0 (the legacy auto-commit
1167        // path), flush() must NOT call wal.flush_until — there is no
1168        // WAL record to wait for. We verify this by attaching a WAL
1169        // whose durable_lsn starts at 8 and confirming flush() does
1170        // not advance it (no append, no flush).
1171        use crate::storage::wal::writer::WalWriter;
1172        use std::sync::{Arc, Mutex};
1173
1174        let db_path = temp_db_path();
1175        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1176        let _ = fs::remove_file(&wal_path);
1177
1178        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1179        let wal = Arc::new(Mutex::new(
1180            WalWriter::open(&wal_path).expect("open() should succeed"),
1181        ));
1182        let initial_durable = {
1183            let g = wal.lock().expect("lock() should succeed");
1184            g.durable_lsn()
1185        };
1186        pager.set_wal_writer(Arc::clone(&wal));
1187
1188        // Allocate and write a page with lsn = 0.
1189        let mut page = pager
1190            .allocate_page(PageType::BTreeLeaf)
1191            .expect("allocate_page() should succeed");
1192        page.insert_cell(b"k", b"v")
1193            .expect("insert_cell() should succeed");
1194        // header.lsn stays at 0 — caller did not stamp.
1195        pager
1196            .write_page(page.page_id(), page)
1197            .expect("write_page() should succeed");
1198        pager.flush().expect("flush() should succeed");
1199
1200        // WAL durable_lsn must be unchanged because flush_until was
1201        // never called (max lsn over dirty pages was 0).
1202        let after_flush = {
1203            let g = wal.lock().expect("lock() should succeed");
1204            g.durable_lsn()
1205        };
1206        assert_eq!(after_flush, initial_durable);
1207
1208        drop(pager);
1209        let _ = fs::remove_file(&wal_path);
1210        cleanup(&db_path);
1211    }
1212
1213    #[test]
1214    fn flush_advances_wal_durable_when_pages_carry_lsn() {
1215        // The full WAL-first dance: append a record, capture the
1216        // returned LSN, stamp it on a page, flush — afterwards the
1217        // WAL must be durable up to at least that LSN.
1218        use crate::storage::wal::record::WalRecord;
1219        use crate::storage::wal::writer::WalWriter;
1220        use std::sync::{Arc, Mutex};
1221
1222        let db_path = temp_db_path();
1223        let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1224        let _ = fs::remove_file(&wal_path);
1225
1226        let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1227        let wal = Arc::new(Mutex::new(
1228            WalWriter::open(&wal_path).expect("open() should succeed"),
1229        ));
1230        pager.set_wal_writer(Arc::clone(&wal));
1231
1232        // Stamp two dirty pages with a real WAL LSN.
1233        let stamped_lsn = {
1234            let mut wal_guard = wal.lock().expect("lock() should succeed");
1235            wal_guard
1236                .append(&WalRecord::Begin { tx_id: 1 })
1237                .expect("append() should succeed");
1238            wal_guard
1239                .append(&WalRecord::Commit { tx_id: 1 })
1240                .expect("append() should succeed");
1241            wal_guard.current_lsn()
1242        };
1243        let mut page = pager
1244            .allocate_page(PageType::BTreeLeaf)
1245            .expect("allocate_page() should succeed");
1246        page.insert_cell(b"k", b"v")
1247            .expect("insert_cell() should succeed");
1248        // Use the public Page API to set the LSN.
1249        page.set_lsn(stamped_lsn);
1250        pager
1251            .write_page(page.page_id(), page)
1252            .expect("write_page() should succeed");
1253        pager.flush().expect("flush() should succeed");
1254
1255        // After flush, the WAL is durable at least up to our stamp.
1256        let after_flush = {
1257            let g = wal.lock().expect("lock() should succeed");
1258            g.durable_lsn()
1259        };
1260        assert!(
1261            after_flush >= stamped_lsn,
1262            "after flush durable_lsn {} must be >= stamped {}",
1263            after_flush,
1264            stamped_lsn
1265        );
1266
1267        drop(pager);
1268        let _ = fs::remove_file(&wal_path);
1269        cleanup(&db_path);
1270    }
1271
1272    // -----------------------------------------------------------------
1273    // gh-892: filesystem block-size alignment diagnostic
1274    // -----------------------------------------------------------------
1275
1276    #[test]
1277    fn block_size_warn_fires_for_mismatched_block_size() {
1278        // A block size that does not divide the 16 KiB page size means a
1279        // page write straddles FS blocks — the predicate must report a
1280        // misalignment so `open()` emits the warning.
1281        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1282        // Block larger than the page (e.g. 1 MiB): 16384 % 1048576 != 0.
1283        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1284        // 6 KiB also fails to divide 16 KiB.
1285        assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1286    }
1287
1288    #[test]
1289    fn block_size_silent_for_divisor() {
1290        // Block sizes that evenly divide the page size: no straddle, no warn.
1291        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1292        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1293        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1294        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1295    }
1296
1297    #[test]
1298    fn block_size_unavailable_is_silent() {
1299        // st_blksize == 0 means the probe is unavailable; never warn on it.
1300        assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1301    }
1302
1303    #[test]
1304    fn page_size_is_unchanged_16kib() {
1305        // The diagnostic must never alter the compile-time page size.
1306        assert_eq!(PAGE_SIZE, 16 * 1024);
1307    }
1308}