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